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.