From 66eb284f3be1e4feb840b2147a3fbc6fe980199c Mon Sep 17 00:00:00 2001 From: mohanmd Date: Fri, 5 Jun 2026 15:02:41 +0530 Subject: [PATCH] safety report module api --- .../Controllers/Admin/WorkerController.php | 46 +- .../Api/EmployerProfileController.php | 4 + app/Http/Controllers/Api/ReportController.php | 304 ++++++++ .../Controllers/Api/WorkerAuthController.php | 4 +- .../Api/WorkerProfileController.php | 2 +- .../Employer/ProfileController.php | 33 +- .../Controllers/Employer/WorkerController.php | 34 +- app/Models/EmployerProfile.php | 4 +- app/Models/ReportReason.php | 13 + app/Models/Worker.php | 6 + ...ation_and_live_fields_to_workers_table.php | 32 + ..._05_130704_add_gender_to_workers_table.php | 28 + ..._05_140000_create_report_reasons_table.php | 40 + ...40500_add_type_to_report_reasons_table.php | 28 + ..._ocr_fields_to_employer_profiles_table.php | 23 + database/seeders/EmployerProfileSeeder.php | 4 + database/seeders/WorkerSeeder.php | 56 ++ public/swagger.json | 306 +++++++- resources/js/Layouts/AdminLayout.jsx | 12 +- resources/js/Pages/Admin/Analytics/Index.jsx | 6 +- resources/js/Pages/Admin/Dashboard.jsx | 6 +- resources/js/Pages/Admin/Disputes/Index.jsx | 8 +- resources/js/Pages/Admin/Employers/Index.jsx | 46 +- .../Pages/Admin/MasterData/ReportReasons.jsx | 298 ++++++++ .../js/Pages/Admin/Notifications/Index.jsx | 4 +- resources/js/Pages/Admin/Payments/Index.jsx | 4 +- resources/js/Pages/Admin/Safety/Index.jsx | 86 +-- resources/js/Pages/Admin/Workers/Index.jsx | 692 +++++++++++++----- resources/js/Pages/Employer/Profile.jsx | 98 ++- resources/js/Pages/Employer/Workers/Show.jsx | 41 +- routes/api.php | 8 + routes/web.php | 51 ++ tests/Feature/EmployerProfileWebTest.php | 88 +++ tests/Feature/ExampleTest.php | 10 + tests/Feature/ReportApiTest.php | 323 ++++++++ vite.config.js | 2 +- 36 files changed, 2338 insertions(+), 412 deletions(-) create mode 100644 app/Http/Controllers/Api/ReportController.php create mode 100644 app/Models/ReportReason.php create mode 100644 database/migrations/2026_06_05_124802_add_location_and_live_fields_to_workers_table.php create mode 100644 database/migrations/2026_06_05_130704_add_gender_to_workers_table.php create mode 100644 database/migrations/2026_06_05_140000_create_report_reasons_table.php create mode 100644 database/migrations/2026_06_05_140500_add_type_to_report_reasons_table.php create mode 100644 database/migrations/2026_06_05_141000_add_ocr_fields_to_employer_profiles_table.php create mode 100644 resources/js/Pages/Admin/MasterData/ReportReasons.jsx create mode 100644 tests/Feature/EmployerProfileWebTest.php create mode 100644 tests/Feature/ReportApiTest.php diff --git a/app/Http/Controllers/Admin/WorkerController.php b/app/Http/Controllers/Admin/WorkerController.php index 5a38740..e5f8b79 100644 --- a/app/Http/Controllers/Admin/WorkerController.php +++ b/app/Http/Controllers/Admin/WorkerController.php @@ -13,20 +13,46 @@ class WorkerController extends Controller */ public function index() { - $workers = \App\Models\Worker::with('category') + $workers = \App\Models\Worker::with(['category', 'skills']) ->latest() ->get() ->map(function ($worker) { + // Map languages from database comma-separated string + $langs = $worker->language ? array_map('trim', explode(',', $worker->language)) : ['English']; + + // Map skills dynamically from database + $mappedSkills = $worker->skills->pluck('name')->toArray(); + if (empty($mappedSkills)) { + $mappedSkills = ['cooking', 'cleaning']; + } + + // Preferred job types: full-time / part-time / live-in / live-out + $jobTypes = ['full-time', 'part-time', 'live-in', 'live-out']; + $preferredJobType = $jobTypes[$worker->id % 4]; + return [ 'id' => $worker->id, 'name' => $worker->name, 'email' => $worker->email, + 'phone' => $worker->phone, + 'gender' => $worker->gender ?? 'Female', + 'language' => $worker->language ?? 'English', + 'languages' => $langs, 'nationality' => $worker->nationality, + 'country' => $worker->country, + 'city' => $worker->city, + 'area' => $worker->area, + 'preferred_location' => $worker->preferred_location, + 'live_in_out' => $worker->live_in_out ?? 'Live-in', 'category' => $worker->category ? $worker->category->name : 'N/A', 'experience' => $worker->experience, + 'salary' => (int)$worker->salary, + 'skills' => $mappedSkills, + 'preferred_job_type' => $preferredJobType, 'status' => $worker->status, 'availability' => $worker->availability, 'verified' => (bool)$worker->verified, + 'bio' => $worker->bio, 'joined_at' => $worker->created_at ? $worker->created_at->format('Y-m-d') : 'N/A', ]; }); @@ -94,8 +120,17 @@ public function updateProfile(Request $request, $id) { $validated = $request->validate([ 'name' => 'required|string', + 'phone' => 'required|string', + 'gender' => 'nullable|string', + 'language' => 'nullable|string', + 'country' => 'nullable|string', + 'city' => 'nullable|string', + 'area' => 'nullable|string', + 'preferred_location' => 'nullable|string', + 'live_in_out' => 'nullable|string', 'category' => 'required|string', 'experience' => 'required|string', + 'salary' => 'nullable|numeric', 'bio' => 'nullable|string' ]); @@ -107,7 +142,16 @@ public function updateProfile(Request $request, $id) } $worker->name = $validated['name']; + $worker->phone = $validated['phone']; + $worker->gender = $validated['gender'] ?? $worker->gender; + $worker->language = $validated['language'] ?? $worker->language; + $worker->country = $validated['country'] ?? $worker->country; + $worker->city = $validated['city'] ?? $worker->city; + $worker->area = $validated['area'] ?? $worker->area; + $worker->preferred_location = $validated['preferred_location'] ?? $worker->preferred_location; + $worker->live_in_out = $validated['live_in_out'] ?? $worker->live_in_out; $worker->experience = $validated['experience']; + $worker->salary = $validated['salary'] ?? $worker->salary; $worker->bio = $validated['bio'] ?? $worker->bio; $worker->save(); diff --git a/app/Http/Controllers/Api/EmployerProfileController.php b/app/Http/Controllers/Api/EmployerProfileController.php index 094a191..5e58630 100644 --- a/app/Http/Controllers/Api/EmployerProfileController.php +++ b/app/Http/Controllers/Api/EmployerProfileController.php @@ -42,6 +42,8 @@ public function getProfile(Request $request) 'language' => $profile->language ?? 'English', 'notifications' => (bool)($profile->notifications ?? true), 'verification_status' => $profile->verification_status ?? 'approved', + 'emirates_id_number' => $profile->emirates_id_number, + 'emirates_id_expiry' => $profile->emirates_id_expiry, ]; return response()->json([ @@ -160,6 +162,8 @@ public function updateProfile(Request $request) 'language' => $profile->language, 'notifications' => (bool)$profile->notifications, 'verification_status' => $profile->verification_status ?? 'approved', + 'emirates_id_number' => $profile->emirates_id_number, + 'emirates_id_expiry' => $profile->emirates_id_expiry, ]; return response()->json([ diff --git a/app/Http/Controllers/Api/ReportController.php b/app/Http/Controllers/Api/ReportController.php new file mode 100644 index 0000000..79b4e96 --- /dev/null +++ b/app/Http/Controllers/Api/ReportController.php @@ -0,0 +1,304 @@ +attributes->get('worker'); + + $validator = Validator::make($request->all(), [ + 'type' => 'required|in:Chat,Review', + 'item_id' => 'required', + 'reason' => 'required|string|max:255', + 'description' => 'nullable|string|max:2000', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + try { + $reportedName = ''; + $reportedRole = 'Sponsor'; + $reportedAvatar = null; + + if ($request->type === 'Chat') { + $conversation = Conversation::where('id', $request->item_id) + ->where('worker_id', $worker->id) + ->first(); + + if (!$conversation) { + return response()->json([ + 'success' => false, + 'message' => 'Conversation not found or unauthorized.' + ], 404); + } + + $employer = $conversation->employer; + if (!$employer) { + return response()->json([ + 'success' => false, + 'message' => 'Employer associated with the conversation not found.' + ], 404); + } + + $reportedName = $employer->name; + } else { + // Review + $review = Review::where('id', $request->item_id) + ->where('worker_id', $worker->id) + ->first(); + + if (!$review) { + return response()->json([ + 'success' => false, + 'message' => 'Review not found or unauthorized.' + ], 404); + } + + $employer = User::find($review->employer_id); + if (!$employer) { + return response()->json([ + 'success' => false, + 'message' => 'Employer associated with the review not found.' + ], 404); + } + + $reportedName = $employer->name; + } + + $reportId = 'REP-' . Str::upper(Str::random(8)); + + // Insert to moderation_reports + DB::table('moderation_reports')->insert([ + 'id' => $reportId, + 'type' => $request->type, + 'reported_user_name' => $reportedName, + 'reported_user_role' => $reportedRole, + 'reported_user_avatar' => $reportedAvatar, + 'reported_by_name' => $worker->name, + 'reported_by_role' => 'Worker', + 'reported_by_avatar' => null, + 'reason' => $request->reason, + 'priority' => 'Medium', + 'status' => 'Pending', + 'description' => $request->description, + 'reported_at' => now(), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // Add Audit Log + DB::table('audit_logs')->insert([ + 'category' => 'user_activity', + 'user' => $worker->name . ' (Worker)', + 'action' => 'Submitted Safety Report ' . $reportId . ' on ' . $reportedName . ' (Type: ' . $request->type . ', Reason: ' . $request->reason . ')', + 'ip_address' => $request->ip() ?: '127.0.0.1', + 'created_at' => now(), + 'updated_at' => now() + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Report submitted successfully. Our safety team will review it shortly.', + 'report_id' => $reportId + ], 201); + + } catch (\Exception $e) { + logger()->error('Worker API Report Submission Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while submitting the report.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * Submit a report from an employer. + * POST /api/employers/report + */ + public function reportFromEmployer(Request $request) + { + /** @var User $employer */ + $employer = $request->attributes->get('employer'); + + $validator = Validator::make($request->all(), [ + 'type' => 'required|in:Chat,Review', + 'item_id' => 'required', + 'reason' => 'required|string|max:255', + 'description' => 'nullable|string|max:2000', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + try { + $reportedName = ''; + $reportedRole = 'Worker'; + $reportedAvatar = null; + + if ($request->type === 'Chat') { + $conversation = Conversation::where('id', $request->item_id) + ->where('employer_id', $employer->id) + ->first(); + + if (!$conversation) { + return response()->json([ + 'success' => false, + 'message' => 'Conversation not found or unauthorized.' + ], 404); + } + + $worker = $conversation->worker; + if (!$worker) { + return response()->json([ + 'success' => false, + 'message' => 'Worker associated with the conversation not found.' + ], 404); + } + + $reportedName = $worker->name; + } else { + // Review + $review = Review::where('id', $request->item_id) + ->where('employer_id', $employer->id) + ->first(); + + if (!$review) { + return response()->json([ + 'success' => false, + 'message' => 'Review not found or unauthorized.' + ], 404); + } + + $worker = Worker::find($review->worker_id); + if (!$worker) { + return response()->json([ + 'success' => false, + 'message' => 'Worker associated with the review not found.' + ], 404); + } + + $reportedName = $worker->name; + } + + $reportId = 'REP-' . Str::upper(Str::random(8)); + + // Insert to moderation_reports + DB::table('moderation_reports')->insert([ + 'id' => $reportId, + 'type' => $request->type, + 'reported_user_name' => $reportedName, + 'reported_user_role' => $reportedRole, + 'reported_user_avatar' => $reportedAvatar, + 'reported_by_name' => $employer->name, + 'reported_by_role' => 'Sponsor', + 'reported_by_avatar' => null, + 'reason' => $request->reason, + 'priority' => 'Medium', + 'status' => 'Pending', + 'description' => $request->description, + 'reported_at' => now(), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // Add Audit Log + DB::table('audit_logs')->insert([ + 'category' => 'user_activity', + 'user' => $employer->name . ' (Sponsor)', + 'action' => 'Submitted Safety Report ' . $reportId . ' on ' . $reportedName . ' (Type: ' . $request->type . ', Reason: ' . $request->reason . ')', + 'ip_address' => $request->ip() ?: '127.0.0.1', + 'created_at' => now(), + 'updated_at' => now() + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Report submitted successfully. Our safety team will review it shortly.', + 'report_id' => $reportId + ], 201); + + } catch (\Exception $e) { + logger()->error('Employer API Report Submission Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while submitting the report.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * Get active report reasons for workers. + * GET /api/workers/report-reasons + */ + public function getReasonsForWorker(Request $request) + { + $type = $request->query('type'); // Chat or Review + + $query = DB::table('report_reasons')->where('status', 'Active'); + + if ($type && in_array($type, ['Chat', 'Review'])) { + $query->whereIn('type', [$type, 'Both']); + } + + $reasons = $query->orderBy('id', 'asc')->get(['id', 'reason', 'type']); + + return response()->json([ + 'success' => true, + 'reasons' => $reasons + ], 200); + } + + /** + * Get active report reasons for employers. + * GET /api/employers/report-reasons + */ + public function getReasonsForEmployer(Request $request) + { + $type = $request->query('type'); // Chat or Review + + $query = DB::table('report_reasons')->where('status', 'Active'); + + if ($type && in_array($type, ['Chat', 'Review'])) { + $query->whereIn('type', [$type, 'Both']); + } + + $reasons = $query->orderBy('id', 'asc')->get(['id', 'reason', 'type']); + + return response()->json([ + 'success' => true, + 'reasons' => $reasons + ], 200); + } +} diff --git a/app/Http/Controllers/Api/WorkerAuthController.php b/app/Http/Controllers/Api/WorkerAuthController.php index 8a1bd9c..40a76d1 100644 --- a/app/Http/Controllers/Api/WorkerAuthController.php +++ b/app/Http/Controllers/Api/WorkerAuthController.php @@ -174,7 +174,7 @@ public function setupProfile(Request $request) 'email' => 'required_without:phone|nullable|email|max:255', 'name' => 'required|string|max:255', 'nationality' => 'required|string|max:100', - 'language' => 'nullable|string|in:HI,SW,TL,TA', + 'language' => 'nullable|string', 'skills' => 'nullable|array', 'skills.*' => 'integer|exists:skills,id', ]); @@ -288,7 +288,7 @@ public function register(Request $request) 'passport_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240', 'skills' => 'nullable', 'visa_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240', - 'language' => 'nullable|string|in:HI,SW,TL,TA', + 'language' => 'nullable|string', 'nationality' => 'nullable|string|max:100', 'age' => 'nullable|integer', 'experience' => 'nullable|string|max:100', diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index 3dba5cd..fd3f3dc 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -52,7 +52,7 @@ public function updateProfile(Request $request) 'name' => 'nullable|string|max:255', 'age' => 'nullable|integer|min:18|max:70', 'nationality' => 'nullable|string|max:100', - 'language' => 'nullable|string|in:HI,SW,TL,TA', + 'language' => 'nullable|string', 'salary' => 'nullable|numeric|min:0', 'availability' => 'nullable|string|max:100', 'experience' => 'nullable|string|max:100', diff --git a/app/Http/Controllers/Employer/ProfileController.php b/app/Http/Controllers/Employer/ProfileController.php index fcbbb2b..27344a6 100644 --- a/app/Http/Controllers/Employer/ProfileController.php +++ b/app/Http/Controllers/Employer/ProfileController.php @@ -68,6 +68,8 @@ public function index(Request $request) 'emirates_id_front' => $profile->emirates_id_front, 'emirates_id_back' => $profile->emirates_id_back, 'verification_status' => $profile->verification_status ?? 'pending', + 'emirates_id_number' => $profile->emirates_id_number, + 'emirates_id_expiry' => $profile->emirates_id_expiry, ]; return Inertia::render('Employer/Profile', [ @@ -141,20 +143,37 @@ public function update(Request $request) $profile->accommodation = $request->accommodation; $profile->district = $request->district; - // Document uploads + // Document uploads with OCR extraction and PDPL compliance secure purge + $uploaded = false; if ($request->hasFile('emirates_id_front')) { $file = $request->file('emirates_id_front'); $fileName = time() . '_front_' . $file->getClientOriginalName(); - $path = $file->storeAs('uploads/employer', $fileName, 'public'); - $profile->emirates_id_front = $path; - $profile->verification_status = 'pending'; // Reset verification to pending upon upload + $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); + } + $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('uploads/employer', $fileName, 'public'); - $profile->emirates_id_back = $path; - $profile->verification_status = 'pending'; // Reset verification to pending upon upload + $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); + } + $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->verification_status = 'approved'; } $profile->save(); diff --git a/app/Http/Controllers/Employer/WorkerController.php b/app/Http/Controllers/Employer/WorkerController.php index 0839611..fe1d9b9 100644 --- a/app/Http/Controllers/Employer/WorkerController.php +++ b/app/Http/Controllers/Employer/WorkerController.php @@ -57,8 +57,8 @@ public function index(Request $request) return null; } - // Map languages with country names - $langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']); + // Map languages from database comma-separated string + $langs = $w->language ? array_map('trim', explode(',', $w->language)) : ['English']; // Preferred job types: full-time / part-time / live-in / live-out $jobTypes = ['full-time', 'part-time', 'live-in', 'live-out']; @@ -67,12 +67,11 @@ public function index(Request $request) // Emirates ID verification status (now dynamic passport status) $emiratesIdStatus = $w->emirates_id_status; - // Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening - $skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening']; - $mappedSkills = [ - $skillsList[$w->id % 6], - $skillsList[($w->id + 2) % 6] - ]; + // Map skills dynamically from database + $mappedSkills = $w->skills->pluck('name')->toArray(); + if (empty($mappedSkills)) { + $mappedSkills = ['cooking', 'cleaning']; + } // Visa status $visaStatus = $w->visa_status; @@ -92,6 +91,8 @@ public function index(Request $request) return [ 'id' => $w->id, 'name' => $w->name, + 'phone' => $w->phone, + 'gender' => $w->gender ?? 'Female', 'nationality' => $w->nationality, 'photo' => $photo, 'emirates_id_status' => $emiratesIdStatus, @@ -106,6 +107,7 @@ public function index(Request $request) 'age' => $w->age, 'verified' => (bool) $w->verified, 'preferred_job_type' => $preferredJobType, + 'live_in_out' => $w->live_in_out ?? 'Live-in', 'bio' => $w->bio, 'rating' => $rating, 'reviews_count' => $reviewsCount, @@ -153,18 +155,19 @@ public function show($id) ); } - $langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']); + $langs = $w->language ? array_map('trim', explode(',', $w->language)) : ['English']; + // Preferred job types: full-time / part-time / live-in / live-out $jobTypes = ['full-time', 'part-time', 'live-in', 'live-out']; $preferredJobType = $jobTypes[$w->id % 4]; $emiratesIdStatus = $w->emirates_id_status; - $skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening']; - $mappedSkills = [ - $skillsList[$w->id % 6], - $skillsList[($w->id + 2) % 6] - ]; + // Map skills dynamically from database + $mappedSkills = $w->skills->pluck('name')->toArray(); + if (empty($mappedSkills)) { + $mappedSkills = ['cooking', 'cleaning']; + } $photos = [ 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200', @@ -238,6 +241,8 @@ public function show($id) $worker = [ 'id' => $w->id, 'name' => $w->name, + 'phone' => $w->phone, + 'gender' => $w->gender ?? 'Female', 'nationality' => $w->nationality, 'photo' => $photo, 'emirates_id_status' => $emiratesIdStatus, @@ -253,6 +258,7 @@ public function show($id) 'age' => $w->age, 'verified' => (bool) $w->verified, 'preferred_job_type' => $preferredJobType, + 'live_in_out' => $w->live_in_out ?? 'Live-in', 'bio' => $w->bio, 'rating' => $rating, 'reviews_count' => $reviewsCount, diff --git a/app/Models/EmployerProfile.php b/app/Models/EmployerProfile.php index d709d1f..3fbadef 100644 --- a/app/Models/EmployerProfile.php +++ b/app/Models/EmployerProfile.php @@ -20,6 +20,8 @@ class EmployerProfile extends Model 'nationality', 'family_size', 'accommodation', - 'district' + 'district', + 'emirates_id_number', + 'emirates_id_expiry' ]; } diff --git a/app/Models/ReportReason.php b/app/Models/ReportReason.php new file mode 100644 index 0000000..a19366b --- /dev/null +++ b/app/Models/ReportReason.php @@ -0,0 +1,13 @@ +string('country')->nullable()->after('nationality'); + $table->string('city')->nullable()->after('country'); + $table->string('area')->nullable()->after('city'); + $table->string('preferred_location')->nullable()->after('area'); + $table->string('live_in_out')->nullable()->after('preferred_location'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('workers', function (Blueprint $table) { + $table->dropColumn(['country', 'city', 'area', 'preferred_location', 'live_in_out']); + }); + } +}; diff --git a/database/migrations/2026_06_05_130704_add_gender_to_workers_table.php b/database/migrations/2026_06_05_130704_add_gender_to_workers_table.php new file mode 100644 index 0000000..a04605c --- /dev/null +++ b/database/migrations/2026_06_05_130704_add_gender_to_workers_table.php @@ -0,0 +1,28 @@ +string('gender')->nullable()->after('age'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('workers', function (Blueprint $table) { + $table->dropColumn('gender'); + }); + } +}; diff --git a/database/migrations/2026_06_05_140000_create_report_reasons_table.php b/database/migrations/2026_06_05_140000_create_report_reasons_table.php new file mode 100644 index 0000000..ec99ac2 --- /dev/null +++ b/database/migrations/2026_06_05_140000_create_report_reasons_table.php @@ -0,0 +1,40 @@ +id(); + $table->string('reason'); + $table->string('status')->default('Active'); // Active, Inactive + $table->timestamps(); + }); + + // Insert 10 default report reasons + $reasons = [ + ['reason' => 'Abusive language / Profanity', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Harassment or stalking', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Fraud, scam, or fake profile', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Off-platform payment request', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Spam or promotional messaging', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Inappropriate photos/avatar', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Unprofessional conduct', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Threats or safety concerns', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Hate speech or discrimination', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Impersonation or identity theft', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ]; + + DB::table('report_reasons')->insert($reasons); + } + + public function down(): void + { + Schema::dropIfExists('report_reasons'); + } +}; diff --git a/database/migrations/2026_06_05_140500_add_type_to_report_reasons_table.php b/database/migrations/2026_06_05_140500_add_type_to_report_reasons_table.php new file mode 100644 index 0000000..7fd6a40 --- /dev/null +++ b/database/migrations/2026_06_05_140500_add_type_to_report_reasons_table.php @@ -0,0 +1,28 @@ +string('type')->default('Both'); // Chat, Review, Both + }); + + // Update default reasons to match appropriate scopes + DB::table('report_reasons')->where('reason', 'Harassment or stalking')->update(['type' => 'Chat']); + DB::table('report_reasons')->where('reason', 'Off-platform payment request')->update(['type' => 'Chat']); + DB::table('report_reasons')->where('reason', 'Spam or promotional messaging')->update(['type' => 'Chat']); + } + + public function down(): void + { + Schema::table('report_reasons', function (Blueprint $table) { + $table->dropColumn('type'); + }); + } +}; diff --git a/database/migrations/2026_06_05_141000_add_ocr_fields_to_employer_profiles_table.php b/database/migrations/2026_06_05_141000_add_ocr_fields_to_employer_profiles_table.php new file mode 100644 index 0000000..e27a026 --- /dev/null +++ b/database/migrations/2026_06_05_141000_add_ocr_fields_to_employer_profiles_table.php @@ -0,0 +1,23 @@ +string('emirates_id_number')->nullable(); + $table->date('emirates_id_expiry')->nullable(); + }); + } + + public function down(): void + { + Schema::table('employer_profiles', function (Blueprint $table) { + $table->dropColumn(['emirates_id_number', 'emirates_id_expiry']); + }); + } +}; diff --git a/database/seeders/EmployerProfileSeeder.php b/database/seeders/EmployerProfileSeeder.php index ebf497a..f638b78 100644 --- a/database/seeders/EmployerProfileSeeder.php +++ b/database/seeders/EmployerProfileSeeder.php @@ -28,6 +28,10 @@ public function run(): void 'company_name' => 'Al Mansoor Household', 'phone' => '+971 50 123 4567', 'verification_status' => 'approved', + 'emirates_id_front' => '[DELETED_FOR_PDPL_COMPLIANCE]', + 'emirates_id_back' => '[DELETED_FOR_PDPL_COMPLIANCE]', + 'emirates_id_number' => '784-1990-1234567-8', + 'emirates_id_expiry' => '2028-12-31', 'created_at' => now(), 'updated_at' => now(), ]); diff --git a/database/seeders/WorkerSeeder.php b/database/seeders/WorkerSeeder.php index a10da51..c145370 100644 --- a/database/seeders/WorkerSeeder.php +++ b/database/seeders/WorkerSeeder.php @@ -18,7 +18,14 @@ public function run(): void 'email' => 'john.doe@example.com', 'phone' => '+971 50 111 2222', 'nationality' => 'India', + 'country' => 'United Arab Emirates', + 'city' => 'Dubai', + 'area' => 'Dubai Marina', + 'preferred_location' => 'Dubai Marina', + 'live_in_out' => 'Live-in (Stay with family)', + 'language' => 'English', 'age' => 30, + 'gender' => 'Male', 'salary' => 2500.00, 'availability' => 'Immediate', 'experience' => '5+ Years', @@ -33,7 +40,14 @@ public function run(): void 'email' => 'ali.hassan@example.com', 'phone' => '+971 50 333 4444', 'nationality' => 'Pakistan', + 'country' => 'United Arab Emirates', + 'city' => 'Dubai', + 'area' => 'Al Barsha', + 'preferred_location' => 'Al Barsha', + 'live_in_out' => 'Live-out (External housing)', + 'language' => 'English', 'age' => 28, + 'gender' => 'Male', 'salary' => 2000.00, 'availability' => '1 Month Notice', 'experience' => '3 Years', @@ -48,7 +62,14 @@ public function run(): void 'email' => 'maria.santos@example.com', 'phone' => '+971 50 555 6666', 'nationality' => 'Philippines', + 'country' => 'United Arab Emirates', + 'city' => 'Abu Dhabi', + 'area' => 'Yas Island', + 'preferred_location' => 'Yas Island', + 'live_in_out' => 'Live-in (Stay with family)', + 'language' => 'Tagalog', 'age' => 32, + 'gender' => 'Female', 'salary' => 1800.00, 'availability' => 'Immediate', 'experience' => '5+ Years', @@ -63,7 +84,14 @@ public function run(): void 'email' => 'lakshmi.sharma@example.com', 'phone' => '+971 50 777 8888', 'nationality' => 'India', + 'country' => 'United Arab Emirates', + 'city' => 'Abu Dhabi', + 'area' => 'Khalifa City', + 'preferred_location' => 'Khalifa City', + 'live_in_out' => 'Live-out (External housing)', + 'language' => 'Hindi', 'age' => 38, + 'gender' => 'Female', 'salary' => 1600.00, 'availability' => '2 Weeks', 'experience' => '3-5 Years', @@ -78,7 +106,14 @@ public function run(): void 'email' => 'siti.aminah@example.com', 'phone' => '+971 50 999 0000', 'nationality' => 'Indonesia', + 'country' => 'United Arab Emirates', + 'city' => 'Sharjah', + 'area' => 'Al Nahda', + 'preferred_location' => 'Al Nahda', + 'live_in_out' => 'Live-in (Stay with family)', + 'language' => 'Arabic', 'age' => 29, + 'gender' => 'Female', 'salary' => 1400.00, 'availability' => 'Immediate', 'experience' => '3-5 Years', @@ -93,7 +128,14 @@ public function run(): void 'email' => 'grace.osei@example.com', 'phone' => '+971 50 222 3333', 'nationality' => 'Ghana', + 'country' => 'Saudi Arabia', + 'city' => 'Riyadh', + 'area' => 'Al Olaya', + 'preferred_location' => 'Al Olaya', + 'live_in_out' => 'Live-out (External housing)', + 'language' => 'English', 'age' => 26, + 'gender' => 'Female', 'salary' => 1500.00, 'availability' => '1 Month', 'experience' => '1-2 Years', @@ -108,7 +150,14 @@ public function run(): void 'email' => 'anoma.perera@example.com', 'phone' => '+971 50 444 5555', 'nationality' => 'Sri Lanka', + 'country' => 'Saudi Arabia', + 'city' => 'Jeddah', + 'area' => 'Al Hamra', + 'preferred_location' => 'Al Hamra', + 'live_in_out' => 'Live-in (Stay with family)', + 'language' => 'English', 'age' => 41, + 'gender' => 'Female', 'salary' => 2200.00, 'availability' => 'Immediate', 'experience' => '5+ Years', @@ -123,7 +172,14 @@ public function run(): void 'email' => 'ramesh.thapa@example.com', 'phone' => '+971 50 666 7777', 'nationality' => 'Nepal', + 'country' => 'Saudi Arabia', + 'city' => 'Riyadh', + 'area' => 'Al Yasmin', + 'preferred_location' => 'Al Yasmin', + 'live_in_out' => 'Live-out (External housing)', + 'language' => 'English', 'age' => 36, + 'gender' => 'Male', 'salary' => 2500.00, 'availability' => '2 Weeks', 'experience' => '5+ Years', diff --git a/public/swagger.json b/public/swagger.json index f1a75f4..946ac40 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -84,14 +84,8 @@ }, "language": { "type": "string", - "enum": [ - "HI", - "SW", - "TL", - "TA" - ], - "example": "HI", - "description": "Preferred interface language (HI=Hindi, SW=Swahili, TL=Tagalog, TA=Tamil)." + "example": "English, Arabic", + "description": "Languages spoken by the worker (e.g. 'English, Arabic, Hindi')." }, "nationality": { "type": "string", @@ -305,7 +299,7 @@ }, "name": { "type": "string", - "example": "Hindi (\u0939\u093f\u0928\u094d\u0926\u0940)" + "example": "Hindi (हिन्दी)" } } } @@ -1092,6 +1086,150 @@ } } }, + "/workers/report": { + "post": { + "tags": [ + "Worker/Profile" + ], + "summary": "Submit a Safety/Moderation Report (Worker)", + "description": "Submits a report on a chat conversation or a review. The report is logged in the safety queue and reviewed by administrators.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "type", + "item_id", + "reason" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Chat", + "Review" + ], + "example": "Chat", + "description": "The type of entity being reported (Chat or Review)." + }, + "item_id": { + "type": "string", + "example": "1", + "description": "The ID of the conversation or review being reported." + }, + "reason": { + "type": "string", + "example": "Inappropriate messages", + "description": "The reason for submitting this report." + }, + "description": { + "type": "string", + "example": "The other party sent highly inappropriate and insulting messages.", + "description": "Optional detailed explanation/comments." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Report submitted successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Report submitted successfully. Our safety team will review it shortly." + }, + "report_id": { + "type": "string", + "example": "REP-ABCD1234" + } + } + } + } + } + }, + "404": { + "description": "Conversation or Review not found, or user is not authorized to report it." + }, + "422": { + "description": "Validation error." + } + } + } + }, + "/workers/report-reasons": { + "get": { + "tags": [ + "Worker/Profile" + ], + "summary": "Get Safety Report Reasons (Worker)", + "description": "Returns a list of active safety/moderation report reasons. Can filter reasons by target type (Chat or Review).", + "parameters": [ + { + "name": "type", + "in": "query", + "required": false, + "description": "Optional type to filter reasons (Chat or Review). If not provided, returns all active reasons.", + "schema": { + "type": "string", + "enum": [ + "Chat", + "Review" + ] + } + } + ], + "responses": { + "200": { + "description": "List of report reasons retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "reasons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "reason": { + "type": "string", + "example": "Abusive language / Profanity" + }, + "type": { + "type": "string", + "example": "Both" + } + } + } + } + } + } + } + } + } + } + } + }, "/employers/register": { "post": { "tags": [ @@ -2724,12 +2862,12 @@ } }, "responses": { - "201": { - "description": "Review added successfully." - }, "200": { "description": "Review updated successfully." }, + "201": { + "description": "Review added successfully." + }, "422": { "description": "Validation error." } @@ -2793,6 +2931,150 @@ } } } + }, + "/employers/report": { + "post": { + "tags": [ + "Employer/Reviews" + ], + "summary": "Submit a Safety/Moderation Report (Employer)", + "description": "Submits a report on a chat conversation or a review. The report is logged in the safety queue and reviewed by administrators.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "type", + "item_id", + "reason" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Chat", + "Review" + ], + "example": "Chat", + "description": "The type of entity being reported (Chat or Review)." + }, + "item_id": { + "type": "string", + "example": "1", + "description": "The ID of the conversation or review being reported." + }, + "reason": { + "type": "string", + "example": "Abusive behavior", + "description": "The reason for submitting this report." + }, + "description": { + "type": "string", + "example": "The worker requested payments off-platform and sent abusive messages.", + "description": "Optional detailed explanation/comments." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Report submitted successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Report submitted successfully. Our safety team will review it shortly." + }, + "report_id": { + "type": "string", + "example": "REP-ABCD1234" + } + } + } + } + } + }, + "404": { + "description": "Conversation or Review not found, or user is not authorized to report it." + }, + "422": { + "description": "Validation error." + } + } + } + }, + "/employers/report-reasons": { + "get": { + "tags": [ + "Employer/Reviews" + ], + "summary": "Get Safety Report Reasons (Employer)", + "description": "Returns a list of active safety/moderation report reasons. Can filter reasons by target type (Chat or Review).", + "parameters": [ + { + "name": "type", + "in": "query", + "required": false, + "description": "Optional type to filter reasons (Chat or Review). If not provided, returns all active reasons.", + "schema": { + "type": "string", + "enum": [ + "Chat", + "Review" + ] + } + } + ], + "responses": { + "200": { + "description": "List of report reasons retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "reasons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "reason": { + "type": "string", + "example": "Abusive language / Profanity" + }, + "type": { + "type": "string", + "example": "Both" + } + } + } + } + } + } + } + } + } + } + } } }, "components": { diff --git a/resources/js/Layouts/AdminLayout.jsx b/resources/js/Layouts/AdminLayout.jsx index 04d180d..73ae720 100644 --- a/resources/js/Layouts/AdminLayout.jsx +++ b/resources/js/Layouts/AdminLayout.jsx @@ -17,7 +17,8 @@ import { Scale, BellRing, BarChart3, - History + History, + List } from 'lucide-react'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; @@ -28,18 +29,19 @@ export default function AdminLayout({ children, title = 'Dashboard' }) { const [open, setOpen] = useState(false); const navItems = [ - { name: 'Dashboard Overview', href: '/admin/dashboard', icon: LayoutDashboard }, + { name: 'Dashboard', href: '/admin/dashboard', icon: LayoutDashboard }, { name: 'Worker Profiles', href: '/admin/workers', icon: Users }, - { name: 'Sponsors', href: '/admin/employers', icon: Briefcase }, + { name: 'Employers', href: '/admin/employers', icon: Briefcase }, { name: 'OCR Verification', href: '/admin/workers/verifications', icon: ShieldCheck }, { name: 'Safety & Moderation', href: '/admin/safety', icon: ShieldAlert }, - { name: 'Disputes System', href: '/admin/disputes', icon: Scale }, - { name: 'Payments & Refunds', href: '/admin/payments', icon: BadgeDollarSign }, + { name: 'Help & Support', href: '/admin/disputes', icon: Scale }, + { name: 'Payments', href: '/admin/payments', icon: BadgeDollarSign }, { name: 'Campaigns & Alerts', href: '/admin/notifications', icon: BellRing }, { name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 }, { name: 'System Audit Logs', href: '/admin/audit-logs', icon: History }, { name: 'Announcements', href: '/admin/announcements', icon: Megaphone }, { name: 'Skills', href: '/admin/master-data/skills', icon: Settings }, + { name: 'Reason Master', href: '/admin/master-data/reasons', icon: List }, ]; const getInitials = (name) => { diff --git a/resources/js/Pages/Admin/Analytics/Index.jsx b/resources/js/Pages/Admin/Analytics/Index.jsx index 6f6f7cd..0396822 100644 --- a/resources/js/Pages/Admin/Analytics/Index.jsx +++ b/resources/js/Pages/Admin/Analytics/Index.jsx @@ -61,7 +61,7 @@ export default function AnalyticsHub({

Platform Reports

-

Summary of workers, sponsors, disputes, and payments.

+

Summary of workers, employers, disputes, and payments.

@@ -118,11 +118,11 @@ export default function AnalyticsHub({
- Sponsors + Employers
- Total Sponsors + Total Employers {sponsor_stats.total}
diff --git a/resources/js/Pages/Admin/Dashboard.jsx b/resources/js/Pages/Admin/Dashboard.jsx index bc813ff..c707945 100644 --- a/resources/js/Pages/Admin/Dashboard.jsx +++ b/resources/js/Pages/Admin/Dashboard.jsx @@ -64,8 +64,8 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip }; return ( - - + + {/* Welcome banner */}
@@ -166,7 +166,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip

- +{stats?.new_employers_this_week || 0} Sponsors Added This Week + +{stats?.new_employers_this_week || 0} Employers Added This Week

diff --git a/resources/js/Pages/Admin/Disputes/Index.jsx b/resources/js/Pages/Admin/Disputes/Index.jsx index f9dbe6c..65210f0 100644 --- a/resources/js/Pages/Admin/Disputes/Index.jsx +++ b/resources/js/Pages/Admin/Disputes/Index.jsx @@ -98,16 +98,16 @@ export default function DisputesHub({ tickets }) { }); return ( - - + +
{/* Header Row */}
-

Dispute Management

-

Handle complaints and resolve disputes between workers and sponsors.

+

Help & Support

+

Handle complaints and resolve disputes between workers and employers.

- {/* Dynamic Sponsors Table */} + {/* Dynamic Employers Table */}
handleSort('full_name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none"> - Sponsor {sortField === 'full_name' && (sortOrder === 'desc' ? '↓' : '↑')} + 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' ? '↓' : '↑')} @@ -386,14 +386,14 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { >
- Edit Sponsor + Edit Employer
{!sponsor.is_verified && ( handleApprove(sponsor.id)}>
- Verify Sponsor + Verify Employer
)} @@ -404,7 +404,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { >
- Suspend Sponsor + Suspend Employer
) : ( @@ -414,7 +414,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { >
- Activate Sponsor + Activate Employer
)} @@ -425,7 +425,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { >
- Delete Sponsor + Delete Employer
@@ -437,7 +437,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { ) : ( - No sponsors match the active search/filters criteria. + No employers match the active search/filters criteria. )} @@ -448,9 +448,9 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
{sponsors.total > 0 ? ( - `Showing ${sponsors.from || 0} to ${sponsors.to || 0} of ${sponsors.total} sponsors` + `Showing ${sponsors.from || 0} to ${sponsors.to || 0} of ${sponsors.total} employers` ) : ( - 'No sponsors found' + 'No employers found' )} {sponsors.last_page > 1 && ( @@ -481,7 +481,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
- {/* Advanced Sponsor Detail Modal */} + {/* Advanced Employer Detail Modal */}
@@ -502,10 +502,10 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {

- UAE Sponsor Details + UAE Employer Details

- Sponsor ID: SPN-{selectedSponsor?.id} + Employer ID: EMP-{selectedSponsor?.id}
@@ -578,7 +578,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { onClick={() => handleApprove(selectedSponsor.id)} className="px-4 py-2.5 bg-emerald-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md shadow-emerald-600/10 hover:bg-emerald-500 transition-colors" > - Verify Sponsor OTP + Verify Employer OTP ) : ( @@ -593,14 +593,14 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { onClick={() => handleSuspend(selectedSponsor.id)} className="px-4 py-2.5 bg-red-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md shadow-red-600/10 hover:bg-red-700 transition-all" > - Suspend Sponsor + Suspend Employer ) : ( )}
- {/* Sponsor Edit Dialog */} + {/* Employer Edit Dialog */} - Edit Sponsor Details + Edit Employer Details
@@ -725,7 +725,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) { type="submit" className="px-5 py-2.5 bg-[#0F6E56] text-white rounded-xl text-xs font-black uppercase tracking-wider" > - Save Sponsor + Save Employer
diff --git a/resources/js/Pages/Admin/MasterData/ReportReasons.jsx b/resources/js/Pages/Admin/MasterData/ReportReasons.jsx new file mode 100644 index 0000000..a9a8568 --- /dev/null +++ b/resources/js/Pages/Admin/MasterData/ReportReasons.jsx @@ -0,0 +1,298 @@ +import React, { useState } from 'react'; +import { Head, router } from '@inertiajs/react'; +import AdminLayout from '@/Layouts/AdminLayout'; +import { + Plus, + Search, + GripVertical, + Edit2, + Trash2, + Info, + ShieldAlert +} from 'lucide-react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; + +export default function ReportReasons({ reasons }) { + const [searchTerm, setSearchTerm] = useState(''); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [editingReason, setEditingReason] = useState(null); + const [reasonName, setReasonName] = useState(''); + const [reasonStatus, setReasonStatus] = useState('Active'); + const [reasonType, setReasonType] = useState('Both'); + const [error, setError] = useState(''); + + const handleOpenAdd = () => { + setEditingReason(null); + setReasonName(''); + setReasonStatus('Active'); + setReasonType('Both'); + setError(''); + setIsDialogOpen(true); + }; + + const handleOpenEdit = (reason) => { + setEditingReason(reason); + setReasonName(reason.reason); + setReasonStatus(reason.status); + setReasonType(reason.type || 'Both'); + setError(''); + setIsDialogOpen(true); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (!reasonName.trim()) { + setError('Reason description is required.'); + return; + } + + if (editingReason) { + router.post(`/admin/master-data/reasons/${editingReason.id}/update`, { + reason: reasonName.trim(), + status: reasonStatus, + type: reasonType + }, { + onSuccess: () => { + setIsDialogOpen(false); + }, + onError: (err) => { + setError(err.reason || 'Failed to update reason.'); + } + }); + } else { + router.post('/master-data/reasons', { + reason: reasonName.trim(), + type: reasonType + }, { + onSuccess: () => { + setIsDialogOpen(false); + }, + onError: (err) => { + setError(err.reason || 'Failed to add reason.'); + } + }); + } + }; + + const handleDelete = (id) => { + if (confirm('Are you sure you want to delete this report reason?')) { + router.delete(`/admin/master-data/reasons/${id}`); + } + }; + + const filteredReasons = reasons.filter(reason => + reason.reason.toLowerCase().includes(searchTerm.toLowerCase()) || + reason.type.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const getTypeBadgeClass = (type) => { + switch (type) { + case 'Chat': + return 'bg-blue-50 text-blue-700 border border-blue-200'; + case 'Review': + return 'bg-purple-50 text-purple-700 border border-purple-200'; + default: + return 'bg-teal-50 text-teal-700 border border-teal-200'; + } + }; + + return ( + + + +
+ {/* Header Actions */} +
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm font-medium focus:ring-4 focus:ring-teal-500/10 outline-none transition-all" + /> +
+ +
+ + {/* Reasons Card */} +
+
+
+
+ +
+
+

Reason Master

+

Manage safety & moderation report reasons

+
+
+
+ +
+ + + + Report Reason + Applicable Type + Status + Action + + + + {filteredReasons.length > 0 ? ( + filteredReasons.map((reason) => ( + + + + + + {reason.reason} + + + + {reason.type} + + + + + {reason.status} + + + +
+ + +
+
+
+ )) + ) : ( + + + No reasons match your search. + + + )} +
+
+ +
+
+ +

+ Tip: Scoping reasons to "Chat" or "Review" helps filter options on the mobile UI. A reason marked "Both" is displayed for all safety reports. +

+
+
+
+ + + {/* Reason Modal */} + + + + + {editingReason ? 'Edit Reason' : 'New Reason'} + + + +
+
+
+ + setReasonName(e.target.value)} + /> + {error && ( +

{error}

+ )} +
+ +
+ + +
+ + {editingReason && ( +
+ + +
+ )} +
+ + + + + +
+
+
+
+ ); +} diff --git a/resources/js/Pages/Admin/Notifications/Index.jsx b/resources/js/Pages/Admin/Notifications/Index.jsx index 9cda42f..018fafd 100644 --- a/resources/js/Pages/Admin/Notifications/Index.jsx +++ b/resources/js/Pages/Admin/Notifications/Index.jsx @@ -38,7 +38,7 @@ export default function NotificationsCampaigns({ campaigns = [] }) { {/* Simplified Header */}

Send Notification

-

Send quick push or WhatsApp alerts to workers and sponsors.

+

Send quick push or WhatsApp alerts to workers and employers.

@@ -77,7 +77,7 @@ export default function NotificationsCampaigns({ campaigns = [] }) { onChange={e => setRecipients(e.target.value)} > - +
diff --git a/resources/js/Pages/Admin/Payments/Index.jsx b/resources/js/Pages/Admin/Payments/Index.jsx index f754c35..39b9287 100644 --- a/resources/js/Pages/Admin/Payments/Index.jsx +++ b/resources/js/Pages/Admin/Payments/Index.jsx @@ -56,8 +56,8 @@ export default function PaymentsIndex({ stats, payments: initialPayments }) { }; return ( - - + +
{/* Stats Overview */} diff --git a/resources/js/Pages/Admin/Safety/Index.jsx b/resources/js/Pages/Admin/Safety/Index.jsx index cbb33b6..dcd4e53 100644 --- a/resources/js/Pages/Admin/Safety/Index.jsx +++ b/resources/js/Pages/Admin/Safety/Index.jsx @@ -62,11 +62,8 @@ export default function SafetyHub({ reports, rules, moderation_queue }) { const filteredReports = (reports || []).filter(report => { // Tab filter if (activeTab !== 'All Reports') { - if (activeTab === 'Profiles' && report.type !== 'Profile') return false; if (activeTab === 'Chats' && report.type !== 'Chat') return false; if (activeTab === 'Reviews' && report.type !== 'Review') return false; - if (activeTab === 'Images' && report.type !== 'Image') return false; - if (activeTab === 'Other Content' && report.type === 'Profile' || report.type === 'Chat' || report.type === 'Review' || report.type === 'Image') return false; } // Type filter dropdown @@ -90,15 +87,15 @@ export default function SafetyHub({ reports, rules, moderation_queue }) { }); return ( - - + +
{/* Header Row */}
-

Content Moderation

+

Safety & Moderation

Review and take action on reported content, users and reviews.

@@ -176,7 +173,7 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
{/* Tabs list */}
- {['All Reports', 'Profiles', 'Chats', 'Reviews'].map((tab) => ( + {['All Reports', 'Chats', 'Reviews'].map((tab) => (
- {/* Subsections: Verification Queue & Auto-Detection Rules */} -
- {/* Content Verification Queue */} -
-
-
-

Profile Content Verification Queue

-

Bio updates or pictures flagged for manual review

-
- Action Required -
- -
- {moderation_queue.map((item) => ( -
-
- {item.id} • {item.type} - {item.user} - {item.flag} -
-
-
- {item.content.startsWith('http') ? ( - Flagged asset - ) : item.content} -
-
- - -
-
-
- ))} -
-
- - {/* Auto-Detection Rules */} -
-
-
-

Auto-Detection Rules

-

Parameters triggering system automated flags

-
- -
- {rules.map((rule) => ( -
-
- {rule.name} - {rule.status} -
-

{rule.trigger}

-
- ))} -
-
- - -
-
{/* Investigation & Action Dialog Modal */} diff --git a/resources/js/Pages/Admin/Workers/Index.jsx b/resources/js/Pages/Admin/Workers/Index.jsx index 807988f..aaccbd6 100644 --- a/resources/js/Pages/Admin/Workers/Index.jsx +++ b/resources/js/Pages/Admin/Workers/Index.jsx @@ -1,14 +1,14 @@ import React, { useState } from 'react'; import { Head, Link, router } from '@inertiajs/react'; import AdminLayout from '@/Layouts/AdminLayout'; -import { - Users, - Search, - Filter, - MoreHorizontal, - CheckCircle2, - XCircle, - UserCheck, +import { + Users, + Search, + Filter, + MoreHorizontal, + CheckCircle2, + XCircle, + UserCheck, UserX, Mail, Globe, @@ -49,10 +49,22 @@ import { DialogFooter } from "@/components/ui/dialog"; +const locationData = { + 'United Arab Emirates': { + 'Dubai': ['Dubai Marina', 'Downtown Dubai', 'Al Barsha', 'Jumeirah', 'Al Quoz', 'JLT'], + 'Abu Dhabi': ['Yas Island', 'Al Reem Island', 'Khalifa City', 'Al Khalidiyah'], + 'Sharjah': ['Al Nahda', 'Al Majaz', 'Muwaileh', 'Al Khan'] + }, + 'Saudi Arabia': { + 'Riyadh': ['Al Olaya', 'Al Malaz', 'Al Yasmin', 'Al Mursalat'], + 'Jeddah': ['Al Hamra', 'Al Naeem', 'Al Safa', 'Al Khalidiyah'] + } +}; + export default function WorkerManagement({ workers }) { const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); - + // Dialog / Worker selection state const [selectedWorker, setSelectedWorker] = useState(null); const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false); @@ -61,9 +73,16 @@ export default function WorkerManagement({ workers }) { // Form inputs for Profile Moderation const [editForm, setEditForm] = useState({ name: '', + phone: '', + language: '', category: '', experience: '', bio: '', + country: '', + city: '', + area: '', + preferred_location: '', + live_in_out: '', }); const [adminNotes, setAdminNotes] = useState(''); @@ -103,12 +122,21 @@ export default function WorkerManagement({ workers }) { router.post(`/admin/workers/${selectedWorker.id}/update-profile`, editForm, { onSuccess: () => { setIsEditMode(false); - setSelectedWorker({ - ...selectedWorker, - name: editForm.name, - category: editForm.category, - experience: editForm.experience, - bio: editForm.bio + setSelectedWorker({ + ...selectedWorker, + name: editForm.name, + phone: editForm.phone, + gender: editForm.gender, + language: editForm.language, + category: editForm.category, + experience: editForm.experience, + salary: editForm.salary, + bio: editForm.bio, + country: editForm.country, + city: editForm.city, + area: editForm.area, + preferred_location: editForm.preferred_location, + live_in_out: editForm.live_in_out }); } }); @@ -139,9 +167,18 @@ export default function WorkerManagement({ workers }) { setSelectedWorker(fullWorker); setEditForm({ name: fullWorker.name, + phone: fullWorker.phone, + gender: fullWorker.gender || 'Female', + language: fullWorker.language || 'English', category: fullWorker.category, experience: fullWorker.experience, + salary: fullWorker.salary || '', bio: fullWorker.bio, + country: fullWorker.country || '', + city: fullWorker.city || '', + area: fullWorker.area || '', + preferred_location: fullWorker.preferred_location || '', + live_in_out: fullWorker.live_in_out || '', }); setAdminNotes(fullWorker.admin_notes); setIsEditMode(false); @@ -153,8 +190,8 @@ export default function WorkerManagement({ workers }) { availability: w.availability || 'Available Now', verified: w.verified !== undefined ? w.verified : (w.status === 'active') })).filter(worker => { - const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) || - worker.email.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) || + worker.email.toLowerCase().includes(searchTerm.toLowerCase()); const matchesStatus = statusFilter === 'all' || worker.status === statusFilter; return matchesSearch && matchesStatus; }); @@ -175,7 +212,7 @@ export default function WorkerManagement({ workers }) { Export CSV - @@ -189,8 +226,8 @@ export default function WorkerManagement({ workers }) {
- setSearchTerm(e.target.value)} @@ -201,7 +238,7 @@ export default function WorkerManagement({ workers }) {
- +
Lifecycle Filter:
@@ -209,11 +246,10 @@ export default function WorkerManagement({ workers }) { @@ -230,7 +266,8 @@ export default function WorkerManagement({ workers }) { Worker Information Placement Status Verification Status - Category & Exp + Category, Exp & Languages + Location Details Lifecycle Actions @@ -239,115 +276,128 @@ export default function WorkerManagement({ workers }) { {filteredWorkers.length > 0 ? ( filteredWorkers.map((worker) => ( - -
-
- {worker.name.charAt(0)} -
-
-
- {worker.name} - {worker.id === 103 && ( - - Suspicious - - )} -
-
{worker.email}
-
-
-
- - - {worker.availability} - - - -
-
- -
- - {worker.verified ? 'OCR Verified' : 'Pending Verification'} - -
-
- -
-
- {worker.category} -
-
- {worker.nationality} • {worker.experience} -
-
-
- - - {worker.status} - - - -
- - - - - - - Quick Actions - - handleToggleStatus(worker.id, worker.status === 'active' ? 'suspended' : 'active')} - className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-red-600 hover:bg-red-50" - > - {worker.status === 'active' ? ( - <> Suspend Account - ) : ( - <> Activate Account - )} - + +
+
+ {worker.name.charAt(0)} +
+
+
+ {worker.name} + {worker.id === 103 && ( + + Suspicious + + )} +
+
{worker.phone} • {worker.email}
+
+
+
+ + + {worker.availability} + + + +
+
+ +
+ + {worker.verified ? 'OCR Verified' : 'Pending Verification'} + +
+
+ +
+
+ {worker.category} +
+
+ {worker.nationality} • {worker.experience} +
+
+ Lang: {worker.language} +
+
+
+ +
+ {worker.country ? ( + <> +
{worker.city}, {worker.country}
+
{worker.area}
+ + ) : ( + Not set + )} +
+
+ + + {worker.status} + + + +
+ + + + + + + Quick Actions - - - handleAvailabilityOverride(worker.id, 'Available Now')} - className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-slate-700 hover:bg-slate-50" - > - Mark Available - + handleToggleStatus(worker.id, worker.status === 'active' ? 'suspended' : 'active')} + className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-red-600 hover:bg-red-50" + > + {worker.status === 'active' ? ( + <> Suspend Account + ) : ( + <> Activate Account + )} + - handleAvailabilityOverride(worker.id, 'Engaged')} - className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-slate-700 hover:bg-slate-50" - > - Mark Engaged - - - -
-
- + + + handleAvailabilityOverride(worker.id, 'Available Now')} + className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-slate-700 hover:bg-slate-50" + > + Mark Available + + + handleAvailabilityOverride(worker.id, 'Engaged')} + className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-slate-700 hover:bg-slate-50" + > + Mark Engaged + +
+
+
+
+
)) ) : ( - +
No workers found matching your criteria.
@@ -370,7 +420,7 @@ export default function WorkerManagement({ workers }) { )}
- {selectedWorker?.name.charAt(0)} + {selectedWorker?.name ? selectedWorker.name.charAt(0) : ''}

{selectedWorker?.name}

@@ -381,39 +431,276 @@ export default function WorkerManagement({ workers }) { {/* Scrollable details tab */}
- -
-
-
-
- -
{selectedWorker?.experience}
+ {isEditMode ? ( +
+
+
+

Basic Information

+
+
+ + setEditForm({ ...editForm, name: e.target.value })} + className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20" + required + /> +
+
+ + setEditForm({ ...editForm, phone: e.target.value })} + className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20" + required + /> +
+
+ + +
+
+ + setEditForm({ ...editForm, language: e.target.value })} + className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20" + /> +
+
-
- -
{selectedWorker?.category}
-
-
-
-
-

Contact & Parameters

-
-
- - {selectedWorker?.email} -
-
- - {selectedWorker?.phone} -
-
- - Age: {selectedWorker?.age} Years Old +
+

Classification & Experience

+
+
+ + setEditForm({ ...editForm, category: e.target.value })} + className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20" + required + /> +
+
+ + setEditForm({ ...editForm, experience: e.target.value })} + className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20" + required + /> +
+
+ + +
+
+ + setEditForm({ ...editForm, salary: e.target.value })} + className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20" + /> +
-
+ + {/* Dependent Location Dropdowns */} +
+

Preferred Location Settings

+
+
+ + +
+
+ + +
+
+ + +
+
+ {editForm.preferred_location && ( +
+ Saved Location String: {editForm.preferred_location} +
+ )} +
+ +
+ +