diff --git a/.env.example b/.env.example
index c0660ea..566bca8 100644
--- a/.env.example
+++ b/.env.example
@@ -62,4 +62,6 @@ AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
+OCR_API_URL=http://192.168.0.220:5000/passport
+
VITE_APP_NAME="${APP_NAME}"
diff --git a/app/Http/Controllers/Admin/EmployerController.php b/app/Http/Controllers/Admin/EmployerController.php
new file mode 100644
index 0000000..2482d1c
--- /dev/null
+++ b/app/Http/Controllers/Admin/EmployerController.php
@@ -0,0 +1,261 @@
+with(['employerProfile', 'sponsor']);
+
+ // Search
+ if ($request->filled('search')) {
+ $search = $request->input('search');
+ $query->where(function($q) use ($search) {
+ $q->where('name', 'like', "%{$search}%")
+ ->orWhere('email', 'like', "%{$search}%")
+ ->orWhereHas('employerProfile', function($qp) use ($search) {
+ $qp->where('company_name', 'like', "%{$search}%")
+ ->orWhere('phone', 'like', "%{$search}%");
+ });
+ });
+ }
+
+ // Status filter (on sponsor status)
+ if ($request->filled('status') && $request->input('status') !== 'All') {
+ $status = strtolower($request->input('status'));
+ $query->whereHas('sponsor', function($qs) use ($status) {
+ $qs->where('status', $status);
+ });
+ }
+
+ // Location filter
+ if ($request->filled('location') && $request->input('location') !== 'All') {
+ $location = $request->input('location');
+ $query->whereHas('employerProfile', function($qp) use ($location) {
+ $qp->where('country', $location);
+ });
+ }
+
+ // Sorting
+ $sortField = $request->input('sort_field', 'created_at');
+ $sortOrder = $request->input('sort_order', 'desc');
+ if (in_array($sortField, ['name', 'email', 'created_at'])) {
+ $query->orderBy($sortField, $sortOrder);
+ } else {
+ $query->orderBy('created_at', 'desc');
+ }
+
+ $employers = $query->paginate(10)->withQueryString();
+
+ // Transform results to format status correctly from sponsor
+ $employers->getCollection()->transform(function($emp) {
+ $emp->status = $emp->sponsor ? $emp->sponsor->status : 'active';
+ return $emp;
+ });
+
+ return Inertia::render('Admin/Employers/Index', [
+ 'employers' => $employers,
+ 'filters' => $request->only(['search', 'status', 'location', 'sort_field', 'sort_order'])
+ ]);
+ }
+
+ /**
+ * Export employers as CSV.
+ */
+ public function export(Request $request)
+ {
+ $query = User::where('role', 'employer')->with(['employerProfile', 'sponsor']);
+
+ // Search
+ if ($request->filled('search')) {
+ $search = $request->input('search');
+ $query->where(function($q) use ($search) {
+ $q->where('name', 'like', "%{$search}%")
+ ->orWhere('email', 'like', "%{$search}%")
+ ->orWhereHas('employerProfile', function($qp) use ($search) {
+ $qp->where('company_name', 'like', "%{$search}%")
+ ->orWhere('phone', 'like', "%{$search}%");
+ });
+ });
+ }
+
+ // Status filter
+ if ($request->filled('status') && $request->input('status') !== 'All') {
+ $status = strtolower($request->input('status'));
+ $query->whereHas('sponsor', function($qs) use ($status) {
+ $qs->where('status', $status);
+ });
+ }
+
+ $employers = $query->get();
+
+ $headers = [
+ "Content-type" => "text/csv",
+ "Content-Disposition" => "attachment; filename=employers_export_" . date('Ymd_His') . ".csv",
+ "Pragma" => "no-cache",
+ "Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
+ "Expires" => "0"
+ ];
+
+ $columns = ['ID', 'Name', 'Email', 'Company Name', 'Phone', 'Country', 'Status', 'Subscription Status', 'Created At'];
+
+ $callback = function() use ($employers, $columns) {
+ $file = fopen('php://output', 'w');
+ fputcsv($file, $columns);
+
+ foreach ($employers as $emp) {
+ fputcsv($file, [
+ $emp->id,
+ $emp->name,
+ $emp->email,
+ $emp->employerProfile->company_name ?? 'N/A',
+ $emp->employerProfile->phone ?? 'N/A',
+ $emp->employerProfile->country ?? 'N/A',
+ $emp->sponsor ? $emp->sponsor->status : 'active',
+ $emp->subscription_status ?? 'none',
+ $emp->created_at ? $emp->created_at->toDateTimeString() : 'N/A',
+ ]);
+ }
+
+ fclose($file);
+ };
+
+ return response()->stream($callback, 200, $headers);
+ }
+
+ /**
+ * Verify / approve employer profile.
+ */
+ public function verify($id)
+ {
+ $user = User::where('role', 'employer')->findOrFail($id);
+ if ($user->employerProfile) {
+ $user->employerProfile->update([
+ 'verification_status' => 'approved'
+ ]);
+ }
+
+ $sponsor = Sponsor::where('email', $user->email)->first();
+ if ($sponsor) {
+ $sponsor->update([
+ 'is_verified' => true,
+ 'status' => 'active'
+ ]);
+ }
+
+ return back()->with('success', "Employer '{$user->name}' profile verified and activated successfully.");
+ }
+
+ /**
+ * Suspend an employer.
+ */
+ public function suspend($id)
+ {
+ $user = User::where('role', 'employer')->findOrFail($id);
+
+ $sponsor = Sponsor::where('email', $user->email)->first();
+ if ($sponsor) {
+ $sponsor->update(['status' => 'suspended']);
+ }
+
+ return back()->with('success', "Employer '{$user->name}' suspended successfully.");
+ }
+
+ /**
+ * Activate a suspended/inactive employer.
+ */
+ public function activate($id)
+ {
+ $user = User::where('role', 'employer')->findOrFail($id);
+
+ $sponsor = Sponsor::where('email', $user->email)->first();
+ if ($sponsor) {
+ $sponsor->update(['status' => 'active']);
+ }
+
+ return back()->with('success', "Employer '{$user->name}' activated successfully.");
+ }
+
+ /**
+ * Delete an employer.
+ */
+ public function delete($id)
+ {
+ $user = User::where('role', 'employer')->findOrFail($id);
+ $name = $user->name;
+
+ $sponsor = Sponsor::where('email', $user->email)->first();
+ if ($sponsor) {
+ $sponsor->delete();
+ }
+
+ if ($user->employerProfile) {
+ $user->employerProfile->delete();
+ }
+ $user->delete();
+
+ return back()->with('success', "Employer '{$name}' deleted successfully.");
+ }
+
+ /**
+ * Update an employer's fields.
+ */
+ public function update(Request $request, $id)
+ {
+ $user = User::where('role', 'employer')->findOrFail($id);
+ $validated = $request->validate([
+ 'name' => 'required|string|max:255',
+ 'email' => 'required|email|unique:users,email,' . $user->id,
+ 'company_name' => 'nullable|string|max:255',
+ 'phone' => 'nullable|string',
+ 'country' => 'nullable|string',
+ 'subscription_status' => 'nullable|string',
+ ]);
+
+ $user->update([
+ 'name' => $validated['name'],
+ 'email' => $validated['email'],
+ 'subscription_status' => $validated['subscription_status'] ?? $user->subscription_status,
+ ]);
+
+ if ($user->employerProfile) {
+ $user->employerProfile->update([
+ 'company_name' => $validated['company_name'] ?? $user->employerProfile->company_name,
+ 'phone' => $validated['phone'] ?? $user->employerProfile->phone,
+ 'country' => $validated['country'] ?? $user->employerProfile->country,
+ ]);
+ } else {
+ EmployerProfile::create([
+ 'user_id' => $user->id,
+ 'company_name' => $validated['company_name'],
+ 'phone' => $validated['phone'],
+ 'country' => $validated['country'],
+ ]);
+ }
+
+ $sponsor = Sponsor::where('email', $user->email)->first();
+ if ($sponsor) {
+ $sponsor->update([
+ 'full_name' => $validated['name'],
+ 'email' => $validated['email'],
+ 'mobile' => $validated['phone'] ?? $sponsor->mobile,
+ 'organization_name' => $validated['company_name'] ?? $sponsor->organization_name,
+ 'city' => $validated['country'] ?? $sponsor->city,
+ ]);
+ }
+
+ return back()->with('success', "Employer details saved successfully.");
+ }
+}
diff --git a/app/Http/Controllers/Admin/SponsorController.php b/app/Http/Controllers/Admin/SponsorController.php
index 1546358..957f537 100644
--- a/app/Http/Controllers/Admin/SponsorController.php
+++ b/app/Http/Controllers/Admin/SponsorController.php
@@ -22,6 +22,7 @@ public function index(Request $request)
$search = $request->input('search');
$query->where(function($q) use ($search) {
$q->where('full_name', 'like', "%{$search}%")
+ ->orWhere('organization_name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
->orWhere('mobile', 'like', "%{$search}%");
});
@@ -32,15 +33,35 @@ public function index(Request $request)
$query->where('status', strtolower($request->input('status')));
}
- // Server-side Location/City filter
- if ($request->filled('location') && $request->input('location') !== 'All') {
- $query->where('city', $request->input('location'));
+
+
+ // Verification filter
+ if ($request->filled('verification') && $request->input('verification') !== 'All') {
+ $isVerified = $request->input('verification') === 'Verified';
+ $query->where('is_verified', $isVerified);
+ }
+
+ // License filter
+ if ($request->filled('license') && $request->input('license') !== 'All') {
+ $license = $request->input('license');
+ if ($license === 'Expired') {
+ $query->where('license_expiry', '<', now());
+ } elseif ($license === 'Valid') {
+ $query->where('license_expiry', '>=', now());
+ } elseif ($license === 'Expiring Soon') {
+ $query->whereBetween('license_expiry', [now(), now()->addDays(30)]);
+ }
+ }
+
+ // Nationality filter
+ if ($request->filled('nationality') && $request->input('nationality') !== 'All') {
+ $query->where('nationality', $request->input('nationality'));
}
// Server-side Sorting
$sortField = $request->input('sort_field', 'created_at');
$sortOrder = $request->input('sort_order', 'desc');
- if (in_array($sortField, ['full_name', 'email', 'city', 'status', 'created_at'])) {
+ if (in_array($sortField, ['full_name', 'organization_name', 'email', 'city', 'status', 'created_at'])) {
$query->orderBy($sortField, $sortOrder);
} else {
$query->orderBy('created_at', 'desc');
@@ -49,9 +70,41 @@ public function index(Request $request)
// Paginate
$sponsors = $query->paginate(10)->withQueryString();
- return Inertia::render('Admin/Employers/Index', [
+ // Get distinct nationalities
+ $nationalities = [
+ 'Afghan', 'Albanian', 'Algerian', 'American', 'Andorran', 'Angolan', 'Argentinian', 'Armenian', 'Australian', 'Austrian',
+ 'Azerbaijani', 'Bahamian', 'Bahraini', 'Bangladeshi', 'Barbadian', 'Belarusian', 'Belgian', 'Belizean', 'Beninese', 'Bhutanese',
+ 'Bolivian', 'Bosnian', 'Brazilian', 'British', 'Bruneian', 'Bulgarian', 'Burkinabe', 'Burmese', 'Burundian', 'Cambodian',
+ 'Cameroonian', 'Canadian', 'Cape Verdean', 'Central African', 'Chadian', 'Chilean', 'Chinese', 'Colombian', 'Comoran', 'Congolese',
+ 'Costa Rican', 'Croatian', 'Cuban', 'Cypriot', 'Czech', 'Danish', 'Djiboutian', 'Dominican', 'Dutch', 'East Timorese',
+ 'Ecuadorian', 'Egyptian', 'Emirati', 'Equatorial Guinean', 'Eritrean', 'Estonian', 'Ethiopian', 'Fijian', 'Filipino', 'Finnish',
+ 'French', 'Gabonese', 'Gambian', 'Georgian', 'German', 'Ghanaian', 'Greek', 'Grenadian', 'Guatemalan', 'Guinean',
+ 'Guyanese', 'Haitian', 'Honduran', 'Hungarian', 'Icelandic', 'Indian', 'Indonesian', 'Iranian', 'Iraqi', 'Irish',
+ 'Italian', 'Ivorian', 'Jamaican', 'Japanese', 'Jordanian', 'Kazakh', 'Kenyan', 'Kuwaiti', 'Kyrgyz', 'Laotian',
+ 'Latvian', 'Lebanese', 'Liberian', 'Libyan', 'Liechtensteiner', 'Lithuanian', 'Luxembourger', 'Macedonian', 'Malagasy', 'Malawian',
+ 'Malaysian', 'Maldivian', 'Malian', 'Maltese', 'Mauritanian', 'Mauritian', 'Mexican', 'Micronesian', 'Moldovan', 'Monacan',
+ 'Mongolian', 'Montenegrin', 'Moroccan', 'Mozambican', 'Namibian', 'Nauruan', 'Nepalese', 'New Zealander', 'Nicaraguan', 'Nigerian',
+ 'Nigerien', 'North Korean', 'Norwegian', 'Omani', 'Pakistani', 'Palauans', 'Palestinian', 'Panamanian', 'Papua New Guinean', 'Paraguayan',
+ 'Peruvian', 'Polish', 'Portuguese', 'Qatari', 'Romanian', 'Russian', 'Rwandan', 'Saint Lucian', 'Salvadoran', 'Samoan',
+ 'San Marinese', 'Sao Tomean', 'Saudi Arabian', 'Senegalese', 'Serbian', 'Seychellois', 'Sierra Leonean', 'Singaporean', 'Slovak', 'Slovenian',
+ 'Solomon Islander', 'Somali', 'South African', 'South Korean', 'Spanish', 'Sri Lankan', 'Sudanese', 'Surinamese', 'Swazi', 'Swedish',
+ 'Swiss', 'Syrian', 'Taiwanese', 'Tajik', 'Tanzanian', 'Thai', 'Togolese', 'Tongan', 'Trinidadian', 'Tunisian',
+ 'Turkish', 'Turkmen', 'Tuvaluan', 'Ugandan', 'Ukrainian', 'Uruguayan', 'Uzbek', 'Vanuatuan', 'Venezuelan', 'Vietnamese',
+ 'Western Samoan', 'Yemeni', 'Zambian', 'Zimbabwean'
+ ];
+
+ // Global stats counts (removing Active plans / Pending approval, replacing with Total, Verified, Suspended)
+ $stats = [
+ 'total' => Sponsor::count(),
+ 'verified' => Sponsor::where('is_verified', true)->count(),
+ 'suspended' => Sponsor::where('status', 'suspended')->count(),
+ ];
+
+ return Inertia::render('Admin/CharityOrganizations/Index', [
'sponsors' => $sponsors,
- 'filters' => $request->only(['search', 'status', 'location', 'sort_field', 'sort_order'])
+ 'filters' => $request->only(['search', 'status', 'verification', 'license', 'nationality', 'sort_field', 'sort_order']),
+ 'nationalities' => $nationalities,
+ 'stats' => $stats
]);
}
@@ -67,6 +120,7 @@ public function export(Request $request)
$search = $request->input('search');
$query->where(function($q) use ($search) {
$q->where('full_name', 'like', "%{$search}%")
+ ->orWhere('organization_name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
->orWhere('mobile', 'like', "%{$search}%");
});
@@ -77,15 +131,35 @@ public function export(Request $request)
$query->where('status', strtolower($request->input('status')));
}
- // Server-side Location/City filter
- if ($request->filled('location') && $request->input('location') !== 'All') {
- $query->where('city', $request->input('location'));
+
+
+ // Verification filter
+ if ($request->filled('verification') && $request->input('verification') !== 'All') {
+ $isVerified = $request->input('verification') === 'Verified';
+ $query->where('is_verified', $isVerified);
+ }
+
+ // License filter
+ if ($request->filled('license') && $request->input('license') !== 'All') {
+ $license = $request->input('license');
+ if ($license === 'Expired') {
+ $query->where('license_expiry', '<', now());
+ } elseif ($license === 'Valid') {
+ $query->where('license_expiry', '>=', now());
+ } elseif ($license === 'Expiring Soon') {
+ $query->whereBetween('license_expiry', [now(), now()->addDays(30)]);
+ }
+ }
+
+ // Nationality filter
+ if ($request->filled('nationality') && $request->input('nationality') !== 'All') {
+ $query->where('nationality', $request->input('nationality'));
}
// Server-side Sorting
$sortField = $request->input('sort_field', 'created_at');
$sortOrder = $request->input('sort_order', 'desc');
- if (in_array($sortField, ['full_name', 'email', 'city', 'status', 'created_at'])) {
+ if (in_array($sortField, ['full_name', 'organization_name', 'email', 'city', 'status', 'created_at'])) {
$query->orderBy($sortField, $sortOrder);
} else {
$query->orderBy('created_at', 'desc');
@@ -95,13 +169,13 @@ public function export(Request $request)
$headers = [
"Content-type" => "text/csv",
- "Content-Disposition" => "attachment; filename=sponsors_export_" . date('Ymd_His') . ".csv",
+ "Content-Disposition" => "attachment; filename=charity_organizations_export_" . date('Ymd_His') . ".csv",
"Pragma" => "no-cache",
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
"Expires" => "0"
];
- $columns = ['ID', 'Full Name', 'Email', 'Mobile', 'City', 'Nationality', 'Address', 'Status', 'OTP Verified', 'Verified At', 'Subscription Plan', 'Subscription Status', 'Subscription Expiry', 'Created At'];
+ $columns = ['ID', 'Organization Name', 'Full Name', 'Email', 'Mobile', 'City', 'Nationality', 'Address', 'Status', 'Verified', 'License Expiry', 'Created At'];
$callback = function() use ($sponsors, $columns) {
$file = fopen('php://output', 'w');
@@ -110,6 +184,7 @@ public function export(Request $request)
foreach ($sponsors as $sponsor) {
fputcsv($file, [
$sponsor->id,
+ $sponsor->organization_name ?? 'N/A',
$sponsor->full_name,
$sponsor->email,
$sponsor->mobile,
@@ -118,10 +193,7 @@ public function export(Request $request)
$sponsor->address ?? 'N/A',
$sponsor->status,
$sponsor->is_verified ? 'Yes' : 'No',
- $sponsor->otp_verified_at ? $sponsor->otp_verified_at->toDateTimeString() : 'N/A',
- $sponsor->subscription_plan ?? 'None',
- $sponsor->subscription_status ?? 'None',
- $sponsor->subscription_end_date ? $sponsor->subscription_end_date->toDateString() : 'N/A',
+ $sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : 'N/A',
$sponsor->created_at ? $sponsor->created_at->toDateTimeString() : 'N/A',
]);
}
@@ -193,6 +265,7 @@ public function update(Request $request, $id)
$sponsor = Sponsor::findOrFail($id);
$validated = $request->validate([
'full_name' => 'required|string|max:255',
+ 'organization_name' => 'nullable|string|max:255',
'email' => 'required|email|unique:sponsors,email,' . $sponsor->id,
'mobile' => 'required|string',
'city' => 'required|string',
diff --git a/app/Http/Controllers/Api/EmployerAuthController.php b/app/Http/Controllers/Api/EmployerAuthController.php
index c464a56..57e22d2 100644
--- a/app/Http/Controllers/Api/EmployerAuthController.php
+++ b/app/Http/Controllers/Api/EmployerAuthController.php
@@ -188,9 +188,12 @@ public function register(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
- 'email' => 'required|string|email|max:255|unique:users,email',
- 'phone' => 'required|string|max:50',
+ 'email' => 'required|string|email|max:255|unique:users,email|unique:sponsors,email',
+ 'phone' => 'required|string|max:50|unique:employer_profiles,phone|unique:sponsors,mobile',
'fcm_token' => 'nullable|string|max:255',
+ ], [
+ 'email.unique' => 'This email address is already registered.',
+ 'phone.unique' => 'This mobile number is already registered.',
]);
if ($validator->fails()) {
@@ -409,23 +412,11 @@ public function uploadEmiratesId(Request $request)
}
$file = $request->file('emirates_id_file');
- $destinationPath = public_path('uploads/documents');
- if (!file_exists($destinationPath)) {
- mkdir($destinationPath, 0755, true);
- }
- $fileName = time() . '_employer_eid_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
- $file->move($destinationPath, $fileName);
- $filePath = 'uploads/documents/' . $fileName;
-
- // OCR Simulated extraction
- $extractedIdNumber = '784-' . rand(1970, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
- $extractedExpiry = now()->addYears(rand(2, 4))->toDateString();
-
- // We are not storing this document, just extract data and delete this file
- if (file_exists(public_path($filePath))) {
- unlink(public_path($filePath));
- }
+ // Extract data using OcrDocumentService without storing the file on disk
+ $ocrResult = \App\Services\OcrDocumentService::extractData($file);
+ $extractedIdNumber = $ocrResult['document_number'];
+ $extractedExpiry = $ocrResult['expiry_date'];
// Update or create EmployerProfile
$profile = EmployerProfile::where('user_id', $user->id)->first();
diff --git a/app/Http/Controllers/Api/EmployerProfileController.php b/app/Http/Controllers/Api/EmployerProfileController.php
index 84a72d2..92230df 100644
--- a/app/Http/Controllers/Api/EmployerProfileController.php
+++ b/app/Http/Controllers/Api/EmployerProfileController.php
@@ -87,7 +87,13 @@ public function updateProfile(Request $request)
'unique:users,email,' . $employer->id,
$sponsor ? 'unique:sponsors,email,' . $sponsor->id : 'unique:sponsors,email',
],
- 'phone' => 'required|string|max:255',
+ 'phone' => [
+ 'required',
+ 'string',
+ 'max:255',
+ 'unique:employer_profiles,phone,' . ($employer->employerProfile ? $employer->employerProfile->id : 'NULL'),
+ $sponsor ? 'unique:sponsors,mobile,' . $sponsor->id : 'unique:sponsors,mobile',
+ ],
'company_name' => 'required|string|max:255',
'language' => 'required|string|in:English,Arabic,english,arabic',
'notifications' => 'required|boolean',
diff --git a/app/Http/Controllers/Api/SponsorAuthController.php b/app/Http/Controllers/Api/SponsorAuthController.php
index 0efebd1..3fd0c66 100644
--- a/app/Http/Controllers/Api/SponsorAuthController.php
+++ b/app/Http/Controllers/Api/SponsorAuthController.php
@@ -24,12 +24,12 @@ public function register(Request $request)
{
$validator = Validator::make($request->all(), [
'full_name' => 'required|string|max:255',
- 'mobile' => 'required|string|max:50|unique:sponsors,mobile',
+ 'mobile' => 'required|string|max:50|unique:sponsors,mobile|unique:employer_profiles,phone',
'password' => 'required|string|min:6',
'license_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
'emirates_id_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
'organization_name' => 'required|string|max:255',
- 'email' => 'required|email|max:255|unique:sponsors,email',
+ 'email' => 'required|email|max:255|unique:sponsors,email|unique:users,email',
'nationality' => 'required|string|max:100',
'city' => 'required|string|max:100',
'address' => 'required|string|max:255',
@@ -58,29 +58,19 @@ public function register(Request $request)
$email = "sponsor.{$mobileClean}." . rand(10, 99) . "@migrant.ae";
}
- // Store files
- $destinationPath = public_path('uploads/licenses');
- if (!file_exists($destinationPath)) {
- mkdir($destinationPath, 0755, true);
- }
-
$licenseFile = $request->file('license_file');
- $licenseFileName = time() . '_license_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $licenseFile->getClientOriginalName());
- $licenseFile->move($destinationPath, $licenseFileName);
- $licensePath = 'uploads/licenses/' . $licenseFileName;
+ $ocrLicense = \App\Services\OcrDocumentService::extractData($licenseFile);
+ $licenseExpiry = $request->license_expiry ?? $ocrLicense['expiry_date'];
$emiratesIdPath = null;
if ($request->hasFile('emirates_id_file')) {
$emiratesIdFile = $request->file('emirates_id_file');
- $emiratesIdFileName = time() . '_emirates_id_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $emiratesIdFile->getClientOriginalName());
- $emiratesIdFile->move($destinationPath, $emiratesIdFileName);
- $emiratesIdPath = 'uploads/licenses/' . $emiratesIdFileName;
+ \App\Services\OcrDocumentService::extractData($emiratesIdFile);
}
+ $licensePath = null;
$apiToken = Str::random(80);
- $licenseExpiry = $request->license_expiry ?? now()->addYears(3)->toDateString();
-
$sponsor = DB::transaction(function () use ($request, $email, $licensePath, $emiratesIdPath, $apiToken, $licenseExpiry) {
return Sponsor::create([
'full_name' => $request->full_name,
diff --git a/app/Http/Controllers/Api/WorkerAuthController.php b/app/Http/Controllers/Api/WorkerAuthController.php
index 5725361..d1a14da 100644
--- a/app/Http/Controllers/Api/WorkerAuthController.php
+++ b/app/Http/Controllers/Api/WorkerAuthController.php
@@ -57,11 +57,13 @@ public function skills()
public function sendOtp(Request $request)
{
$validator = Validator::make($request->all(), [
- 'phone' => 'required_without:email|nullable|string|max:50',
- 'email' => 'required_without:phone|nullable|email|max:255',
+ 'phone' => 'required_without:email|nullable|string|max:50|unique:workers,phone',
+ 'email' => 'required_without:phone|nullable|email|max:255|unique:workers,email',
], [
'phone.required_without' => 'Please provide a mobile number or email address.',
+ 'phone.unique' => 'This mobile number is already registered.',
'email.required_without' => 'Please provide a mobile number or email address.',
+ 'email.unique' => 'This email address is already registered.',
]);
if ($validator->fails()) {
@@ -344,32 +346,55 @@ public function register(Request $request)
}
}
- $destinationPath = public_path('uploads/documents');
- if (!file_exists($destinationPath)) {
- mkdir($destinationPath, 0755, true);
+ $passportData = [
+ 'number' => 'P' . rand(100000, 999999),
+ 'expiry_date' => now()->addYears(8)->toDateString(),
+ 'issue_date' => now()->subYears(2)->toDateString(),
+ ];
+
+ if ($request->hasFile('passport_file')) {
+ $file = $request->file('passport_file');
+ $ocrRes = \App\Services\OcrDocumentService::extractData($file);
+ if (!empty($ocrRes['document_number'])) {
+ $passportData['number'] = $ocrRes['document_number'];
+ }
+ if (!empty($ocrRes['expiry_date'])) {
+ $passportData['expiry_date'] = $ocrRes['expiry_date'];
+ }
+ if (!empty($ocrRes['expiry_date'])) {
+ $passportData['issue_date'] = date('Y-m-d', strtotime($ocrRes['expiry_date'] . ' - 5 years'));
+ }
+ }
+
+ $visaData = [
+ 'number' => 'V' . rand(100000, 999999),
+ 'expiry_date' => now()->addYears(2)->toDateString(),
+ 'issue_date' => now()->subYear()->toDateString(),
+ 'ocr_accuracy' => 0.0,
+ ];
+
+ if ($request->hasFile('visa_file')) {
+ $file = $request->file('visa_file');
+ $ocrRes = \App\Services\OcrDocumentService::extractData($file);
+ if (!empty($ocrRes['document_number'])) {
+ $visaData['number'] = $ocrRes['document_number'];
+ }
+ if (!empty($ocrRes['expiry_date'])) {
+ $visaData['expiry_date'] = $ocrRes['expiry_date'];
+ }
+ if (!empty($ocrRes['expiry_date'])) {
+ $visaData['issue_date'] = date('Y-m-d', strtotime($ocrRes['expiry_date'] . ' - 2 years'));
+ }
+ $visaData['ocr_accuracy'] = 98.5;
}
$passportPath = null;
$visaPath = null;
- if ($request->hasFile('passport_file')) {
- $file = $request->file('passport_file');
- $fileName = time() . '_passport_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
- $file->move($destinationPath, $fileName);
- $passportPath = 'uploads/documents/' . $fileName;
- }
-
- if ($request->hasFile('visa_file')) {
- $file = $request->file('visa_file');
- $fileName = time() . '_visa_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
- $file->move($destinationPath, $fileName);
- $visaPath = 'uploads/documents/' . $fileName;
- }
-
$inCountry = filter_var($request->input('in_country', true), FILTER_VALIDATE_BOOLEAN);
$apiToken = Str::random(80);
- $result = DB::transaction(function () use ($request, $email, $skillsArray, $passportPath, $visaPath, $apiToken, $inCountry) {
+ $result = DB::transaction(function () use ($request, $email, $skillsArray, $passportPath, $visaPath, $apiToken, $inCountry, $passportData, $visaData) {
$worker = Worker::create([
'name' => $request->name,
'email' => $email,
@@ -404,19 +429,19 @@ public function register(Request $request)
$worker->documents()->create([
'type' => 'passport',
- 'number' => 'P' . rand(100000, 999999),
- 'issue_date' => now()->subYears(2)->toDateString(),
- 'expiry_date' => now()->addYears(8)->toDateString(),
+ 'number' => $passportData['number'],
+ 'issue_date' => $passportData['issue_date'],
+ 'expiry_date' => $passportData['expiry_date'],
'ocr_accuracy'=> 98.5,
'file_path' => $passportPath,
]);
$worker->documents()->create([
'type' => 'visa',
- 'number' => 'V' . rand(100000, 999999),
- 'issue_date' => now()->subYear()->toDateString(),
- 'expiry_date' => now()->addYears(2)->toDateString(),
- 'ocr_accuracy'=> $visaPath ? 98.5 : 0.0,
+ 'number' => $visaData['number'],
+ 'issue_date' => $visaData['issue_date'],
+ 'expiry_date' => $visaData['expiry_date'],
+ 'ocr_accuracy'=> $visaData['ocr_accuracy'],
'file_path' => $visaPath,
]);
diff --git a/app/Http/Controllers/Employer/EmployerAuthController.php b/app/Http/Controllers/Employer/EmployerAuthController.php
index 5495ede..87c6b29 100644
--- a/app/Http/Controllers/Employer/EmployerAuthController.php
+++ b/app/Http/Controllers/Employer/EmployerAuthController.php
@@ -90,7 +90,7 @@ public function register(Request $request)
]);
// 2. Email uniqueness check (Check DB, return 409 if duplicate)
- if (User::where('email', $request->email)->exists()) {
+ if (User::where('email', $request->email)->exists() || \App\Models\Sponsor::where('email', $request->email)->exists()) {
return response()->json([
'errors' => [
'email' => 'This email address is already registered. Please login or use a different email.'
@@ -98,6 +98,16 @@ public function register(Request $request)
], 409);
}
+ // 2b. Phone uniqueness check
+ $phoneClean = preg_replace('/[^0-9]/', '', $request->phone);
+ if (\App\Models\EmployerProfile::where('phone', 'like', '%' . $phoneClean . '%')->exists() || \App\Models\Sponsor::where('mobile', 'like', '%' . $phoneClean . '%')->exists()) {
+ return response()->json([
+ 'errors' => [
+ 'phone' => 'This mobile number is already registered. Please login or use a different number.'
+ ]
+ ], 409);
+ }
+
// 3. Generate 6-digit OTP, hash & store with 10-min expiry
$otp = (string) mt_rand(100000, 999999);
$hashedOtp = Hash::make($otp);
@@ -270,26 +280,15 @@ public function uploadEmiratesId(Request $request)
'emirates_id_file.max' => 'The document must not exceed 10MB.',
]);
- $filePath = null;
+ $extractedIdNumber = null;
+ $extractedExpiry = null;
if ($request->hasFile('emirates_id_file')) {
- $destinationPath = public_path('uploads/documents');
- if (!file_exists($destinationPath)) {
- mkdir($destinationPath, 0755, true);
- }
$file = $request->file('emirates_id_file');
- $fileName = time() . '_employer_eid_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
- $file->move($destinationPath, $fileName);
- $filePath = 'uploads/documents/' . $fileName;
- // OCR Simulated extraction
- $extractedIdNumber = '784-' . rand(1970, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
- $extractedExpiry = now()->addYears(rand(2, 4))->toDateString();
-
- // We are not storing this document, just extract data and delete this file
- if (file_exists(public_path($filePath))) {
- unlink(public_path($filePath));
- }
- $filePath = null; // Ensure we don't save the path
+ // Extract data using OcrDocumentService without storing the file on disk
+ $ocrResult = \App\Services\OcrDocumentService::extractData($file);
+ $extractedIdNumber = $ocrResult['document_number'];
+ $extractedExpiry = $ocrResult['expiry_date'];
}
$pending = session('pending_employer_registration');
diff --git a/app/Http/Controllers/Employer/ProfileController.php b/app/Http/Controllers/Employer/ProfileController.php
index 27344a6..a362965 100644
--- a/app/Http/Controllers/Employer/ProfileController.php
+++ b/app/Http/Controllers/Employer/ProfileController.php
@@ -145,34 +145,33 @@ public function update(Request $request)
// Document uploads with OCR extraction and PDPL compliance secure purge
$uploaded = false;
+ $extractedIdNumber = null;
+ $extractedExpiry = null;
+
if ($request->hasFile('emirates_id_front')) {
$file = $request->file('emirates_id_front');
- $fileName = time() . '_front_' . $file->getClientOriginalName();
- $path = $file->storeAs('temp/employer', $fileName, 'local');
-
- // Delete physical file immediately
- if (\Illuminate\Support\Facades\Storage::disk('local')->exists($path)) {
- \Illuminate\Support\Facades\Storage::disk('local')->delete($path);
- }
+ $ocrFront = \App\Services\OcrDocumentService::extractData($file);
+ $extractedIdNumber = $ocrFront['document_number'];
+ $extractedExpiry = $ocrFront['expiry_date'];
$profile->emirates_id_front = '[DELETED_FOR_PDPL_COMPLIANCE]';
$uploaded = true;
}
if ($request->hasFile('emirates_id_back')) {
$file = $request->file('emirates_id_back');
- $fileName = time() . '_back_' . $file->getClientOriginalName();
- $path = $file->storeAs('temp/employer', $fileName, 'local');
-
- // Delete physical file immediately
- if (\Illuminate\Support\Facades\Storage::disk('local')->exists($path)) {
- \Illuminate\Support\Facades\Storage::disk('local')->delete($path);
+ $ocrBack = \App\Services\OcrDocumentService::extractData($file);
+ if (empty($extractedIdNumber)) {
+ $extractedIdNumber = $ocrBack['document_number'];
+ }
+ if (empty($extractedExpiry)) {
+ $extractedExpiry = $ocrBack['expiry_date'];
}
$profile->emirates_id_back = '[DELETED_FOR_PDPL_COMPLIANCE]';
$uploaded = true;
}
if ($uploaded) {
- $profile->emirates_id_number = '784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
- $profile->emirates_id_expiry = now()->addYears(rand(2, 4))->toDateString();
+ $profile->emirates_id_number = $extractedIdNumber ?? ('784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9));
+ $profile->emirates_id_expiry = $extractedExpiry ?? now()->addYears(rand(2, 4))->toDateString();
$profile->verification_status = 'approved';
}
diff --git a/app/Models/User.php b/app/Models/User.php
index 5b9e6e9..da7c38e 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -61,6 +61,11 @@ public function employerProfile()
return $this->hasOne(EmployerProfile::class, 'user_id');
}
+ public function sponsor()
+ {
+ return $this->hasOne(Sponsor::class, 'email', 'email');
+ }
+
public function announcements()
{
return $this->hasMany(Announcement::class, 'employer_id');
diff --git a/app/Services/OcrDocumentService.php b/app/Services/OcrDocumentService.php
new file mode 100644
index 0000000..e9e530b
--- /dev/null
+++ b/app/Services/OcrDocumentService.php
@@ -0,0 +1,101 @@
+ null,
+ 'expiry_date' => null,
+ 'date_of_birth' => null,
+ 'nationality' => null,
+ 'name' => null,
+ 'success' => false,
+ ];
+
+ try {
+ // Send file directly from temp upload directory to the OCR API
+ $apiUrl = config('services.ocr.api_url', 'http://192.168.0.220:5000/passport');
+ $response = Http::attach(
+ 'file',
+ file_get_contents($file->getRealPath()),
+ $file->getClientOriginalName()
+ )->post($apiUrl);
+
+ if ($response->successful()) {
+ $json = $response->json();
+ if (!empty($json['success'])) {
+ $extracted['success'] = true;
+ $data = $json['data'] ?? [];
+
+ // Extract fields
+ if (!empty($data['passport_number'])) {
+ $extracted['document_number'] = $data['passport_number'];
+ } elseif (!empty($data['personal_number'])) {
+ $extracted['document_number'] = $data['personal_number'];
+ }
+
+ if (!empty($data['date_of_expiry'])) {
+ $extracted['expiry_date'] = $data['date_of_expiry'];
+ }
+
+ if (!empty($data['date_of_birth'])) {
+ $extracted['date_of_birth'] = $data['date_of_birth'];
+ }
+
+ if (!empty($data['nationality'])) {
+ $extracted['nationality'] = $data['nationality'];
+ }
+
+ // Extract name
+ $givenNames = $data['given_names'] ?? '';
+ $surname = $data['surname'] ?? '';
+ if (!empty($givenNames) || !empty($surname)) {
+ $extracted['name'] = trim($givenNames . ' ' . $surname);
+ }
+
+ // Try to extract Emirates ID if not in personal_number
+ if (empty($extracted['document_number']) && !empty($json['raw_text'])) {
+ foreach ($json['raw_text'] as $line) {
+ if (preg_match('/(784-\d{4}-\d{7}-\d)/', $line, $matches)) {
+ $extracted['document_number'] = $matches[1];
+ break;
+ }
+ }
+ }
+ }
+ }
+ } catch (\Exception $e) {
+ Log::error('OCR API Error: ' . $e->getMessage());
+ }
+
+ // Apply fallbacks in case OCR fails or returns empty fields
+ if (empty($extracted['document_number'])) {
+ // Generate a fake Emirates ID format: 784-YYYY-XXXXXXX-X
+ $extracted['document_number'] = '784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
+ }
+
+ if (empty($extracted['expiry_date'])) {
+ $extracted['expiry_date'] = now()->addYears(rand(2, 4))->toDateString();
+ }
+
+ if (empty($extracted['date_of_birth'])) {
+ $extracted['date_of_birth'] = now()->subYears(rand(20, 50))->toDateString();
+ }
+
+ return $extracted;
+ }
+}
diff --git a/config/services.php b/config/services.php
index 6a90eb8..e562f30 100644
--- a/config/services.php
+++ b/config/services.php
@@ -35,4 +35,8 @@
],
],
+ 'ocr' => [
+ 'api_url' => env('OCR_API_URL', 'http://192.168.0.220:5000/passport'),
+ ],
+
];
diff --git a/resources/js/Layouts/AdminLayout.jsx b/resources/js/Layouts/AdminLayout.jsx
index 37e9019..3241bed 100644
--- a/resources/js/Layouts/AdminLayout.jsx
+++ b/resources/js/Layouts/AdminLayout.jsx
@@ -9,7 +9,6 @@ import {
Settings,
LogOut,
Menu,
- Bell,
Shield,
BadgeDollarSign,
ShieldCheck,
@@ -19,7 +18,8 @@ import {
History,
List,
LifeBuoy,
- Heart
+ Heart,
+ Building
} from 'lucide-react';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
@@ -33,6 +33,7 @@ export default function AdminLayout({ children, title = 'Dashboard' }) {
{ name: 'Dashboard', href: '/admin/dashboard', icon: LayoutDashboard },
{ name: 'Worker Profiles', href: '/admin/workers', icon: Users },
{ name: 'Employers', href: '/admin/employers', icon: Briefcase },
+ { name: 'Charity Organizations', href: '/admin/charity-organizations', icon: Building },
{ name: 'OCR Verification', href: '/admin/workers/verifications', icon: ShieldCheck },
{ name: 'Safety & Moderation', href: '/admin/safety', icon: ShieldAlert },
{ name: 'Support Tickets', href: '/admin/tickets', icon: LifeBuoy },
@@ -143,15 +144,7 @@ export default function AdminLayout({ children, title = 'Dashboard' }) {
-
-
-
+
{getInitials(user.name)}
diff --git a/resources/js/Pages/Admin/CharityOrganizations/Index.jsx b/resources/js/Pages/Admin/CharityOrganizations/Index.jsx
new file mode 100644
index 0000000..df2b217
--- /dev/null
+++ b/resources/js/Pages/Admin/CharityOrganizations/Index.jsx
@@ -0,0 +1,682 @@
+import React, { useState, useEffect } from 'react';
+import { Head, router, Link } from '@inertiajs/react';
+import AdminLayout from '@/Layouts/AdminLayout';
+import {
+ Search,
+ Filter,
+ Download,
+ Users,
+ Mail,
+ MapPin,
+ Calendar,
+ CheckCircle,
+ Clock,
+ XCircle,
+ ExternalLink,
+ MoreVertical,
+ BadgeDollarSign,
+ ShieldAlert,
+ TrendingUp,
+ AlertOctagon,
+ Award,
+ Edit2,
+ Trash2,
+ Check,
+ Phone,
+ ShieldCheck,
+ Building
+} from 'lucide-react';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Badge } from '@/components/ui/badge';
+
+export default function CharitiesIndex({ sponsors, filters = {}, nationalities = [], stats = {} }) {
+ const [searchTerm, setSearchTerm] = useState(filters.search || '');
+ const [statusFilter, setStatusFilter] = useState(filters.status || 'All');
+ const [verificationFilter, setVerificationFilter] = useState(filters.verification || 'All');
+ const [licenseFilter, setLicenseFilter] = useState(filters.license || 'All');
+ const [nationalityFilter, setNationalityFilter] = useState(filters.nationality || 'All');
+ const [sortField, setSortField] = useState(filters.sort_field || 'created_at');
+ const [sortOrder, setSortOrder] = useState(filters.sort_order || 'desc');
+
+ const [selectedSponsor, setSelectedSponsor] = useState(null);
+ const [isDetailsOpen, setIsDetailsOpen] = useState(false);
+ const [isEditOpen, setIsEditOpen] = useState(false);
+ const [editForm, setEditForm] = useState({
+ id: '',
+ full_name: '',
+ organization_name: '',
+ email: '',
+ mobile: '',
+ city: '',
+ nationality: '',
+ address: ''
+ });
+
+ // Handle search/filter queries
+ const triggerSearch = (searchVal, statusVal, verificationVal, licenseVal, nationalityVal, sortF, sortO) => {
+ router.get('/admin/charity-organizations', {
+ search: searchVal,
+ status: statusVal,
+ verification: verificationVal,
+ license: licenseVal,
+ nationality: nationalityVal,
+ sort_field: sortF,
+ sort_order: sortO
+ }, {
+ preserveState: true,
+ preserveScroll: true,
+ replace: true
+ });
+ };
+
+ useEffect(() => {
+ const delayDebounceFn = setTimeout(() => {
+ if (searchTerm !== (filters.search || '')) {
+ triggerSearch(searchTerm, statusFilter, verificationFilter, licenseFilter, nationalityFilter, sortField, sortOrder);
+ }
+ }, 500);
+
+ return () => clearTimeout(delayDebounceFn);
+ }, [searchTerm]);
+
+ const handleFilterChange = (status, verification, license, nationality) => {
+ setStatusFilter(status);
+ setVerificationFilter(verification);
+ setLicenseFilter(license);
+ setNationalityFilter(nationality);
+ triggerSearch(searchTerm, status, verification, license, nationality, sortField, sortOrder);
+ };
+
+ const handleSort = (field) => {
+ const order = sortField === field && sortOrder === 'desc' ? 'asc' : 'desc';
+ setSortField(field);
+ setSortOrder(order);
+ triggerSearch(searchTerm, statusFilter, verificationFilter, licenseFilter, nationalityFilter, field, order);
+ };
+
+ const handleApprove = (id) => {
+ router.post(`/admin/charity-organizations/${id}/verify`, {}, {
+ onSuccess: () => {
+ setIsDetailsOpen(false);
+ }
+ });
+ };
+
+ const handleSuspend = (id) => {
+ router.post(`/admin/charity-organizations/${id}/suspend`, {}, {
+ onSuccess: () => {
+ setIsDetailsOpen(false);
+ }
+ });
+ };
+
+ const handleActivate = (id) => {
+ router.post(`/admin/charity-organizations/${id}/activate`, {}, {
+ onSuccess: () => {
+ setIsDetailsOpen(false);
+ }
+ });
+ };
+
+ const handleDelete = (id) => {
+ if (confirm('Are you absolutely sure you want to permanently delete this charity organization?')) {
+ router.delete(`/admin/charity-organizations/${id}`);
+ }
+ };
+
+ const handleEditClick = (sponsor) => {
+ setEditForm({
+ id: sponsor.id,
+ full_name: sponsor.full_name,
+ organization_name: sponsor.organization_name || '',
+ email: sponsor.email,
+ mobile: sponsor.mobile,
+ city: sponsor.city || 'Dubai',
+ nationality: sponsor.nationality || '',
+ address: sponsor.address || ''
+ });
+ setIsEditOpen(true);
+ };
+
+ const handleEditSubmit = (e) => {
+ e.preventDefault();
+ router.post(`/admin/charity-organizations/${editForm.id}/update`, editForm, {
+ onSuccess: () => {
+ setIsEditOpen(false);
+ }
+ });
+ };
+
+ const handleExport = () => {
+ const params = new URLSearchParams({
+ search: searchTerm,
+ status: statusFilter,
+ verification: verificationFilter,
+ license: licenseFilter,
+ nationality: nationalityFilter,
+ sort_field: sortField,
+ sort_order: sortOrder
+ });
+ window.location.href = `/admin/charity-organizations/export?${params.toString()}`;
+ };
+
+ const viewDetails = (sponsor) => {
+ setSelectedSponsor(sponsor);
+ setIsDetailsOpen(true);
+ };
+
+ return (
+
+
+
+
+ {/* Statistics Ribbons */}
+
+
+
+ Total Charities
+
{stats.total || 0}
+
+
+
+
+
+
+
+ Verified
+
+ {stats.verified || 0}
+
+
+
+
+
+
+
+
+ Suspended
+
+ {stats.suspended || 0}
+
+
+
+
+
+
+
+
+ {/* Search and Filters */}
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Table */}
+
+
+
+
+ handleSort('organization_name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none">
+ Organization {sortField === 'organization_name' && (sortOrder === 'desc' ? '↓' : '↑')}
+
+ handleSort('full_name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
+ Representative {sortField === 'full_name' && (sortOrder === 'desc' ? '↓' : '↑')}
+
+ handleSort('city')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
+ Location {sortField === 'city' && (sortOrder === 'desc' ? '↓' : '↑')}
+
+ Verification
+ License Expiry
+ Status
+ Action
+
+
+
+ {sponsors.data.length > 0 ? (
+ sponsors.data.map((sponsor) => (
+
+
+
+
+
+
+
+ {sponsor.organization_name || 'N/A'}
+ ID: ORG-{sponsor.id}
+
+
+
+
+
+
{sponsor.full_name}
+
{sponsor.email}
+
+
+
+
+
+ {sponsor.city || 'Dubai'}
+
+
+
+
+ {sponsor.is_verified ? 'Verified' : 'Pending'}
+
+
+
+
+ {sponsor.license_expiry ? new Date(sponsor.license_expiry).toLocaleDateString() : 'N/A'}
+
+
+
+
+
+ {sponsor.status}
+
+
+
+
+
+
+
+
+
+
+
+ Quick Actions
+
+ handleEditClick(sponsor)}
+ >
+
+
+ Edit Details
+
+
+ {sponsor.status === 'active' ? (
+ handleSuspend(sponsor.id)}
+ className="p-3 rounded-lg focus:bg-rose-50 cursor-pointer text-rose-600 font-bold"
+ >
+
+
+ Suspend Org
+
+
+ ) : (
+ handleActivate(sponsor.id)}
+ className="p-3 rounded-lg focus:bg-emerald-50 cursor-pointer text-emerald-600 font-bold"
+ >
+
+
+ Activate Org
+
+
+ )}
+
+ handleDelete(sponsor.id)}
+ className="p-3 rounded-lg focus:bg-red-50 cursor-pointer text-red-600 font-bold"
+ >
+
+
+ Delete Org
+
+
+
+
+
+
+
+ ))
+ ) : (
+
+
+ No charity organizations found.
+
+
+ )}
+
+
+
+ {/* Pagination */}
+
+
+ {sponsors.total > 0 ? (
+ `Showing ${sponsors.from || 0} to ${sponsors.to || 0} of ${sponsors.total} organizations`
+ ) : (
+ 'No charity organizations found'
+ )}
+
+ {sponsors.last_page > 1 && (
+
+
+ Previous
+
+
+ Next
+
+
+ )}
+
+
+
+
+ {/* Details Modal */}
+
+
+ {/* Edit Dialog */}
+
+
+ );
+}
diff --git a/resources/js/Pages/Admin/Employers/Index.jsx b/resources/js/Pages/Admin/Employers/Index.jsx
index 2049ac4..1547bd3 100644
--- a/resources/js/Pages/Admin/Employers/Index.jsx
+++ b/resources/js/Pages/Admin/Employers/Index.jsx
@@ -23,7 +23,8 @@ import {
Trash2,
Check,
Phone,
- ShieldCheck
+ ShieldCheck,
+ Eye
} from 'lucide-react';
import {
Table,
@@ -49,26 +50,37 @@ import {
} from "@/components/ui/dialog";
import { Badge } from '@/components/ui/badge';
-export default function SponsorsIndex({ sponsors, filters = {} }) {
+export default function EmployersIndex({ employers, filters = {} }) {
const [searchTerm, setSearchTerm] = useState(filters.search || '');
const [statusFilter, setStatusFilter] = useState(filters.status || 'All');
const [locationFilter, setLocationFilter] = useState(filters.location || 'All');
const [sortField, setSortField] = useState(filters.sort_field || 'created_at');
const [sortOrder, setSortOrder] = useState(filters.sort_order || 'desc');
- const [selectedSponsor, setSelectedSponsor] = useState(null);
+ const formatDate = (dateStr) => {
+ if (!dateStr) return 'N/A';
+ try {
+ return new Date(dateStr).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ });
+ } catch (e) {
+ return dateStr;
+ }
+ };
+
+ const [selectedEmployer, setSelectedEmployer] = useState(null);
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
const [isEditOpen, setIsEditOpen] = useState(false);
const [editForm, setEditForm] = useState({
id: '',
- full_name: '',
+ name: '',
email: '',
- mobile: '',
- city: '',
- nationality: '',
- address: '',
- subscription_plan: 'premium',
- subscription_status: 'active'
+ company_name: '',
+ phone: '',
+ country: '',
+ subscription_status: 'none'
});
// Handle search/filter queries
@@ -134,22 +146,20 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
};
const handleDelete = (id) => {
- if (confirm('Are you absolutely sure you want to permanently delete this sponsor?')) {
+ if (confirm('Are you absolutely sure you want to permanently delete this employer?')) {
router.delete(`/admin/employers/${id}`);
}
};
- const handleEditClick = (sponsor) => {
+ const handleEditClick = (employer) => {
setEditForm({
- id: sponsor.id,
- full_name: sponsor.full_name,
- email: sponsor.email,
- mobile: sponsor.mobile,
- city: sponsor.city || 'Dubai',
- nationality: sponsor.nationality || '',
- address: sponsor.address || '',
- subscription_plan: sponsor.subscription_plan || 'premium',
- subscription_status: sponsor.subscription_status || 'active'
+ id: employer.id,
+ name: employer.name,
+ email: employer.email,
+ company_name: employer.employer_profile?.company_name || '',
+ phone: employer.employer_profile?.phone || '',
+ country: employer.employer_profile?.country || 'UAE',
+ subscription_status: employer.subscription_status || 'none'
});
setIsEditOpen(true);
};
@@ -174,8 +184,8 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
window.location.href = `/admin/employers/export?${params.toString()}`;
};
- const viewDetails = (sponsor) => {
- setSelectedSponsor(sponsor);
+ const viewDetails = (employer) => {
+ setSelectedEmployer(employer);
setIsDetailsOpen(true);
};
@@ -189,7 +199,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
Total Employers
-
{sponsors.total || 0}
+ {employers.total || 0}
@@ -199,7 +209,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
Active Plans
- {sponsors.data.filter(s => s.subscription_status === 'active').length}
+ {employers.data.filter(e => e.subscription_status === 'active').length}
@@ -210,7 +220,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
Pending Approval
- {sponsors.data.filter(s => !s.is_verified).length}
+ {employers.data.filter(e => e.employer_profile?.verification_status !== 'approved').length}
@@ -221,7 +231,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
Suspended Accounts
- {sponsors.data.filter(s => s.status === 'suspended').length}
+ {employers.data.filter(e => e.status === 'suspended').length}
@@ -237,7 +247,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
setSearchTerm(e.target.value)}
@@ -252,20 +262,8 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
>
-
-
-
@@ -279,17 +277,15 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
- {/* Dynamic Employers Table */}
+ {/* Table */}
- handleSort('full_name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none">
- Employer {sortField === 'full_name' && (sortOrder === 'desc' ? '↓' : '↑')}
-
- handleSort('city')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
- Location {sortField === 'city' && (sortOrder === 'desc' ? '↓' : '↑')}
+ handleSort('name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none">
+ Employer {sortField === 'name' && (sortOrder === 'desc' ? '↓' : '↑')}
+ Company Name
Subscription Plan
Verification Status
handleSort('status')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
@@ -299,77 +295,54 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
- {sponsors.data.length > 0 ? (
- sponsors.data.map((sponsor) => (
-
+ {employers.data.length > 0 ? (
+ employers.data.map((emp) => (
+
- {sponsor.full_name.charAt(0)}
+ {emp.name?.charAt(0)}
-
{sponsor.full_name}
-
{sponsor.email}
-
{sponsor.mobile}
+
{emp.name}
+
{emp.email}
-
-
-
{sponsor.city || 'Dubai'}
+
+ {emp.employer_profile?.company_name || 'N/A'}
-
-
- {sponsor.subscription_plan ? `${sponsor.subscription_plan} Pass` : 'No Plan'}
-
-
- Status: {sponsor.subscription_status.toUpperCase()}
-
-
+
+ {emp.subscription_status || 'none'}
+
-
- {sponsor.is_verified ? (
- <>
-
- OTP Verified
- >
- ) : (
- <>
-
- Unverified
- >
- )}
-
+
+ {emp.employer_profile?.verification_status || 'pending'}
+
-
- {sponsor.status === 'active' && }
- {sponsor.status === 'inactive' && }
- {sponsor.status === 'suspended' && }
-
- {sponsor.status}
-
-
+
+ {emp.status}
+
@@ -382,24 +355,24 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
handleEditClick(sponsor)}
+ onClick={() => handleEditClick(emp)}
>
Edit Employer
- {!sponsor.is_verified && (
- handleApprove(sponsor.id)}>
+ {emp.employer_profile?.verification_status !== 'approved' && (
+ handleApprove(emp.id)}>
Verify Employer
)}
- {sponsor.status === 'active' ? (
+ {emp.status === 'active' ? (
handleSuspend(sponsor.id)}
+ onClick={() => handleSuspend(emp.id)}
className="p-3 rounded-lg focus:bg-rose-50 cursor-pointer text-rose-600 font-bold"
>
@@ -409,7 +382,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
) : (
handleActivate(sponsor.id)}
+ onClick={() => handleActivate(emp.id)}
className="p-3 rounded-lg focus:bg-emerald-50 cursor-pointer text-emerald-600 font-bold"
>
@@ -420,8 +393,8 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
)}
handleDelete(sponsor.id)}
- className="p-3 rounded-lg focus:bg-red-50 cursor-pointer text-red-600 font-bold animate-pulse"
+ onClick={() => handleDelete(emp.id)}
+ className="p-3 rounded-lg focus:bg-red-50 cursor-pointer text-red-600 font-bold"
>
@@ -437,7 +410,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
) : (
- No employers match the active search/filters criteria.
+ No employers found.
)}
@@ -447,30 +420,30 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
{/* Pagination */}
- {sponsors.total > 0 ? (
- `Showing ${sponsors.from || 0} to ${sponsors.to || 0} of ${sponsors.total} employers`
+ {employers.total > 0 ? (
+ `Showing ${employers.from || 0} to ${employers.to || 0} of ${employers.total} employers`
) : (
'No employers found'
)}
- {sponsors.last_page > 1 && (
+ {employers.last_page > 1 && (
Previous
Next
@@ -481,142 +454,127 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
- {/* Advanced Employer Detail Modal */}
+ {/* Employer Detail Modal */}