Compare commits

..

No commits in common. "25510d1b5e7ab2744455a20890fb5ee36b8a1799" and "48a770343661613de07ce1b2ddaa6d5c6c7f9401" have entirely different histories.

25 changed files with 588 additions and 1835 deletions

View File

@ -559,6 +559,15 @@ public function password(Request $request)
}
});
$tokenToSend = $request->fcm_token ?? ($user ? $user->fcm_token : null) ?? ($sponsor ? $sponsor->fcm_token : null);
if ($tokenToSend) {
\App\Services\FCMService::sendPushNotification(
$tokenToSend,
'Welcome to Migrant',
'Your employer registration has been completed successfully.'
);
}
return response()->json([
'success' => true,
'message' => 'Password created successfully. Registration finalized.',

View File

@ -28,8 +28,8 @@ public function getProfile(Request $request)
if (!$profile) {
$profile = EmployerProfile::create([
'user_id' => $employer->id,
'company_name' => 'Al Mansoor Household',
'phone' => '+971 50 123 4567',
'address' => 'Dubai, UAE',
'verification_status' => 'approved',
]);
}
@ -37,23 +37,13 @@ public function getProfile(Request $request)
$employerProfile = [
'name' => $employer->name,
'email' => $employer->email,
'company_name' => $profile->company_name,
'phone' => $profile->phone,
'address' => $profile->address,
'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,
'id_number' => $profile->emirates_id_number,
'card_number' => $profile->emirates_id_card_number,
'full_name' => $profile->emirates_id_name,
'date_of_birth' => $profile->emirates_id_dob,
'nationality' => $profile->nationality,
'gender' => $profile->emirates_id_gender,
'issue_date' => $profile->emirates_id_issue_date,
'expiry_date' => $profile->emirates_id_expiry,
'occupation' => $profile->emirates_id_occupation,
'employer' => $profile->emirates_id_employer,
'issuing_place' => $profile->emirates_id_issue_place,
];
return response()->json([
@ -104,22 +94,12 @@ public function updateProfile(Request $request)
'unique:employer_profiles,phone,' . ($employer->employerProfile ? $employer->employerProfile->id : 'NULL'),
$sponsor ? 'unique:sponsors,mobile,' . $sponsor->id : 'unique:sponsors,mobile',
],
'address' => 'required|string|max:255',
'notifications' => 'nullable|boolean',
'company_name' => 'required|string|max:255',
'language' => 'required|string|in:English,Arabic,english,arabic',
'notifications' => 'required|boolean',
'current_password' => 'nullable|required_with:new_password|string',
'new_password' => 'nullable|string|min:8|confirmed',
'fcm_token' => 'nullable|string|max:255',
// Emirates ID optional fields
'id_number' => 'nullable|string|max:255',
'card_number' => 'nullable|string|max:255',
'full_name' => 'nullable|string|max:255',
'date_of_birth' => 'nullable|string|max:255',
'nationality' => 'nullable|string|max:255',
'gender' => 'nullable|string|max:255',
'issue_date' => 'nullable|string|max:255',
'expiry_date' => 'nullable|string|max:255',
'occupation' => 'nullable|string|max:255',
'employer' => 'nullable|string|max:255',
'issuing_place' => 'nullable|string|max:255',
]);
if ($validator->fails()) {
@ -131,15 +111,14 @@ public function updateProfile(Request $request)
}
try {
// Validate Emirates ID immutability — once set, it cannot be changed
if ($request->filled('id_number')) {
$existingProfile = $employer->employerProfile;
if ($existingProfile && $existingProfile->emirates_id_number && $existingProfile->emirates_id_number !== $request->id_number) {
// Check current password if new password is provided
if ($request->filled('new_password')) {
if (!Hash::check($request->current_password, $employer->password)) {
return response()->json([
'success' => false,
'message' => 'Emirates ID mismatch. You cannot change your registered Emirates ID number.',
'message' => 'The provided current password does not match.',
'errors' => [
'id_number' => ['Emirates ID mismatch.']
'current_password' => ['The provided password does not match your current password.']
]
], 422);
}
@ -161,48 +140,11 @@ public function updateProfile(Request $request)
// Sync with corresponding sponsor record if found
$matchingSponsor = \App\Models\Sponsor::where('email', $oldEmail)->first();
if ($matchingSponsor) {
$sponsorData = [
$matchingSponsor->update([
'full_name' => $request->name,
'email' => $request->email,
'mobile' => $request->phone,
'address' => $request->address,
];
if ($request->has('id_number')) {
$sponsorData['emirates_id'] = $request->id_number;
}
if ($request->has('card_number')) {
$sponsorData['emirates_id_card_number'] = $request->card_number;
}
if ($request->has('full_name')) {
$sponsorData['emirates_id_name'] = $request->full_name;
}
if ($request->has('date_of_birth')) {
$sponsorData['emirates_id_dob'] = $request->date_of_birth;
}
if ($request->has('nationality')) {
$sponsorData['nationality'] = $request->nationality;
}
if ($request->has('gender')) {
$sponsorData['emirates_id_gender'] = $request->gender;
}
if ($request->has('issue_date')) {
$sponsorData['emirates_id_issue_date'] = $request->issue_date;
}
if ($request->has('expiry_date')) {
$sponsorData['emirates_id_expiry'] = $request->expiry_date;
}
if ($request->has('occupation')) {
$sponsorData['emirates_id_occupation'] = $request->occupation;
}
if ($request->has('employer')) {
$sponsorData['emirates_id_employer'] = $request->employer;
}
if ($request->has('issuing_place')) {
$sponsorData['emirates_id_issue_place'] = $request->issuing_place;
}
$matchingSponsor->update($sponsorData);
]);
}
// Update employer_profiles table
@ -210,66 +152,29 @@ public function updateProfile(Request $request)
if (!$profile) {
$profile = new EmployerProfile(['user_id' => $employer->id]);
}
$profile->address = $request->address;
$profile->company_name = $request->company_name;
$profile->phone = $request->phone;
$profile->notifications = $request->has('notifications') ? $request->notifications : ($profile->notifications ?? true);
if ($request->has('id_number')) {
$profile->emirates_id_number = $request->id_number;
}
if ($request->has('card_number')) {
$profile->emirates_id_card_number = $request->card_number;
}
if ($request->has('full_name')) {
$profile->emirates_id_name = $request->full_name;
}
if ($request->has('date_of_birth')) {
$profile->emirates_id_dob = $request->date_of_birth;
}
if ($request->has('nationality')) {
$profile->nationality = $request->nationality;
}
if ($request->has('gender')) {
$profile->emirates_id_gender = $request->gender;
}
if ($request->has('issue_date')) {
$profile->emirates_id_issue_date = $request->issue_date;
}
if ($request->has('expiry_date')) {
$profile->emirates_id_expiry = $request->expiry_date;
}
if ($request->has('occupation')) {
$profile->emirates_id_occupation = $request->occupation;
}
if ($request->has('employer')) {
$profile->emirates_id_employer = $request->employer;
}
if ($request->has('issuing_place')) {
$profile->emirates_id_issue_place = $request->issuing_place;
}
$profile->language = ucfirst(strtolower($request->language));
$profile->notifications = $request->notifications;
$profile->save();
// Update password if present
if ($request->filled('new_password')) {
$employer->update([
'password' => Hash::make($request->new_password),
]);
}
$employerProfile = [
'name' => $employer->name,
'email' => $employer->email,
'company_name' => $profile->company_name,
'phone' => $profile->phone,
'address' => $profile->address,
'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,
'id_number' => $profile->emirates_id_number,
'card_number' => $profile->emirates_id_card_number,
'full_name' => $profile->emirates_id_name,
'date_of_birth' => $profile->emirates_id_dob,
'nationality' => $profile->nationality,
'gender' => $profile->emirates_id_gender,
'issue_date' => $profile->emirates_id_issue_date,
'expiry_date' => $profile->emirates_id_expiry,
'occupation' => $profile->emirates_id_occupation,
'employer' => $profile->emirates_id_employer,
'issuing_place' => $profile->emirates_id_issue_place,
];
return response()->json([
@ -309,8 +214,6 @@ public function getDashboard(Request $request)
$savedCandidates = \App\Models\Shortlist::where('employer_id', $employer->id)->count();
$totalWorkers = \App\Models\Worker::where('status', 'active')->count();
// Resolve plan
$sub = \Illuminate\Support\Facades\DB::table('subscriptions')
->where('user_id', $employer->id)
@ -377,7 +280,6 @@ public function getDashboard(Request $request)
'contacted_workers_count' => $contactedWorkersCount,
'total_hired_workers' => $totalHiredWorkers,
'saved_candidates' => $savedCandidates,
'total_workers' => $totalWorkers,
],
'current_plan' => $currentPlan,
'recent_announcements' => $recentAnnouncements,

View File

@ -22,16 +22,23 @@ class EmployerWorkerController extends Controller
*/
private function formatWorker(Worker $w)
{
// Map languages from database comma-separated string
$langs = $w->language ? array_map('trim', explode(',', $w->language)) : ['English'];
// Map languages with country names
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
// Preferred job types: full-time / part-time / live-in / live-out
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
$preferredJobType = $w->preferred_job_type ?? $jobTypes[$w->id % 4];
// Emirates ID verification status
// Emirates ID verification status (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]
];
// Visa status
$visaStatus = $w->visa_status;
@ -44,71 +51,28 @@ private function formatWorker(Worker $w)
return [
'id' => $w->id,
'name' => $w->name,
'email' => $w->email,
'phone' => $w->phone,
'nationality' => $w->nationality,
'age' => $w->age,
'salary' => $w->salary,
'experience' => $w->experience,
'verified' => (bool)$w->verified,
'status' => $w->status,
'created_at' => $w->created_at?->toISOString(),
'updated_at' => $w->updated_at?->toISOString(),
'deleted_at' => $w->deleted_at?->toISOString(),
'language' => $w->language,
'languages' => $langs,
'preferred_location' => $w->preferred_location,
'country' => $w->country,
'city' => $w->city,
'area' => $w->area,
'live_in_out' => $w->live_in_out,
'gender' => $w->gender,
'in_country' => (bool)$w->in_country,
'visa_status' => $visaStatus,
'preferred_job_type' => $preferredJobType,
'fcm_token' => $w->fcm_token,
'passport_status' => $w->passport_status,
'emirates_id_status' => $emiratesIdStatus,
'document_expiry_days' => $w->document_expiry_days,
'document_expiry_status' => $w->document_expiry_status,
'visa_expiry_date' => $w->visa_expiry_date,
'bio' => $w->bio,
'photo' => $photo,
'emirates_id_status' => $emiratesIdStatus,
'passport_status' => $w->passport_status,
'skills' => $mappedSkills,
'visa_status' => $visaStatus,
'experience' => $w->experience,
'religion' => $w->religion,
'languages' => $langs,
'age' => $w->age,
'gender' => $w->gender,
'verified' => (bool)$w->verified,
'preferred_job_type' => $preferredJobType,
'bio' => $w->bio,
'rating' => $rating,
'reviews_count' => $reviewsCount,
'skills' => $w->skills->map(function ($s) {
return [
'id' => $s->id,
'name' => $s->name,
'created_at' => $s->created_at?->toISOString(),
'updated_at' => $s->updated_at?->toISOString(),
'image_path' => $s->image_path,
'pivot' => [
'worker_id' => $s->pivot->worker_id,
'skill_id' => $s->pivot->skill_id,
]
];
})->toArray(),
'documents' => $w->documents->map(function ($doc) {
$ocrData = $doc->ocr_data;
if ($doc->type === 'visa' && is_array($ocrData)) {
unset($ocrData['sponsor']);
unset($ocrData['valid_until']);
}
return [
'id' => $doc->id,
'worker_id' => $doc->worker_id,
'type' => $doc->type,
'number' => $doc->number,
'issue_date' => $doc->issue_date,
'expiry_date' => $doc->expiry_date,
'ocr_accuracy' => $doc->ocr_accuracy,
'file_path' => $doc->file_path ? url($doc->file_path) : null,
'ocr_data' => $ocrData,
'created_at' => $doc->created_at?->toISOString(),
'updated_at' => $doc->updated_at?->toISOString(),
];
})->toArray(),
'preferred_location' => $w->preferred_location,
'area' => $w->area,
'live_in_out' => $w->live_in_out,
'in_country' => (bool)$w->in_country,
'document_expiry_days' => $w->document_expiry_days,
'document_expiry_status' => $w->document_expiry_status,
];
}
@ -178,8 +142,7 @@ public function getWorkers(Request $request)
$workersArray = array_values(array_filter($workersArray, function ($c) use ($skillsArray) {
if (!isset($c['skills']) || !is_array($c['skills'])) return false;
foreach ($c['skills'] as $s) {
$skillName = is_array($s) ? ($s['name'] ?? '') : (is_object($s) ? ($s->name ?? '') : $s);
if (in_array(strtolower($skillName), $skillsArray)) {
if (in_array(strtolower($s), $skillsArray)) {
return true;
}
}
@ -351,22 +314,10 @@ public function getWorkers(Request $request)
}));
}
$page = (int)$request->input('page', 1);
$perPage = (int)$request->input('per_page', 15);
$total = count($workersArray);
$offset = ($page - 1) * $perPage;
$paginatedWorkers = array_slice($workersArray, $offset, $perPage);
return response()->json([
'success' => true,
'data' => [
'workers' => $paginatedWorkers,
'pagination' => [
'total' => $total,
'per_page' => $perPage,
'current_page' => $page,
'last_page' => max(1, (int)ceil($total / $perPage)),
]
'workers' => $workersArray
]
], 200);
@ -983,74 +934,25 @@ public function getWorkerDetail(Request $request, $id)
$workerProfile = [
'id' => $w->id,
'name' => $w->name,
'email' => $w->email,
'phone' => $w->phone,
'nationality' => $w->nationality,
'age' => $w->age,
'salary' => $w->salary,
'experience' => $w->experience,
'verified' => (bool)$w->verified,
'status' => $w->status,
'created_at' => $w->created_at?->toISOString(),
'updated_at' => $w->updated_at?->toISOString(),
'deleted_at' => $w->deleted_at?->toISOString(),
'language' => $w->language,
'languages' => $langs,
'preferred_location' => $w->preferred_location,
'country' => $w->country,
'city' => $w->city,
'area' => $w->area,
'live_in_out' => $w->live_in_out,
'gender' => $w->gender,
'in_country' => (bool)$w->in_country,
'visa_status' => $visaStatus,
'preferred_job_type' => $preferredJobType,
'fcm_token' => $w->fcm_token,
'passport_status' => $w->passport_status,
'emirates_id_status' => $emiratesIdStatus,
'document_expiry_days' => $w->document_expiry_days,
'document_expiry_status' => $w->document_expiry_status,
'visa_expiry_date' => $w->visa_expiry_date,
'bio' => $w->bio,
'photo' => $photo,
'emirates_id_status' => $emiratesIdStatus,
'skills' => $mappedSkills,
'visa_status' => $visaStatus,
'experience' => $w->experience,
'experience_years' => 5,
'religion' => $w->religion,
'languages' => $langs,
'age' => $w->age,
'gender' => $w->gender,
'verified' => (bool)$w->verified,
'preferred_job_type' => $preferredJobType,
'bio' => $w->bio,
'rating' => $rating,
'reviews_count' => $reviewsCount,
'reviews' => $reviews,
'similar_workers' => $similarWorkers,
'conversation_id' => $conversation ? $conversation->id : null,
'skills' => $w->skills->map(function ($s) {
return [
'id' => $s->id,
'name' => $s->name,
'created_at' => $s->created_at?->toISOString(),
'updated_at' => $s->updated_at?->toISOString(),
'image_path' => $s->image_path,
'pivot' => [
'worker_id' => $s->pivot->worker_id,
'skill_id' => $s->pivot->skill_id,
]
];
})->toArray(),
'documents' => $w->documents->map(function ($doc) {
$ocrData = $doc->ocr_data;
if ($doc->type === 'visa' && is_array($ocrData)) {
unset($ocrData['sponsor']);
unset($ocrData['valid_until']);
}
return [
'id' => $doc->id,
'worker_id' => $doc->worker_id,
'type' => $doc->type,
'number' => $doc->number,
'issue_date' => $doc->issue_date,
'expiry_date' => $doc->expiry_date,
'ocr_accuracy' => $doc->ocr_accuracy,
'file_path' => $doc->file_path ? url($doc->file_path) : null,
'ocr_data' => $ocrData,
'created_at' => $doc->created_at?->toISOString(),
'updated_at' => $doc->updated_at?->toISOString(),
];
})->toArray(),
];
return response()->json([
@ -1085,7 +987,7 @@ public function getShortlist(Request $request)
$perPage = (int)$request->input('per_page', 15);
$shortlists = Shortlist::where('employer_id', $employer->id)
->with(['worker.skills', 'worker.documents'])
->with(['worker.skills'])
->get();
$formattedWorkers = $shortlists->map(function ($s) {

View File

@ -317,11 +317,12 @@ public function forgotPassword(Request $request)
$sponsor = Sponsor::where('mobile', $request->mobile)->first();
}
// Prevent email enumeration — always return success
if (!$sponsor || !$sponsor->email) {
return response()->json([
'success' => false,
'message' => 'user not found this email id',
], 404);
'success' => true,
'message' => 'If an account with that contact exists, a reset OTP has been sent.',
]);
}
$otp = (string) mt_rand(100000, 999999);

View File

@ -261,6 +261,14 @@ public function setupProfile(Request $request)
return $worker;
});
if ($request->filled('fcm_token')) {
\App\Services\FCMService::sendPushNotification(
$request->fcm_token,
'Welcome to Migrant',
'Registration complete! Welcome to Migrant.'
);
}
// Clear the OTP verification gate
Cache::forget('worker_otp_verified_' . $identifier);
@ -330,34 +338,7 @@ public function register(Request $request)
return $query->where('type', 'passport');
}),
],
'passport.document_type' => 'nullable|string|max:10',
'passport.issuing_country' => 'nullable|string|max:10',
'passport.surname' => 'nullable|string|max:255',
'passport.given_names' => 'nullable|string|max:255',
'passport.nationality' => 'nullable|string|max:100',
'passport.date_of_birth' => 'nullable|string',
'passport.date_of_expiry' => 'nullable|string',
'passport.place_of_birth' => 'nullable|string|max:255',
'passport.authority' => 'nullable|string|max:255',
'passport.sex' => 'nullable|string|max:10',
'passport.date_of_issue' => 'nullable|string',
'passport.ocr_accuracy' => 'nullable|numeric',
'visa' => 'nullable|array',
'visa.document_type' => 'nullable|string|max:100',
'visa.country' => 'nullable|string|max:100',
'visa.visa_type' => 'nullable|string|max:100',
'visa.entry_permit_no' => 'nullable|string|max:100',
'visa.issue_date' => 'nullable|string',
'visa.valid_until' => 'nullable|string',
'visa.uid_no' => 'nullable|string|max:100',
'visa.full_name' => 'nullable|string|max:255',
'visa.nationality' => 'nullable|string|max:100',
'visa.place_of_birth' => 'nullable|string|max:255',
'visa.date_of_birth' => 'nullable|string',
'visa.passport_no' => 'nullable|string|max:100',
'visa.profession' => 'nullable|string|max:255',
'visa.sponsor_name' => 'nullable|string|max:255',
'visa.ocr_accuracy' => 'nullable|numeric',
], [
'phone.unique' => 'This mobile number is already registered.',
'passport.passport_number.unique' => 'This passport number is already registered.',
@ -489,7 +470,7 @@ public function register(Request $request)
'number' => $passportNum,
'issue_date' => $issueDateFormatted,
'expiry_date' => $expiryDateFormatted,
'ocr_accuracy' => isset($passportDataInput['ocr_accuracy']) ? (float)$passportDataInput['ocr_accuracy'] : 99.0,
'ocr_accuracy' => 99.0,
'file_path' => null,
'ocr_data' => $passportDataInput,
]);
@ -497,8 +478,8 @@ public function register(Request $request)
// Create visa document if provided
if ($visaDataInput) {
$visaNum = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999));
$expiryDate = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
$visaNum = $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999));
$expiryDate = $visaDataInput['expiry_date'] ?? null;
$issueDate = $visaDataInput['issue_date'] ?? null;
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(2)->toDateString();
@ -509,7 +490,7 @@ public function register(Request $request)
'number' => $visaNum,
'issue_date' => $issueDateFormatted,
'expiry_date' => $expiryDateFormatted,
'ocr_accuracy' => isset($visaDataInput['ocr_accuracy']) ? (float)$visaDataInput['ocr_accuracy'] : 99.0,
'ocr_accuracy' => 99.0,
'file_path' => null,
'ocr_data' => $visaDataInput,
]);
@ -518,6 +499,14 @@ public function register(Request $request)
return $worker;
});
if ($request->filled('fcm_token')) {
\App\Services\FCMService::sendPushNotification(
$request->fcm_token,
'Welcome to Migrant',
'Worker registered successfully.'
);
}
$result->load(['skills', 'documents']);
return response()->json([
@ -1261,11 +1250,12 @@ public function forgotPassword(Request $request)
$worker = Worker::where('phone', $request->phone)->first();
// Always return success to prevent phone enumeration
if (!$worker) {
return response()->json([
'success' => false,
'message' => 'user not found this mobile number',
], 404);
'success' => true,
'message' => 'If an account with that mobile number exists, a reset OTP has been sent.',
]);
}
$otp = (string) mt_rand(100000, 999999);

View File

@ -290,35 +290,17 @@ public function uploadEmiratesId(Request $request)
$extractedIdNumber = null;
$extractedExpiry = null;
$extractedName = null;
$extractedDob = null;
$extractedNationality = null;
$extractedIssueDate = null;
$extractedEmployer = null;
$extractedIssuePlace = null;
$extractedOccupation = null;
$extractedCardNumber = null;
$extractedGender = null;
if ($request->hasFile('emirates_id_front')) {
$frontFile = $request->file('emirates_id_front');
$frontOcr = \App\Services\OcrDocumentService::extractEmiratesIdFrontData($frontFile);
$extractedIdNumber = $frontOcr['emirates_id_number'];
$extractedName = $frontOcr['name'] ?? null;
$extractedDob = $frontOcr['date_of_birth'] ?? null;
$extractedNationality = $frontOcr['nationality'] ?? null;
$extractedCardNumber = $frontOcr['card_number'] ?? null;
$extractedGender = $frontOcr['gender'] ?? null;
$extractedOccupation = $frontOcr['occupation'] ?? null;
$extractedEmployer = $frontOcr['employer'] ?? null;
$extractedIssuePlace = $frontOcr['issue_place'] ?? null;
}
if ($request->hasFile('emirates_id_back')) {
$backFile = $request->file('emirates_id_back');
$backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($backFile);
$extractedExpiry = $backOcr['expiry_date'];
$extractedIssueDate = $backOcr['issue_date'] ?? null;
}
if ($request->hasFile('emirates_id_file')) {
@ -327,30 +309,12 @@ public function uploadEmiratesId(Request $request)
$backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($file);
$extractedIdNumber = $frontOcr['emirates_id_number'];
$extractedExpiry = $backOcr['expiry_date'];
$extractedName = $frontOcr['name'] ?? null;
$extractedDob = $frontOcr['date_of_birth'] ?? null;
$extractedNationality = $frontOcr['nationality'] ?? null;
$extractedCardNumber = $frontOcr['card_number'] ?? null;
$extractedGender = $frontOcr['gender'] ?? null;
$extractedOccupation = $frontOcr['occupation'] ?? null;
$extractedEmployer = $frontOcr['employer'] ?? null;
$extractedIssuePlace = $frontOcr['issue_place'] ?? null;
$extractedIssueDate = $backOcr['issue_date'] ?? null;
}
$pending = session('pending_employer_registration');
$pending['emirates_id_file'] = null;
$pending['emirates_id_number'] = $extractedIdNumber;
$pending['emirates_id_expiry'] = $extractedExpiry;
$pending['emirates_id_name'] = $extractedName;
$pending['emirates_id_dob'] = $extractedDob;
$pending['emirates_id_nationality'] = $extractedNationality;
$pending['emirates_id_card_number'] = $extractedCardNumber;
$pending['emirates_id_gender'] = $extractedGender;
$pending['emirates_id_occupation'] = $extractedOccupation;
$pending['emirates_id_employer'] = $extractedEmployer;
$pending['emirates_id_issue_place'] = $extractedIssuePlace;
$pending['emirates_id_issue_date'] = $extractedIssueDate;
session(['pending_employer_registration' => $pending]);
session(['employer_emirates_id_uploaded' => true]);
@ -474,15 +438,6 @@ public function createPassword(Request $request)
'emirates_id_front' => null, // We did not store the file
'emirates_id_number' => $pending['emirates_id_number'] ?? null,
'emirates_id_expiry' => $pending['emirates_id_expiry'] ?? null,
'emirates_id_name' => $pending['emirates_id_name'] ?? null,
'emirates_id_dob' => $pending['emirates_id_dob'] ?? null,
'emirates_id_issue_date' => $pending['emirates_id_issue_date'] ?? null,
'emirates_id_employer' => $pending['emirates_id_employer'] ?? null,
'emirates_id_issue_place' => $pending['emirates_id_issue_place'] ?? null,
'emirates_id_occupation' => $pending['emirates_id_occupation'] ?? null,
'emirates_id_card_number' => $pending['emirates_id_card_number'] ?? null,
'emirates_id_gender' => $pending['emirates_id_gender'] ?? null,
'nationality' => $pending['emirates_id_nationality'] ?? null,
'address' => $pending['address'] ?? null,
]);
@ -504,17 +459,6 @@ public function createPassword(Request $request)
'last_login_at' => now(),
'emirates_id_file' => null, // We did not store the file
'address' => $pending['address'] ?? null,
'emirates_id' => $pending['emirates_id_number'] ?? null,
'emirates_id_name' => $pending['emirates_id_name'] ?? null,
'emirates_id_dob' => $pending['emirates_id_dob'] ?? null,
'emirates_id_issue_date' => $pending['emirates_id_issue_date'] ?? null,
'emirates_id_expiry' => $pending['emirates_id_expiry'] ?? null,
'emirates_id_employer' => $pending['emirates_id_employer'] ?? null,
'emirates_id_issue_place' => $pending['emirates_id_issue_place'] ?? null,
'emirates_id_occupation' => $pending['emirates_id_occupation'] ?? null,
'emirates_id_card_number' => $pending['emirates_id_card_number'] ?? null,
'emirates_id_gender' => $pending['emirates_id_gender'] ?? null,
'nationality' => $pending['emirates_id_nationality'] ?? null,
]);
// Create active subscription in database

View File

@ -42,17 +42,15 @@ private function buildProfileData($user, $profile)
'email' => $user->email,
'phone' => $profile->phone ?? '+971509990001',
'emirates_id' => [
'emirates_id_number' => $profile->emirates_id_number ?? '784-1987-5493842-5',
'emirates_id_number' => $profile->emirates_id_number ?? '784-1988-5310327-2',
'name' => $profile->emirates_id_name ?? $user->name,
'date_of_birth' => $profile->emirates_id_dob ?? '1987-04-14',
'nationality' => $profile->nationality ?? 'Bangladesh',
'issue_date' => $profile->emirates_id_issue_date ?? '2025-12-12',
'expiry_date' => $profile->emirates_id_expiry ?? '2027-12-11',
'employer' => $profile->emirates_id_employer ?? 'Msj International Technical Services L.L.C UAE',
'issue_place' => $profile->emirates_id_issue_place ?? 'Dubai',
'occupation' => $profile->emirates_id_occupation ?? 'Electrician',
'card_number' => $profile->emirates_id_card_number ?? '151023946',
'gender' => $profile->emirates_id_gender ?? 'M',
'date_of_birth' => $profile->emirates_id_dob ?? '1990-01-01',
'nationality' => $profile->nationality ?? 'Emirati',
'issue_date' => $profile->emirates_id_issue_date ?? '2023-04-11',
'expiry_date' => $profile->emirates_id_expiry ?? '2028-04-11',
'employer' => $profile->emirates_id_employer ?? 'Federal Authority',
'issue_place' => $profile->emirates_id_issue_place ?? 'Abu Dhabi',
'occupation' => $profile->emirates_id_occupation ?? 'Manager',
],
'fcm_token' => $user->fcm_token ?? 'fcm_token_example',
'address' => $profile->address ?? 'Villa 45, Street 12',
@ -76,17 +74,15 @@ public function index(Request $request)
$profile = EmployerProfile::create([
'user_id' => $user->id,
'phone' => '+971509990001',
'emirates_id_number' => '784-1987-5493842-5',
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
'emirates_id_dob' => '1987-04-14',
'nationality' => 'Bangladesh',
'emirates_id_issue_date' => '2025-12-12',
'emirates_id_expiry' => '2027-12-11',
'emirates_id_employer' => 'Msj International Technical Services L.L.C UAE',
'emirates_id_issue_place' => 'Dubai',
'emirates_id_occupation' => 'Electrician',
'emirates_id_card_number' => '151023946',
'emirates_id_gender' => 'M',
'emirates_id_number' => '784-1988-5310327-2',
'emirates_id_name' => 'Ahmad Bin Ahmed',
'emirates_id_dob' => '1990-01-01',
'nationality' => 'Emirati',
'emirates_id_issue_date' => '2023-04-11',
'emirates_id_expiry' => '2028-04-11',
'emirates_id_employer' => 'Federal Authority',
'emirates_id_issue_place' => 'Abu Dhabi',
'emirates_id_occupation' => 'Manager',
'address' => 'Villa 45, Street 12',
]);
}
@ -110,17 +106,15 @@ public function edit(Request $request)
$profile = EmployerProfile::create([
'user_id' => $user->id,
'phone' => '+971509990001',
'emirates_id_number' => '784-1987-5493842-5',
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
'emirates_id_dob' => '1987-04-14',
'nationality' => 'Bangladesh',
'emirates_id_issue_date' => '2025-12-12',
'emirates_id_expiry' => '2027-12-11',
'emirates_id_employer' => 'Msj International Technical Services L.L.C UAE',
'emirates_id_issue_place' => 'Dubai',
'emirates_id_occupation' => 'Electrician',
'emirates_id_card_number' => '151023946',
'emirates_id_gender' => 'M',
'emirates_id_number' => '784-1988-5310327-2',
'emirates_id_name' => 'Ahmad Bin Ahmed',
'emirates_id_dob' => '1990-01-01',
'nationality' => 'Emirati',
'emirates_id_issue_date' => '2023-04-11',
'emirates_id_expiry' => '2028-04-11',
'emirates_id_employer' => 'Federal Authority',
'emirates_id_issue_place' => 'Abu Dhabi',
'emirates_id_occupation' => 'Manager',
'address' => 'Villa 45, Street 12',
]);
}

View File

@ -370,6 +370,7 @@ public function show($id)
'skills' => $mappedSkills,
'visa_status' => $visaStatus,
'experience' => $w->experience,
'experience_years' => 5,
'salary' => (int) $w->salary,
'religion' => $w->religion,
'languages' => $langs,

View File

@ -29,8 +29,6 @@ class EmployerProfile extends Model
'emirates_id_employer',
'emirates_id_issue_place',
'emirates_id_occupation',
'emirates_id_card_number',
'emirates_id_gender',
'address',
'property_type',
'profile_photo_url',

View File

@ -43,8 +43,6 @@ class Sponsor extends Authenticatable
'emirates_id_employer',
'emirates_id_issue_place',
'emirates_id_occupation',
'emirates_id_card_number',
'emirates_id_gender',
];
protected $hidden = [

View File

@ -316,11 +316,6 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
'name' => null,
'nationality' => null,
'date_of_birth' => null,
'card_number' => null,
'gender' => null,
'occupation' => null,
'employer' => null,
'issue_place' => null,
'ocr_accuracy' => 0.0,
'success' => false,
];
@ -381,12 +376,6 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
if (!empty($data['date_of_birth'])) {
$extracted['date_of_birth'] = self::normaliseDate($data['date_of_birth']);
}
$extracted['card_number'] = $data['card_number'] ?? null;
$extracted['gender'] = $data['gender'] ?? $data['sex'] ?? null;
$extracted['occupation'] = $data['occupation'] ?? null;
$extracted['employer'] = $data['employer'] ?? $data['company_name'] ?? null;
$extracted['issue_place'] = $data['issue_place'] ?? $data['issuing_place'] ?? $data['place_of_issue'] ?? null;
} else {
Log::warning('Emirates ID Front OCR API success=false', ['response' => $json]);
}
@ -403,31 +392,13 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
// Fallbacks
if (empty($extracted['emirates_id_number'])) {
$extracted['emirates_id_number'] = '784-1987-5493842-5';
$extracted['emirates_id_number'] = '784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
}
if (empty($extracted['name'])) {
$extracted['name'] = 'Mohammad Jobaier Mohammad Abul Kalam';
$extracted['name'] = 'KRISHNA PRASAD';
}
if (empty($extracted['nationality'])) {
$extracted['nationality'] = 'Bangladesh';
}
if (empty($extracted['date_of_birth'])) {
$extracted['date_of_birth'] = '1987-04-14';
}
if (empty($extracted['card_number'])) {
$extracted['card_number'] = '151023946';
}
if (empty($extracted['gender'])) {
$extracted['gender'] = 'M';
}
if (empty($extracted['occupation'])) {
$extracted['occupation'] = 'Electrician';
}
if (empty($extracted['employer'])) {
$extracted['employer'] = 'Msj International Technical Services L.L.C UAE';
}
if (empty($extracted['issue_place'])) {
$extracted['issue_place'] = 'Dubai';
$extracted['nationality'] = 'NPL';
}
return $extracted;
@ -546,15 +517,10 @@ private static function emiratesIdFrontTestFallback(): array
{
return [
'success' => true,
'emirates_id_number' => '784-1987-5493842-5',
'name' => 'Mohammad Jobaier Mohammad Abul Kalam',
'nationality' => 'Bangladesh',
'date_of_birth' => '1987-04-14',
'card_number' => '151023946',
'gender' => 'M',
'occupation' => 'Electrician',
'employer' => 'Msj International Technical Services L.L.C UAE',
'issue_place' => 'Dubai',
'emirates_id_number' => '784-1988-5310327-2',
'name' => 'KRISHNA PRASAD',
'nationality' => 'NPL',
'date_of_birth' => '1988-03-22',
'ocr_accuracy' => 98.5,
];
}
@ -563,8 +529,8 @@ private static function emiratesIdBackTestFallback(): array
{
return [
'success' => true,
'expiry_date' => '2027-12-11',
'issue_date' => '2025-12-12',
'expiry_date' => '2028-04-11',
'issue_date' => '2023-04-11',
'ocr_accuracy' => 98.5,
];
}

View File

@ -1,28 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('employer_profiles', function (Blueprint $table) {
$table->string('company_name')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('employer_profiles', function (Blueprint $table) {
$table->string('company_name')->nullable(false)->change();
});
}
};

View File

@ -1,38 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('employer_profiles', function (Blueprint $table) {
$table->string('emirates_id_card_number')->nullable();
$table->string('emirates_id_gender')->nullable();
});
Schema::table('sponsors', function (Blueprint $table) {
$table->string('emirates_id_card_number')->nullable();
$table->string('emirates_id_gender')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('employer_profiles', function (Blueprint $table) {
$table->dropColumn(['emirates_id_card_number', 'emirates_id_gender']);
});
Schema::table('sponsors', function (Blueprint $table) {
$table->dropColumn(['emirates_id_card_number', 'emirates_id_gender']);
});
}
};

View File

@ -1,4 +1,4 @@
{
{
"openapi": "3.0.0",
"info": {
"title": "Migrant Mobile API",
@ -23,7 +23,7 @@
"Sponsor/Auth"
],
"summary": "Register Sponsor Account",
"description": "Registers a new sponsor with basic information and uploads their organization/trade license. Sponsors have restricted access \u2014 dashboard and charity events only. No payment required.",
"description": "Registers a new sponsor with basic information and uploads their organization/trade license. Sponsors have restricted access dashboard and charity events only. No payment required.",
"security": [],
"requestBody": {
"required": true,
@ -51,7 +51,7 @@
"mobile": {
"type": "string",
"example": "+971501112233",
"description": "Mobile phone number \u00e2\u20ac\u201d must be unique. (REQUIRED)"
"description": "Mobile phone number — must be unique. (REQUIRED)"
},
"password": {
"type": "string",
@ -600,7 +600,7 @@
"Worker/Auth"
],
"summary": "Register Worker Account (Step 1)",
"description": "Creates the worker account and returns a secure Bearer token.\n\nDocument upload is a **separate step** after registration:\n- `POST /workers/register/passport` \u2014 upload passport (OCR-processed)\n- `POST /workers/register/visa` \u2014 upload visa (OCR-processed)\n\nBoth document endpoints require the Bearer token returned here.",
"description": "Creates the worker account and returns a secure Bearer token.\n\nDocument upload is a **separate step** after registration:\n- `POST /workers/register/passport` — upload passport (OCR-processed)\n- `POST /workers/register/visa` — upload visa (OCR-processed)\n\nBoth document endpoints require the Bearer token returned here.",
"security": [],
"requestBody": {
"required": true,
@ -743,11 +743,6 @@
"nationality": {
"type": "string"
},
"ocr_accuracy": {
"type": "number",
"format": "float",
"example": 99
},
"passport_number": {
"type": "string"
},
@ -772,22 +767,6 @@
"accompanied_by": {
"type": "string"
},
"country": {
"type": "string",
"example": "United Arab Emirates"
},
"date_of_birth": {
"type": "string",
"example": "25-JUN-1999"
},
"document_type": {
"type": "string",
"example": "uae_visa"
},
"entry_permit_no": {
"type": "string",
"example": "77003098 / 2019 / 204"
},
"expiry_date": {
"type": "string",
"example": "2024-02-15"
@ -795,10 +774,6 @@
"file_number": {
"type": "string"
},
"full_name": {
"type": "string",
"example": "Mr.MUHAMMAD NADEEM RASHEED"
},
"id_number": {
"type": "string"
},
@ -809,50 +784,17 @@
"name": {
"type": "string"
},
"nationality": {
"type": "string",
"example": "PAKISTAN"
},
"ocr_accuracy": {
"type": "number",
"format": "float",
"example": 99
},
"passport_no": {
"type": "string",
"example": "EN9458281"
},
"passport_number": {
"type": "string"
},
"place_of_birth": {
"type": "string",
"example": "SIALKOT PAK"
},
"place_of_issue": {
"type": "string"
},
"profession": {
"type": "string",
"example": "SALES REPRESENTATIVE"
"type": "string"
},
"sponsor": {
"type": "string"
},
"sponsor_name": {
"type": "string"
},
"uid_no": {
"type": "string",
"example": "207404887"
},
"valid_until": {
"type": "string",
"example": "04-MAR-2019"
},
"visa_type": {
"type": "string",
"example": "ENTRY PERMIT"
}
}
}
@ -895,7 +837,7 @@
}
},
"422": {
"description": "Validation error \u2014 field validation failed or Passport OCR extraction failed.",
"description": "Validation error field validation failed or Passport OCR extraction failed.",
"content": {
"application/json": {
"schema": {
@ -1182,7 +1124,7 @@
},
"name": {
"type": "string",
"example": "Hindi (\u0939\u093f\u0928\u094d\u0926\u0940)"
"example": "Hindi (हिन्दी)"
}
}
}
@ -3638,110 +3580,11 @@
"tags": [
"Employer/Profile"
],
"summary": "Get Employer Profile Details",
"description": "Retrieves the authenticated employer's profile details.",
"summary": "Get Profile details (Employer)",
"description": "Retrieves the authenticated employer's profile details including their selected company name, phone, language preference, and notification settings.",
"responses": {
"200": {
"description": "Profile retrieved successfully.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"profile": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "Ahmad"
},
"email": {
"type": "string",
"example": "ahmad@example.com"
},
"phone": {
"type": "string",
"example": "+971509990001"
},
"address": {
"type": "string",
"example": "Villa 12, Jumeirah 2"
},
"notifications": {
"type": "boolean",
"example": true
},
"verification_status": {
"type": "string",
"example": "approved"
},
"emirates_id_number": {
"type": "string",
"example": "784-1988-5310327-2"
},
"emirates_id_expiry": {
"type": "string",
"example": "2028-04-11"
},
"id_number": {
"type": "string",
"example": "784-1987-5493842-5"
},
"card_number": {
"type": "string",
"example": "151023946"
},
"full_name": {
"type": "string",
"example": "Mohammad Jobaier Mohammad Abul Kalam"
},
"date_of_birth": {
"type": "string",
"example": "14/04/1987"
},
"nationality": {
"type": "string",
"example": "Bangladesh"
},
"gender": {
"type": "string",
"example": "M"
},
"issue_date": {
"type": "string",
"example": "12/12/2025"
},
"expiry_date": {
"type": "string",
"example": "11/12/2027"
},
"occupation": {
"type": "string",
"example": "Electrician"
},
"employer": {
"type": "string",
"example": "Msj International Technical Services L.L.C UAE"
},
"issuing_place": {
"type": "string",
"example": "Dubai"
}
}
}
}
}
}
}
}
}
"description": "Employer profile details retrieved successfully."
}
}
}
@ -3751,8 +3594,8 @@
"tags": [
"Employer/Profile"
],
"summary": "Update Employer Profile Details",
"description": "Updates the authenticated employer's profile details including optional Emirates ID metadata. Password changes are not supported via this endpoint.",
"summary": "Update Profile details (Employer)",
"description": "Allows employers to update their profile details (company name, phone number, name, email, language preference, and notification settings) and change their account password.",
"requestBody": {
"required": true,
"content": {
@ -3763,7 +3606,9 @@
"name",
"email",
"phone",
"address"
"company_name",
"language",
"notifications"
],
"properties": {
"name": {
@ -3778,75 +3623,40 @@
"type": "string",
"example": "+971509990001"
},
"address": {
"company_name": {
"type": "string",
"example": "Villa 12, Jumeirah 2"
"example": "Ahmad Tech Ltd"
},
"language": {
"type": "string",
"enum": [
"English",
"Arabic",
"english",
"arabic"
],
"example": "English"
},
"notifications": {
"type": "boolean",
"example": true,
"description": "Optional. Enable/disable push notifications. Defaults to true if not provided.",
"default": true
"example": true
},
"current_password": {
"type": "string",
"example": "Password@123"
},
"new_password": {
"type": "string",
"example": "NewSecurePassword@123"
},
"new_password_confirmation": {
"type": "string",
"example": "NewSecurePassword@123"
},
"fcm_token": {
"type": "string",
"example": "fcm_token_example",
"description": "Firebase Cloud Messaging token for push notifications."
},
"id_number": {
"type": "string",
"example": "784-1987-5493842-5",
"description": "Emirates ID card number/ID number (optional)"
},
"card_number": {
"type": "string",
"example": "151023946",
"description": "Emirates ID card physical serial number (optional)"
},
"full_name": {
"type": "string",
"example": "Mohammad Jobaier Mohammad Abul Kalam",
"description": "Emirates ID full name (optional)"
},
"date_of_birth": {
"type": "string",
"example": "14/04/1987",
"description": "Emirates ID date of birth (optional)"
},
"nationality": {
"type": "string",
"example": "Bangladesh",
"description": "Emirates ID nationality (optional)"
},
"gender": {
"type": "string",
"example": "M",
"description": "Emirates ID gender (optional)"
},
"issue_date": {
"type": "string",
"example": "12/12/2025",
"description": "Emirates ID card issue date (optional)"
},
"expiry_date": {
"type": "string",
"example": "11/12/2027",
"description": "Emirates ID card expiry date (optional)"
},
"occupation": {
"type": "string",
"example": "Electrician",
"description": "Emirates ID card holder occupation (optional)"
},
"employer": {
"type": "string",
"example": "Msj International Technical Services L.L.C UAE",
"description": "Emirates ID card holder sponsor/employer name (optional)"
},
"issuing_place": {
"type": "string",
"example": "Dubai",
"description": "Emirates ID card place of issue (optional)"
}
}
}
@ -3855,113 +3665,7 @@
},
"responses": {
"200": {
"description": "Profile updated successfully.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"message": {
"type": "string",
"example": "Profile updated successfully."
},
"data": {
"type": "object",
"properties": {
"profile": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "Ahmad"
},
"email": {
"type": "string",
"example": "ahmad@example.com"
},
"phone": {
"type": "string",
"example": "+971509990001"
},
"address": {
"type": "string",
"example": "Villa 12, Jumeirah 2"
},
"notifications": {
"type": "boolean",
"example": true
},
"verification_status": {
"type": "string",
"example": "approved"
},
"emirates_id_number": {
"type": "string",
"example": "784-1988-5310327-2"
},
"emirates_id_expiry": {
"type": "string",
"example": "2028-04-11"
},
"id_number": {
"type": "string",
"example": "784-1987-5493842-5"
},
"card_number": {
"type": "string",
"example": "151023946"
},
"full_name": {
"type": "string",
"example": "Mohammad Jobaier Mohammad Abul Kalam"
},
"date_of_birth": {
"type": "string",
"example": "14/04/1987"
},
"nationality": {
"type": "string",
"example": "Bangladesh"
},
"gender": {
"type": "string",
"example": "M"
},
"issue_date": {
"type": "string",
"example": "12/12/2025"
},
"expiry_date": {
"type": "string",
"example": "11/12/2027"
},
"occupation": {
"type": "string",
"example": "Electrician"
},
"employer": {
"type": "string",
"example": "Msj International Technical Services L.L.C UAE"
},
"issuing_place": {
"type": "string",
"example": "Dubai"
}
}
}
}
}
}
}
}
}
},
"422": {
"description": "Validation error."
"description": "Profile updated successfully."
}
}
}
@ -4114,52 +3818,7 @@
],
"responses": {
"200": {
"description": "Available workers list retrieved successfully.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"workers": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Worker"
}
},
"pagination": {
"type": "object",
"properties": {
"total": {
"type": "integer",
"example": 12
},
"per_page": {
"type": "integer",
"example": 15
},
"current_page": {
"type": "integer",
"example": 1
},
"last_page": {
"type": "integer",
"example": 1
}
}
}
}
}
}
}
}
}
"description": "Available workers list retrieved successfully."
}
}
}
@ -4198,13 +3857,105 @@
"type": "object",
"properties": {
"worker": {
"allOf": [
{
"$ref": "#/components/schemas/Worker"
},
{
"type": "object",
"properties": {
"id": {
"type": "integer",
"example": 1
},
"name": {
"type": "string",
"example": "Jane Doe"
},
"nationality": {
"type": "string",
"example": "Filipino"
},
"photo": {
"type": "string",
"example": "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200"
},
"emirates_id_status": {
"type": "string",
"example": "Passport Verified",
"description": "Deprecated - Use passport_status instead"
},
"passport_status": {
"type": "string",
"example": "Passport Verified"
},
"category": {
"type": "string",
"example": "Domestic Worker"
},
"skills": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"childcare",
"cooking"
]
},
"availability_status": {
"type": "string",
"example": "Active"
},
"visa_status": {
"type": "string",
"example": "Residence Visa"
},
"experience": {
"type": "string",
"example": "5 Years"
},
"experience_years": {
"type": "integer",
"example": 5
},
"salary": {
"type": "integer",
"example": 2500
},
"religion": {
"type": "string",
"example": "Christian"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"English",
"Tagalog"
]
},
"age": {
"type": "integer",
"example": 28
},
"verified": {
"type": "boolean",
"example": true
},
"preferred_job_type": {
"type": "string",
"example": "live-in"
},
"bio": {
"type": "string",
"example": "Experienced and caring domestic worker specialing in childcare and housekeeping..."
},
"rating": {
"type": "number",
"example": 4.3
},
"reviews_count": {
"type": "integer",
"example": 6
},
"reviews": {
"type": "array",
"items": {
@ -4224,8 +3975,6 @@
}
}
}
]
}
}
}
}
@ -5166,7 +4915,7 @@
"Workers - Auth"
],
"summary": "Forgot Password (Send OTP)",
"description": "Sends a 6-digit OTP to verify the worker's identity using their registered mobile number. Workers do not use email \u2014 mobile/phone is the primary identifier. OTP is valid for 10 minutes.",
"description": "Sends a 6-digit OTP to verify the worker's identity using their registered mobile number. Workers do not use email mobile/phone is the primary identifier. OTP is valid for 10 minutes.",
"operationId": "workerForgotPassword",
"requestBody": {
"required": true,
@ -5190,7 +4939,7 @@
},
"responses": {
"200": {
"description": "OTP sent successfully.",
"description": "OTP sent successfully (response is identical whether account exists or not, to prevent enumeration)",
"content": {
"application/json": {
"schema": {
@ -5209,28 +4958,8 @@
}
}
},
"404": {
"description": "Worker account not found with the provided mobile number.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"message": {
"type": "string",
"example": "user not found this mobile number"
}
}
}
}
}
},
"422": {
"description": "Validation error \u2014 phone is required"
"description": "Validation error — phone is required"
}
}
}
@ -5418,47 +5147,10 @@
},
"responses": {
"200": {
"description": "OTP sent successfully.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"message": {
"type": "string",
"example": "If an account with that contact exists, a reset OTP has been sent."
}
}
}
}
}
},
"404": {
"description": "Sponsor account not found with the provided email or mobile.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"message": {
"type": "string",
"example": "user not found this email id"
}
}
}
}
}
"description": "OTP sent (response identical whether account exists or not)"
},
"422": {
"description": "Validation error \u2014 email or mobile required"
"description": "Validation error — email or mobile required"
}
}
}
@ -5614,16 +5306,6 @@
"type": "string",
"example": "HI"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"English",
"Arabic"
]
},
"fcm_token": {
"type": "string",
"example": "fcm_token_example"
@ -5648,6 +5330,10 @@
"type": "string",
"example": "4 Years"
},
"religion": {
"type": "string",
"example": "Muslim"
},
"bio": {
"type": "string",
"example": "Certified infant caregiver and housekeeper with excellent cooking skills."
@ -5676,19 +5362,6 @@
"type": "string",
"example": "Passport Verified"
},
"document_expiry_days": {
"type": "integer",
"example": -857
},
"document_expiry_status": {
"type": "string",
"example": "Visa Expired"
},
"visa_expiry_date": {
"type": "string",
"format": "date",
"example": "2024-02-15"
},
"preferred_job_type": {
"type": "string",
"example": "full-time"
@ -5722,19 +5395,6 @@
"type": "string",
"example": "Marina"
},
"rating": {
"type": "number",
"example": 4.5
},
"reviews_count": {
"type": "integer",
"example": 2
},
"photo": {
"type": "string",
"nullable": true,
"example": "https://example.com/photo.jpg"
},
"created_at": {
"type": "string",
"format": "date-time",
@ -5745,12 +5405,6 @@
"format": "date-time",
"example": "2026-05-20T14:54:26.000000Z"
},
"deleted_at": {
"type": "string",
"format": "date-time",
"nullable": true,
"example": null
},
"category": {
"type": "object",
"properties": {
@ -5776,32 +5430,6 @@
"name": {
"type": "string",
"example": "Cleaning"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"image_path": {
"type": "string",
"nullable": true,
"example": "skills/cleaning.png"
},
"pivot": {
"type": "object",
"properties": {
"worker_id": {
"type": "integer",
"example": 136
},
"skill_id": {
"type": "integer",
"example": 6
}
}
}
}
}
@ -5849,21 +5477,7 @@
},
"file_path": {
"type": "string",
"nullable": true,
"example": "uploads/documents/1716200000_passport_my_file.jpg"
},
"ocr_data": {
"type": "object",
"nullable": true,
"description": "OCR raw parsed data or confidence data"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"updated_at": {
"type": "string",
"format": "date-time"
}
}
},

View File

@ -40,7 +40,6 @@ export default function AdminLayout({ children, title = 'Dashboard' }) {
{ name: 'Support Tickets', href: '/admin/tickets', icon: LifeBuoy },
{ name: 'Frequently Asked Questions', href: '/admin/faqs', icon: HelpCircle },
{ name: 'Payments', href: '/admin/payments', icon: BadgeDollarSign },
{ name: 'Plan Configuration', href: '/admin/subscriptions', icon: CreditCard },
{ name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 },
{ name: 'System Audit Logs', href: '/admin/audit-logs', icon: History },
{ name: 'Charity Events', href: '/admin/events', icon: Heart },

View File

@ -286,8 +286,9 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
<TableHead onClick={() => handleSort('name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none">
Employer {sortField === 'name' && (sortOrder === 'desc' ? '↓' : '↑')}
</TableHead>
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Company Name</TableHead>
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Subscription Plan</TableHead>
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Country</TableHead>
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Verification Status</TableHead>
<TableHead onClick={() => handleSort('status')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
Status {sortField === 'status' && (sortOrder === 'desc' ? '↓' : '↑')}
</TableHead>
@ -309,6 +310,11 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
</div>
</div>
</TableCell>
<TableCell>
<div className="text-xs font-bold text-slate-700">
{emp.employer_profile?.company_name || 'N/A'}
</div>
</TableCell>
<TableCell>
<span className={`px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-tight ${
emp.subscription_status === 'active' ? 'bg-blue-50 text-blue-600 border border-blue-100' : 'bg-slate-100 text-slate-500'
@ -317,8 +323,10 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
</span>
</TableCell>
<TableCell>
<span className="text-xs font-bold text-slate-700">
{emp.employer_profile?.country || 'United Arab Emirates'}
<span className={`text-xs font-bold uppercase ${
emp.employer_profile?.verification_status === 'approved' ? 'text-emerald-600' : 'text-amber-500'
}`}>
{emp.employer_profile?.verification_status || 'pending'}
</span>
</TableCell>
<TableCell>
@ -402,7 +410,7 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
))
) : (
<TableRow>
<TableCell colSpan={5} className="py-12 text-center text-slate-400 font-semibold text-sm">
<TableCell colSpan={6} className="py-12 text-center text-slate-400 font-semibold text-sm">
No employers found.
</TableCell>
</TableRow>
@ -470,6 +478,10 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
Employer Profile
</h3>
<div className="grid grid-cols-2 gap-4 text-xs">
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Company / Household</span>
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.company_name || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Email Address</span>
<span className="font-bold text-slate-800">{selectedEmployer?.email || 'N/A'}</span>
@ -523,52 +535,28 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-xs space-y-4">
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest border-b border-slate-100 pb-2 flex items-center gap-2">
<ShieldCheck className="w-4 h-4 text-[#0F6E56]" />
Emirates ID Details
Emirates ID & Document Extracted Details
</h3>
<div className="grid grid-cols-2 gap-4 text-xs">
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">ID Number</span>
<span className="font-mono font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_number || 'N/A'}</span>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Emirates ID Number</span>
<span className="font-mono font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_number || 'Not Extracted / Pending'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Card Number</span>
<span className="font-mono font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_card_number || 'N/A'}</span>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Emirates ID Expiry</span>
<span className="font-bold text-slate-800">{formatDate(selectedEmployer?.employer_profile?.emirates_id_expiry)}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Full Name</span>
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_name || 'N/A'}</span>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Verification Status</span>
<span className={`font-bold uppercase ${
selectedEmployer?.employer_profile?.verification_status === 'approved' ? 'text-emerald-600' : 'text-amber-500'
}`}>
{selectedEmployer?.employer_profile?.verification_status || 'Pending'}
</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Date of Birth</span>
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_dob || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Nationality</span>
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.nationality || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Gender</span>
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_gender || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Issue Date</span>
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_issue_date || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Expiry Date</span>
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_expiry || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Occupation</span>
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_occupation || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Employer</span>
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_employer || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Issuing Place</span>
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_issue_place || 'N/A'}</span>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Trade License Expiry</span>
<span className="font-bold text-slate-800">{formatDate(selectedEmployer?.sponsor?.license_expiry)}</span>
</div>
</div>
</div>
@ -602,7 +590,15 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
required
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Company Name</label>
<input
type="text"
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
value={editForm.company_name}
onChange={e => setEditForm({ ...editForm, company_name: e.target.value })}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Email Address</label>

View File

@ -43,78 +43,19 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingPlan, setEditingPlan] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
// Form states
const [name, setName] = useState('');
const [price, setPrice] = useState('');
const [duration, setDuration] = useState('Monthly');
const [features, setFeatures] = useState('');
const handleCreateClick = () => {
setEditingPlan(null);
setName('');
setPrice('');
setDuration('Monthly');
setFeatures('');
setIsDialogOpen(true);
};
const handleEdit = (plan) => {
setEditingPlan(plan);
setName(plan.name);
setPrice(plan.price);
setDuration(plan.duration);
setFeatures(plan.features.join('\n'));
setIsDialogOpen(true);
};
const handleDelete = (id) => {
if (confirm('Are you sure you want to delete this plan?')) {
// router.delete(`/admin/subscriptions/plans/${id}`);
setPlans(plans.filter(p => p.id !== id));
}
};
const handleSave = (e) => {
e.preventDefault();
if (!name.trim() || !price) {
alert('Please fill out Name and Price.');
return;
}
const featuresArray = features
.split('\n')
.map(f => f.trim())
.filter(Boolean);
if (editingPlan) {
setPlans(plans.map(p => p.id === editingPlan.id ? {
...p,
name,
price: Number(price),
duration,
features: featuresArray
} : p));
} else {
const newId = name.toLowerCase().replace(/\s+/g, '-');
setPlans([...plans, {
id: newId || `plan-${Date.now()}`,
name,
price: Number(price),
duration,
features: featuresArray,
status: 'Active'
}]);
}
setIsDialogOpen(false);
};
const filteredPlans = plans.filter(plan =>
plan.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
plan.duration.toLowerCase().includes(searchQuery.toLowerCase()) ||
plan.features.some(f => f.toLowerCase().includes(searchQuery.toLowerCase()))
);
return (
<AdminLayout title="Subscription Management">
<Head title="Plans Management" />
@ -127,13 +68,11 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
<input
type="text"
placeholder="Search plans..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500/10 focus:border-teal-500 outline-none transition-all"
/>
</div>
<button
onClick={handleCreateClick}
onClick={() => { setEditingPlan(null); setIsDialogOpen(true); }}
className="bg-[#0F6E56] text-white px-4 py-2 rounded-lg text-sm font-semibold flex items-center justify-center space-x-2 hover:bg-[#085041] transition-colors shadow-sm"
>
<Plus className="w-4 h-4" />
@ -155,14 +94,7 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
</TableRow>
</TableHeader>
<TableBody>
{filteredPlans.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-slate-400 text-sm">
No plans found
</TableCell>
</TableRow>
) : (
filteredPlans.map((plan) => (
{plans.map((plan) => (
<TableRow key={plan.id} className="hover:bg-slate-50/50 transition-colors">
<TableCell className="py-4">
<div className="flex items-center space-x-3">
@ -213,14 +145,13 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
</div>
</TableCell>
</TableRow>
))
)}
))}
</TableBody>
</Table>
</div>
</div>
{/* Plan Modal */}
{/* Plan Modal (Placeholder UI) */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="sm:max-w-[500px] rounded-[24px]">
<DialogHeader>
@ -232,70 +163,44 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSave}>
<div className="grid gap-6 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Plan Name</label>
<input
required
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
placeholder="e.g. Pro Search"
/>
<input className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none" placeholder="e.g. Pro Search" />
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Price (AED)</label>
<input
required
type="number"
value={price}
onChange={(e) => setPrice(e.target.value)}
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
placeholder="299"
/>
<input type="number" className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none" placeholder="299" />
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label>
<select
value={duration}
onChange={(e) => setDuration(e.target.value)}
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
>
<option value="Monthly">Monthly</option>
<option value="Quarterly">Quarterly</option>
<option value="Yearly">Yearly</option>
<select className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none">
<option>Monthly</option>
<option>Quarterly</option>
<option>Yearly</option>
</select>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Features (one per line)</label>
<textarea
value={features}
onChange={(e) => setFeatures(e.target.value)}
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none min-h-[100px]"
placeholder="Unlimited search&#10;Verified badges"
/>
<textarea className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none min-h-[100px]" placeholder="Unlimited search&#10;Verified badges" />
</div>
</div>
<DialogFooter className="sm:justify-end gap-3 pt-4 border-t border-slate-100">
<DialogFooter className="sm:justify-end gap-3">
<button
type="button"
onClick={() => setIsDialogOpen(false)}
className="px-6 py-2.5 rounded-xl text-sm font-bold text-slate-400 uppercase tracking-widest hover:bg-slate-50 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="bg-[#0F6E56] text-white px-8 py-2.5 rounded-xl text-sm font-bold uppercase tracking-widest shadow-lg shadow-teal-500/20 active:scale-95 transition-all"
>
Save Changes
</button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</AdminLayout>

View File

@ -208,7 +208,7 @@ export default function WorkerManagement({ workers }) {
})).filter(worker => {
const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
worker.email.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || worker.status?.toLowerCase() === statusFilter.toLowerCase();
const matchesStatus = statusFilter === 'all' || worker.status === statusFilter;
return matchesSearch && matchesStatus;
});
@ -250,13 +250,15 @@ export default function WorkerManagement({ workers }) {
className="w-full pl-10 pr-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm focus:bg-white focus:ring-4 focus:ring-[#0F6E56]/10 outline-none transition-all font-medium"
/>
</div>
<button className="p-2.5 bg-white border border-slate-200 rounded-xl text-slate-400 hover:bg-slate-50 transition-colors">
<Filter className="w-4 h-4" />
</button>
</div>
<div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mr-2">Lifecycle Filter:</span>
<div className="flex bg-slate-100 p-1 rounded-xl">
{['all', 'active', 'hired', 'suspended'].map((f) => (
{['all', 'active', 'inactive', 'suspended'].map((f) => (
<button
key={f}
onClick={() => setStatusFilter(f)}
@ -375,13 +377,9 @@ export default function WorkerManagement({ workers }) {
<Badge
variant="outline"
className={`px-2 py-0.5 rounded-full font-black text-[9px] uppercase tracking-wider block w-fit ${
worker.status?.toLowerCase() === 'active'
worker.status === 'active'
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
: worker.status?.toLowerCase() === 'hired'
? 'bg-blue-50 text-blue-700 border-blue-200'
: worker.status?.toLowerCase() === 'suspended'
? 'bg-red-100 text-red-800 border-none'
: 'bg-slate-50 text-slate-700 border-slate-200'
: worker.status === 'suspended' ? 'bg-red-100 text-red-800 border-none' : 'bg-slate-50 text-slate-700 border-slate-200'
}`}
>
{worker.status}
@ -528,7 +526,15 @@ export default function WorkerManagement({ workers }) {
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"
/>
</div>
<div>
<label className="text-[10px] font-bold text-slate-500">Religion</label>
<input
type="text"
value={editForm.religion}
onChange={e => setEditForm({ ...editForm, religion: 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"
/>
</div>
</div>
</div>
@ -710,7 +716,10 @@ export default function WorkerManagement({ workers }) {
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Visa Status</label>
<span className="text-slate-800 font-extrabold">{selectedWorker?.visa_status || 'N/A'}</span>
</div>
<div>
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Religion</label>
<span className="text-slate-800 font-extrabold">{selectedWorker?.religion || 'N/A'}</span>
</div>
<div>
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Age</label>
<span className="text-slate-800 font-extrabold">{selectedWorker?.age || 'N/A'}</span>
@ -751,26 +760,14 @@ export default function WorkerManagement({ workers }) {
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
<span className="font-mono font-bold text-slate-800">{passportDoc.ocr_data?.passport_number || passportDoc.number || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Surname</span>
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.surname || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Given Names</span>
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.given_names || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sex</span>
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.sex || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Birth</span>
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_birth || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Birth</span>
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.place_of_birth || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Nationality</span>
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.nationality || 'N/A'}</span>
@ -779,6 +776,10 @@ export default function WorkerManagement({ workers }) {
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issuing Country</span>
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.issuing_country || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Birth</span>
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.place_of_birth || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Issue</span>
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_issue || passportDoc.issue_date || 'N/A'}</span>
@ -787,18 +788,6 @@ export default function WorkerManagement({ workers }) {
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Expiry</span>
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_expiry || passportDoc.expiry_date || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Authority</span>
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.authority || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Document Type</span>
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.document_type || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Personal Number</span>
<span className="font-mono font-bold text-slate-800">{passportDoc.ocr_data?.personal_number || 'N/A'}</span>
</div>
</div>
{passportDoc.file_path && (
<div className="pt-2 border-t border-slate-200/60 flex justify-end">
@ -833,14 +822,6 @@ export default function WorkerManagement({ workers }) {
return (
<div className="p-4 bg-slate-50 rounded-xl border border-slate-100 space-y-3">
<div className="grid grid-cols-2 gap-3 text-xs">
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Entry Permit No</span>
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.entry_permit_no || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">UID No</span>
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.uid_no || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">File Number</span>
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.file_number || visaDoc.number || 'N/A'}</span>
@ -849,55 +830,6 @@ export default function WorkerManagement({ workers }) {
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">ID Number</span>
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.id_number || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Full Name</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.full_name || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Name</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.name || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Visa Type</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.visa_type || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Profession</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.profession || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sponsor Name</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.sponsor_name || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Nationality</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.nationality || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Country</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.country || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport No</span>
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.passport_no || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.passport_number || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Birth</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.date_of_birth || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Birth</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.place_of_birth || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Issue</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.place_of_issue || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issue Date</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.issue_date || visaDoc.issue_date || 'N/A'}</span>
@ -906,18 +838,17 @@ export default function WorkerManagement({ workers }) {
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Expiry Date</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.expiry_date || visaDoc.expiry_date || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Accompanied By</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.accompanied_by || 'N/A'}</span>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.passport_number || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Document Type</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.document_type || 'N/A'}</span>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Issue</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.place_of_issue || 'N/A'}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">OCR Accuracy</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.ocr_accuracy || 'N/A'}</span>
<div className="col-span-2">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sponsor</span>
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.sponsor || 'N/A'}</span>
</div>
</div>
{visaDoc.file_path && (
@ -938,18 +869,50 @@ export default function WorkerManagement({ workers }) {
})()}
</div>
{/* Compliance Notes */}
<div className="space-y-2 bg-yellow-50/60 p-4 border border-yellow-200 rounded-2xl">
<label className="text-[10px] font-black text-yellow-800 uppercase tracking-widest flex items-center gap-1">
<FileText className="w-3.5 h-3.5" />
<span>Internal Admin Compliance Notes Logs</span>
</label>
<textarea
rows="3"
placeholder="Write admin logs regarding candidate verification reviews or user complaints..."
className="w-full bg-white border border-yellow-200 rounded-xl p-3 text-xs font-bold text-slate-700 focus:ring-2 focus:ring-yellow-500/20 outline-none"
value={adminNotes}
onChange={e => setAdminNotes(e.target.value)}
/>
</div>
</div>
{/* Right Column (1/3) */}
<div className="space-y-6">
{/* Contact Detail */}
{/* Contact & Location */}
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm space-y-4">
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Contact Detail</h3>
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Contact & Location</h3>
<div className="space-y-3.5 text-xs font-bold text-slate-700">
<div className="flex items-center space-x-2 bg-slate-50 p-2.5 rounded-xl border border-slate-100">
<Phone className="w-4 h-4 text-teal-600 flex-shrink-0" />
<span className="text-slate-800 font-extrabold">{selectedWorker?.phone || '+971 52 489 1209'}</span>
</div>
<div className="grid grid-cols-2 gap-3 pt-1">
<div>
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Country</label>
<span className="text-slate-800 font-extrabold block">{selectedWorker?.country || 'N/A'}</span>
</div>
<div>
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">City</label>
<span className="text-slate-800 font-extrabold block">{selectedWorker?.city || 'N/A'}</span>
</div>
<div className="col-span-2 border-t border-slate-100 pt-2">
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Area</label>
<span className="text-slate-800 font-extrabold block">{selectedWorker?.area || 'N/A'}</span>
</div>
<div className="col-span-2 border-t border-slate-100 pt-2">
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Preferred Location</label>
<span className="text-slate-800 font-extrabold block text-xs leading-normal">{selectedWorker?.preferred_location || 'N/A'}</span>
</div>
</div>
</div>
</div>
@ -989,6 +952,31 @@ export default function WorkerManagement({ workers }) {
)}
</div>
{/* Fraud Flagging */}
<div className="border-t border-slate-100 pt-4">
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-2">Trustworthiness</label>
{selectedWorker?.is_fraud ? (
<div className="space-y-2">
<div className="flex items-center space-x-1.5 text-red-600 bg-red-50 border border-red-100 px-3 py-2 rounded-xl text-xs font-extrabold">
<ShieldAlert className="w-4 h-4" />
<span>Flagged as Fraud</span>
</div>
<button
onClick={() => handleFlagFraud(selectedWorker.id, false, '')}
className="w-full py-2 bg-slate-900 hover:bg-slate-800 text-white rounded-xl text-[10px] font-black uppercase tracking-wider transition-all"
>
Clear Fraud Flag
</button>
</div>
) : (
<button
onClick={() => handleFlagFraud(selectedWorker.id, true, 'Flagged by system admin review.')}
className="w-full py-2 border border-red-200 hover:bg-red-50 text-red-600 rounded-xl text-[10px] font-black uppercase tracking-wider transition-all"
>
Flag as Fraud
</button>
)}
</div>
</div>
</div>
</div>

View File

@ -351,6 +351,13 @@ export default function Show({ worker }) {
<span className="font-black text-slate-900 text-xs uppercase">{worker.live_in_out}</span>
</div>
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<MapPin className="w-4 h-4 text-slate-500" />
<span>{t('preferred_location', 'Preferred Location')}</span>
</div>
<span className="font-extrabold text-slate-900 text-xs capitalize">{worker.preferred_location || t('any', 'Any')}</span>
</div>
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">

View File

@ -30,9 +30,10 @@ protected function setUp(): void
EmployerProfile::create([
'user_id' => $this->employer->id,
'address' => 'Original Address',
'company_name' => 'Original Company',
'phone' => '+971501112222',
'verification_status' => 'approved',
'language' => 'English',
'notifications' => true,
]);
}
@ -53,8 +54,9 @@ public function test_employer_can_get_profile()
'profile' => [
'name',
'email',
'company_name',
'phone',
'address',
'language',
'notifications',
'verification_status',
]
@ -62,7 +64,7 @@ public function test_employer_can_get_profile()
]);
$this->assertEquals('Original Name', $response->json('data.profile.name'));
$this->assertEquals('Original Address', $response->json('data.profile.address'));
$this->assertEquals('Original Company', $response->json('data.profile.company_name'));
}
/**
@ -76,7 +78,8 @@ public function test_employer_can_update_profile()
'name' => 'Updated Name',
'email' => 'employer_updated@example.com',
'phone' => '+971509999999',
'address' => 'Updated Address Info',
'company_name' => 'Updated Company Corp',
'language' => 'Arabic',
'notifications' => false,
]);
@ -88,8 +91,9 @@ public function test_employer_can_update_profile()
'profile' => [
'name',
'email',
'company_name',
'phone',
'address',
'language',
'notifications',
]
]
@ -103,94 +107,67 @@ public function test_employer_can_update_profile()
$this->assertDatabaseHas('employer_profiles', [
'user_id' => $this->employer->id,
'address' => 'Updated Address Info',
'company_name' => 'Updated Company Corp',
'language' => 'Arabic',
'notifications' => false,
]);
}
/**
* Test employer can update profile with optional Emirates ID fields.
* Test employer can update password when providing correct current password.
*/
public function test_employer_can_update_profile_with_emirates_id_fields()
public function test_employer_can_update_password()
{
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
])->postJson('/api/employers/profile/update', [
'name' => 'Ahmad',
'email' => 'ahmad@example.com',
'phone' => '+971509990001',
'address' => 'Villa 12, Jumeirah 2',
'notifications' => true,
'id_number' => '784-1987-5493842-5',
'card_number' => '151023946',
'full_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
'date_of_birth' => '14/04/1987',
'nationality' => 'Bangladesh',
'gender' => 'M',
'issue_date' => '12/12/2025',
'expiry_date' => '11/12/2027',
'occupation' => 'Electrician',
'employer' => 'Msj International Technical Services L.L.C UAE',
'issuing_place' => 'Dubai',
]);
$response->assertStatus(200)
->assertJson([
'success' => true,
'data' => [
'profile' => [
'name' => 'Ahmad',
'email' => 'ahmad@example.com',
'phone' => '+971509990001',
'address' => 'Villa 12, Jumeirah 2',
'id_number' => '784-1987-5493842-5',
'card_number' => '151023946',
'full_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
'gender' => 'M',
]
]
]);
$this->assertDatabaseHas('employer_profiles', [
'user_id' => $this->employer->id,
'emirates_id_number' => '784-1987-5493842-5',
'emirates_id_card_number' => '151023946',
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
'emirates_id_gender' => 'M',
]);
}
/**
* Test employer cannot change an existing Emirates ID number.
*/
public function test_employer_cannot_change_existing_emirates_id_number()
{
// First, set the Emirates ID
$profile = $this->employer->employerProfile;
$profile->emirates_id_number = '784-1987-5493842-5';
$profile->save();
// Attempt to change to a different ID
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
])->postJson('/api/employers/profile/update', [
'name' => 'Original Name',
'email' => 'employer@example.com',
'phone' => '+971501112222',
'address' => 'Original Address',
'id_number' => '784-9999-0000000-0',
'company_name' => 'Original Company',
'language' => 'English',
'notifications' => true,
'current_password' => 'Password@123',
'new_password' => 'NewSecurePassword@123',
'new_password_confirmation' => 'NewSecurePassword@123',
]);
$response->assertStatus(200);
// Verify password hash updated
$this->assertTrue(Hash::check('NewSecurePassword@123', $this->employer->fresh()->password));
}
/**
* Test employer password update fails with incorrect current password.
*/
public function test_employer_password_update_fails_with_incorrect_current_password()
{
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
])->postJson('/api/employers/profile/update', [
'name' => 'Original Name',
'email' => 'employer@example.com',
'phone' => '+971501112222',
'company_name' => 'Original Company',
'language' => 'English',
'notifications' => true,
'current_password' => 'WrongCurrentPassword',
'new_password' => 'NewSecurePassword@123',
'new_password_confirmation' => 'NewSecurePassword@123',
]);
$response->assertStatus(422)
->assertJson([
'success' => false,
->assertJsonStructure([
'success',
'message',
'errors' => [
'id_number' => ['Emirates ID mismatch.']
'current_password'
]
]);
// Verify ID was NOT changed
$this->assertEquals('784-1987-5493842-5', $profile->fresh()->emirates_id_number);
// Verify password was NOT changed
$this->assertTrue(Hash::check('Password@123', $this->employer->fresh()->password));
}
/**
@ -241,51 +218,4 @@ public function test_employer_dashboard_returns_only_two_charity_drives()
$this->assertFalse($titles->contains('Charity Drive 1'));
$this->assertFalse($titles->contains('Pending Charity Drive'));
}
public function test_employer_dashboard_returns_total_workers_active()
{
// Delete any existing workers seeded or created to make count precise
\App\Models\Worker::query()->delete();
// Create 3 active workers
\App\Models\Worker::create([
'name' => 'Active 1', 'email' => 'a1@test.com', 'phone' => '+971501110001',
'password' => bcrypt('password'), 'status' => 'active', 'nationality' => 'Indian',
'preferred_location' => 'Dubai', 'age' => 25, 'salary' => 1500, 'experience' => '3 Years',
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
]);
\App\Models\Worker::create([
'name' => 'Active 2', 'email' => 'a2@test.com', 'phone' => '+971501110002',
'password' => bcrypt('password'), 'status' => 'active', 'nationality' => 'Indian',
'preferred_location' => 'Dubai', 'age' => 26, 'salary' => 1500, 'experience' => '3 Years',
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
]);
\App\Models\Worker::create([
'name' => 'Active 3', 'email' => 'a3@test.com', 'phone' => '+971501110003',
'password' => bcrypt('password'), 'status' => 'active', 'nationality' => 'Indian',
'preferred_location' => 'Dubai', 'age' => 27, 'salary' => 1500, 'experience' => '3 Years',
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
]);
// Create 2 inactive/hidden workers
\App\Models\Worker::create([
'name' => 'Inactive 1', 'email' => 'i1@test.com', 'phone' => '+971502220001',
'password' => bcrypt('password'), 'status' => 'hidden', 'nationality' => 'Indian',
'preferred_location' => 'Dubai', 'age' => 28, 'salary' => 1500, 'experience' => '3 Years',
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
]);
\App\Models\Worker::create([
'name' => 'Inactive 2', 'email' => 'i2@test.com', 'phone' => '+971502220002',
'password' => bcrypt('password'), 'status' => 'Hired', 'nationality' => 'Indian',
'preferred_location' => 'Dubai', 'age' => 29, 'salary' => 1500, 'experience' => '3 Years',
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
]);
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
])->getJson('/api/employers/dashboard');
$response->assertStatus(200);
$this->assertEquals(3, $response->json('data.stats.total_workers'));
}
}

View File

@ -435,184 +435,11 @@ public function test_worker_list_includes_document_expiry_status()
$response->assertStatus(200);
$workers = $response->json('data.workers');
// Find the worker we just created in the response
$matchingWorker = collect($workers)->firstWhere('id', $worker->id);
$this->assertNotNull($matchingWorker);
$this->assertEquals(45, $matchingWorker['document_expiry_days']);
$this->assertEquals('Visa expires in 45 days', $matchingWorker['document_expiry_status']);
}
public function test_worker_list_api_has_full_details_and_pagination()
{
$worker = Worker::create([
'name' => 'Full Detail Worker',
'email' => 'full_detail@example.com',
'phone' => '+971509999888',
'password' => bcrypt('password'),
'nationality' => 'Indian',
'verified' => true,
'status' => 'active',
'preferred_location' => 'Dubai Marina',
'live_in_out' => 'live_out',
'gender' => 'male',
'in_country' => true,
'visa_status' => 'Tourist Visa',
'preferred_job_type' => 'full-time',
'fcm_token' => 'fcm_token_example',
'age' => 36,
'salary' => 1500.00,
'experience' => '4 Years',
'language' => 'English, Arabic',
'availability' => 'Immediate',
'religion' => 'Christian',
'bio' => 'Experienced helper',
]);
$doc1 = WorkerDocument::create([
'worker_id' => $worker->id,
'type' => 'passport',
'number' => 'YB98239279327',
'issue_date' => '2017-07-05',
'expiry_date' => '2022-07-05',
'ocr_accuracy' => 99,
'ocr_data' => ['authority' => 'FMP LLC'],
]);
$doc2 = WorkerDocument::create([
'worker_id' => $worker->id,
'type' => 'visa',
'number' => '343434',
'issue_date' => '2022-02-16',
'expiry_date' => '2024-02-15',
'ocr_accuracy' => 99,
'ocr_data' => ['sponsor' => 'string'],
]);
// Add a skill
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Ironing']);
$worker->skills()->attach($skill->id);
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
])->getJson('/api/employers/workers?page=1&per_page=15');
$response->assertStatus(200);
$response->assertJsonStructure([
'success',
'data' => [
'workers',
'pagination' => [
'total',
'per_page',
'current_page',
'last_page',
],
],
]);
$workers = $response->json('data.workers');
$matchingWorker = collect($workers)->firstWhere('id', $worker->id);
$this->assertNotNull($matchingWorker);
$this->assertEquals('+971509999888', $matchingWorker['phone']);
$this->assertEquals('1500.00', $matchingWorker['salary']);
$this->assertEquals('active', $matchingWorker['status']);
$this->assertEquals('English, Arabic', $matchingWorker['language']);
$this->assertEquals('fcm_token_example', $matchingWorker['fcm_token']);
$this->assertEquals('2024-02-15', $matchingWorker['visa_expiry_date']);
// Check skills object array
$this->assertIsArray($matchingWorker['skills']);
$this->assertCount(1, $matchingWorker['skills']);
$this->assertEquals('Ironing', $matchingWorker['skills'][0]['name']);
// Check documents object array
$this->assertIsArray($matchingWorker['documents']);
$this->assertCount(2, $matchingWorker['documents']);
$this->assertEquals('passport', $matchingWorker['documents'][0]['type']);
$this->assertEquals('visa', $matchingWorker['documents'][1]['type']);
}
public function test_worker_detail_api_has_full_details_without_religion_and_cleaned_ocr_data()
{
$worker = Worker::create([
'name' => 'Detail Worker Test',
'email' => 'detail_test@example.com',
'phone' => '+971509999777',
'password' => bcrypt('password'),
'nationality' => 'Indian',
'verified' => true,
'status' => 'active',
'preferred_location' => 'Dubai Marina',
'live_in_out' => 'live_out',
'gender' => 'male',
'in_country' => true,
'visa_status' => 'Tourist Visa',
'preferred_job_type' => 'full-time',
'fcm_token' => 'fcm_token_example',
'age' => 36,
'salary' => 1500.00,
'experience' => '4 Years',
'language' => 'English, Arabic',
'availability' => 'Immediate',
'religion' => 'Christian',
'bio' => 'Experienced helper',
]);
$doc1 = WorkerDocument::create([
'worker_id' => $worker->id,
'type' => 'passport',
'number' => 'YB98239279327',
'issue_date' => '2017-07-05',
'expiry_date' => '2022-07-05',
'ocr_accuracy' => 99,
'ocr_data' => ['authority' => 'FMP LLC'],
]);
$doc2 = WorkerDocument::create([
'worker_id' => $worker->id,
'type' => 'visa',
'number' => '343434',
'issue_date' => '2022-02-16',
'expiry_date' => '2024-02-15',
'ocr_accuracy' => 99,
'ocr_data' => [
'sponsor' => 'some sponsor',
'sponsor_name' => 'some sponsor name',
'valid_until' => '2024-02-15',
'expiry_date' => '2024-02-15'
],
]);
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
])->getJson('/api/employers/workers/' . $worker->id);
$response->assertStatus(200);
$wData = $response->json('data.worker');
$this->assertNotNull($wData);
$this->assertEquals('+971509999777', $wData['phone']);
$this->assertEquals('detail_test@example.com', $wData['email']);
$this->assertEquals('1500.00', $wData['salary']);
$this->assertEquals('active', $wData['status']);
$this->assertEquals('English, Arabic', $wData['language']);
$this->assertEquals('fcm_token_example', $wData['fcm_token']);
$this->assertEquals('2024-02-15', $wData['visa_expiry_date']);
// Assert religion is removed
$this->assertArrayNotHasKey('religion', $wData);
// Check documents object array
$this->assertIsArray($wData['documents']);
$this->assertCount(2, $wData['documents']);
// Assert visa ocr_data is cleaned up
$visaDoc = collect($wData['documents'])->firstWhere('type', 'visa');
$this->assertNotNull($visaDoc);
$this->assertArrayNotHasKey('sponsor', $visaDoc['ocr_data']);
$this->assertArrayNotHasKey('valid_until', $visaDoc['ocr_data']);
$this->assertEquals('some sponsor name', $visaDoc['ocr_data']['sponsor_name']);
$this->assertEquals('2024-02-15', $visaDoc['ocr_data']['expiry_date']);
}
}

View File

@ -571,36 +571,4 @@ public function test_sponsor_registration_with_emirates_id_and_separate_license(
$sponsor->refresh();
$this->assertEquals('2028-06-12 00:00:00', $sponsor->license_expiry->toDateTimeString());
}
/**
* Test worker forgot password user not found error.
*/
public function test_worker_forgot_password_user_not_found()
{
$response = $this->postJson('/api/workers/forgot-password', [
'phone' => '+971501111111',
]);
$response->assertStatus(404)
->assertJson([
'success' => false,
'message' => 'user not found this mobile number',
]);
}
/**
* Test sponsor forgot password user not found error.
*/
public function test_sponsor_forgot_password_user_not_found()
{
$response = $this->postJson('/api/sponsors/forgot-password', [
'email' => 'nonexistent.sponsor@example.com',
]);
$response->assertStatus(404)
->assertJson([
'success' => false,
'message' => 'user not found this email id',
]);
}
}

View File

@ -90,30 +90,14 @@ public function test_employer_web_registration_with_emirates_id()
]);
$profile = EmployerProfile::where('phone', '501234567')->first();
$this->assertEquals('784-1987-5493842-5', $profile->emirates_id_number);
$this->assertEquals('2027-12-11', $profile->emirates_id_expiry);
$this->assertEquals('Mohammad Jobaier Mohammad Abul Kalam', $profile->emirates_id_name);
$this->assertEquals('1987-04-14', $profile->emirates_id_dob);
$this->assertEquals('Bangladesh', $profile->nationality);
$this->assertEquals('2025-12-12', $profile->emirates_id_issue_date);
$this->assertEquals('Msj International Technical Services L.L.C UAE', $profile->emirates_id_employer);
$this->assertEquals('Dubai', $profile->emirates_id_issue_place);
$this->assertEquals('Electrician', $profile->emirates_id_occupation);
$this->assertNotNull($profile->emirates_id_number);
$this->assertNotNull($profile->emirates_id_expiry);
$this->assertDatabaseHas('sponsors', [
'email' => 'abdullah@example.com',
'mobile' => '501234567',
'emirates_id_file' => null, // File is not stored
'address' => 'Villa 14, Al Safa, Dubai',
'emirates_id' => '784-1987-5493842-5',
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
'emirates_id_dob' => '1987-04-14',
'nationality' => 'Bangladesh',
'emirates_id_issue_date' => '2025-12-12',
'emirates_id_expiry' => '2027-12-11',
'emirates_id_employer' => 'Msj International Technical Services L.L.C UAE',
'emirates_id_issue_place' => 'Dubai',
'emirates_id_occupation' => 'Electrician',
]);
}
@ -160,8 +144,8 @@ public function test_employer_web_registration_with_two_sided_emirates_id()
$this->assertTrue(session('employer_emirates_id_uploaded'));
$pendingRegAfterId = session('pending_employer_registration');
$this->assertEquals('784-1987-5493842-5', $pendingRegAfterId['emirates_id_number']); // Mock fallback value
$this->assertEquals('2027-12-11', $pendingRegAfterId['emirates_id_expiry']); // Mock fallback value
$this->assertEquals('784-1988-5310327-2', $pendingRegAfterId['emirates_id_number']); // Mock fallback value
$this->assertEquals('2028-04-11', $pendingRegAfterId['emirates_id_expiry']); // Mock fallback value
// Step 4: Payment Choice
$responseStep4 = $this->post('/employer/register-payment', [
@ -188,15 +172,8 @@ public function test_employer_web_registration_with_two_sided_emirates_id()
$profile = EmployerProfile::where('phone', '509876543')->first();
$this->assertNotNull($profile);
$this->assertEquals('784-1987-5493842-5', $profile->emirates_id_number);
$this->assertEquals('2027-12-11', $profile->emirates_id_expiry);
$this->assertEquals('Mohammad Jobaier Mohammad Abul Kalam', $profile->emirates_id_name);
$this->assertEquals('1987-04-14', $profile->emirates_id_dob);
$this->assertEquals('Bangladesh', $profile->nationality);
$this->assertEquals('2025-12-12', $profile->emirates_id_issue_date);
$this->assertEquals('Msj International Technical Services L.L.C UAE', $profile->emirates_id_employer);
$this->assertEquals('Dubai', $profile->emirates_id_issue_place);
$this->assertEquals('Electrician', $profile->emirates_id_occupation);
$this->assertEquals('784-1988-5310327-2', $profile->emirates_id_number);
$this->assertEquals('2028-04-11', $profile->emirates_id_expiry);
$this->assertEquals('Apartment 402, Al Nahda, Sharjah', $profile->address);
}
}

View File

@ -536,103 +536,6 @@ public function test_register_passport_and_visa_with_direct_json_payload()
$this->assertEquals('Sponsor Name', $visaDoc->ocr_data['sponsor']);
}
/**
* Test worker registration with the specific passport fields and ocr_accuracy.
*/
public function test_register_passport_with_full_ocr_fields()
{
$response = $this->postJson('/api/workers/register', [
'name' => 'Temp Name',
'phone' => '+971507777777',
'salary' => 2000,
'password' => 'secret123',
'language' => 'HI',
'passport' => [
'document_type' => 'P',
'issuing_country' => 'HAS',
'surname' => 'KHALED',
'given_names' => 'KHALED AL',
'passport_number' => 'Y34B67890',
'nationality' => 'RE1',
'date_of_birth' => '1990-05-14',
'date_of_expiry' => '2022-05-07',
'place_of_birth' => 'SHARJAH',
'authority' => 'MINISTRY OF INTERIOR U.S. 21 is ,,,, is',
'sex' => 'M',
'date_of_issue' => '2017-07-05',
'ocr_accuracy' => 98.7,
]
]);
$response->assertStatus(201);
$workerId = $response->json('data.worker.id');
$this->assertDatabaseHas('worker_documents', [
'worker_id' => $workerId,
'type' => 'passport',
'number' => 'Y34B67890',
'expiry_date' => '2022-05-07',
'issue_date' => '2017-07-05',
'ocr_accuracy' => 98.7,
]);
$passportDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'passport')->first();
$this->assertNotNull($passportDoc->ocr_data);
$this->assertEquals('KHALED AL', $passportDoc->ocr_data['given_names']);
$this->assertEquals('KHALED', $passportDoc->ocr_data['surname']);
$this->assertEquals('MINISTRY OF INTERIOR U.S. 21 is ,,,, is', $passportDoc->ocr_data['authority']);
$this->assertEquals(98.7, $passportDoc->ocr_data['ocr_accuracy']);
}
/**
* Test worker registration with the specific visa fields and ocr_accuracy.
*/
public function test_register_visa_with_full_ocr_fields()
{
$response = $this->postJson('/api/workers/register', [
'name' => 'Temp Name',
'phone' => '+971507777778',
'salary' => 2000,
'password' => 'secret123',
'language' => 'HI',
'visa' => [
'document_type' => 'uae_visa',
'country' => 'United Arab Emirates',
'visa_type' => 'ENTRY PERMIT',
'entry_permit_no' => '77003098 / 2019 / 204',
'issue_date' => '',
'valid_until' => '04-MAR-2019',
'uid_no' => '207404887',
'full_name' => 'Mr.MUHAMMAD NADEEM RASHEED',
'nationality' => 'PAKISTAN',
'place_of_birth' => 'SIALKOT PAK',
'date_of_birth' => '25-JUN-1999',
'passport_no' => 'EN9458281',
'profession' => 'SALES REPRESENTATIVE',
'sponsor_name' => '',
'ocr_accuracy' => 97.4,
]
]);
$response->assertStatus(201);
$workerId = $response->json('data.worker.id');
$this->assertDatabaseHas('worker_documents', [
'worker_id' => $workerId,
'type' => 'visa',
'number' => '77003098 / 2019 / 204',
'expiry_date' => '2019-03-04',
'ocr_accuracy' => 97.4,
]);
$visaDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'visa')->first();
$this->assertNotNull($visaDoc->ocr_data);
$this->assertEquals('uae_visa', $visaDoc->ocr_data['document_type']);
$this->assertEquals('ENTRY PERMIT', $visaDoc->ocr_data['visa_type']);
$this->assertEquals('Mr.MUHAMMAD NADEEM RASHEED', $visaDoc->ocr_data['full_name']);
$this->assertEquals(97.4, $visaDoc->ocr_data['ocr_accuracy']);
}
/**
* Test worker passport and visa registration when they are passed as JSON strings.
*/

View File

@ -23,7 +23,7 @@ export default defineConfig({
port: 5173,
cors: true,
hmr: {
host: '192.168.0.166',
host: '192.168.29.131',
},
watch: {
ignored: ['**/storage/framework/views/**'],