This commit is contained in:
pavithrak27 2026-06-24 21:34:20 +05:30
commit 9cea1f2605
44 changed files with 5219 additions and 925 deletions

View File

@ -21,32 +21,54 @@ public function showLogin()
*/
public function login(Request $request)
{
$credentials = $request->validate([
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
'email' => ['required', 'email'],
'password' => ['required'],
], [
'email.required' => 'Please enter your email address.',
'email.email' => 'Please enter a valid email address.',
'password.required' => 'Please enter your password.',
]);
if ($validator->fails()) {
return back()->withErrors($validator)->withInput($request->only('email'));
}
$credentials = $validator->validated();
// Database-backed authentication check
$user = \App\Models\User::where('email', $credentials['email'])->first();
if ($user && \Illuminate\Support\Facades\Hash::check($credentials['password'], $user->password) && $user->role === 'admin') {
$request->session()->regenerate();
auth()->login($user);
session(['user' => (object)[
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'role' => $user->role,
]]);
return redirect()->intended('/admin/dashboard');
if (!$user) {
return back()->withErrors([
'email' => 'This email is not registered.',
])->withInput($request->only('email'));
}
return back()->withErrors([
'email' => 'The provided credentials do not match our records. (Use admin@example.com / password)',
]);
if ($user->role !== 'admin') {
return back()->withErrors([
'email' => 'This email is not registered as an admin.',
])->withInput($request->only('email'));
}
if (!\Illuminate\Support\Facades\Hash::check($credentials['password'], $user->password)) {
return back()->withErrors([
'password' => 'The password you entered is incorrect.',
])->withInput($request->only('email'));
}
$request->session()->regenerate();
auth()->login($user);
session(['user' => (object)[
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'role' => $user->role,
]]);
return redirect()->intended('/admin/dashboard');
}
/**

View File

@ -11,12 +11,89 @@
class EmployerAnnouncementController extends Controller
{
/**
* Get all announcements posted by this employer.
* Get all approved announcements for employers.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getAnnouncements(Request $request)
{
try {
$page = (int)$request->input('page', 1);
$perPage = (int)$request->input('per_page', 15);
$query = Announcement::with(['employer.employerProfile', 'sponsor'])->where('status', 'approved')->latest();
$total = $query->count();
$offset = ($page - 1) * $perPage;
$announcements = $query->skip($offset)->take($perPage)->get()
->map(function ($announcement) {
$postedBy = 'System';
$organization = 'Migrant Support';
if ($announcement->sponsor_id) {
$postedBy = $announcement->sponsor->full_name;
$organization = $announcement->sponsor->organization_name;
} elseif ($announcement->employer_id) {
$postedBy = $announcement->employer->name;
$organization = $announcement->employer->employerProfile->company_name ?? 'Employer';
}
// Decode charity details if they exist in json
$charityDetails = null;
$content = $announcement->body;
if (strpos($announcement->body, '{"type":"Charity"') === 0) {
$decoded = json_decode($announcement->body, true);
if ($decoded) {
$charityDetails = $decoded;
$content = $decoded['content'] ?? $announcement->body;
}
}
return [
'id' => $announcement->id,
'title' => $announcement->title,
'body' => $content,
'type' => $announcement->type,
'employer_name' => $postedBy,
'company_name' => $organization,
'created_at' => $announcement->created_at->toISOString(),
'time_ago' => $announcement->created_at->diffForHumans(),
'charity_details' => $charityDetails,
];
});
return response()->json([
'success' => true,
'data' => [
'announcements' => $announcements,
'pagination' => [
'total' => $total,
'per_page' => $perPage,
'current_page' => $page,
'last_page' => max(1, (int)ceil($total / $perPage)),
]
]
], 200);
} catch (\Exception $e) {
logger()->error('Mobile Employer Get All Announcements Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while fetching announcements.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* Get all announcements posted by this employer.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getMyAnnouncements(Request $request)
{
/** @var User $employer */
$employer = $request->attributes->get('employer');
@ -57,7 +134,7 @@ public function getAnnouncements(Request $request)
], 200);
} catch (\Exception $e) {
logger()->error('Mobile Employer Get Announcements Failure: ' . $e->getMessage());
logger()->error('Mobile Employer Get My Announcements Failure: ' . $e->getMessage());
return response()->json([
'success' => false,

View File

@ -198,6 +198,13 @@ public function register(Request $request)
'phone.unique' => 'This mobile number is already registered.',
]);
$validator->after(function ($validator) use ($request) {
$emiratesIdInput = $request->input('emirates_id');
if (is_array($emiratesIdInput) && empty($emiratesIdInput['name'])) {
$validator->errors()->add('emirates_id.name', 'Failed to extract name from the Emirates ID. Please upload a clearer document.');
}
});
if ($validator->fails()) {
return response()->json([
'success' => false,
@ -232,6 +239,9 @@ public function register(Request $request)
$emiratesIdNumber = $emiratesIdInput['emirates_id_number'] ?? $emiratesIdInput['id_number'] ?? $emiratesIdInput['id number'] ?? $emiratesIdInput['card_number'] ?? null;
$emiratesIdExpiry = $emiratesIdInput['expiry_date'] ?? $emiratesIdInput['expiry date'] ?? $emiratesIdInput['emirates_id_expiry'] ?? null;
$emiratesIdName = $emiratesIdInput['name'] ?? null;
if ($emiratesIdName) {
$request->merge(['name' => $emiratesIdName]);
}
$emiratesIdDob = $emiratesIdInput['date_of_birth'] ?? $emiratesIdInput['date of birth'] ?? $emiratesIdInput['dob'] ?? null;
$emiratesIdNationality = $emiratesIdInput['nationality'] ?? null;
$emiratesIdGender = $emiratesIdInput['gender'] ?? $emiratesIdInput['sex'] ?? null;
@ -562,15 +572,6 @@ 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,7 +37,6 @@ public function getProfile(Request $request)
$employerProfile = [
'name' => $employer->name,
'email' => $employer->email,
'company_name' => $profile->company_name,
'phone' => $profile->phone,
'country' => $profile->country,
'language' => $profile->language ?? 'English',
@ -105,14 +104,30 @@ public function updateProfile(Request $request)
'unique:employer_profiles,phone,' . ($employer->employerProfile ? $employer->employerProfile->id : 'NULL'),
$sponsor ? 'unique:sponsors,mobile,' . $sponsor->id : 'unique:sponsors,mobile',
],
'company_name' => 'required|string|max:255',
'language' => 'required|string|in:English,Arabic,english,arabic',
'notifications' => 'required|boolean',
'current_password' => 'nullable|required_with:new_password|string',
'new_password' => 'nullable|string|min:8|confirmed',
'address' => 'required|string|max:255',
'notifications' => 'nullable|boolean',
'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',
]);
$validator->after(function ($validator) use ($request) {
if ($request->has('full_name') && !$request->filled('full_name')) {
$validator->errors()->add('full_name', 'Failed to extract name from the Emirates ID. Please upload a clearer document.');
}
});
if ($validator->fails()) {
return response()->json([
'success' => false,
@ -122,14 +137,19 @@ public function updateProfile(Request $request)
}
try {
// Check current password if new password is provided
if ($request->filled('new_password')) {
if (!Hash::check($request->current_password, $employer->password)) {
if ($request->filled('full_name')) {
$request->merge(['name' => $request->full_name]);
}
// 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) {
return response()->json([
'success' => false,
'message' => 'The provided current password does not match.',
'message' => 'Emirates ID mismatch. You cannot change your registered Emirates ID number.',
'errors' => [
'current_password' => ['The provided password does not match your current password.']
'id_number' => ['Emirates ID mismatch.']
]
], 422);
}
@ -151,11 +171,48 @@ public function updateProfile(Request $request)
// Sync with corresponding sponsor record if found
$matchingSponsor = \App\Models\Sponsor::where('email', $oldEmail)->first();
if ($matchingSponsor) {
$matchingSponsor->update([
$sponsorData = [
'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
@ -163,23 +220,49 @@ public function updateProfile(Request $request)
if (!$profile) {
$profile = new EmployerProfile(['user_id' => $employer->id]);
}
$profile->company_name = $request->company_name;
$profile->address = $request->address;
$profile->phone = $request->phone;
$profile->language = ucfirst(strtolower($request->language));
$profile->notifications = $request->notifications;
$profile->save();
$profile->notifications = $request->has('notifications') ? $request->notifications : ($profile->notifications ?? true);
// Update password if present
if ($request->filled('new_password')) {
$employer->update([
'password' => Hash::make($request->new_password),
]);
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->save();
$employerProfile = [
'name' => $employer->name,
'email' => $employer->email,
'company_name' => $profile->company_name,
'phone' => $profile->phone,
'country' => $profile->country,
'language' => $profile->language,
@ -236,6 +319,15 @@ public function getDashboard(Request $request)
$savedCandidates = \App\Models\Shortlist::where('employer_id', $employer->id)->count();
$totalWorkers = \App\Models\Worker::with('documents')
->where('status', '!=', 'Hired')
->where('status', '!=', 'hidden')
->get()
->filter(function ($w) {
return !str_contains(strtolower($w->passport_status), 'pending');
})
->count();
// Resolve plan
$sub = \Illuminate\Support\Facades\DB::table('subscriptions')
->where('user_id', $employer->id)
@ -302,6 +394,7 @@ 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,
@ -320,4 +413,70 @@ public function getDashboard(Request $request)
}
}
/**
* Change employer's password.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function changePassword(Request $request)
{
/** @var User $employer */
$employer = $request->attributes->get('employer');
$validator = Validator::make($request->all(), [
'current_password' => 'required|string',
'new_password' => 'required|string|min:8|confirmed',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
if (!Hash::check($request->current_password, $employer->password)) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => [
'current_password' => ['The current password is incorrect.']
]
], 422);
}
try {
$hashedPassword = Hash::make($request->new_password);
// Update user table
$employer->update([
'password' => $hashedPassword
]);
// Sync with corresponding sponsor record if found
$matchingSponsor = \App\Models\Sponsor::where('email', $employer->email)->first();
if ($matchingSponsor) {
$matchingSponsor->update([
'password' => $hashedPassword
]);
}
return response()->json([
'success' => true,
'message' => 'Password changed successfully.'
], 200);
} catch (\Exception $e) {
logger()->error('Mobile Employer Change Password Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while changing password.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
}

View File

@ -22,23 +22,16 @@ class EmployerWorkerController extends Controller
*/
private function formatWorker(Worker $w)
{
// Map languages with country names
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
// Map languages from database comma-separated string
$langs = $w->language ? array_map('trim', explode(',', $w->language)) : ['English'];
// Preferred job types: full-time / part-time / live-in / live-out
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
$preferredJobType = $w->preferred_job_type ?? $jobTypes[$w->id % 4];
// Emirates ID verification status (dynamic passport status)
// Emirates ID verification 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;
@ -51,28 +44,79 @@ private function formatWorker(Worker $w)
return [
'id' => $w->id,
'name' => $w->name,
'email' => $w->email,
'phone' => $w->phone,
'nationality' => $w->nationality,
'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,
'salary' => $w->salary,
'experience' => $w->experience,
'verified' => (bool)$w->verified,
'preferred_job_type' => $preferredJobType,
'bio' => $w->bio,
'rating' => $rating,
'reviews_count' => $reviewsCount,
'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,
'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['file_number']);
unset($ocrData['name']);
unset($ocrData['passport_number']);
unset($ocrData['accompanied_by']);
unset($ocrData['valid_until']);
unset($ocrData['sponsor']);
}
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(),
'category' => [
'id' => 7,
'name' => 'General Helper',
],
];
}
@ -142,7 +186,8 @@ 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) {
if (in_array(strtolower($s), $skillsArray)) {
$skillName = is_array($s) ? ($s['name'] ?? '') : (is_object($s) ? ($s->name ?? '') : $s);
if (in_array(strtolower($skillName), $skillsArray)) {
return true;
}
}
@ -314,10 +359,22 @@ 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' => $workersArray
'workers' => $paginatedWorkers,
'pagination' => [
'total' => $total,
'per_page' => $perPage,
'current_page' => $page,
'last_page' => max(1, (int)ceil($total / $perPage)),
]
]
], 200);
@ -347,11 +404,11 @@ public function getCandidates(Request $request)
// Fetch Job Applications
$jobIds = JobPost::where('employer_id', $employerId)->pluck('id');
$applicationsQuery = JobApplication::whereIn('job_id', $jobIds)
->with(['worker', 'jobPost']);
->with(['worker.skills', 'worker.documents', 'jobPost']);
// Fetch Direct Hiring Offers
$directOffersQuery = JobOffer::where('employer_id', $employerId)
->with('worker');
->with('worker.skills', 'worker.documents');
// Apply search filter if provided
if ($request->filled('search')) {
@ -381,34 +438,15 @@ public function getCandidates(Request $request)
elseif ($app->status === 'applied') $status = 'Reviewing';
else $status = ucfirst($app->status);
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
$mappedSkills = [
$skillsList[$w->id % 6],
$skillsList[($w->id + 2) % 6]
];
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
$preferredJobType = $w->preferred_job_type ?? $jobTypes[$w->id % 4];
$formattedWorker = $this->formatWorker($w);
return [
return array_merge($formattedWorker, [
'id' => $app->id,
'worker_id' => $w->id,
'name' => $w->name,
'nationality' => $w->nationality,
'status' => $status,
'applied_at' => $app->created_at->format('Y-m-d H:i:s'),
'type' => 'application',
'skills' => $mappedSkills,
'languages' => $langs,
'availability' => $w->availability ?? 'Immediate',
'preferred_location' => $w->preferred_location,
'area' => $w->area,
'preferred_job_type' => $preferredJobType,
'live_in_out' => $w->live_in_out,
'in_country' => (bool)$w->in_country,
'visa_status' => $w->visa_status,
'gender' => $w->gender,
];
]);
})->filter()->values()->toArray();
// Fetch Direct Hiring Offers
@ -423,34 +461,15 @@ public function getCandidates(Request $request)
elseif ($offer->status === 'rejected') $status = 'Rejected';
elseif ($offer->status === 'pending') $status = 'Offer Sent';
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
$mappedSkills = [
$skillsList[$w->id % 6],
$skillsList[($w->id + 2) % 6]
];
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
$preferredJobType = $w->preferred_job_type ?? $jobTypes[$w->id % 4];
$formattedWorker = $this->formatWorker($w);
return [
return array_merge($formattedWorker, [
'id' => 'offer_' . $offer->id,
'worker_id' => $w->id,
'name' => $w->name,
'nationality' => $w->nationality,
'status' => $status,
'applied_at' => $offer->created_at->format('Y-m-d H:i:s'),
'type' => 'direct_offer',
'skills' => $mappedSkills,
'languages' => $langs,
'availability' => $w->availability ?? 'Immediate',
'preferred_location' => $w->preferred_location,
'area' => $w->area,
'preferred_job_type' => $preferredJobType,
'live_in_out' => $w->live_in_out,
'in_country' => (bool)$w->in_country,
'visa_status' => $w->visa_status,
'gender' => $w->gender,
];
]);
})->filter()->values()->toArray();
// Merge and sort
@ -934,25 +953,78 @@ public function getWorkerDetail(Request $request, $id)
$workerProfile = [
'id' => $w->id,
'name' => $w->name,
'email' => $w->email,
'phone' => $w->phone,
'nationality' => $w->nationality,
'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,
'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,
'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['file_number']);
unset($ocrData['name']);
unset($ocrData['passport_number']);
unset($ocrData['accompanied_by']);
unset($ocrData['valid_until']);
unset($ocrData['sponsor']);
}
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([
@ -987,7 +1059,7 @@ public function getShortlist(Request $request)
$perPage = (int)$request->input('per_page', 15);
$shortlists = Shortlist::where('employer_id', $employer->id)
->with(['worker.skills'])
->with(['worker.skills', 'worker.documents'])
->get();
$formattedWorkers = $shortlists->map(function ($s) {

View File

@ -280,12 +280,17 @@ public function reportFromEmployer(Request $request)
*/
public function getReasonsForWorker(Request $request)
{
$type = $request->query('type'); // Chat or Review
$type = $request->query('type'); // Chat or Support
$query = DB::table('report_reasons')->where('status', 'Active');
if ($type && in_array($type, ['Chat', 'Review'])) {
$query->whereIn('type', [$type, 'Both']);
if ($type && in_array(strtolower($type), ['chat', 'support'])) {
$mappedType = ucfirst(strtolower($type));
if ($mappedType === 'Support') {
$query->where('type', 'Support');
} else {
$query->whereIn('type', [$mappedType, 'Both']);
}
}
$reasons = $query->orderBy('id', 'asc')->get(['id', 'reason', 'type']);
@ -302,12 +307,17 @@ public function getReasonsForWorker(Request $request)
*/
public function getReasonsForEmployer(Request $request)
{
$type = $request->query('type'); // Chat or Review
$type = $request->query('type'); // Chat or Support
$query = DB::table('report_reasons')->where('status', 'Active');
if ($type && in_array($type, ['Chat', 'Review'])) {
$query->whereIn('type', [$type, 'Both']);
if ($type && in_array(strtolower($type), ['chat', 'support'])) {
$mappedType = ucfirst(strtolower($type));
if ($mappedType === 'Support') {
$query->where('type', 'Support');
} else {
$query->whereIn('type', [$mappedType, 'Both']);
}
}
$reasons = $query->orderBy('id', 'asc')->get(['id', 'reason', 'type']);

View File

@ -41,6 +41,13 @@ public function register(Request $request)
'email.unique' => 'This email address is already registered.',
]);
$validator->after(function ($validator) use ($request) {
$emiratesIdInput = $request->input('emirates_id');
if (is_array($emiratesIdInput) && empty($emiratesIdInput['name'])) {
$validator->errors()->add('emirates_id.name', 'Failed to extract name from the Emirates ID. Please upload a clearer document.');
}
});
if ($validator->fails()) {
return response()->json([
'success' => false,
@ -83,6 +90,9 @@ public function register(Request $request)
$emiratesId = $emiratesIdInput['emirates_id_number'] ?? $emiratesIdInput['id_number'] ?? $emiratesIdInput['id number'] ?? $emiratesIdInput['card_number'] ?? null;
$emiratesIdExpiry = $emiratesIdInput['expiry_date'] ?? $emiratesIdInput['expiry date'] ?? $emiratesIdInput['emirates_id_expiry'] ?? null;
$emiratesIdName = $emiratesIdInput['name'] ?? null;
if ($emiratesIdName) {
$request->merge(['full_name' => $emiratesIdName]);
}
$emiratesIdDob = $emiratesIdInput['date_of_birth'] ?? $emiratesIdInput['date of birth'] ?? $emiratesIdInput['dob'] ?? null;
$emiratesIdNationality = $emiratesIdInput['nationality'] ?? null;
$emiratesIdGender = $emiratesIdInput['gender'] ?? $emiratesIdInput['sex'] ?? null;
@ -100,7 +110,7 @@ public function register(Request $request)
$sponsor = DB::transaction(function () use (
$request, $email, $licensePath, $emiratesIdPath, $emiratesId, $apiToken, $licenseExpiry, $licenseNumber, $licenseInput,
$emiratesIdExpiry, $emiratesIdName, $emiratesIdDob, $emiratesIdIssueDate, $emiratesIdEmployer,
$emiratesIdIssuePlace, $emiratesIdOccupation
$emiratesIdIssuePlace, $emiratesIdOccupation, $licenseData
) {
return Sponsor::create([
'full_name' => $request->full_name,
@ -390,12 +400,11 @@ 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' => true,
'message' => 'If an account with that contact exists, a reset OTP has been sent.',
]);
'success' => false,
'message' => 'user not found this email id',
], 404);
}
$otp = (string) mt_rand(100000, 999999);

View File

@ -354,4 +354,408 @@ public function getProfile(Request $request)
]
], 200);
}
/**
* Build the standardized sponsor response array with nested license and emirates_id objects.
*/
private function buildSponsorResponse(Sponsor $sponsor): array
{
$licenseData = [
'license_number' => $sponsor->license_data['license_no'] ?? $sponsor->license_data['license_number'] ?? null,
'organization_name' => $sponsor->organization_name,
'expiry_date' => $sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : ($sponsor->license_data['expiry_date'] ?? null),
'document_type' => $sponsor->license_data['document_type'] ?? null,
'country' => $sponsor->license_data['country'] ?? null,
'authority' => $sponsor->license_data['authority'] ?? null,
'license_no' => $sponsor->license_data['license_no'] ?? null,
'company_name' => $sponsor->license_data['company_name'] ?? null,
'business_name' => $sponsor->license_data['business_name'] ?? null,
'license_category' => $sponsor->license_data['license_category'] ?? null,
'legal_type' => $sponsor->license_data['legal_type'] ?? null,
'issue_date' => $sponsor->license_data['issue_date'] ?? null,
'main_license_no' => $sponsor->license_data['main_license_no'] ?? null,
'register_no' => $sponsor->license_data['register_no'] ?? null,
'dcci_no' => $sponsor->license_data['dcci_no'] ?? null,
'members' => $sponsor->license_data['members'] ?? [],
];
$emiratesIdData = null;
if ($sponsor->emirates_id) {
$emiratesIdData = [
'emirates_id_number' => $sponsor->emirates_id,
'name' => $sponsor->emirates_id_name,
'nationality' => $sponsor->nationality,
'date_of_birth' => $sponsor->emirates_id_dob,
'expiry_date' => $sponsor->emirates_id_expiry,
'issue_date' => $sponsor->emirates_id_issue_date,
'employer' => $sponsor->emirates_id_employer,
'issue_place' => $sponsor->emirates_id_issue_place,
'occupation' => $sponsor->emirates_id_occupation,
'card_number' => $sponsor->emirates_id_card_number,
'gender' => $sponsor->emirates_id_gender,
];
}
return [
'id' => $sponsor->id,
'full_name' => $sponsor->full_name,
'organization_name' => $sponsor->organization_name,
'mobile' => $sponsor->mobile,
'email' => $sponsor->email,
'nationality' => $sponsor->nationality,
'city' => $sponsor->city,
'address' => $sponsor->address,
'country_code' => $sponsor->country_code,
'is_verified' => $sponsor->is_verified,
'status' => $sponsor->status,
'license_file' => $sponsor->license_file ? asset($sponsor->license_file) : null,
'emirates_id_file' => $sponsor->emirates_id_file ? asset($sponsor->emirates_id_file) : null,
'license_expiry' => $sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : null,
'validity' => $sponsor->license_expiry ? ($sponsor->license_expiry->isFuture() ? 'Valid' : 'Expired') : 'Pending Review',
'joined_at' => $sponsor->created_at->toIso8601String(),
'fcm_token' => $sponsor->fcm_token,
'license' => $licenseData,
'emirates_id' => $emiratesIdData,
];
}
/**
* Change sponsor's password.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function changePassword(Request $request)
{
/** @var Sponsor $sponsor */
$sponsor = $request->attributes->get('sponsor');
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
'current_password' => 'required|string',
'new_password' => 'required|string|min:8|confirmed',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
if (!\Illuminate\Support\Facades\Hash::check($request->current_password, $sponsor->password)) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => [
'current_password' => ['The current password is incorrect.']
]
], 422);
}
try {
$hashedPassword = \Illuminate\Support\Facades\Hash::make($request->new_password);
// Update sponsor table
$sponsor->update([
'password' => $hashedPassword
]);
// Sync with corresponding employer user record if found
$matchingEmployer = \App\Models\User::where('email', $sponsor->email)
->where('role', 'employer')
->first();
if ($matchingEmployer) {
$matchingEmployer->update([
'password' => $hashedPassword
]);
}
return response()->json([
'success' => true,
'message' => 'Password changed successfully.'
], 200);
} catch (\Exception $e) {
logger()->error('Mobile Sponsor Change Password Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while changing password.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* POST /api/sponsors/profile/update
*
* Updates the authenticated sponsor's profile.
*/
public function updateProfile(Request $request)
{
/** @var Sponsor $sponsor */
$sponsor = $request->attributes->get('sponsor');
if (!$sponsor) {
return response()->json([
'success' => false,
'message' => 'Unauthorized.'
], 401);
}
// Find matching employer user using sponsor's original email
$oldEmail = $sponsor->getOriginal('email') ?? $sponsor->email;
$matchingUser = \App\Models\User::where('email', $oldEmail)
->where('role', 'employer')
->first();
$matchingUserId = $matchingUser ? $matchingUser->id : 'NULL';
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
'name' => 'required_without:full_name|nullable|string|max:255',
'full_name' => 'required_without:name|nullable|string|max:255',
'email' => 'required|email|max:255|unique:sponsors,email,' . $sponsor->id . '|unique:users,email,' . $matchingUserId,
'mobile' => 'required|string|max:255|unique:sponsors,mobile,' . $sponsor->id,
'organization_name' => 'required|string|max:255',
'city' => 'required|string|max:100',
'address' => 'required|string|max:255',
'nationality' => 'required|string|max:100',
'country_code' => 'required|string|max:10',
'fcm_token' => 'nullable|string|max:255',
// Emirates ID — accepts nested object OR flat fields
'emirates_id' => 'nullable|array',
'emirates_id.emirates_id_number' => 'nullable|string|max:255',
'emirates_id.name' => 'nullable|string|max:255',
'emirates_id.nationality' => 'nullable|string|max:255',
'emirates_id.date_of_birth' => 'nullable|string|max:255',
'emirates_id.expiry_date' => 'nullable|string|max:255',
'emirates_id.issue_date' => 'nullable|string|max:255',
'emirates_id.employer' => 'nullable|string|max:255',
'emirates_id.issue_place' => 'nullable|string|max:255',
'emirates_id.occupation' => 'nullable|string|max:255',
// Flat emirates ID fields (backward compatibility)
'id_number' => 'nullable|string|max:255',
'card_number' => 'nullable|string|max:255',
'date_of_birth' => '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',
// License — accepts nested object OR flat fields
'license' => 'nullable|array',
'license.document_type' => 'nullable|string|max:255',
'license.country' => 'nullable|string|max:255',
'license.authority' => 'nullable|string|max:255',
'license.license_no' => 'nullable|string|max:255',
'license.company_name' => 'nullable|string|max:255',
'license.business_name' => 'nullable|string|max:255',
'license.license_category' => 'nullable|string|max:255',
'license.legal_type' => 'nullable|string|max:255',
'license.main_license_no' => 'nullable|string|max:255',
'license.register_no' => 'nullable|string|max:255',
'license.dcci_no' => 'nullable|string|max:255',
'license.members' => 'nullable|array',
// Flat license fields (backward compatibility)
'document_type' => 'nullable|string|max:255',
'country' => 'nullable|string|max:255',
'authority' => 'nullable|string|max:255',
'license_no' => 'nullable|string|max:255',
'company_name' => 'nullable|string|max:255',
'business_name' => 'nullable|string|max:255',
'license_category' => 'nullable|string|max:255',
'legal_type' => 'nullable|string|max:255',
'main_license_no' => 'nullable|string|max:255',
'register_no' => 'nullable|string|max:255',
'dcci_no' => 'nullable|string|max:255',
'members' => 'nullable|array',
]);
$validator->after(function ($validator) use ($request) {
$nameFromEid = $request->input('emirates_id.name') ?? $request->input('full_name');
if ($request->has('full_name') && !$request->filled('full_name') && !$nameFromEid) {
$validator->errors()->add('full_name', 'Failed to extract name from the Emirates ID. Please upload a clearer document.');
}
});
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
try {
// Resolve nested emirates_id input into flat variables
$eidInput = $request->input('emirates_id');
$eidNumber = null;
$eidName = null;
$eidDob = null;
$eidExpiry = null;
$eidIssueDate = null;
$eidEmployer = null;
$eidIssuePlace = null;
$eidOccupation = null;
$eidCardNumber = null;
$eidGender = null;
if (is_array($eidInput)) {
$eidNumber = $eidInput['emirates_id_number'] ?? $eidInput['id_number'] ?? null;
$eidName = $eidInput['name'] ?? null;
$eidDob = $eidInput['date_of_birth'] ?? $eidInput['dob'] ?? null;
$eidExpiry = $eidInput['expiry_date'] ?? null;
$eidIssueDate = $eidInput['issue_date'] ?? null;
$eidEmployer = $eidInput['employer'] ?? null;
$eidIssuePlace = $eidInput['issue_place'] ?? $eidInput['issuing_place'] ?? null;
$eidOccupation = $eidInput['occupation'] ?? null;
$eidCardNumber = $eidInput['card_number'] ?? null;
$eidGender = $eidInput['gender'] ?? null;
}
// Flat field fallbacks (backward compatibility)
$eidNumber = $eidNumber ?? $request->input('id_number');
$eidName = $eidName ?? $request->input('full_name');
$eidDob = $eidDob ?? $request->input('date_of_birth');
$eidExpiry = $eidExpiry ?? $request->input('expiry_date');
$eidIssueDate = $eidIssueDate ?? $request->input('issue_date');
$eidEmployer = $eidEmployer ?? $request->input('employer');
$eidIssuePlace = $eidIssuePlace ?? $request->input('issuing_place');
$eidOccupation = $eidOccupation ?? $request->input('occupation');
$eidCardNumber = $eidCardNumber ?? $request->input('card_number');
$eidGender = $eidGender ?? $request->input('gender');
$nameToUse = $eidName ?? $request->name ?? $request->full_name;
// Validate Emirates ID immutability — once set, it cannot be changed
if ($eidNumber) {
if ($sponsor->emirates_id && $sponsor->emirates_id !== $eidNumber) {
return response()->json([
'success' => false,
'message' => 'Emirates ID mismatch. You cannot change your registered Emirates ID number.',
'errors' => [
'id_number' => ['Emirates ID mismatch.']
]
], 422);
}
}
// Sync with corresponding employer user and profile records if found
if ($matchingUser) {
$userData = [
'name' => $nameToUse,
'email' => $request->email,
];
if ($request->has('fcm_token')) {
$userData['fcm_token'] = $request->fcm_token;
}
$matchingUser->update($userData);
$profile = $matchingUser->employerProfile;
if (!$profile) {
$profile = new \App\Models\EmployerProfile(['user_id' => $matchingUser->id]);
}
$profile->address = $request->address;
$profile->phone = $request->mobile;
if ($eidNumber) { $profile->emirates_id_number = $eidNumber; }
if ($eidCardNumber) { $profile->emirates_id_card_number = $eidCardNumber; }
if ($eidName) { $profile->emirates_id_name = $eidName; }
if ($eidDob) { $profile->emirates_id_dob = $eidDob; }
if ($request->has('nationality')) { $profile->nationality = $request->nationality; }
if ($eidGender) { $profile->emirates_id_gender = $eidGender; }
if ($eidIssueDate) { $profile->emirates_id_issue_date = $eidIssueDate; }
if ($eidExpiry) { $profile->emirates_id_expiry = $eidExpiry; }
if ($eidOccupation) { $profile->emirates_id_occupation = $eidOccupation; }
if ($eidEmployer) { $profile->emirates_id_employer = $eidEmployer; }
if ($eidIssuePlace) { $profile->emirates_id_issue_place = $eidIssuePlace; }
$profile->save();
}
// Update Sponsor profile
$sponsorData = [
'full_name' => $nameToUse,
'email' => $request->email,
'mobile' => $request->mobile,
'organization_name' => $request->organization_name,
'city' => $request->city,
'address' => $request->address,
'nationality' => $request->nationality,
'country_code' => $request->country_code,
];
if ($request->has('fcm_token')) {
$sponsorData['fcm_token'] = $request->fcm_token;
}
if ($eidNumber) { $sponsorData['emirates_id'] = $eidNumber; }
if ($eidCardNumber) { $sponsorData['emirates_id_card_number'] = $eidCardNumber; }
if ($eidName) { $sponsorData['emirates_id_name'] = $eidName; }
if ($eidDob) { $sponsorData['emirates_id_dob'] = $eidDob; }
if ($eidGender) { $sponsorData['emirates_id_gender'] = $eidGender; }
if ($eidIssueDate) { $sponsorData['emirates_id_issue_date'] = $eidIssueDate; }
if ($eidExpiry) { $sponsorData['emirates_id_expiry'] = $eidExpiry; }
if ($eidOccupation) { $sponsorData['emirates_id_occupation'] = $eidOccupation; }
if ($eidEmployer) { $sponsorData['emirates_id_employer'] = $eidEmployer; }
if ($eidIssuePlace) { $sponsorData['emirates_id_issue_place'] = $eidIssuePlace; }
// Parse and merge license data — from nested license object or flat fields
$currentLicenseData = is_array($sponsor->license_data) ? $sponsor->license_data : [];
$licenseInput = $request->input('license');
$newLicenseData = [];
if (is_array($licenseInput)) {
foreach ([
'document_type', 'country', 'authority', 'license_no', 'company_name',
'business_name', 'license_category', 'legal_type', 'issue_date', 'expiry_date',
'main_license_no', 'register_no', 'dcci_no', 'members'
] as $field) {
if (isset($licenseInput[$field])) {
$newLicenseData[$field] = $licenseInput[$field];
}
}
}
// Flat field fallbacks (backward compatibility)
foreach ([
'document_type', 'country', 'authority', 'license_no', 'company_name',
'business_name', 'license_category', 'legal_type', 'issue_date', 'expiry_date',
'main_license_no', 'register_no', 'dcci_no', 'members'
] as $field) {
if (!isset($newLicenseData[$field]) && $request->has($field)) {
$newLicenseData[$field] = $request->input($field);
}
}
if (!empty($newLicenseData)) {
$sponsorData['license_data'] = array_merge($currentLicenseData, $newLicenseData);
}
$sponsor->update($sponsorData);
return response()->json([
'success' => true,
'message' => 'Profile updated successfully.',
'data' => [
'sponsor' => $this->buildSponsorResponse($sponsor)
]
], 200);
} catch (\Exception $e) {
logger()->error('Mobile Sponsor Update Profile Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while updating profile.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
}

View File

@ -261,14 +261,6 @@ 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);
@ -338,7 +330,34 @@ public function register(Request $request)
return $query->where('type', 'passport');
}),
],
'visa' => 'nullable|array',
'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.',
@ -470,7 +489,7 @@ public function register(Request $request)
'number' => $passportNum,
'issue_date' => $issueDateFormatted,
'expiry_date' => $expiryDateFormatted,
'ocr_accuracy' => 99.0,
'ocr_accuracy' => isset($passportDataInput['ocr_accuracy']) ? (float)$passportDataInput['ocr_accuracy'] : 99.0,
'file_path' => null,
'ocr_data' => $passportDataInput,
]);
@ -478,9 +497,10 @@ public function register(Request $request)
// Create visa document if provided
if ($visaDataInput) {
$visaNum = $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999));
$expiryDate = $visaDataInput['expiry_date'] ?? null;
$issueDate = $visaDataInput['issue_date'] ?? null;
$cleanedVisaOcr = $this->cleanVisaData($visaDataInput);
$visaNum = $cleanedVisaOcr['entry_permit_no'] ?: ('V' . rand(1000000, 9999999));
$expiryDate = $cleanedVisaOcr['valid_until'] ?? null;
$issueDate = $cleanedVisaOcr['issue_date'] ?? null;
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(2)->toDateString();
$issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : now()->subYears(2)->toDateString();
@ -490,23 +510,15 @@ public function register(Request $request)
'number' => $visaNum,
'issue_date' => $issueDateFormatted,
'expiry_date' => $expiryDateFormatted,
'ocr_accuracy' => 99.0,
'ocr_accuracy' => isset($visaDataInput['ocr_accuracy']) ? (float)$visaDataInput['ocr_accuracy'] : 99.0,
'file_path' => null,
'ocr_data' => $visaDataInput,
'ocr_data' => $cleanedVisaOcr,
]);
}
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([
@ -623,10 +635,238 @@ public function nationalities(Request $request)
$rawNationalities = \App\Models\Nationality::all();
$callingCodes = [
'AF' => '+93',
'AL' => '+355',
'DZ' => '+213',
'AD' => '+376',
'AO' => '+244',
'AG' => '+1-268',
'AR' => '+54',
'AM' => '+374',
'AU' => '+61',
'AT' => '+43',
'AZ' => '+994',
'BS' => '+1-242',
'BH' => '+973',
'BD' => '+880',
'BB' => '+1-246',
'BY' => '+375',
'BE' => '+32',
'BZ' => '+501',
'BJ' => '+229',
'BT' => '+975',
'BO' => '+591',
'BA' => '+387',
'BW' => '+267',
'BR' => '+55',
'BN' => '+673',
'BG' => '+359',
'BF' => '+226',
'BI' => '+257',
'KH' => '+855',
'CM' => '+237',
'CA' => '+1',
'CV' => '+238',
'CF' => '+236',
'TD' => '+235',
'CL' => '+56',
'CN' => '+86',
'CO' => '+57',
'KM' => '+269',
'CG' => '+242',
'CD' => '+243',
'CR' => '+506',
'HR' => '+385',
'CU' => '+53',
'CY' => '+357',
'CZ' => '+420',
'DK' => '+45',
'DJ' => '+253',
'DM' => '+1-767',
'DO' => '+1-809',
'TL' => '+670',
'EC' => '+593',
'EG' => '+20',
'SV' => '+503',
'GQ' => '+240',
'ER' => '+291',
'EE' => '+372',
'ET' => '+251',
'FJ' => '+679',
'FI' => '+358',
'FR' => '+33',
'GA' => '+241',
'GM' => '+220',
'GE' => '+995',
'DE' => '+49',
'GH' => '+233',
'GR' => '+30',
'GD' => '+1-473',
'GT' => '+502',
'GN' => '+224',
'GW' => '+245',
'GY' => '+592',
'HT' => '+509',
'HN' => '+504',
'HU' => '+36',
'IS' => '+354',
'IN' => '+91',
'ID' => '+62',
'IR' => '+98',
'IQ' => '+964',
'IE' => '+353',
'IL' => '+972',
'IT' => '+39',
'JM' => '+1-876',
'JP' => '+81',
'JO' => '+962',
'KZ' => '+7',
'KE' => '+254',
'KI' => '+686',
'KP' => '+850',
'KR' => '+82',
'KW' => '+965',
'KG' => '+996',
'LA' => '+856',
'LV' => '+371',
'LB' => '+961',
'LS' => '+266',
'LR' => '+231',
'LY' => '+218',
'LI' => '+423',
'LT' => '+370',
'LU' => '+352',
'MK' => '+389',
'MG' => '+261',
'MW' => '+265',
'MY' => '+60',
'MV' => '+960',
'ML' => '+223',
'MT' => '+356',
'MH' => '+692',
'MR' => '+222',
'MU' => '+230',
'MX' => '+52',
'FM' => '+691',
'MD' => '+373',
'MC' => '+377',
'MN' => '+976',
'ME' => '+382',
'MA' => '+212',
'MZ' => '+258',
'MM' => '+95',
'NA' => '+264',
'NR' => '+674',
'NP' => '+977',
'NL' => '+31',
'NZ' => '+64',
'NI' => '+505',
'NE' => '+227',
'NG' => '+234',
'NO' => '+47',
'OM' => '+968',
'PK' => '+92',
'PW' => '+680',
'PA' => '+507',
'PG' => '+675',
'PY' => '+595',
'PE' => '+51',
'PH' => '+63',
'PL' => '+48',
'PT' => '+351',
'QA' => '+974',
'RO' => '+40',
'RU' => '+7',
'RW' => '+250',
'KN' => '+1-869',
'LC' => '+1-758',
'VC' => '+1-784',
'WS' => '+685',
'SM' => '+378',
'ST' => '+239',
'SA' => '+966',
'SN' => '+221',
'RS' => '+381',
'SC' => '+248',
'SL' => '+232',
'SG' => '+65',
'SK' => '+421',
'SI' => '+386',
'SB' => '+677',
'SO' => '+252',
'ZA' => '+27',
'SS' => '+211',
'ES' => '+34',
'LK' => '+94',
'SD' => '+249',
'SR' => '+597',
'SZ' => '+268',
'SE' => '+46',
'CH' => '+41',
'SY' => '+963',
'TJ' => '+992',
'TZ' => '+255',
'TH' => '+66',
'TG' => '+228',
'TO' => '+676',
'TT' => '+1-868',
'TN' => '+216',
'TR' => '+90',
'TM' => '+993',
'TV' => '+688',
'UG' => '+256',
'UA' => '+380',
'AE' => '+971',
'GB' => '+44',
'US' => '+1',
'UY' => '+598',
'UZ' => '+998',
'VU' => '+678',
'VE' => '+58',
'VN' => '+84',
'YE' => '+967',
'ZM' => '+260',
'ZW' => '+263',
'AI' => '+1-264',
'VG' => '+1-284',
'BM' => '+1-441',
'KY' => '+1-345',
'CK' => '+682',
'CY-W' => '+44',
'CY-M' => '+44',
'FO' => '+298',
'GI' => '+350',
'GL' => '+299',
'GU' => '+1-671',
'HK' => '+852',
'XK' => '+383',
'MO' => '+853',
'MQ' => '+596',
'MS' => '+1-664',
'NU' => '+683',
'GB-NIR' => '+44',
'PS' => '+970',
'PN' => '+64',
'PR' => '+1-787',
'GB-SCT' => '+44',
'SH' => '+290',
'ST-L' => '',
'TA-T' => '+290',
'TC' => '+1-649',
'VA' => '+379',
'WF' => '+681',
'GB-WLS' => '+44',
];
$list = [];
foreach ($rawNationalities as $item) {
$list[] = [
'code' => $item->code,
'country_code' => $item->code,
'phone_code' => $callingCodes[$item->code] ?? '',
'dial_code' => $callingCodes[$item->code] ?? '',
'calling_code' => $callingCodes[$item->code] ?? '',
'name' => $item->{$lang} ?? $item->name,
];
}
@ -997,6 +1237,62 @@ private function normaliseDateForController(?string $raw): ?string
}
}
/**
* Standardize and clean Visa OCR data structure to exactly the 15 required fields.
*/
private function cleanVisaData(array $visaDataInput): array
{
$rawVisaType = $visaDataInput['visa_type'] ?? null;
$rawEntryPermitNo = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? $visaDataInput['number'] ?? null;
$rawIssueDate = $visaDataInput['issue_date'] ?? null;
$rawValidUntil = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
$rawUidNo = $visaDataInput['uid_no'] ?? $visaDataInput['id_number'] ?? null;
$rawFullName = $visaDataInput['full_name'] ?? $visaDataInput['name'] ?? null;
$rawNationality = $visaDataInput['nationality'] ?? null;
$rawPlaceOfBirth = $visaDataInput['place_of_birth'] ?? null;
$rawDateOfBirth = $visaDataInput['date_of_birth'] ?? $visaDataInput['dob'] ?? null;
$rawPassportNo = $visaDataInput['passport_no'] ?? $visaDataInput['passport_number'] ?? null;
$rawProfession = $visaDataInput['profession'] ?? null;
$rawSponsorName = $visaDataInput['sponsor_name'] ?? null;
// Clean sponsor
$sponsorData = $visaDataInput['sponsor'] ?? [];
if (is_string($sponsorData)) {
$sponsorNameFromObj = $sponsorData;
$sponsorAddress = '';
$sponsorPhone = '';
} else {
$sponsorNameFromObj = $sponsorData['name'] ?? $sponsorData['sponsor_name'] ?? $sponsorData['full_name'] ?? '';
$sponsorAddress = $sponsorData['address'] ?? '';
$sponsorPhone = $sponsorData['phone'] ?? $sponsorData['mobile'] ?? $sponsorData['mobile_number'] ?? $sponsorData['phone_number'] ?? '';
}
if (empty($rawSponsorName)) {
$rawSponsorName = $sponsorNameFromObj;
}
return [
'document_type' => 'uae_visa',
'country' => 'United Arab Emirates',
'visa_type' => $rawVisaType ?: 'ENTRY PERMIT',
'entry_permit_no' => $rawEntryPermitNo ?: '',
'issue_date' => $rawIssueDate ?: '',
'valid_until' => $rawValidUntil ?: '',
'uid_no' => $rawUidNo ?: '',
'full_name' => $rawFullName ?: '',
'nationality' => $rawNationality ?: '',
'place_of_birth' => $rawPlaceOfBirth ?: '',
'date_of_birth' => $rawDateOfBirth ?: '',
'passport_no' => $rawPassportNo ?: '',
'profession' => $rawProfession ?: '',
'sponsor_name' => $rawSponsorName ?: '',
'sponsor' => [
'name' => $sponsorNameFromObj ?: '',
'address' => $sponsorAddress ?: '',
'phone' => $sponsorPhone ?: '',
]
];
}
// -------------------------------------------------------------------------
// Forgot / Reset Password (via Mobile Number)
// -------------------------------------------------------------------------
@ -1022,12 +1318,11 @@ 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' => true,
'message' => 'If an account with that mobile number exists, a reset OTP has been sent.',
]);
'success' => false,
'message' => 'user not found this mobile number',
], 404);
}
$otp = (string) mt_rand(100000, 999999);

View File

@ -201,14 +201,15 @@ public function updateProfile(Request $request)
$visaDataInput = $request->input('visa');
if ($visaDataInput) {
$existingVisa = $worker->documents()->where('type', 'visa')->first();
$expiryDate = $visaDataInput['expiry_date'] ?? null;
$issueDate = $visaDataInput['issue_date'] ?? null;
$cleanedVisaOcr = $this->cleanVisaData($visaDataInput);
$expiryDate = $cleanedVisaOcr['valid_until'] ?? null;
$issueDate = $cleanedVisaOcr['issue_date'] ?? null;
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : null;
$issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : null;
if ($existingVisa) {
$updateFields = [
'ocr_data' => $visaDataInput,
'ocr_data' => $cleanedVisaOcr,
];
if ($expiryDateFormatted) {
$updateFields['expiry_date'] = $expiryDateFormatted;
@ -216,9 +217,12 @@ public function updateProfile(Request $request)
if ($issueDateFormatted) {
$updateFields['issue_date'] = $issueDateFormatted;
}
if (!empty($cleanedVisaOcr['entry_permit_no'])) {
$updateFields['number'] = $cleanedVisaOcr['entry_permit_no'];
}
$existingVisa->update($updateFields);
} else {
$visaNum = $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999));
$visaNum = $cleanedVisaOcr['entry_permit_no'] ?: ('V' . rand(1000000, 9999999));
$expiryDateFormatted = $expiryDateFormatted ?: now()->addYears(2)->toDateString();
$issueDateFormatted = $issueDateFormatted ?: now()->subYears(2)->toDateString();
@ -227,9 +231,9 @@ public function updateProfile(Request $request)
'number' => $visaNum,
'issue_date' => $issueDateFormatted,
'expiry_date' => $expiryDateFormatted,
'ocr_accuracy' => 99.0,
'ocr_accuracy' => isset($visaDataInput['ocr_accuracy']) ? (float)$visaDataInput['ocr_accuracy'] : 99.0,
'file_path' => null,
'ocr_data' => $visaDataInput,
'ocr_data' => $cleanedVisaOcr,
]);
}
}
@ -669,6 +673,61 @@ public function getDashboard(Request $request)
}
}
/**
* Change worker's password.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function changePassword(Request $request)
{
/** @var Worker $worker */
$worker = $request->attributes->get('worker');
$validator = Validator::make($request->all(), [
'current_password' => 'required|string',
'new_password' => 'required|string|min:8|confirmed',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
if (!\Illuminate\Support\Facades\Hash::check($request->current_password, $worker->password)) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => [
'current_password' => ['The current password is incorrect.']
]
], 422);
}
try {
$worker->update([
'password' => \Illuminate\Support\Facades\Hash::make($request->new_password)
]);
return response()->json([
'success' => true,
'message' => 'Password changed successfully.'
], 200);
} catch (\Exception $e) {
logger()->error('Mobile Worker Change Password Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while changing password.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* E.g. "14/05/1990" -> "1990-05-14"
*/
@ -699,4 +758,60 @@ private function normaliseDateForController(?string $raw): ?string
return $raw;
}
}
/**
* Standardize and clean Visa OCR data structure to exactly the 15 required fields.
*/
private function cleanVisaData(array $visaDataInput): array
{
$rawVisaType = $visaDataInput['visa_type'] ?? null;
$rawEntryPermitNo = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? $visaDataInput['number'] ?? null;
$rawIssueDate = $visaDataInput['issue_date'] ?? null;
$rawValidUntil = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
$rawUidNo = $visaDataInput['uid_no'] ?? $visaDataInput['id_number'] ?? null;
$rawFullName = $visaDataInput['full_name'] ?? $visaDataInput['name'] ?? null;
$rawNationality = $visaDataInput['nationality'] ?? null;
$rawPlaceOfBirth = $visaDataInput['place_of_birth'] ?? null;
$rawDateOfBirth = $visaDataInput['date_of_birth'] ?? $visaDataInput['dob'] ?? null;
$rawPassportNo = $visaDataInput['passport_no'] ?? $visaDataInput['passport_number'] ?? null;
$rawProfession = $visaDataInput['profession'] ?? null;
$rawSponsorName = $visaDataInput['sponsor_name'] ?? null;
// Clean sponsor
$sponsorData = $visaDataInput['sponsor'] ?? [];
if (is_string($sponsorData)) {
$sponsorNameFromObj = $sponsorData;
$sponsorAddress = '';
$sponsorPhone = '';
} else {
$sponsorNameFromObj = $sponsorData['name'] ?? $sponsorData['sponsor_name'] ?? $sponsorData['full_name'] ?? '';
$sponsorAddress = $sponsorData['address'] ?? '';
$sponsorPhone = $sponsorData['phone'] ?? $sponsorData['mobile'] ?? $sponsorData['mobile_number'] ?? $sponsorData['phone_number'] ?? '';
}
if (empty($rawSponsorName)) {
$rawSponsorName = $sponsorNameFromObj;
}
return [
'document_type' => 'uae_visa',
'country' => 'United Arab Emirates',
'visa_type' => $rawVisaType ?: 'ENTRY PERMIT',
'entry_permit_no' => $rawEntryPermitNo ?: '',
'issue_date' => $rawIssueDate ?: '',
'valid_until' => $rawValidUntil ?: '',
'uid_no' => $rawUidNo ?: '',
'full_name' => $rawFullName ?: '',
'nationality' => $rawNationality ?: '',
'place_of_birth' => $rawPlaceOfBirth ?: '',
'date_of_birth' => $rawDateOfBirth ?: '',
'passport_no' => $rawPassportNo ?: '',
'profession' => $rawProfession ?: '',
'sponsor_name' => $rawSponsorName ?: '',
'sponsor' => [
'name' => $sponsorNameFromObj ?: '',
'address' => $sponsorAddress ?: '',
'phone' => $sponsorPhone ?: '',
]
];
}
}

View File

@ -290,17 +290,35 @@ 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')) {
@ -309,12 +327,46 @@ 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;
}
if (($request->hasFile('emirates_id_front') || $request->hasFile('emirates_id_file')) && empty($extractedName)) {
return response()->json([
'errors' => [
'emirates_id_front' => ['Failed to extract name from the Emirates ID. Please upload a clear image.']
]
], 422);
}
$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;
if (!empty($extractedName)) {
if (isset($pending['company_name']) && (empty($pending['company_name']) || $pending['company_name'] === ($pending['name'] ?? '') . ' Household')) {
$pending['company_name'] = $extractedName . ' Household';
}
$pending['name'] = $extractedName;
}
session(['pending_employer_registration' => $pending]);
session(['employer_emirates_id_uploaded' => true]);
@ -438,6 +490,15 @@ 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,
]);
@ -459,6 +520,17 @@ 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,15 +42,17 @@ private function buildProfileData($user, $profile)
'email' => $user->email,
'phone' => $profile->phone ?? '+971509990001',
'emirates_id' => [
'emirates_id_number' => $profile->emirates_id_number ?? '784-1988-5310327-2',
'emirates_id_number' => $profile->emirates_id_number ?? '784-1987-5493842-5',
'name' => $profile->emirates_id_name ?? $user->name,
'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',
'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',
],
'fcm_token' => $user->fcm_token ?? 'fcm_token_example',
'address' => $profile->address ?? 'Villa 45, Street 12',
@ -74,15 +76,17 @@ public function index(Request $request)
$profile = EmployerProfile::create([
'user_id' => $user->id,
'phone' => '+971509990001',
'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',
'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',
'address' => 'Villa 45, Street 12',
]);
}
@ -106,15 +110,17 @@ public function edit(Request $request)
$profile = EmployerProfile::create([
'user_id' => $user->id,
'phone' => '+971509990001',
'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',
'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',
'address' => 'Villa 45, Street 12',
]);
}
@ -164,6 +170,62 @@ public function update(Request $request)
$oldEmail = $user->getOriginal('email') ?? $user->email;
// Update EmployerProfile
$profile = $user->employerProfile;
if (!$profile) {
$profile = new EmployerProfile(['user_id' => $user->id]);
}
// Handle physical uploads/OCR for Emirates ID first to allow name autofill
$uploaded = false;
$extractedIdNumber = null;
$extractedExpiry = null;
$extractedName = null;
$extractedDob = null;
$extractedNationality = null;
$extractedCardNumber = null;
$extractedGender = null;
$extractedOccupation = null;
$extractedEmployer = null;
$extractedIssuePlace = null;
$extractedIssueDate = null;
if ($request->hasFile('emirates_id_front')) {
$file = $request->file('emirates_id_front');
$ocrFront = \App\Services\OcrDocumentService::extractEmiratesIdFrontData($file);
$extractedIdNumber = $ocrFront['emirates_id_number'] ?? null;
$extractedName = $ocrFront['name'] ?? null;
$extractedDob = $ocrFront['date_of_birth'] ?? null;
$extractedNationality = $ocrFront['nationality'] ?? null;
$extractedCardNumber = $ocrFront['card_number'] ?? null;
$extractedGender = $ocrFront['gender'] ?? null;
$extractedOccupation = $ocrFront['occupation'] ?? null;
$extractedEmployer = $ocrFront['employer'] ?? null;
$extractedIssuePlace = $ocrFront['issue_place'] ?? null;
$profile->emirates_id_front = '[DELETED_FOR_PDPL_COMPLIANCE]';
$uploaded = true;
}
if ($request->hasFile('emirates_id_back')) {
$file = $request->file('emirates_id_back');
$ocrBack = \App\Services\OcrDocumentService::extractEmiratesIdBackData($file);
$extractedExpiry = $ocrBack['expiry_date'] ?? null;
$extractedIssueDate = $ocrBack['issue_date'] ?? null;
$profile->emirates_id_back = '[DELETED_FOR_PDPL_COMPLIANCE]';
$uploaded = true;
}
if ($uploaded) {
// Validate that name was extracted
if ($request->hasFile('emirates_id_front') && empty($extractedName)) {
return back()->withErrors(['emirates_id_front' => 'Failed to extract name from the Emirates ID. Please upload a clear image.']);
}
// Autofill user's name
if (!empty($extractedName)) {
$request->merge(['name' => $extractedName]);
}
}
// Update User Model
$user->update([
'name' => $request->name,
@ -173,18 +235,50 @@ public function update(Request $request)
// Sync with corresponding sponsor record if found
$matchingSponsor = \App\Models\Sponsor::where('email', $oldEmail)->first();
if ($matchingSponsor) {
$matchingSponsor->update([
$sponsorData = [
'full_name' => $request->name,
'email' => $request->email,
'mobile' => $request->phone,
]);
];
if ($uploaded) {
if ($extractedIdNumber) {
$sponsorData['emirates_id'] = $extractedIdNumber;
}
if ($extractedExpiry) {
$sponsorData['emirates_id_expiry'] = $extractedExpiry;
}
if ($extractedName) {
$sponsorData['emirates_id_name'] = $extractedName;
}
if ($extractedDob) {
$sponsorData['emirates_id_dob'] = $extractedDob;
}
if ($extractedNationality) {
$sponsorData['nationality'] = $extractedNationality;
}
if ($extractedGender) {
$sponsorData['emirates_id_gender'] = $extractedGender;
}
if ($extractedOccupation) {
$sponsorData['emirates_id_occupation'] = $extractedOccupation;
}
if ($extractedEmployer) {
$sponsorData['emirates_id_employer'] = $extractedEmployer;
}
if ($extractedIssuePlace) {
$sponsorData['emirates_id_issue_place'] = $extractedIssuePlace;
}
if ($extractedIssueDate) {
$sponsorData['emirates_id_issue_date'] = $extractedIssueDate;
}
if ($extractedCardNumber) {
$sponsorData['emirates_id_card_number'] = $extractedCardNumber;
}
}
$matchingSponsor->update($sponsorData);
}
// Update EmployerProfile
$profile = $user->employerProfile;
if (!$profile) {
$profile = new EmployerProfile(['user_id' => $user->id]);
}
$profile->phone = $request->phone;
if ($request->has('company_name')) {
@ -196,7 +290,7 @@ public function update(Request $request)
if ($request->has('notifications')) {
$profile->notifications = $request->notifications;
}
if ($request->has('nationality')) {
if ($request->has('nationality') && !$uploaded) {
$profile->nationality = $request->nationality;
}
if ($request->has('family_size')) {
@ -221,35 +315,38 @@ public function update(Request $request)
$profile->push_notifications = $request->push_notifications;
}
// Handle physical uploads/OCR for Emirates ID
$uploaded = false;
$extractedIdNumber = null;
$extractedExpiry = null;
if ($request->hasFile('emirates_id_front')) {
$file = $request->file('emirates_id_front');
$ocrFront = \App\Services\OcrDocumentService::extractData($file);
$extractedIdNumber = $ocrFront['document_number'];
$extractedExpiry = $ocrFront['expiry_date'];
$profile->emirates_id_front = '[DELETED_FOR_PDPL_COMPLIANCE]';
$uploaded = true;
}
if ($request->hasFile('emirates_id_back')) {
$file = $request->file('emirates_id_back');
$ocrBack = \App\Services\OcrDocumentService::extractData($file);
if (empty($extractedIdNumber)) {
$extractedIdNumber = $ocrBack['document_number'];
}
if (empty($extractedExpiry)) {
$extractedExpiry = $ocrBack['expiry_date'];
}
$profile->emirates_id_back = '[DELETED_FOR_PDPL_COMPLIANCE]';
$uploaded = true;
}
if ($uploaded) {
$profile->emirates_id_number = $extractedIdNumber ?? ('784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9));
$profile->emirates_id_expiry = $extractedExpiry ?? now()->addYears(rand(2, 4))->toDateString();
$profile->emirates_id_number = $extractedIdNumber ?? $profile->emirates_id_number ?? ('784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9));
$profile->emirates_id_expiry = $extractedExpiry ?? $profile->emirates_id_expiry ?? now()->addYears(rand(2, 4))->toDateString();
if ($extractedName) {
$profile->emirates_id_name = $extractedName;
}
if ($extractedDob) {
$profile->emirates_id_dob = $extractedDob;
}
if ($extractedNationality) {
$profile->nationality = $extractedNationality;
}
if ($extractedCardNumber) {
$profile->emirates_id_card_number = $extractedCardNumber;
}
if ($extractedGender) {
$profile->emirates_id_gender = $extractedGender;
}
if ($extractedOccupation) {
$profile->emirates_id_occupation = $extractedOccupation;
}
if ($extractedEmployer) {
$profile->emirates_id_employer = $extractedEmployer;
}
if ($extractedIssuePlace) {
$profile->emirates_id_issue_place = $extractedIssuePlace;
}
if ($extractedIssueDate) {
$profile->emirates_id_issue_date = $extractedIssueDate;
}
$profile->verification_status = 'approved';
}

View File

@ -370,7 +370,6 @@ public function show($id)
'skills' => $mappedSkills,
'visa_status' => $visaStatus,
'experience' => $w->experience,
'experience_years' => 5,
'salary' => (int) $w->salary,
'religion' => $w->religion,
'languages' => $langs,
@ -391,6 +390,14 @@ public function show($id)
'in_country' => (bool) $w->in_country,
'preferred_location' => $w->preferred_location,
'documents' => $w->documents->map(function ($doc) {
$ocrData = $doc->ocr_data;
if ($doc->type === 'visa' && is_array($ocrData)) {
unset($ocrData['file_number']);
unset($ocrData['name']);
unset($ocrData['passport_number']);
unset($ocrData['accompanied_by']);
unset($ocrData['valid_until']);
}
return [
'id' => $doc->id,
'type' => $doc->type,
@ -399,7 +406,7 @@ public function show($id)
'expiry_date' => $doc->expiry_date,
'ocr_accuracy' => $doc->ocr_accuracy,
'file_path' => $doc->file_path ? url($doc->file_path) : null,
'ocr_data' => $doc->ocr_data,
'ocr_data' => $ocrData,
];
})->toArray(),
];

View File

@ -30,6 +30,7 @@ class EmployerProfile extends Model
'emirates_id_issue_place',
'emirates_id_occupation',
'emirates_id_nationality',
'emirates_id_card_number',
'emirates_id_gender',
'address',
'property_type',

View File

@ -47,6 +47,8 @@ class Sponsor extends Authenticatable
'emirates_id_gender',
'license_number',
'license_data',
'emirates_id_card_number',
'emirates_id_gender',
];
protected $hidden = [
@ -69,4 +71,58 @@ public function hasActiveSubscription(): bool
return $this->subscription_status === 'active' &&
($this->subscription_end_date === null || $this->subscription_end_date->isFuture());
}
/**
* Convert the model instance to an array.
*
* @return array
*/
public function toArray()
{
$array = parent::toArray();
// Remove any flat properties that should be nested
unset($array['license_data']);
$licenseDataObj = [
'license_number' => $this->license_data['license_no'] ?? $this->license_data['license_number'] ?? null,
'organization_name' => $this->organization_name,
'expiry_date' => $this->license_expiry ? $this->license_expiry->toDateString() : ($this->license_data['expiry_date'] ?? null),
'document_type' => $this->license_data['document_type'] ?? null,
'country' => $this->license_data['country'] ?? null,
'authority' => $this->license_data['authority'] ?? null,
'license_no' => $this->license_data['license_no'] ?? null,
'company_name' => $this->license_data['company_name'] ?? null,
'business_name' => $this->license_data['business_name'] ?? null,
'license_category' => $this->license_data['license_category'] ?? null,
'legal_type' => $this->license_data['legal_type'] ?? null,
'issue_date' => $this->license_data['issue_date'] ?? null,
'main_license_no' => $this->license_data['main_license_no'] ?? null,
'register_no' => $this->license_data['register_no'] ?? null,
'dcci_no' => $this->license_data['dcci_no'] ?? null,
'members' => $this->license_data['members'] ?? [],
];
$emiratesIdData = null;
if ($this->emirates_id) {
$emiratesIdData = [
'emirates_id_number' => $this->emirates_id,
'name' => $this->emirates_id_name,
'nationality' => $this->nationality,
'date_of_birth' => $this->emirates_id_dob,
'expiry_date' => $this->emirates_id_expiry,
'issue_date' => $this->emirates_id_issue_date,
'employer' => $this->emirates_id_employer,
'issue_place' => $this->emirates_id_issue_place,
'occupation' => $this->emirates_id_occupation,
'card_number' => $this->emirates_id_card_number,
'gender' => $this->emirates_id_gender,
];
}
return array_merge($array, [
'license' => $licenseDataObj,
'emirates_id' => $emiratesIdData,
]);
}
}

View File

@ -316,6 +316,11 @@ 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,
];
@ -376,6 +381,12 @@ 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]);
}
@ -392,13 +403,31 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
// Fallbacks
if (empty($extracted['emirates_id_number'])) {
$extracted['emirates_id_number'] = '784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
$extracted['emirates_id_number'] = '784-1987-5493842-5';
}
if (empty($extracted['name'])) {
$extracted['name'] = 'KRISHNA PRASAD';
$extracted['name'] = 'Mohammad Jobaier Mohammad Abul Kalam';
}
if (empty($extracted['nationality'])) {
$extracted['nationality'] = 'NPL';
$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';
}
return $extracted;
@ -517,10 +546,15 @@ private static function emiratesIdFrontTestFallback(): array
{
return [
'success' => true,
'emirates_id_number' => '784-1988-5310327-2',
'name' => 'KRISHNA PRASAD',
'nationality' => 'NPL',
'date_of_birth' => '1988-03-22',
'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',
'ocr_accuracy' => 98.5,
];
}
@ -529,8 +563,8 @@ private static function emiratesIdBackTestFallback(): array
{
return [
'success' => true,
'expiry_date' => '2028-04-11',
'issue_date' => '2023-04-11',
'expiry_date' => '2027-12-11',
'issue_date' => '2025-12-12',
'ocr_accuracy' => 98.5,
];
}

View File

@ -0,0 +1,28 @@
<?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

@ -0,0 +1,38 @@
<?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

@ -0,0 +1,32 @@
<?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('sponsors', function (Blueprint $table) {
if (!Schema::hasColumn('sponsors', 'license_data')) {
$table->json('license_data')->nullable()->after('license_expiry');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sponsors', function (Blueprint $table) {
if (Schema::hasColumn('sponsors', 'license_data')) {
$table->dropColumn('license_data');
}
});
}
};

View File

@ -0,0 +1,89 @@
<?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
{
$visaDocuments = \DB::table('worker_documents')->where('type', 'visa')->get();
foreach ($visaDocuments as $doc) {
$ocrData = json_decode($doc->ocr_data, true);
if (is_array($ocrData)) {
$cleaned = $this->cleanVisaData($ocrData);
\DB::table('worker_documents')->where('id', $doc->id)->update([
'ocr_data' => json_encode($cleaned)
]);
}
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Cleaning up or reverting structure is not strictly necessary or possible
// since we map legacy fields to modern ones.
}
private function cleanVisaData(array $visaDataInput): array
{
$rawVisaType = $visaDataInput['visa_type'] ?? null;
$rawEntryPermitNo = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? $visaDataInput['number'] ?? null;
$rawIssueDate = $visaDataInput['issue_date'] ?? null;
$rawValidUntil = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
$rawUidNo = $visaDataInput['uid_no'] ?? $visaDataInput['id_number'] ?? null;
$rawFullName = $visaDataInput['full_name'] ?? $visaDataInput['name'] ?? null;
$rawNationality = $visaDataInput['nationality'] ?? null;
$rawPlaceOfBirth = $visaDataInput['place_of_birth'] ?? null;
$rawDateOfBirth = $visaDataInput['date_of_birth'] ?? $visaDataInput['dob'] ?? null;
$rawPassportNo = $visaDataInput['passport_no'] ?? $visaDataInput['passport_number'] ?? null;
$rawProfession = $visaDataInput['profession'] ?? null;
$rawSponsorName = $visaDataInput['sponsor_name'] ?? null;
$rawOcrAccuracy = $visaDataInput['ocr_accuracy'] ?? null;
// Clean sponsor
$sponsorData = $visaDataInput['sponsor'] ?? [];
if (is_string($sponsorData)) {
$sponsorNameFromObj = $sponsorData;
$sponsorAddress = '';
$sponsorPhone = '';
} else {
$sponsorNameFromObj = $sponsorData['name'] ?? $sponsorData['sponsor_name'] ?? $sponsorData['full_name'] ?? '';
$sponsorAddress = $sponsorData['address'] ?? '';
$sponsorPhone = $sponsorData['phone'] ?? $sponsorData['mobile'] ?? $sponsorData['mobile_number'] ?? $sponsorData['phone_number'] ?? '';
}
if (empty($rawSponsorName)) {
$rawSponsorName = $sponsorNameFromObj;
}
return [
'document_type' => 'uae_visa',
'country' => 'United Arab Emirates',
'visa_type' => $rawVisaType ?: 'ENTRY PERMIT',
'entry_permit_no' => $rawEntryPermitNo ?: '',
'issue_date' => $rawIssueDate ?: '',
'valid_until' => $rawValidUntil ?: '',
'uid_no' => $rawUidNo ?: '',
'full_name' => $rawFullName ?: '',
'nationality' => $rawNationality ?: '',
'place_of_birth' => $rawPlaceOfBirth ?: '',
'date_of_birth' => $rawDateOfBirth ?: '',
'passport_no' => $rawPassportNo ?: '',
'profession' => $rawProfession ?: '',
'sponsor_name' => $rawSponsorName ?: '',
'ocr_accuracy' => $rawOcrAccuracy !== null ? (int)$rawOcrAccuracy : 99,
'sponsor' => [
'name' => $sponsorNameFromObj ?: '',
'address' => $sponsorAddress ?: '',
'phone' => $sponsorPhone ?: '',
]
];
}
};

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,7 @@ 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

@ -54,7 +54,7 @@ export default function Login() {
<hr className="border-slate-100 mb-6" />
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-5">
<form onSubmit={handleSubmit} noValidate className="space-y-5">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1.5">
Email Address

View File

@ -286,9 +286,8 @@ 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">Verification Status</TableHead>
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Country</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>
@ -310,11 +309,6 @@ 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'
@ -323,10 +317,8 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
</span>
</TableCell>
<TableCell>
<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 className="text-xs font-bold text-slate-700">
{emp.employer_profile?.country || 'United Arab Emirates'}
</span>
</TableCell>
<TableCell>
@ -410,7 +402,7 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
))
) : (
<TableRow>
<TableCell colSpan={6} className="py-12 text-center text-slate-400 font-semibold text-sm">
<TableCell colSpan={5} className="py-12 text-center text-slate-400 font-semibold text-sm">
No employers found.
</TableCell>
</TableRow>
@ -478,10 +470,6 @@ 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>
@ -535,28 +523,52 @@ 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 & Document Extracted Details
Emirates ID 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">Emirates ID Number</span>
<span className="font-mono font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_number || 'Not Extracted / Pending'}</span>
<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>
</div>
<div>
<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>
<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>
</div>
<div>
<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>
<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>
</div>
<div>
<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>
<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>
</div>
</div>
</div>
@ -590,15 +602,7 @@ 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,19 +43,78 @@ 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" />
@ -68,11 +127,13 @@ 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={() => { setEditingPlan(null); setIsDialogOpen(true); }}
onClick={handleCreateClick}
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" />
@ -94,64 +155,72 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
</TableRow>
</TableHeader>
<TableBody>
{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">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
plan.id === 'basic' ? 'bg-blue-50 text-blue-600' :
plan.id === 'premium' ? 'bg-teal-50 text-[#0F6E56]' :
'bg-amber-50 text-amber-600'
}`}>
{plan.id === 'basic' ? <Zap className="w-4 h-4" /> :
plan.id === 'premium' ? <Shield className="w-4 h-4" /> :
<Trophy className="w-4 h-4" />}
</div>
<span className="font-bold text-slate-900 text-sm">{plan.name}</span>
</div>
</TableCell>
<TableCell className="font-bold text-slate-900">{plan.price}</TableCell>
<TableCell className="text-sm text-slate-500 font-medium">{plan.duration}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{plan.features.map((f, i) => (
<span key={i} className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-[10px] font-bold uppercase tracking-tight">
{f}
</span>
))}
</div>
</TableCell>
<TableCell>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest ${
plan.status === 'Active' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-500'
}`}>
{plan.status}
</span>
</TableCell>
<TableCell className="text-right pr-6">
<div className="flex items-center justify-end space-x-2">
<button
onClick={() => handleEdit(plan)}
className="p-2 text-slate-400 hover:text-[#0F6E56] hover:bg-teal-50 rounded-lg transition-all"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(plan.id)}
className="p-2 text-slate-400 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
{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) => (
<TableRow key={plan.id} className="hover:bg-slate-50/50 transition-colors">
<TableCell className="py-4">
<div className="flex items-center space-x-3">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
plan.id === 'basic' ? 'bg-blue-50 text-blue-600' :
plan.id === 'premium' ? 'bg-teal-50 text-[#0F6E56]' :
'bg-amber-50 text-amber-600'
}`}>
{plan.id === 'basic' ? <Zap className="w-4 h-4" /> :
plan.id === 'premium' ? <Shield className="w-4 h-4" /> :
<Trophy className="w-4 h-4" />}
</div>
<span className="font-bold text-slate-900 text-sm">{plan.name}</span>
</div>
</TableCell>
<TableCell className="font-bold text-slate-900">{plan.price}</TableCell>
<TableCell className="text-sm text-slate-500 font-medium">{plan.duration}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{plan.features.map((f, i) => (
<span key={i} className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-[10px] font-bold uppercase tracking-tight">
{f}
</span>
))}
</div>
</TableCell>
<TableCell>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest ${
plan.status === 'Active' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-500'
}`}>
{plan.status}
</span>
</TableCell>
<TableCell className="text-right pr-6">
<div className="flex items-center justify-end space-x-2">
<button
onClick={() => handleEdit(plan)}
className="p-2 text-slate-400 hover:text-[#0F6E56] hover:bg-teal-50 rounded-lg transition-all"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(plan.id)}
className="p-2 text-slate-400 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
{/* Plan Modal (Placeholder UI) */}
{/* Plan Modal */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="sm:max-w-[500px] rounded-[24px]">
<DialogHeader>
@ -163,44 +232,70 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
</DialogDescription>
</DialogHeader>
<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 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" />
<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"
/>
</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"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Price (AED)</label>
<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" />
<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>
</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"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label>
<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 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">
<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
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>
<DialogFooter className="sm:justify-end gap-3 pt-4 border-t border-slate-100">
<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

@ -62,6 +62,16 @@ const locationData = {
}
};
const safeRender = (value) => {
if (value === null || value === undefined) return 'N/A';
if (typeof value === 'object') {
if (value.name) return String(value.name);
if (value.full_name) return String(value.full_name);
return JSON.stringify(value);
}
return String(value);
};
export default function WorkerManagement({ workers }) {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
@ -208,7 +218,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 === statusFilter;
const matchesStatus = statusFilter === 'all' || worker.status?.toLowerCase() === statusFilter.toLowerCase();
return matchesSearch && matchesStatus;
});
@ -250,15 +260,13 @@ 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', 'inactive', 'suspended'].map((f) => (
{['all', 'active', 'hired', 'suspended'].map((f) => (
<button
key={f}
onClick={() => setStatusFilter(f)}
@ -377,9 +385,13 @@ 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 === 'active'
worker.status?.toLowerCase() === 'active'
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
: worker.status === 'suspended' ? 'bg-red-100 text-red-800 border-none' : 'bg-slate-50 text-slate-700 border-slate-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}
@ -526,15 +538,7 @@ 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>
@ -716,10 +720,7 @@ 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>
@ -758,36 +759,60 @@ export default function WorkerManagement({ workers }) {
<div className="grid grid-cols-2 gap-3 text-xs">
<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">{passportDoc.ocr_data?.passport_number || passportDoc.number || 'N/A'}</span>
<span className="font-mono font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.passport_number || passportDoc.number)}</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">{safeRender(passportDoc.ocr_data?.surname)}</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>
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.given_names)}</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">{safeRender(passportDoc.ocr_data?.sex)}</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">Nationality</span>
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.nationality || 'N/A'}</span>
</div>
<div>
<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>
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.date_of_birth)}</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>
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.place_of_birth)}</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">{safeRender(passportDoc.ocr_data?.nationality)}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issuing Country</span>
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.issuing_country)}</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>
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.date_of_issue || passportDoc.issue_date)}</span>
</div>
<div>
<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>
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.date_of_expiry || passportDoc.expiry_date)}</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">{safeRender(passportDoc.ocr_data?.authority)}</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">{safeRender(passportDoc.ocr_data?.document_type)}</span>
</div>
{passportDoc.ocr_data?.personal_number &&
passportDoc.ocr_data?.personal_number !== 'N/A' &&
passportDoc.ocr_data?.personal_number !== (passportDoc.ocr_data?.passport_number || passportDoc.number) && (
<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">{safeRender(passportDoc.ocr_data?.personal_number)}</span>
</div>
)}
</div>
{passportDoc.file_path && (
<div className="pt-2 border-t border-slate-200/60 flex justify-end">
@ -819,36 +844,90 @@ export default function WorkerManagement({ workers }) {
</div>
);
}
const sponsorObj = visaDoc.ocr_data?.sponsor;
const isSponsorObj = sponsorObj && typeof sponsorObj === 'object' && !Array.isArray(sponsorObj);
const sponsorName = isSponsorObj
? (sponsorObj.name || sponsorObj.sponsor_name || sponsorObj.full_name)
: (visaDoc.ocr_data?.sponsor_name || (typeof sponsorObj === 'string' ? sponsorObj : null));
const sponsorMobile = isSponsorObj
? (sponsorObj.mobile || sponsorObj.mobile_number || sponsorObj.phone)
: null;
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">File Number</span>
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.file_number || visaDoc.number || 'N/A'}</span>
<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">{safeRender(visaDoc.ocr_data?.entry_permit_no)}</span>
</div>
<div>
<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>
<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">{safeRender(visaDoc.ocr_data?.uid_no)}</span>
</div>
{visaDoc.ocr_data?.id_number && visaDoc.ocr_data?.id_number !== 'N/A' && (
<div>
<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">{safeRender(visaDoc.ocr_data?.id_number)}</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">{safeRender(visaDoc.ocr_data?.visa_type)}</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>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Profession</span>
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.profession)}</span>
</div>
{sponsorName && (
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sponsor Name</span>
<span className="font-bold text-slate-800">{safeRender(sponsorName)}</span>
</div>
)}
{sponsorMobile && (
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sponsor Mobile</span>
<span className="font-mono font-bold text-slate-800">{safeRender(sponsorMobile)}</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">{safeRender(visaDoc.ocr_data?.nationality)}</span>
</div>
<div>
<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>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Country</span>
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.country)}</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>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Birth</span>
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.date_of_birth)}</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">{safeRender(visaDoc.ocr_data?.place_of_birth)}</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>
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.place_of_issue)}</span>
</div>
<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>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issue Date</span>
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.issue_date || visaDoc.issue_date)}</span>
</div>
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Expiry Date</span>
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.expiry_date || visaDoc.expiry_date)}</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">{safeRender(visaDoc.ocr_data?.document_type)}</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">{safeRender(visaDoc.ocr_data?.ocr_accuracy)}</span>
</div>
</div>
{visaDoc.file_path && (
@ -869,50 +948,18 @@ 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 & Location */}
{/* Contact Detail */}
<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 & Location</h3>
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Contact Detail</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>
@ -952,31 +999,6 @@ 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

@ -72,9 +72,10 @@ export default function UploadEmiratesId({ email }) {
} catch (err) {
if (err.response && err.response.data && err.response.data.errors) {
const errors = err.response.data.errors;
const errorMsg = errors.emirates_id_front?.[0] || errors.emirates_id_back?.[0] || 'Validation failed.';
const firstError = Object.values(errors).flat()[0];
const errorMsg = firstError || 'Validation failed.';
setError(errorMsg);
toast.error('Validation failed. Please verify the document.');
toast.error(errorMsg);
} else {
toast.error('Scan extraction failed. Please try again.');
}

View File

@ -181,7 +181,6 @@ export default function Dashboard({
<div>
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">{t('total_hired_workers', 'Total Hired Workers')}</div>
<div className="text-3xl font-black text-slate-800 mt-1">{stats.hired_count}</div>
<div className="text-[11px] text-slate-500 font-medium mt-1">{t('direct_contracts', 'Direct contracts initiated')}</div>
</div>
<Link href="/employer/candidates" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1 pt-2 border-t border-slate-100">
<span>{t('track_active_staff', 'Track active staff')}</span>
@ -200,9 +199,8 @@ export default function Dashboard({
</span>
</div>
<div>
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">{t('shortlisted_candidates', 'Shortlisted Candidates')}</div>
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">{t('saved_workers', 'Saved Workers')}</div>
<div className="text-3xl font-black text-slate-800 mt-1">{stats.shortlisted_count}</div>
<div className="text-[11px] text-slate-500 font-medium mt-1">{t('bookmark_list', 'Bookmark list for interviews')}</div>
</div>
<Link href="/employer/shortlist" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1 pt-2 border-t border-slate-100">
<span>{t('open_shortlist', 'Open shortlist book')}</span>

View File

@ -22,7 +22,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import FilterDrawer, { FilterSection, FilterSelect, FilterInput, FilterChips, FilterCheckboxList } from '../../Components/Employer/FilterDrawer';
import FilterDrawer, { FilterSection, FilterSelect, FilterInput, FilterChips, FilterCheckboxList } from '../../components/Employer/FilterDrawer';
export default function SelectedCandidates({
selectedWorkers,

View File

@ -123,23 +123,7 @@ export default function Shortlist({ shortlistedWorkers }) {
{/* Passport Status Verification Badge & Visa Status */}
<div className="flex flex-wrap gap-1 mt-0.5">
{worker.visa_expiry_date ? (
<div className={`flex items-center space-x-1 text-[9px] font-black uppercase px-1.5 py-0.5 rounded border ${
worker.document_expiry_days !== null && worker.document_expiry_days <= 30
? 'text-rose-700 bg-rose-50 border-rose-200 animate-pulse'
: worker.document_expiry_days !== null && worker.document_expiry_days <= 90
? 'text-amber-700 bg-amber-50 border-amber-200'
: 'text-[#185FA5] bg-blue-50 border-blue-100'
}`}>
<Calendar className="w-3 h-3 flex-shrink-0" />
<span>Visa Exp: {worker.visa_expiry_date}</span>
</div>
) : (
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
<Calendar className="w-3 h-3 flex-shrink-0 text-amber-600" />
<span>Visa Expiry Pending</span>
</div>
)}
{/* Removed visa expiry warning badge for cleaner UI */}
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
<AlertTriangle className="w-3 h-3 text-amber-600 flex-shrink-0 animate-bounce" />

View File

@ -28,7 +28,7 @@ import {
ShieldCheck,
User
} from 'lucide-react';
import FilterDrawer, { FilterSection, FilterSelect, FilterInput, FilterChips, FilterCheckboxList } from '../../../Components/Employer/FilterDrawer';
import FilterDrawer, { FilterSection, FilterSelect, FilterInput, FilterChips, FilterCheckboxList } from '../../../components/Employer/FilterDrawer';
const getLanguageFlag = (lang) => {
const flags = {
@ -704,17 +704,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
<span>{worker.visa_status}</span>
</div>
)}
{worker.document_expiry_status && (
<div className={`flex items-center space-x-1 text-[9px] font-black uppercase px-1.5 py-0.5 rounded border ${
worker.document_expiry_days !== null && worker.document_expiry_days <= 30
? 'text-rose-700 bg-rose-50 border-rose-200 animate-pulse'
: worker.document_expiry_days !== null && worker.document_expiry_days <= 90
? 'text-amber-700 bg-amber-50 border-amber-200'
: 'text-[#185FA5] bg-blue-50 border-blue-100'
}`}>
<span>📅 {worker.document_expiry_status}</span>
</div>
)}
{/* Removed visa expiry status badge for cleaner UI */}
</div>
<div className="flex items-center space-x-1.5 text-xs text-slate-500 mt-1">

View File

@ -54,7 +54,7 @@ const getExpiryStatus = (expiryDate) => {
if (diffDays < 0) {
return {
text: `Expired \${Math.abs(diffDays)} days ago`,
text: 'Expired',
color: 'text-rose-700 bg-rose-50 border-rose-200 font-black'
};
} else if (diffDays === 0) {
@ -64,15 +64,26 @@ const getExpiryStatus = (expiryDate) => {
};
} else {
return {
text: `Valid until \${expiryDate} (Expires in \${diffDays} days)`,
text: `Valid until ${expiryDate}`,
color: 'text-emerald-700 bg-emerald-50 border-emerald-150 font-black'
};
}
};
const safeRender = (value) => {
if (value === null || value === undefined) return 'N/A';
if (typeof value === 'object') {
if (value.name) return String(value.name);
if (value.full_name) return String(value.full_name);
return JSON.stringify(value);
}
return String(value);
};
export default function Show({ worker }) {
const { t } = useTranslation();
const passportDoc = worker.documents?.find(d => d.type === 'passport');
const emiratesIdDoc = worker.documents?.find(d => d.type === 'emirates_id' || d.type === 'eid');
const visaDoc = worker.documents?.find(d => d.type === 'visa');
const [showReportModal, setShowReportModal] = useState(false);
@ -351,13 +362,6 @@ 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">
@ -431,8 +435,8 @@ export default function Show({ worker }) {
{(() => {
const expiryStatus = getExpiryStatus(passportDoc.expiry_date);
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider \${expiryStatus.color}`}>
\${expiryStatus.text}
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider ${expiryStatus.color}`}>
{expiryStatus.text}
</span>
);
})()}
@ -449,39 +453,51 @@ export default function Show({ worker }) {
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('given_names', 'Given Names')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.given_names || 'N/A'}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.given_names)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('surname', 'Surname')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.surname || 'N/A'}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.surname)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('passport_number', 'Passport Number')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.number}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.passport_number || passportDoc.number)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sex', 'Sex')}</div>
<div className="text-xs font-bold text-slate-800 uppercase">{passportDoc.ocr_data.sex || 'N/A'}</div>
<div className="text-xs font-bold text-slate-800 uppercase">{safeRender(passportDoc.ocr_data.sex)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('date_of_birth', 'Date of Birth')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.date_of_birth || 'N/A'}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.date_of_birth)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_birth', 'Place of Birth')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.place_of_birth || 'N/A'}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.place_of_birth)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Date of Issue')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.issue_date || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Date of Expiry')}</div>
<div className="text-xs font-bold text-emerald-600">{passportDoc.expiry_date || 'N/A'}</div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('nationality', 'Nationality')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.nationality || worker.nationality)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issuing_country', 'Issuing Country')}</div>
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.issuing_country || 'N/A'}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.issuing_country)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Date of Issue')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.date_of_issue || passportDoc.issue_date)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Date of Expiry')}</div>
<div className="text-xs font-bold text-emerald-600">{safeRender(passportDoc.ocr_data.date_of_expiry || passportDoc.expiry_date)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('authority', 'Authority')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.authority)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_type', 'Document Type')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.document_type || passportDoc.type || 'passport')}</div>
</div>
</div>
) : (
@ -534,6 +550,106 @@ export default function Show({ worker }) {
</div>
)}
{/* Document Card: Emirates ID */}
{emiratesIdDoc && (
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm lg:col-span-2">
<div className="bg-slate-50 px-5 py-3.5 border-b border-slate-100 flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="text-[11px] font-black text-slate-800 uppercase tracking-wider">{t('emirates_id_details', 'Emirates ID Details')}</span>
{(() => {
const expiryStatus = getExpiryStatus(emiratesIdDoc.expiry_date);
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider ${expiryStatus.color}`}>
{expiryStatus.text}
</span>
);
})()}
</div>
{emiratesIdDoc.ocr_accuracy && (
<span className="text-[9px] font-bold text-emerald-600 bg-emerald-50/50 border border-emerald-100 px-2 py-0.5 rounded-full">
{emiratesIdDoc.ocr_accuracy}% Match
</span>
)}
</div>
<div className="p-6">
{emiratesIdDoc.ocr_data ? (
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('emirates_id_number', 'Emirates ID Number')}</div>
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.emirates_id_number || emiratesIdDoc.number)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('full_name', 'Full Name')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.name || emiratesIdDoc.ocr_data.full_name)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('nationality', 'Nationality')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.nationality)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('date_of_birth', 'Date of Birth')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.date_of_birth || emiratesIdDoc.ocr_data.dob)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('gender', 'Gender')}</div>
<div className="text-xs font-bold text-slate-800 uppercase">{safeRender(emiratesIdDoc.ocr_data.gender || emiratesIdDoc.ocr_data.sex)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('occupation', 'Occupation')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.occupation || emiratesIdDoc.ocr_data.profession)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('employer', 'Employer')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.employer)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue Date')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.issue_date || emiratesIdDoc.issue_date)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Expiry Date')}</div>
<div className="text-xs font-bold text-emerald-600">{safeRender(emiratesIdDoc.ocr_data.expiry_date || emiratesIdDoc.expiry_date)}</div>
</div>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_id', 'Document ID')}</div>
<div className="text-xs font-bold text-slate-800">{emiratesIdDoc.number}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue')}</div>
<div className="text-xs font-bold text-slate-800">{emiratesIdDoc.issue_date || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Expiry')}</div>
<div className="text-xs font-bold text-emerald-600">{emiratesIdDoc.expiry_date || 'N/A'}</div>
</div>
</div>
)}
<div className="mt-6 pt-4 border-t border-slate-100 flex items-center justify-between text-xs text-slate-500 font-medium">
<span className="flex items-center space-x-1.5">
<ShieldCheck className="w-4 h-4 text-emerald-600" />
<span>UAE PDPL Compliant Scan Storage (Permanently Purged)</span>
</span>
{emiratesIdDoc.file_path && (
<a
href={emiratesIdDoc.file_path}
target="_blank"
rel="noreferrer"
className="inline-flex items-center space-x-1 text-[9px] font-black text-blue-600 hover:text-blue-700 bg-blue-50 px-2.5 py-1.5 rounded-lg border border-blue-155 transition-colors uppercase tracking-wider"
>
<FileText className="w-3.5 h-3.5" />
<span>View Emirates ID Scan</span>
</a>
)}
</div>
</div>
</div>
)}
{/* Document Card: Visa */}
{visaDoc ? (
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm lg:col-span-2">
@ -541,10 +657,10 @@ export default function Show({ worker }) {
<div className="flex items-center space-x-2">
<span className="text-[11px] font-black text-slate-800 uppercase tracking-wider">{t('visa_details', 'Entry Visa Details')}</span>
{(() => {
const expiryStatus = getExpiryStatus(passportDoc.expiry_date);
const expiryStatus = getExpiryStatus(visaDoc.expiry_date || worker.visa_expiry_date);
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider \${expiryStatus.color}`}>
\${expiryStatus.text}
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider ${expiryStatus.color}`}>
{expiryStatus.text}
</span>
);
})()}
@ -561,46 +677,92 @@ export default function Show({ worker }) {
</div>
<div className="p-6">
{visaDoc.ocr_data ? (
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('full_name', 'Full Name')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.name || 'N/A'}</div>
{visaDoc.ocr_data ? (() => {
const sponsorObj = visaDoc.ocr_data?.sponsor;
const isSponsorObj = sponsorObj && typeof sponsorObj === 'object' && !Array.isArray(sponsorObj);
const sponsorName = isSponsorObj
? (sponsorObj.name || sponsorObj.sponsor_name)
: (visaDoc.ocr_data?.sponsor_name || (typeof sponsorObj === 'string' ? sponsorObj : ''));
const sponsorPhone = isSponsorObj
? (sponsorObj.phone || sponsorObj.mobile || sponsorObj.mobile_number)
: '';
const sponsorAddress = isSponsorObj
? (sponsorObj.address || '')
: '';
return (
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('full_name', 'Full Name')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.full_name || worker.name)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('entry_permit_no', 'Entry Permit No')}</div>
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(visaDoc.ocr_data.entry_permit_no || visaDoc.number)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('uid_no', 'UID No')}</div>
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(visaDoc.ocr_data.uid_no)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('visa_type', 'Visa Type')}</div>
<div className="text-xs font-bold text-slate-800 uppercase">{safeRender(visaDoc.ocr_data.visa_type || 'ENTRY PERMIT')}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('passport_no', 'Passport No')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.passport_no || visaDoc.ocr_data.passport_number)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('profession', 'Profession')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.profession || worker.visa_status)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sponsor_name', 'Sponsor Name')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(sponsorName || visaDoc.ocr_data.sponsor_name)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sponsor_phone', 'Sponsor Phone')}</div>
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(sponsorPhone)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sponsor_address', 'Sponsor Address')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(sponsorAddress)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('nationality', 'Nationality')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.nationality || worker.nationality)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('country', 'Country')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.country || 'United Arab Emirates')}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('date_of_birth', 'Date of Birth')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.date_of_birth || visaDoc.ocr_data.dob)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_birth', 'Place of Birth')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.place_of_birth)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue Date')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.issue_date || visaDoc.issue_date)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('valid_until', 'Valid Until')}</div>
<div className="text-xs font-bold text-emerald-600">{safeRender(visaDoc.ocr_data.valid_until || visaDoc.ocr_data.expiry_date || visaDoc.expiry_date)}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_type', 'Document Type')}</div>
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.document_type || visaDoc.type || 'uae_visa')}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('ocr_accuracy', 'OCR Accuracy')}</div>
<div className="text-xs font-bold text-emerald-600">{(visaDoc.ocr_accuracy || visaDoc.ocr_data?.ocr_accuracy || 99)}%</div>
</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('file_number', 'File Number')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.number}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('profession', 'Profession')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.profession || worker.visa_status || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sponsor', 'Sponsor')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.sponsor || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('passport_number', 'Passport Number')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.passport_number || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_issue', 'Place of Issue')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.place_of_issue || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Date of Issue')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.issue_date || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Date of Expiry')}</div>
<div className="text-xs font-bold text-emerald-600">{visaDoc.expiry_date || 'N/A'}</div>
</div>
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('accompanied_by', 'Accompanied By')}</div>
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.accompanied_by || 'N/A'}</div>
</div>
</div>
) : (
);
})() : (
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
<div>
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_id', 'Document ID')}</div>

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import * as React from 'react';
import { X, RotateCcw, SlidersHorizontal } from 'lucide-react';
/**
@ -14,7 +14,7 @@ import { X, RotateCcw, SlidersHorizontal } from 'lucide-react';
*/
export default function FilterDrawer({ open, onClose, onReset, activeCount = 0, title = 'Filters', children }) {
// Lock body scroll when drawer is open
useEffect(() => {
React.useEffect(() => {
if (open) {
document.body.style.overflow = 'hidden';
} else {
@ -24,8 +24,8 @@ export default function FilterDrawer({ open, onClose, onReset, activeCount = 0,
}, [open]);
// Close on Escape key
useEffect(() => {
const handler = (e) => { if (e.key === 'Escape') onClose(); };
React.useEffect(() => {
const handler = (e) => { if (e.key === 'Escape' && typeof onClose === 'function') onClose(); };
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [onClose]);
@ -164,14 +164,15 @@ export function FilterInput({ id, value, onChange, placeholder }) {
* FilterChips a row of selectable chip buttons for single-select options.
* options: [{ value, label }]
*/
export function FilterChips({ options, selected, onChange }) {
export function FilterChips({ options = [], selected, onChange }) {
const opts = options || [];
return (
<div className="flex flex-wrap gap-2">
{options.map(opt => (
{opts.map(opt => (
<button
key={opt.value}
type="button"
onClick={() => onChange(opt.value)}
onClick={() => onChange && onChange(opt.value)}
className={`px-3 py-1.5 rounded-full text-xs font-bold transition-all border ${
selected === opt.value
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-sm'
@ -185,16 +186,19 @@ export function FilterChips({ options, selected, onChange }) {
);
}
export function FilterCheckboxList({ items, selected, onToggle, placeholder = 'Search...' }) {
export function FilterCheckboxList({ items = [], selected = [], onToggle, placeholder = 'Search...' }) {
const [search, setSearch] = React.useState('');
const filteredItems = items.filter(item =>
item.toLowerCase().includes(search.toLowerCase())
const itemsList = items || [];
const selectedList = selected || [];
const filteredItems = itemsList.filter(item =>
item && item.toLowerCase().includes((search || '').toLowerCase())
);
return (
<div className="space-y-2">
{items.length > 5 && (
{itemsList.length > 5 && (
<div className="relative">
<input
type="text"
@ -216,12 +220,12 @@ export function FilterCheckboxList({ items, selected, onToggle, placeholder = 'S
)}
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
{filteredItems.map(item => {
const isChecked = selected.includes(item);
const isChecked = selectedList.includes(item);
return (
<button
key={item}
type="button"
onClick={() => onToggle(item)}
onClick={() => onToggle && onToggle(item)}
className="w-full flex items-center space-x-3 text-left focus:outline-none group cursor-pointer"
>
<div className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 transition-all ${

View File

@ -43,7 +43,8 @@
"total_hired_workers": "Total Hired Workers",
"direct_contracts": "Direct contracts initiated",
"track_active_staff": "Track active staff",
"shortlisted_candidates": "Shortlisted Candidates",
"shortlisted_candidates": "Saved Workers",
"saved_workers": "Saved Workers",
"bookmark_list": "Bookmark list for interviews",
"open_shortlist": "Open shortlist book",
"discover_insights": "Discover Insights & Market Stats",

View File

@ -75,6 +75,7 @@
Route::post('/workers/profile/mark-hired', [WorkerProfileController::class, 'markHired']);
Route::get('/workers/dashboard/views', [WorkerProfileController::class, 'getProfileViews']);
Route::get('/workers/dashboard', [WorkerProfileController::class, 'getDashboard']);
Route::post('/workers/change-password', [WorkerProfileController::class, 'changePassword']);
@ -110,6 +111,7 @@
Route::get('/employers/profile', [EmployerProfileController::class, 'getProfile']);
Route::post('/employers/profile/update', [EmployerProfileController::class, 'updateProfile']);
Route::get('/employers/dashboard', [EmployerProfileController::class, 'getDashboard']);
Route::post('/employers/change-password', [EmployerProfileController::class, 'changePassword']);
// Messaging Management
Route::get('/employers/conversations', [EmployerMessageController::class, 'getConversations']);
@ -119,6 +121,7 @@
// Announcement Management
Route::get('/employers/announcements', [EmployerAnnouncementController::class, 'getAnnouncements']);
Route::get('/employers/my-announcements', [EmployerAnnouncementController::class, 'getMyAnnouncements']);
Route::post('/employers/announcements', [EmployerAnnouncementController::class, 'createAnnouncement']);
Route::delete('/employers/announcements/{id}', [EmployerAnnouncementController::class, 'deleteAnnouncement']);
@ -156,7 +159,9 @@
// Sponsors can ONLY access: dashboard and charity events. No payments, no worker browsing.
Route::middleware(['auth.sponsor'])->group(function () {
Route::get('/sponsors/profile', [SponsorController::class, 'getProfile']);
Route::post('/sponsors/profile/update', [SponsorController::class, 'updateProfile']);
Route::get('/sponsors/dashboard', [SponsorController::class, 'getDashboard']);
Route::get('/sponsors/charity-events', [SponsorController::class, 'getCharityEvents']);
Route::post('/sponsors/charity-events', [SponsorController::class, 'postCharityEvent']);
Route::post('/sponsors/change-password', [SponsorController::class, 'changePassword']);
});

View File

@ -0,0 +1,124 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminLoginTest extends TestCase
{
use RefreshDatabase;
/**
* Test admin login page can be rendered.
*/
public function test_admin_can_view_login_page()
{
$response = $this->get('/admin/login');
$response->assertStatus(200);
}
/**
* Test admin login validation fails if email is invalid.
*/
public function test_admin_login_fails_if_email_is_invalid()
{
$response = $this->post('/admin/login', [
'email' => 'invalid-email',
'password' => 'password',
]);
$response->assertSessionHasErrors(['email']);
$this->assertEquals(
'Please enter a valid email address.',
session('errors')->get('email')[0]
);
}
/**
* Test admin login validation fails if email is not registered.
*/
public function test_admin_login_fails_if_email_is_not_registered()
{
$response = $this->post('/admin/login', [
'email' => 'unregistered@example.com',
'password' => 'password',
]);
$response->assertSessionHasErrors(['email']);
$this->assertEquals(
'This email is not registered.',
session('errors')->get('email')[0]
);
}
/**
* Test admin login validation fails if email is registered but not as an admin.
*/
public function test_admin_login_fails_if_user_is_not_an_admin()
{
User::create([
'name' => 'Regular User',
'email' => 'user@example.com',
'password' => bcrypt('password'),
'role' => 'employer', // Not admin
]);
$response = $this->post('/admin/login', [
'email' => 'user@example.com',
'password' => 'password',
]);
$response->assertSessionHasErrors(['email']);
$this->assertEquals(
'This email is not registered as an admin.',
session('errors')->get('email')[0]
);
}
/**
* Test admin login validation fails if password is wrong.
*/
public function test_admin_login_fails_if_password_is_incorrect()
{
User::create([
'name' => 'Admin User',
'email' => 'admin_test@example.com',
'password' => bcrypt('correct_password'),
'role' => 'admin',
]);
$response = $this->post('/admin/login', [
'email' => 'admin_test@example.com',
'password' => 'wrong_password',
]);
$response->assertSessionHasErrors(['password']);
$this->assertEquals(
'The password you entered is incorrect.',
session('errors')->get('password')[0]
);
}
/**
* Test admin login succeeds with correct credentials.
*/
public function test_admin_login_succeeds_with_correct_credentials()
{
$admin = User::create([
'name' => 'Admin User',
'email' => 'admin_test@example.com',
'password' => bcrypt('correct_password'),
'role' => 'admin',
]);
$response = $this->post('/admin/login', [
'email' => 'admin_test@example.com',
'password' => 'correct_password',
]);
$response->assertRedirect('/admin/dashboard');
$this->assertAuthenticatedAs($admin);
}
}

View File

@ -103,9 +103,9 @@ public function test_worker_can_get_announcements()
}
/**
* Test employer can get only their posted announcements.
* Test employer can get only their posted announcements via my-announcements.
*/
public function test_employer_can_get_posted_announcements()
public function test_employer_can_get_my_announcements()
{
// Announcement by this employer
Announcement::create([
@ -131,13 +131,66 @@ public function test_employer_can_get_posted_announcements()
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->employerToken,
])->getJson('/api/employers/announcements');
])->getJson('/api/employers/my-announcements');
$response->assertStatus(200);
$this->assertCount(1, $response->json('data.announcements'));
$this->assertEquals('Employer Ann', $response->json('data.announcements.0.title'));
}
/**
* Test employer can get all approved announcements.
*/
public function test_employer_can_get_all_approved_announcements()
{
// Approved announcement by this employer
Announcement::create([
'title' => 'Approved Ann 1',
'body' => 'Message 1',
'type' => 'info',
'employer_id' => $this->employer->id,
'status' => 'approved',
]);
// Pending announcement by this employer
Announcement::create([
'title' => 'Pending Ann 1',
'body' => 'Message 2',
'type' => 'info',
'employer_id' => $this->employer->id,
'status' => 'pending',
]);
// Approved announcement by another sponsor
$sponsor = \App\Models\Sponsor::create([
'full_name' => 'Sponsor Name',
'email' => 'sponsor@example.com',
'mobile' => '+971501112233',
'organization_name' => 'Charity Org',
'address' => 'Dubai',
'role' => 'sponsor',
'password' => bcrypt('password'),
]);
Announcement::create([
'title' => 'Approved Ann 2',
'body' => 'Message 3',
'type' => 'info',
'sponsor_id' => $sponsor->id,
'status' => 'approved',
]);
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->employerToken,
])->getJson('/api/employers/announcements');
$response->assertStatus(200);
$this->assertCount(2, $response->json('data.announcements'));
$titles = collect($response->json('data.announcements'))->pluck('title');
$this->assertTrue($titles->contains('Approved Ann 1'));
$this->assertTrue($titles->contains('Approved Ann 2'));
$this->assertFalse($titles->contains('Pending Ann 1'));
}
/**
* Test employer can create a new announcement.
*/

View File

@ -30,10 +30,9 @@ protected function setUp(): void
EmployerProfile::create([
'user_id' => $this->employer->id,
'company_name' => 'Original Company',
'address' => 'Original Address',
'phone' => '+971501112222',
'verification_status' => 'approved',
'language' => 'English',
'notifications' => true,
]);
}
@ -54,17 +53,17 @@ public function test_employer_can_get_profile()
'profile' => [
'name',
'email',
'company_name',
'phone',
'language',
'address',
'notifications',
'verification_status',
'emirates_id',
]
]
]);
$this->assertEquals('Original Name', $response->json('data.profile.name'));
$this->assertEquals('Original Company', $response->json('data.profile.company_name'));
$this->assertEquals('Original Address', $response->json('data.profile.address'));
}
/**
@ -78,8 +77,7 @@ public function test_employer_can_update_profile()
'name' => 'Updated Name',
'email' => 'employer_updated@example.com',
'phone' => '+971509999999',
'company_name' => 'Updated Company Corp',
'language' => 'Arabic',
'address' => 'Updated Address Info',
'notifications' => false,
]);
@ -91,10 +89,10 @@ public function test_employer_can_update_profile()
'profile' => [
'name',
'email',
'company_name',
'phone',
'language',
'address',
'notifications',
'emirates_id',
]
]
]);
@ -107,67 +105,105 @@ public function test_employer_can_update_profile()
$this->assertDatabaseHas('employer_profiles', [
'user_id' => $this->employer->id,
'company_name' => 'Updated Company Corp',
'language' => 'Arabic',
'address' => 'Updated Address Info',
'notifications' => false,
]);
}
/**
* Test employer can update password when providing correct current password.
* Test employer can update profile with optional Emirates ID fields.
*/
public function test_employer_can_update_password()
public function test_employer_can_update_profile_with_emirates_id_fields()
{
$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',
'name' => 'Ahmad',
'email' => 'ahmad@example.com',
'phone' => '+971509990001',
'address' => 'Villa 12, Jumeirah 2',
'notifications' => true,
'current_password' => 'Password@123',
'new_password' => 'NewSecurePassword@123',
'new_password_confirmation' => 'NewSecurePassword@123',
'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);
// 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)
->assertJsonStructure([
'success',
'message',
'errors' => [
'current_password'
$response->assertStatus(200)
->assertJson([
'success' => true,
'data' => [
'profile' => [
'name' => 'Mohammad Jobaier Mohammad Abul Kalam',
'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',
'emirates_id' => [
'emirates_id_number' => '784-1987-5493842-5',
'name' => 'Mohammad Jobaier Mohammad Abul Kalam',
'date_of_birth' => '14/04/1987',
'nationality' => 'Bangladesh',
'issue_date' => '12/12/2025',
'expiry_date' => '11/12/2027',
'employer' => 'Msj International Technical Services L.L.C UAE',
'issue_place' => 'Dubai',
'occupation' => 'Electrician',
]
]
]
]);
// Verify password was NOT changed
$this->assertTrue(Hash::check('Password@123', $this->employer->fresh()->password));
$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',
]);
$response->assertStatus(422)
->assertJson([
'success' => false,
'errors' => [
'id_number' => ['Emirates ID mismatch.']
]
]);
// Verify ID was NOT changed
$this->assertEquals('784-1987-5493842-5', $profile->fresh()->emirates_id_number);
}
/**
@ -218,4 +254,121 @@ 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
$w1 = \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',
'verified' => true,
]);
\App\Models\WorkerDocument::create([
'worker_id' => $w1->id, 'type' => 'passport', 'number' => 'P111',
'issue_date' => '2020-01-01', 'expiry_date' => '2030-01-01',
]);
$w2 = \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',
'verified' => true,
]);
\App\Models\WorkerDocument::create([
'worker_id' => $w2->id, 'type' => 'passport', 'number' => 'P222',
'issue_date' => '2020-01-01', 'expiry_date' => '2030-01-01',
]);
$w3 = \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',
'verified' => true,
]);
\App\Models\WorkerDocument::create([
'worker_id' => $w3->id, 'type' => 'passport', 'number' => 'P333',
'issue_date' => '2020-01-01', 'expiry_date' => '2030-01-01',
]);
// 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'));
}
/**
* Test employer can change password successfully.
*/
public function test_employer_can_change_password()
{
// 1. Create a matching sponsor
$sponsor = \App\Models\Sponsor::create([
'full_name' => 'Employer One',
'email' => $this->employer->email,
'mobile' => '+971509990011',
'organization_name' => 'Elite Services',
'address' => 'Dubai',
'role' => 'sponsor',
'password' => bcrypt('Password@123'),
]);
// Verify initial password hash is set
$this->employer->update([
'password' => bcrypt('Password@123')
]);
// 2. Try with wrong current password
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
])->postJson('/api/employers/change-password', [
'current_password' => 'WrongPassword',
'new_password' => 'NewPassword@123',
'new_password_confirmation' => 'NewPassword@123',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['current_password']);
// 3. Try with correct current password
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
])->postJson('/api/employers/change-password', [
'current_password' => 'Password@123',
'new_password' => 'NewPassword@123',
'new_password_confirmation' => 'NewPassword@123',
]);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'Password changed successfully.'
]);
// Verify password is updated for both User and Sponsor
$this->assertTrue(\Illuminate\Support\Facades\Hash::check('NewPassword@123', $this->employer->fresh()->password));
$this->assertTrue(\Illuminate\Support\Facades\Hash::check('NewPassword@123', $sponsor->fresh()->password));
}
}

View File

@ -435,11 +435,184 @@ 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

@ -54,6 +54,7 @@ public function test_employer_registration_and_login_stores_fcm_token_and_sends_
'fcm_token' => 'employer_register_fcm_token',
'emirates_id' => [
'emirates_id_number' => '784-1988-5310327-2',
'name' => 'Employer FCM Test',
'expiry_date' => '2028-04-11',
]
]);
@ -182,6 +183,7 @@ public function test_sponsor_registration_and_login_stores_fcm_token_and_sends_n
],
'emirates_id' => [
'emirates_id_number' => '784-1988-5310327-2',
'name' => 'Sponsor FCM Org',
],
'organization_name' => 'Sponsor FCM Co',
'email' => 'sponsor.fcm@example.com',

View File

@ -276,6 +276,7 @@ public function test_get_report_reasons_filtering()
['reason' => 'Review Abuse', 'status' => 'Active', 'type' => 'Review'],
['reason' => 'General Impersonation', 'status' => 'Active', 'type' => 'Both'],
['reason' => 'Inactive Reason', 'status' => 'Inactive', 'type' => 'Both'],
['reason' => 'Billing Issue', 'status' => 'Active', 'type' => 'Support'],
]);
// 1. Worker retrieves all Active reasons
@ -284,33 +285,43 @@ public function test_get_report_reasons_filtering()
])->getJson('/api/workers/report-reasons');
$response->assertStatus(200)
->assertJsonCount(3, 'reasons')
->assertJsonCount(4, 'reasons')
->assertJsonFragment(['reason' => 'Chat Spam'])
->assertJsonFragment(['reason' => 'Review Abuse'])
->assertJsonFragment(['reason' => 'General Impersonation'])
->assertJsonFragment(['reason' => 'Billing Issue'])
->assertJsonMissing(['reason' => 'Inactive Reason']);
// 2. Worker retrieves Chat reasons only
// 2. Worker retrieves Chat reasons only (case-insensitive)
$responseChat = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->workerToken,
])->getJson('/api/workers/report-reasons?type=Chat');
])->getJson('/api/workers/report-reasons?type=chat');
$responseChat->assertStatus(200)
->assertJsonCount(2, 'reasons')
->assertJsonFragment(['reason' => 'Chat Spam'])
->assertJsonFragment(['reason' => 'General Impersonation'])
->assertJsonMissing(['reason' => 'Review Abuse']);
->assertJsonMissing(['reason' => 'Review Abuse'])
->assertJsonMissing(['reason' => 'Billing Issue']);
// 3. Worker retrieves Review reasons only
// 3. Worker retrieves Review reasons (unsupported type, ignored)
$responseReview = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->workerToken,
])->getJson('/api/workers/report-reasons?type=Review');
$responseReview->assertStatus(200)
->assertJsonCount(2, 'reasons')
->assertJsonFragment(['reason' => 'Review Abuse'])
->assertJsonFragment(['reason' => 'General Impersonation'])
->assertJsonMissing(['reason' => 'Chat Spam']);
->assertJsonCount(4, 'reasons');
// 4. Worker retrieves Support reasons only (case-insensitive)
$responseSupport = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->workerToken,
])->getJson('/api/workers/report-reasons?type=support');
$responseSupport->assertStatus(200)
->assertJsonCount(1, 'reasons')
->assertJsonFragment(['reason' => 'Billing Issue'])
->assertJsonMissing(['reason' => 'Chat Spam'])
->assertJsonMissing(['reason' => 'General Impersonation']);
}
}

View File

@ -71,7 +71,7 @@ public function test_sponsor_can_register_with_license()
$this->assertDatabaseHas('sponsors', [
'mobile' => '+971509990001',
'full_name' => 'Test Sponsor Organization',
'full_name' => 'Ahmad Bin Ahmed',
'license_expiry' => '2028-06-12 00:00:00',
'emirates_id_name' => 'Ahmad Bin Ahmed',
'emirates_id_dob' => '1990-01-01',
@ -537,6 +537,7 @@ public function test_sponsor_registration_with_emirates_id_and_separate_license(
'country_code' => '+971',
'emirates_id' => [
'emirates_id_number' => '784-1988-5310327-2',
'name' => 'Test Sponsor Sep',
],
]);
@ -571,4 +572,313 @@ 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',
]);
}
/**
* Test sponsor can change password successfully.
*/
public function test_sponsor_can_change_password()
{
$sponsor = Sponsor::create([
'full_name' => 'Sponsor Changer',
'email' => 'changer.sponsor@example.com',
'mobile' => '+971509997788',
'password' => Hash::make('Password@123'),
'api_token' => 'sponsor-changer-token',
'status' => 'active',
]);
// Sync with corresponding employer user record to test sync
$user = User::create([
'name' => 'Sponsor Changer',
'email' => 'changer.sponsor@example.com',
'password' => Hash::make('Password@123'),
'role' => 'employer',
]);
// 1. Try with wrong current password
$response = $this->withHeaders([
'Authorization' => 'Bearer sponsor-changer-token',
])->postJson('/api/sponsors/change-password', [
'current_password' => 'WrongPassword',
'new_password' => 'NewPassword@123',
'new_password_confirmation' => 'NewPassword@123',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['current_password']);
// 2. Try with correct current password
$response = $this->withHeaders([
'Authorization' => 'Bearer sponsor-changer-token',
])->postJson('/api/sponsors/change-password', [
'current_password' => 'Password@123',
'new_password' => 'NewPassword@123',
'new_password_confirmation' => 'NewPassword@123',
]);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'Password changed successfully.'
]);
// Verify password is updated for both Sponsor and User
$this->assertTrue(Hash::check('NewPassword@123', $sponsor->fresh()->password));
$this->assertTrue(Hash::check('NewPassword@123', $user->fresh()->password));
}
/**
* Test sponsor can update profile and it correctly syncs with matching user/employer profile.
*/
public function test_sponsor_can_update_profile_and_syncs_with_matching_user()
{
$sponsor = Sponsor::create([
'full_name' => 'Original Sponsor Name',
'email' => 'sync-test@example.com',
'mobile' => '+971500000001',
'password' => Hash::make('Password@123'),
'organization_name' => 'Original Org',
'city' => 'Dubai',
'address' => 'Old Address',
'nationality' => 'Emirati',
'country_code' => '+971',
'api_token' => 'sync-test-token',
'status' => 'active',
]);
$user = User::create([
'name' => 'Original Sponsor Name',
'email' => 'sync-test@example.com',
'password' => Hash::make('Password@123'),
'role' => 'employer',
]);
$profile = EmployerProfile::create([
'user_id' => $user->id,
'company_name' => 'Original Org',
'phone' => '+971500000001',
'address' => 'Old Address',
]);
$response = $this->withHeaders([
'Authorization' => 'Bearer sync-test-token',
])->postJson('/api/sponsors/profile/update', [
'name' => 'Updated Sponsor Name',
'email' => 'new-sync-test@example.com',
'mobile' => '+971500000002',
'organization_name' => 'Updated Org',
'city' => 'Abu Dhabi',
'address' => 'New Address',
'nationality' => 'Jordanian',
'country_code' => '+962',
'emirates_id' => [
'emirates_id_number' => '784-2000-1234567-1',
'name' => 'Updated Sponsor Name',
'date_of_birth' => '2000-01-01',
'expiry_date' => '2028-01-01',
'issue_date' => '2023-01-01',
'occupation' => 'Business Owner',
'employer' => 'Self',
'issue_place' => 'Abu Dhabi',
],
'license' => [
'document_type' => 'dubai_commercial_license',
'country' => 'United Arab Emirates',
'authority' => 'Dubai Economy and Tourism',
'license_no' => '828302',
'company_name' => 'MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C',
'business_name' => 'MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C',
'license_category' => 'Dep. of Economic Development',
'legal_type' => 'Limited Liability Company Single Owner(LLC- - SO)',
'main_license_no' => '828302',
'register_no' => '1656486',
'dcci_no' => '317412',
'members' => [
[
'person_no' => '752967',
'name' => 'Lo lo wallus issued in',
'nationality' => 'India',
'role' => 'Shares Owner Who'
]
]
]
]);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'Profile updated successfully.',
])
->assertJsonPath('data.sponsor.full_name', 'Updated Sponsor Name')
->assertJsonPath('data.sponsor.email', 'new-sync-test@example.com')
->assertJsonPath('data.sponsor.mobile', '+971500000002')
->assertJsonPath('data.sponsor.organization_name', 'Updated Org')
->assertJsonPath('data.sponsor.city', 'Abu Dhabi')
->assertJsonPath('data.sponsor.address', 'New Address')
->assertJsonPath('data.sponsor.nationality', 'Jordanian')
->assertJsonPath('data.sponsor.country_code', '+962')
->assertJsonPath('data.sponsor.license.document_type', 'dubai_commercial_license')
->assertJsonPath('data.sponsor.license.license_no', '828302')
->assertJsonPath('data.sponsor.license.members.0.name', 'Lo lo wallus issued in')
->assertJsonPath('data.sponsor.emirates_id.emirates_id_number', '784-2000-1234567-1')
->assertJsonPath('data.sponsor.emirates_id.name', 'Updated Sponsor Name')
->assertJsonPath('data.sponsor.emirates_id.occupation', 'Business Owner')
->assertJsonPath('data.sponsor.emirates_id.employer', 'Self');
// Check Sponsor table
$this->assertDatabaseHas('sponsors', [
'id' => $sponsor->id,
'full_name' => 'Updated Sponsor Name',
'email' => 'new-sync-test@example.com',
'mobile' => '+971500000002',
'emirates_id' => '784-2000-1234567-1',
'emirates_id_name' => 'Updated Sponsor Name',
'emirates_id_dob' => '2000-01-01',
'emirates_id_issue_date' => '2023-01-01',
'emirates_id_expiry' => '2028-01-01',
'emirates_id_occupation' => 'Business Owner',
'emirates_id_employer' => 'Self',
'emirates_id_issue_place' => 'Abu Dhabi',
]);
$dbSponsor = Sponsor::find($sponsor->id);
$this->assertEquals('dubai_commercial_license', $dbSponsor->license_data['document_type']);
$this->assertEquals('Lo lo wallus issued in', $dbSponsor->license_data['members'][0]['name']);
// Check User table
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Updated Sponsor Name',
'email' => 'new-sync-test@example.com',
]);
// Check EmployerProfile table
$this->assertDatabaseHas('employer_profiles', [
'id' => $profile->id,
'address' => 'New Address',
'phone' => '+971500000002',
'emirates_id_number' => '784-2000-1234567-1',
'emirates_id_name' => 'Updated Sponsor Name',
'emirates_id_dob' => '2000-01-01',
'emirates_id_issue_date' => '2023-01-01',
'emirates_id_expiry' => '2028-01-01',
'emirates_id_occupation' => 'Business Owner',
'emirates_id_employer' => 'Self',
'emirates_id_issue_place' => 'Abu Dhabi',
]);
}
/**
* Test sponsor cannot change immutable Emirates ID.
*/
public function test_sponsor_cannot_change_immutable_emirates_id()
{
$sponsor = Sponsor::create([
'full_name' => 'Sponsor Name',
'email' => 'immutable-id-test@example.com',
'mobile' => '+971500000003',
'password' => Hash::make('Password@123'),
'organization_name' => 'Org',
'city' => 'Dubai',
'address' => 'Address',
'nationality' => 'Emirati',
'country_code' => '+971',
'api_token' => 'immutable-id-token',
'status' => 'active',
'emirates_id' => '784-2000-1234567-1',
]);
$response = $this->withHeaders([
'Authorization' => 'Bearer immutable-id-token',
])->postJson('/api/sponsors/profile/update', [
'name' => 'Sponsor Name',
'email' => 'immutable-id-test@example.com',
'mobile' => '+971500000003',
'organization_name' => 'Org',
'city' => 'Dubai',
'address' => 'Address',
'nationality' => 'Emirati',
'country_code' => '+971',
'id_number' => '784-2000-7654321-2', // different EID
]);
$response->assertStatus(422)
->assertJson([
'success' => false,
'message' => 'Emirates ID mismatch. You cannot change your registered Emirates ID number.',
])
->assertJsonValidationErrors(['id_number']);
}
/**
* Test sponsor profile update validates Emirates ID name OCR.
*/
public function test_sponsor_profile_update_validates_emirates_id_name_ocr()
{
$sponsor = Sponsor::create([
'full_name' => 'Sponsor Name',
'email' => 'ocr-test@example.com',
'mobile' => '+971500000004',
'password' => Hash::make('Password@123'),
'organization_name' => 'Org',
'city' => 'Dubai',
'address' => 'Address',
'nationality' => 'Emirati',
'country_code' => '+971',
'api_token' => 'ocr-token',
'status' => 'active',
]);
$response = $this->withHeaders([
'Authorization' => 'Bearer ocr-token',
])->postJson('/api/sponsors/profile/update', [
'name' => 'Sponsor Name',
'email' => 'ocr-test@example.com',
'mobile' => '+971500000004',
'organization_name' => 'Org',
'city' => 'Dubai',
'address' => 'Address',
'nationality' => 'Emirati',
'country_code' => '+971',
'id_number' => '784-2000-1234567-1',
'full_name' => '', // Failed OCR name extraction
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['full_name'])
->assertJsonPath('errors.full_name.0', 'Failed to extract name from the Emirates ID. Please upload a clearer document.');
}
}

View File

@ -80,24 +80,42 @@ public function test_employer_web_registration_with_emirates_id()
$this->assertDatabaseHas('users', [
'email' => 'abdullah@example.com',
'role' => 'employer',
'name' => 'Mohammad Jobaier Mohammad Abul Kalam',
]);
$this->assertDatabaseHas('employer_profiles', [
'phone' => '501234567',
'company_name' => 'Abdullah Ahmed Household',
'company_name' => 'Mohammad Jobaier Mohammad Abul Kalam Household',
'emirates_id_front' => null, // File is not stored
'address' => 'Villa 14, Al Safa, Dubai',
]);
$profile = EmployerProfile::where('phone', '501234567')->first();
$this->assertNotNull($profile->emirates_id_number);
$this->assertNotNull($profile->emirates_id_expiry);
$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->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',
'full_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
'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',
]);
}
@ -144,8 +162,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-1988-5310327-2', $pendingRegAfterId['emirates_id_number']); // Mock fallback value
$this->assertEquals('2028-04-11', $pendingRegAfterId['emirates_id_expiry']); // Mock fallback value
$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
// Step 4: Payment Choice
$responseStep4 = $this->post('/employer/register-payment', [
@ -168,12 +186,32 @@ public function test_employer_web_registration_with_two_sided_emirates_id()
$this->assertDatabaseHas('users', [
'email' => 'fatima@example.com',
'role' => 'employer',
'name' => 'Mohammad Jobaier Mohammad Abul Kalam',
]);
$this->assertDatabaseHas('employer_profiles', [
'phone' => '509876543',
'company_name' => 'Mohammad Jobaier Mohammad Abul Kalam Household',
'address' => 'Apartment 402, Al Nahda, Sharjah',
]);
$this->assertDatabaseHas('sponsors', [
'email' => 'fatima@example.com',
'mobile' => '509876543',
'full_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
]);
$profile = EmployerProfile::where('phone', '509876543')->first();
$this->assertNotNull($profile);
$this->assertEquals('784-1988-5310327-2', $profile->emirates_id_number);
$this->assertEquals('2028-04-11', $profile->emirates_id_expiry);
$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('Apartment 402, Al Nahda, Sharjah', $profile->address);
}
}

View File

@ -531,9 +531,110 @@ public function test_register_passport_and_visa_with_direct_json_payload()
$visaDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'visa')->first();
$this->assertNotNull($visaDoc->ocr_data);
$this->assertEquals('784198839607839', $visaDoc->ocr_data['id_number']);
$this->assertEquals('784198839607839', $visaDoc->ocr_data['uid_no']);
$this->assertEquals('CLEANER', $visaDoc->ocr_data['profession']);
$this->assertEquals('Sponsor Name', $visaDoc->ocr_data['sponsor']);
$this->assertEquals([
'name' => 'Sponsor Name',
'address' => '',
'phone' => '',
], $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_accuracy);
}
/**
@ -660,10 +761,18 @@ public function test_nationalities_list_api()
])
->assertJsonFragment([
'code' => 'IN',
'country_code' => 'IN',
'phone_code' => '+91',
'dial_code' => '+91',
'calling_code' => '+91',
'name' => 'Indian',
])
->assertJsonFragment([
'code' => 'PH',
'country_code' => 'PH',
'phone_code' => '+63',
'dial_code' => '+63',
'calling_code' => '+63',
'name' => 'Filipino',
]);
@ -728,6 +837,10 @@ public function test_nationalities_list_api()
$this->assertGreaterThanOrEqual(1, count($responseSearch->json('data.nationalities')));
$responseSearch->assertJsonFragment([
'code' => 'IN',
'country_code' => 'IN',
'phone_code' => '+91',
'dial_code' => '+91',
'calling_code' => '+91',
'name' => 'Indian'
]);
}
@ -1431,4 +1544,55 @@ public function test_profile_response_does_not_contain_deprecated_fields()
$this->assertArrayNotHasKey('city', $json);
$this->assertArrayNotHasKey('area', $json);
}
/**
* Test worker can change password successfully.
*/
public function test_worker_can_change_password()
{
$worker = Worker::create([
'name' => 'Password Changer',
'email' => 'changer@example.com',
'phone' => '+971509990099',
'language' => 'HI',
'password' => bcrypt('OldPassword@123'),
'nationality' => 'Indian',
'age' => 25,
'salary' => 1500,
'availability' => 'Immediate',
'experience' => 'None',
'religion' => 'Christian',
'bio' => 'Some Bio',
'api_token' => 'changer-token',
]);
// 1. Invalid current password
$response = $this->withHeaders([
'Authorization' => 'Bearer changer-token',
])->postJson('/api/workers/change-password', [
'current_password' => 'WrongPassword',
'new_password' => 'NewPassword@123',
'new_password_confirmation' => 'NewPassword@123',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['current_password']);
// 2. Successful change
$response = $this->withHeaders([
'Authorization' => 'Bearer changer-token',
])->postJson('/api/workers/change-password', [
'current_password' => 'OldPassword@123',
'new_password' => 'NewPassword@123',
'new_password_confirmation' => 'NewPassword@123',
]);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'Password changed successfully.'
]);
$this->assertTrue(\Illuminate\Support\Facades\Hash::check('NewPassword@123', $worker->fresh()->password));
}
}