diff --git a/app/Http/Controllers/Admin/AdminAuthController.php b/app/Http/Controllers/Admin/AdminAuthController.php index 001dc73..9bab742 100644 --- a/app/Http/Controllers/Admin/AdminAuthController.php +++ b/app/Http/Controllers/Admin/AdminAuthController.php @@ -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'); } /** diff --git a/app/Http/Controllers/Api/EmployerAnnouncementController.php b/app/Http/Controllers/Api/EmployerAnnouncementController.php index daa98a7..751ec5b 100644 --- a/app/Http/Controllers/Api/EmployerAnnouncementController.php +++ b/app/Http/Controllers/Api/EmployerAnnouncementController.php @@ -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, diff --git a/app/Http/Controllers/Api/EmployerAuthController.php b/app/Http/Controllers/Api/EmployerAuthController.php index cc22750..8955fff 100644 --- a/app/Http/Controllers/Api/EmployerAuthController.php +++ b/app/Http/Controllers/Api/EmployerAuthController.php @@ -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.', diff --git a/app/Http/Controllers/Api/EmployerProfileController.php b/app/Http/Controllers/Api/EmployerProfileController.php index 87227ff..1aa0978 100644 --- a/app/Http/Controllers/Api/EmployerProfileController.php +++ b/app/Http/Controllers/Api/EmployerProfileController.php @@ -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); + } + } + } diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php index dffd0b6..b37d03a 100644 --- a/app/Http/Controllers/Api/EmployerWorkerController.php +++ b/app/Http/Controllers/Api/EmployerWorkerController.php @@ -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) { diff --git a/app/Http/Controllers/Api/ReportController.php b/app/Http/Controllers/Api/ReportController.php index e9b2b16..72159aa 100644 --- a/app/Http/Controllers/Api/ReportController.php +++ b/app/Http/Controllers/Api/ReportController.php @@ -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']); diff --git a/app/Http/Controllers/Api/SponsorAuthController.php b/app/Http/Controllers/Api/SponsorAuthController.php index 4fb57f7..fb9c641 100644 --- a/app/Http/Controllers/Api/SponsorAuthController.php +++ b/app/Http/Controllers/Api/SponsorAuthController.php @@ -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); diff --git a/app/Http/Controllers/Api/SponsorController.php b/app/Http/Controllers/Api/SponsorController.php index 27c5f85..43606a7 100644 --- a/app/Http/Controllers/Api/SponsorController.php +++ b/app/Http/Controllers/Api/SponsorController.php @@ -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); + } + } } diff --git a/app/Http/Controllers/Api/WorkerAuthController.php b/app/Http/Controllers/Api/WorkerAuthController.php index 530da1c..ab9d800 100644 --- a/app/Http/Controllers/Api/WorkerAuthController.php +++ b/app/Http/Controllers/Api/WorkerAuthController.php @@ -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); diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index 3af8bc4..5eded75 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -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 ?: '', + ] + ]; + } } diff --git a/app/Http/Controllers/Employer/EmployerAuthController.php b/app/Http/Controllers/Employer/EmployerAuthController.php index 64e1962..bb368dd 100644 --- a/app/Http/Controllers/Employer/EmployerAuthController.php +++ b/app/Http/Controllers/Employer/EmployerAuthController.php @@ -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 diff --git a/app/Http/Controllers/Employer/ProfileController.php b/app/Http/Controllers/Employer/ProfileController.php index 3b2198f..aa03e47 100644 --- a/app/Http/Controllers/Employer/ProfileController.php +++ b/app/Http/Controllers/Employer/ProfileController.php @@ -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'; } diff --git a/app/Http/Controllers/Employer/WorkerController.php b/app/Http/Controllers/Employer/WorkerController.php index 2632dc4..8fe0a9f 100644 --- a/app/Http/Controllers/Employer/WorkerController.php +++ b/app/Http/Controllers/Employer/WorkerController.php @@ -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(), ]; diff --git a/app/Models/EmployerProfile.php b/app/Models/EmployerProfile.php index 89bfce4..ee58164 100644 --- a/app/Models/EmployerProfile.php +++ b/app/Models/EmployerProfile.php @@ -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', diff --git a/app/Models/Sponsor.php b/app/Models/Sponsor.php index 53090ff..f5c4913 100644 --- a/app/Models/Sponsor.php +++ b/app/Models/Sponsor.php @@ -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, + ]); + } } diff --git a/app/Services/OcrDocumentService.php b/app/Services/OcrDocumentService.php index 6268d37..f22727b 100644 --- a/app/Services/OcrDocumentService.php +++ b/app/Services/OcrDocumentService.php @@ -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, ]; } diff --git a/database/migrations/2026_06_21_183000_make_company_name_nullable_in_employer_profiles_table.php b/database/migrations/2026_06_21_183000_make_company_name_nullable_in_employer_profiles_table.php new file mode 100644 index 0000000..da2941b --- /dev/null +++ b/database/migrations/2026_06_21_183000_make_company_name_nullable_in_employer_profiles_table.php @@ -0,0 +1,28 @@ +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(); + }); + } +}; diff --git a/database/migrations/2026_06_23_175000_add_card_number_and_gender_to_employer_profiles_and_sponsors.php b/database/migrations/2026_06_23_175000_add_card_number_and_gender_to_employer_profiles_and_sponsors.php new file mode 100644 index 0000000..c520317 --- /dev/null +++ b/database/migrations/2026_06_23_175000_add_card_number_and_gender_to_employer_profiles_and_sponsors.php @@ -0,0 +1,38 @@ +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']); + }); + } +}; diff --git a/database/migrations/2026_06_24_124100_add_license_data_to_sponsors_table.php b/database/migrations/2026_06_24_124100_add_license_data_to_sponsors_table.php new file mode 100644 index 0000000..ddaf9d3 --- /dev/null +++ b/database/migrations/2026_06_24_124100_add_license_data_to_sponsors_table.php @@ -0,0 +1,32 @@ +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'); + } + }); + } +}; diff --git a/database/migrations/2026_06_24_145817_standardize_visa_ocr_data_in_worker_documents.php b/database/migrations/2026_06_24_145817_standardize_visa_ocr_data_in_worker_documents.php new file mode 100644 index 0000000..114f01e --- /dev/null +++ b/database/migrations/2026_06_24_145817_standardize_visa_ocr_data_in_worker_documents.php @@ -0,0 +1,89 @@ +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 ?: '', + ] + ]; + } +}; diff --git a/public/swagger.json b/public/swagger.json index 2b7fd62..08dbc44 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -23,7 +23,7 @@ "Sponsor/Auth" ], "summary": "Register Sponsor Account", - "description": "Registers a new sponsor with basic information and uploads their organization/trade license. Sponsors have restricted access — dashboard and charity events only. No payment required.", + "description": "Registers a new sponsor with basic information and uploads their organization/trade license. Sponsors have restricted access \u2014 dashboard and charity events only. No payment required.", "security": [], "requestBody": { "required": true, @@ -51,7 +51,7 @@ "mobile": { "type": "string", "example": "+971501112233", - "description": "Mobile phone number — must be unique. (REQUIRED)" + "description": "Mobile phone number \u00e2\u20ac\u201d must be unique. (REQUIRED)" }, "password": { "type": "string", @@ -63,30 +63,17 @@ "type": "object", "description": "Optional JSON object containing extracted organization/trade license details.", "properties": { - "document_type": { "type": "string", "example": "dubai_commercial_license" }, - "country": { "type": "string", "example": "United Arab Emirates" }, - "authority": { "type": "string", "example": "Dubai Economy and Tourism" }, - "license_no": { "type": "string", "example": "1426961" }, - "company_name": { "type": "string", "example": "NEST CLIMATE TECHNICAL SERVICES L.L.C" }, - "business_name": { "type": "string", "example": "NEST CLIMATE TECHNICAL SERVICES L.L.C" }, - "license_category": { "type": "string", "example": "Dep. of Economic Development" }, - "legal_type": { "type": "string", "example": "Limited Liability Company(LLC)" }, - "issue_date": { "type": "string", "example": "22/10/2024" }, - "expiry_date": { "type": "string", "example": "21/10/2026" }, - "main_license_no": { "type": "string", "example": "1426961" }, - "register_no": { "type": "string", "example": "2436760" }, - "dcci_no": { "type": "string", "example": "570232" }, - "members": { - "type": "array", - "items": { - "type": "object", - "properties": { - "person_no": { "type": "string", "example": "1709285" }, - "name": { "type": "string", "example": "SEYED ABDUL RAHMAN SHERIF NISAR" }, - "nationality": { "type": "string", "example": "India" }, - "role": { "type": "string", "example": "Manager" } - } - } + "license_number": { + "type": "string", + "example": "123456" + }, + "organization_name": { + "type": "string", + "example": "Al-Rashidi Charitable Foundation" + }, + "expiry_date": { + "type": "string", + "example": "2028-06-12" } } }, @@ -133,6 +120,14 @@ "occupation": { "type": "string", "example": "Manager" + }, + "card_number": { + "type": "string", + "example": "151023946" + }, + "gender": { + "type": "string", + "example": "M" } } }, @@ -603,7 +598,28 @@ "description": "Returns the full profile of the authenticated sponsor.", "responses": { "200": { - "description": "Profile retrieved successfully." + "description": "Profile retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "sponsor": { + "$ref": "#/components/schemas/Sponsor" + } + } + } + } + } + } + } }, "401": { "description": "Unauthenticated." @@ -611,13 +627,263 @@ } } }, + "/sponsors/profile/update": { + "post": { + "tags": [ + "Sponsor" + ], + "summary": "Update Sponsor Profile Details", + "description": "Updates the authenticated sponsor's profile details including optional Emirates ID metadata. Password changes are not supported via this endpoint.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email", + "mobile", + "organization_name", + "city", + "address", + "nationality", + "country_code" + ], + "properties": { + "name": { + "type": "string", + "example": "Ahmad Bin Ahmed", + "description": "Sponsor's full name (required if full_name is not provided)" + }, + "full_name": { + "type": "string", + "example": "Ahmad Bin Ahmed", + "description": "Sponsor's full name (required if name is not provided)" + }, + "email": { + "type": "string", + "format": "email", + "example": "test.sponsor@example.com" + }, + "mobile": { + "type": "string", + "example": "+971509990001" + }, + "organization_name": { + "type": "string", + "example": "Charity Co" + }, + "city": { + "type": "string", + "example": "Abu Dhabi" + }, + "address": { + "type": "string", + "example": "Villa 45, Street 12" + }, + "nationality": { + "type": "string", + "example": "Emirati" + }, + "country_code": { + "type": "string", + "example": "+971" + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example", + "description": "Firebase Cloud Messaging token for push notifications." + }, + "license": { + "type": "object", + "description": "Optional JSON object containing extracted commercial license details.", + "properties": { + "license_number": { + "type": "string", + "example": "828302" + }, + "organization_name": { + "type": "string", + "example": "MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C" + }, + "expiry_date": { + "type": "string", + "example": "05/03/2026" + }, + "document_type": { + "type": "string", + "example": "dubai_commercial_license" + }, + "country": { + "type": "string", + "example": "United Arab Emirates" + }, + "authority": { + "type": "string", + "example": "Dubai Economy and Tourism" + }, + "license_no": { + "type": "string", + "example": "828302" + }, + "company_name": { + "type": "string", + "example": "MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C" + }, + "business_name": { + "type": "string", + "example": "MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C" + }, + "license_category": { + "type": "string", + "example": "Dep. of Economic Development" + }, + "legal_type": { + "type": "string", + "example": "Limited Liability Company Single Owner(LLC- - SO)" + }, + "issue_date": { + "type": "string", + "example": "06/03/2019" + }, + "main_license_no": { + "type": "string", + "example": "828302" + }, + "register_no": { + "type": "string", + "example": "1656486" + }, + "dcci_no": { + "type": "string", + "example": "317412" + }, + "members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "person_no": { + "type": "string", + "example": "752967" + }, + "name": { + "type": "string", + "example": "Lo lo wallus issued in" + }, + "nationality": { + "type": "string", + "example": "India" + }, + "role": { + "type": "string", + "example": "Shares Owner Who" + } + } + } + } + } + }, + "emirates_id": { + "type": "object", + "description": "Optional JSON object containing extracted Emirates ID details.", + "properties": { + "emirates_id_number": { + "type": "string", + "example": "784-1988-5310327-2" + }, + "name": { + "type": "string", + "example": "KRISHNA PRASAD" + }, + "nationality": { + "type": "string", + "example": "NPL" + }, + "date_of_birth": { + "type": "string", + "example": "1988-03-22" + }, + "expiry_date": { + "type": "string", + "example": "2028-04-11" + }, + "issue_date": { + "type": "string", + "example": "2023-04-11" + }, + "employer": { + "type": "string", + "example": "Federal Authority" + }, + "issue_place": { + "type": "string", + "example": "Abu Dhabi" + }, + "occupation": { + "type": "string", + "example": "Manager" + }, + "card_number": { + "type": "string", + "example": "151023946" + }, + "gender": { + "type": "string", + "example": "M" + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Profile updated successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Profile updated successfully." + }, + "data": { + "type": "object", + "properties": { + "sponsor": { + "$ref": "#/components/schemas/Sponsor" + } + } + } + } + } + } + } + }, + "401": { + "description": "Unauthenticated." + }, + "422": { + "description": "Validation error (duplicate mobile/email, mismatched immutable fields, or name extraction failure)." + } + } + } + }, "/workers/register": { "post": { "tags": [ "Worker/Auth" ], "summary": "Register Worker Account (Step 1)", - "description": "Creates the worker account and returns a secure Bearer token.\n\nDocument upload is a **separate step** after registration:\n- `POST /workers/register/passport` — upload passport (OCR-processed)\n- `POST /workers/register/visa` — upload visa (OCR-processed)\n\nBoth document endpoints require the Bearer token returned here.", + "description": "Creates the worker account and returns a secure Bearer token.\n\nDocument upload is a **separate step** after registration:\n- `POST /workers/register/passport` \u2014 upload passport (OCR-processed)\n- `POST /workers/register/visa` \u2014 upload visa (OCR-processed)\n\nBoth document endpoints require the Bearer token returned here.", "security": [], "requestBody": { "required": true, @@ -760,6 +1026,11 @@ "nationality": { "type": "string" }, + "ocr_accuracy": { + "type": "number", + "format": "float", + "example": 99 + }, "passport_number": { "type": "string" }, @@ -779,39 +1050,85 @@ }, "visa": { "type": "object", - "description": "Optional JSON object containing extracted visa details.", + "description": "Standardized UAE Visa metadata.", "properties": { - "accompanied_by": { - "type": "string" - }, - "expiry_date": { + "document_type": { "type": "string", - "example": "2024-02-15" + "example": "uae_visa" }, - "file_number": { - "type": "string" + "country": { + "type": "string", + "example": "United Arab Emirates" }, - "id_number": { - "type": "string" + "visa_type": { + "type": "string", + "example": "ENTRY PERMIT" + }, + "entry_permit_no": { + "type": "string", + "example": "77003098 / 2019 / 204" }, "issue_date": { "type": "string", "example": "2022-02-16" }, - "name": { - "type": "string" + "valid_until": { + "type": "string", + "example": "2024-02-15" }, - "passport_number": { - "type": "string" + "uid_no": { + "type": "string", + "example": "207404887" }, - "place_of_issue": { - "type": "string" + "full_name": { + "type": "string", + "example": "Mr.MUHAMMAD NADEEM RASHEED" + }, + "nationality": { + "type": "string", + "example": "PAKISTAN" + }, + "place_of_birth": { + "type": "string", + "example": "SIALKOT PAK" + }, + "date_of_birth": { + "type": "string", + "example": "25-JUN-1999" + }, + "passport_no": { + "type": "string", + "example": "EN9458281" }, "profession": { - "type": "string" + "type": "string", + "example": "SALES REPRESENTATIVE" + }, + "sponsor_name": { + "type": "string", + "example": "Sponsor Name" + }, + "ocr_accuracy": { + "type": "number", + "format": "float", + "example": 97.4 }, "sponsor": { - "type": "string" + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Sponsor Name" + }, + "address": { + "type": "string", + "example": "Dubai Marina, Dubai" + }, + "phone": { + "type": "string", + "example": "2977725" + } + } } } } @@ -854,7 +1171,7 @@ } }, "422": { - "description": "Validation error — field validation failed or Passport OCR extraction failed.", + "description": "Validation error \u2014 field validation failed or Passport OCR extraction failed.", "content": { "application/json": { "schema": { @@ -1141,7 +1458,7 @@ }, "name": { "type": "string", - "example": "Hindi (हिन्दी)" + "example": "Hindi (\u0939\u093f\u0928\u094d\u0926\u0940)" } } } @@ -2188,14 +2505,80 @@ }, "visa": { "type": "object", + "description": "Standardized UAE Visa metadata.", "properties": { - "file_number": { + "document_type": { "type": "string", - "example": "202/2022/2840234" + "example": "uae_visa" }, - "expiry_date": { + "country": { "type": "string", - "example": "2028-12-31" + "example": "United Arab Emirates" + }, + "visa_type": { + "type": "string", + "example": "ENTRY PERMIT" + }, + "entry_permit_no": { + "type": "string", + "example": "87654091 / 2016 / 210" + }, + "issue_date": { + "type": "string", + "example": "2016-11-18" + }, + "valid_until": { + "type": "string", + "example": "2017-01-18" + }, + "uid_no": { + "type": "string", + "example": "181825009" + }, + "full_name": { + "type": "string", + "example": "GILBERT DSOUZA W/O TIMOTHY" + }, + "nationality": { + "type": "string", + "example": "INDIAN" + }, + "place_of_birth": { + "type": "string", + "example": "MANGALORE" + }, + "date_of_birth": { + "type": "string", + "example": "1980-02-02" + }, + "passport_no": { + "type": "string", + "example": "U4690639" + }, + "profession": { + "type": "string", + "example": "BUSINESS PERSON" + }, + "sponsor_name": { + "type": "string", + "example": "Sponsor LLC" + }, + "sponsor": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Sponsor LLC" + }, + "address": { + "type": "string", + "example": "Dubai Marina, Dubai" + }, + "phone": { + "type": "string", + "example": "2977725" + } + } } } } @@ -2649,18 +3032,20 @@ "Worker/Profile" ], "summary": "Get Safety Report Reasons (Worker)", - "description": "Returns a list of active safety/moderation report reasons. Can filter reasons by target type (Chat or Review).", + "description": "Returns a list of active safety/moderation report reasons. Can filter reasons by target type (Chat or Support).", "parameters": [ { "name": "type", "in": "query", "required": false, - "description": "Optional type to filter reasons (Chat or Review). If not provided, returns all active reasons.", + "description": "Optional type to filter reasons (Chat or Support). Case-insensitive.", "schema": { "type": "string", "enum": [ "Chat", - "Review" + "Support", + "chat", + "support" ] } } @@ -2917,6 +3302,14 @@ "occupation": { "type": "string", "example": "Manager" + }, + "card_number": { + "type": "string", + "example": "151023946" + }, + "gender": { + "type": "string", + "example": "M" } } }, @@ -3486,7 +3879,91 @@ ], "responses": { "200": { - "description": "List of posted charity events retrieved successfully." + "description": "List of posted charity events retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "announcements": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "title": { + "type": "string", + "example": "Holiday Notice" + }, + "body": { + "type": "string", + "example": "Office will be closed tomorrow." + }, + "type": { + "type": "string", + "example": "info" + }, + "employer_name": { + "type": "string", + "example": "Employer One" + }, + "company_name": { + "type": "string", + "example": "Elite Services" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2026-06-23T10:13:17.000000Z" + }, + "time_ago": { + "type": "string", + "example": "2 hours ago" + }, + "charity_details": { + "type": "object", + "nullable": true + } + } + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "example": 1 + }, + "per_page": { + "type": "integer", + "example": 15 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "last_page": { + "type": "integer", + "example": 1 + } + } + } + } + } + } + } + } + } } } }, @@ -3587,7 +4064,85 @@ "description": "Retrieves stats including contacted workers count, total hired workers, saved candidates, current plan details, and recent announcements or events.", "responses": { "200": { - "description": "Employer dashboard stats retrieved successfully." + "description": "Employer dashboard stats retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "stats": { + "type": "object", + "properties": { + "contacted_workers_count": { + "type": "integer", + "example": 0 + }, + "total_hired_workers": { + "type": "integer", + "example": 0 + }, + "saved_candidates": { + "type": "integer", + "example": 0 + }, + "total_workers": { + "type": "integer", + "example": 15 + } + } + }, + "current_plan": { + "type": "object", + "properties": { + "plan_id": { + "type": "string", + "example": "premium" + }, + "name": { + "type": "string", + "example": "Premium Pass" + }, + "status": { + "type": "string", + "example": "active" + }, + "starts_at": { + "type": "string", + "format": "date", + "example": "2026-06-23" + }, + "expires_at": { + "type": "string", + "format": "date", + "example": "2026-07-23" + } + } + }, + "recent_announcements": { + "type": "array", + "items": { + "type": "object" + } + }, + "recent_events": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + } + } } } } @@ -3597,11 +4152,160 @@ "tags": [ "Employer/Profile" ], - "summary": "Get Profile details (Employer)", - "description": "Retrieves the authenticated employer's profile details including their selected company name, phone, language preference, and notification settings.", + "summary": "Get Employer Profile Details", + "description": "Retrieves the authenticated employer's profile details.", "responses": { "200": { - "description": "Employer profile details retrieved successfully." + "description": "Profile retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "profile": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Ahmad" + }, + "email": { + "type": "string", + "example": "ahmad@example.com" + }, + "phone": { + "type": "string", + "example": "+971509990001" + }, + "address": { + "type": "string", + "example": "Villa 12, Jumeirah 2" + }, + "notifications": { + "type": "boolean", + "example": true + }, + "verification_status": { + "type": "string", + "example": "approved" + }, + "emirates_id_number": { + "type": "string", + "example": "784-1988-5310327-2" + }, + "emirates_id_expiry": { + "type": "string", + "example": "2028-04-11" + }, + "id_number": { + "type": "string", + "example": "784-1987-5493842-5" + }, + "card_number": { + "type": "string", + "example": "151023946" + }, + "full_name": { + "type": "string", + "example": "Mohammad Jobaier Mohammad Abul Kalam" + }, + "date_of_birth": { + "type": "string", + "example": "14/04/1987" + }, + "nationality": { + "type": "string", + "example": "Bangladesh" + }, + "gender": { + "type": "string", + "example": "M" + }, + "issue_date": { + "type": "string", + "example": "12/12/2025" + }, + "expiry_date": { + "type": "string", + "example": "11/12/2027" + }, + "occupation": { + "type": "string", + "example": "Electrician" + }, + "employer": { + "type": "string", + "example": "Msj International Technical Services L.L.C UAE" + }, + "issuing_place": { + "type": "string", + "example": "Dubai" + }, + "emirates_id": { + "type": "object", + "description": "Consolidated Emirates ID details.", + "properties": { + "emirates_id_number": { + "type": "string", + "example": "784-1988-5310327-2" + }, + "name": { + "type": "string", + "example": "Ahmad Bin Ahmed" + }, + "date_of_birth": { + "type": "string", + "example": "1990-01-01" + }, + "nationality": { + "type": "string", + "example": "Emirati" + }, + "issue_date": { + "type": "string", + "example": "2023-04-11" + }, + "expiry_date": { + "type": "string", + "example": "2028-04-11" + }, + "employer": { + "type": "string", + "example": "Federal Authority" + }, + "issue_place": { + "type": "string", + "example": "Abu Dhabi" + }, + "occupation": { + "type": "string", + "example": "Manager" + }, + "card_number": { + "type": "string", + "example": "151023946" + }, + "gender": { + "type": "string", + "example": "M" + } + } + } + } + } + } + } + } + } + } + } } } } @@ -3611,8 +4315,8 @@ "tags": [ "Employer/Profile" ], - "summary": "Update Profile details (Employer)", - "description": "Allows employers to update their profile details (company name, phone number, name, email, language preference, and notification settings) and change their account password.", + "summary": "Update Employer Profile Details", + "description": "Updates the authenticated employer's profile details including optional Emirates ID metadata. Password changes are not supported via this endpoint.", "requestBody": { "required": true, "content": { @@ -3623,9 +4327,7 @@ "name", "email", "phone", - "company_name", - "language", - "notifications" + "address" ], "properties": { "name": { @@ -3640,40 +4342,75 @@ "type": "string", "example": "+971509990001" }, - "company_name": { + "address": { "type": "string", - "example": "Ahmad Tech Ltd" - }, - "language": { - "type": "string", - "enum": [ - "English", - "Arabic", - "english", - "arabic" - ], - "example": "English" + "example": "Villa 12, Jumeirah 2" }, "notifications": { "type": "boolean", - "example": true - }, - "current_password": { - "type": "string", - "example": "Password@123" - }, - "new_password": { - "type": "string", - "example": "NewSecurePassword@123" - }, - "new_password_confirmation": { - "type": "string", - "example": "NewSecurePassword@123" + "example": true, + "description": "Optional. Enable/disable push notifications. Defaults to true if not provided.", + "default": true }, "fcm_token": { "type": "string", "example": "fcm_token_example", "description": "Firebase Cloud Messaging token for push notifications." + }, + "id_number": { + "type": "string", + "example": "784-1987-5493842-5", + "description": "Emirates ID card number/ID number (optional)" + }, + "card_number": { + "type": "string", + "example": "151023946", + "description": "Emirates ID card physical serial number (optional)" + }, + "full_name": { + "type": "string", + "example": "Mohammad Jobaier Mohammad Abul Kalam", + "description": "Emirates ID full name (optional)" + }, + "date_of_birth": { + "type": "string", + "example": "14/04/1987", + "description": "Emirates ID date of birth (optional)" + }, + "nationality": { + "type": "string", + "example": "Bangladesh", + "description": "Emirates ID nationality (optional)" + }, + "gender": { + "type": "string", + "example": "M", + "description": "Emirates ID gender (optional)" + }, + "issue_date": { + "type": "string", + "example": "12/12/2025", + "description": "Emirates ID card issue date (optional)" + }, + "expiry_date": { + "type": "string", + "example": "11/12/2027", + "description": "Emirates ID card expiry date (optional)" + }, + "occupation": { + "type": "string", + "example": "Electrician", + "description": "Emirates ID card holder occupation (optional)" + }, + "employer": { + "type": "string", + "example": "Msj International Technical Services L.L.C UAE", + "description": "Emirates ID card holder sponsor/employer name (optional)" + }, + "issuing_place": { + "type": "string", + "example": "Dubai", + "description": "Emirates ID card place of issue (optional)" } } } @@ -3682,7 +4419,163 @@ }, "responses": { "200": { - "description": "Profile updated successfully." + "description": "Profile updated successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Profile updated successfully." + }, + "data": { + "type": "object", + "properties": { + "profile": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Ahmad" + }, + "email": { + "type": "string", + "example": "ahmad@example.com" + }, + "phone": { + "type": "string", + "example": "+971509990001" + }, + "address": { + "type": "string", + "example": "Villa 12, Jumeirah 2" + }, + "notifications": { + "type": "boolean", + "example": true + }, + "verification_status": { + "type": "string", + "example": "approved" + }, + "emirates_id_number": { + "type": "string", + "example": "784-1988-5310327-2" + }, + "emirates_id_expiry": { + "type": "string", + "example": "2028-04-11" + }, + "id_number": { + "type": "string", + "example": "784-1987-5493842-5" + }, + "card_number": { + "type": "string", + "example": "151023946" + }, + "full_name": { + "type": "string", + "example": "Mohammad Jobaier Mohammad Abul Kalam" + }, + "date_of_birth": { + "type": "string", + "example": "14/04/1987" + }, + "nationality": { + "type": "string", + "example": "Bangladesh" + }, + "gender": { + "type": "string", + "example": "M" + }, + "issue_date": { + "type": "string", + "example": "12/12/2025" + }, + "expiry_date": { + "type": "string", + "example": "11/12/2027" + }, + "occupation": { + "type": "string", + "example": "Electrician" + }, + "employer": { + "type": "string", + "example": "Msj International Technical Services L.L.C UAE" + }, + "issuing_place": { + "type": "string", + "example": "Dubai" + }, + "emirates_id": { + "type": "object", + "description": "Consolidated Emirates ID details.", + "properties": { + "emirates_id_number": { + "type": "string", + "example": "784-1988-5310327-2" + }, + "name": { + "type": "string", + "example": "Ahmad Bin Ahmed" + }, + "date_of_birth": { + "type": "string", + "example": "1990-01-01" + }, + "nationality": { + "type": "string", + "example": "Emirati" + }, + "issue_date": { + "type": "string", + "example": "2023-04-11" + }, + "expiry_date": { + "type": "string", + "example": "2028-04-11" + }, + "employer": { + "type": "string", + "example": "Federal Authority" + }, + "issue_place": { + "type": "string", + "example": "Abu Dhabi" + }, + "occupation": { + "type": "string", + "example": "Manager" + }, + "card_number": { + "type": "string", + "example": "151023946" + }, + "gender": { + "type": "string", + "example": "M" + } + } + } + } + } + } + } + } + } + } + } + }, + "422": { + "description": "Validation error." } } } @@ -3835,7 +4728,52 @@ ], "responses": { "200": { - "description": "Available workers list retrieved successfully." + "description": "Available workers list retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "workers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Worker" + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "example": 12 + }, + "per_page": { + "type": "integer", + "example": 15 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "last_page": { + "type": "integer", + "example": 1 + } + } + } + } + } + } + } + } + } } } } @@ -3874,123 +4812,33 @@ "type": "object", "properties": { "worker": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 + "allOf": [ + { + "$ref": "#/components/schemas/Worker" }, - "name": { - "type": "string", - "example": "Jane Doe" - }, - "nationality": { - "type": "string", - "example": "Filipino" - }, - "photo": { - "type": "string", - "example": "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200" - }, - "emirates_id_status": { - "type": "string", - "example": "Passport Verified", - "description": "Deprecated - Use passport_status instead" - }, - "passport_status": { - "type": "string", - "example": "Passport Verified" - }, - "category": { - "type": "string", - "example": "Domestic Worker" - }, - "skills": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "childcare", - "cooking" - ] - }, - "availability_status": { - "type": "string", - "example": "Active" - }, - "visa_status": { - "type": "string", - "example": "Residence Visa" - }, - "experience": { - "type": "string", - "example": "5 Years" - }, - "experience_years": { - "type": "integer", - "example": 5 - }, - "salary": { - "type": "integer", - "example": 2500 - }, - "religion": { - "type": "string", - "example": "Christian" - }, - "languages": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "English", - "Tagalog" - ] - }, - "age": { - "type": "integer", - "example": 28 - }, - "verified": { - "type": "boolean", - "example": true - }, - "preferred_job_type": { - "type": "string", - "example": "live-in" - }, - "bio": { - "type": "string", - "example": "Experienced and caring domestic worker specialing in childcare and housekeeping..." - }, - "rating": { - "type": "number", - "example": 4.3 - }, - "reviews_count": { - "type": "integer", - "example": 6 - }, - "reviews": { - "type": "array", - "items": { - "type": "object" + { + "type": "object", + "properties": { + "reviews": { + "type": "array", + "items": { + "type": "object" + } + }, + "similar_workers": { + "type": "array", + "items": { + "type": "object" + } + }, + "conversation_id": { + "type": "integer", + "nullable": true, + "example": 4 + } } - }, - "similar_workers": { - "type": "array", - "items": { - "type": "object" - } - }, - "conversation_id": { - "type": "integer", - "nullable": true, - "example": 4 } - } + ] } } } @@ -4732,18 +5580,20 @@ "Employer/Reviews" ], "summary": "Get Safety Report Reasons (Employer)", - "description": "Returns a list of active safety/moderation report reasons. Can filter reasons by target type (Chat or Review).", + "description": "Returns a list of active safety/moderation report reasons. Can filter reasons by target type (Chat or Support).", "parameters": [ { "name": "type", "in": "query", "required": false, - "description": "Optional type to filter reasons (Chat or Review). If not provided, returns all active reasons.", + "description": "Optional type to filter reasons (Chat or Support). Case-insensitive.", "schema": { "type": "string", "enum": [ "Chat", - "Review" + "Support", + "chat", + "support" ] } } @@ -4932,7 +5782,7 @@ "Workers - Auth" ], "summary": "Forgot Password (Send OTP)", - "description": "Sends a 6-digit OTP to verify the worker's identity using their registered mobile number. Workers do not use email — mobile/phone is the primary identifier. OTP is valid for 10 minutes.", + "description": "Sends a 6-digit OTP to verify the worker's identity using their registered mobile number. Workers do not use email \u2014 mobile/phone is the primary identifier. OTP is valid for 10 minutes.", "operationId": "workerForgotPassword", "requestBody": { "required": true, @@ -4956,7 +5806,7 @@ }, "responses": { "200": { - "description": "OTP sent successfully (response is identical whether account exists or not, to prevent enumeration)", + "description": "OTP sent successfully.", "content": { "application/json": { "schema": { @@ -4975,8 +5825,28 @@ } } }, + "404": { + "description": "Worker account not found with the provided mobile number.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "user not found this mobile number" + } + } + } + } + } + }, "422": { - "description": "Validation error — phone is required" + "description": "Validation error \u2014 phone is required" } } } @@ -5164,10 +6034,47 @@ }, "responses": { "200": { - "description": "OTP sent (response identical whether account exists or not)" + "description": "OTP sent successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "If an account with that contact exists, a reset OTP has been sent." + } + } + } + } + } + }, + "404": { + "description": "Sponsor account not found with the provided email or mobile.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "user not found this email id" + } + } + } + } + } }, "422": { - "description": "Validation error — email or mobile required" + "description": "Validation error \u2014 email or mobile required" } } } @@ -5224,6 +6131,422 @@ } } } + }, + "/employers/my-announcements": { + "get": { + "tags": [ + "Employer/CharityEvents" + ], + "summary": "Get My Posted Announcements/Charity Events (Employer)", + "description": "Retrieves the list of announcements and charity events posted specifically by the logged-in employer.", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "description": "Page number.", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "required": false, + "description": "Number of items per page.", + "schema": { + "type": "integer", + "default": 15 + } + } + ], + "responses": { + "200": { + "description": "List of my posted announcements retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "announcements": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "title": { + "type": "string", + "example": "Holiday Notice" + }, + "body": { + "type": "string", + "example": "Office will be closed tomorrow." + }, + "type": { + "type": "string", + "example": "info" + }, + "status": { + "type": "string", + "example": "pending" + }, + "remarks": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2026-06-23T10:13:17.000000Z" + }, + "time_ago": { + "type": "string", + "example": "2 hours ago" + } + } + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "example": 1 + }, + "per_page": { + "type": "integer", + "example": 15 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "last_page": { + "type": "integer", + "example": 1 + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/workers/change-password": { + "post": { + "tags": [ + "Worker/Profile" + ], + "summary": "Change Password", + "description": "Securely changes the user's password by validating the current password and confirming the new password.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "current_password", + "new_password", + "new_password_confirmation" + ], + "properties": { + "current_password": { + "type": "string", + "format": "password", + "description": "The current login password. (REQUIRED)", + "example": "OldPassword@123" + }, + "new_password": { + "type": "string", + "format": "password", + "description": "The new secure password (minimum 8 characters). (REQUIRED)", + "example": "NewPassword@123" + }, + "new_password_confirmation": { + "type": "string", + "format": "password", + "description": "Confirmation of the new password. (REQUIRED)", + "example": "NewPassword@123" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Password changed successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Password changed successfully." + } + } + } + } + } + }, + "422": { + "description": "Validation error (e.g. invalid current password, password too short, or mismatch).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Validation error." + }, + "errors": { + "type": "object", + "properties": { + "current_password": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "The current password is incorrect." + ] + } + } + } + } + } + } + } + } + } + } + }, + "/employers/change-password": { + "post": { + "tags": [ + "Employer/Profile" + ], + "summary": "Change Password", + "description": "Securely changes the user's password by validating the current password and confirming the new password.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "current_password", + "new_password", + "new_password_confirmation" + ], + "properties": { + "current_password": { + "type": "string", + "format": "password", + "description": "The current login password. (REQUIRED)", + "example": "OldPassword@123" + }, + "new_password": { + "type": "string", + "format": "password", + "description": "The new secure password (minimum 8 characters). (REQUIRED)", + "example": "NewPassword@123" + }, + "new_password_confirmation": { + "type": "string", + "format": "password", + "description": "Confirmation of the new password. (REQUIRED)", + "example": "NewPassword@123" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Password changed successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Password changed successfully." + } + } + } + } + } + }, + "422": { + "description": "Validation error (e.g. invalid current password, password too short, or mismatch).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Validation error." + }, + "errors": { + "type": "object", + "properties": { + "current_password": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "The current password is incorrect." + ] + } + } + } + } + } + } + } + } + } + } + }, + "/sponsors/change-password": { + "post": { + "tags": [ + "Sponsor" + ], + "summary": "Change Password", + "description": "Securely changes the user's password by validating the current password and confirming the new password.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "current_password", + "new_password", + "new_password_confirmation" + ], + "properties": { + "current_password": { + "type": "string", + "format": "password", + "description": "The current login password. (REQUIRED)", + "example": "OldPassword@123" + }, + "new_password": { + "type": "string", + "format": "password", + "description": "The new secure password (minimum 8 characters). (REQUIRED)", + "example": "NewPassword@123" + }, + "new_password_confirmation": { + "type": "string", + "format": "password", + "description": "Confirmation of the new password. (REQUIRED)", + "example": "NewPassword@123" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Password changed successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Password changed successfully." + } + } + } + } + } + }, + "422": { + "description": "Validation error (e.g. invalid current password, password too short, or mismatch).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Validation error." + }, + "errors": { + "type": "object", + "properties": { + "current_password": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "The current password is incorrect." + ] + } + } + } + } + } + } + } + } + } + } } }, "components": { @@ -5290,43 +6613,23 @@ "type": "string", "example": "uploads/licenses/1234567890_emirates_id.pdf" }, + "license_expiry": { + "type": "string", + "nullable": true, + "example": "2026-05-03" + }, + "validity": { + "type": "string", + "example": "Valid" + }, + "joined_at": { + "type": "string", + "format": "date-time" + }, "fcm_token": { "type": "string", "example": "fcm_token_example" }, - "license": { - "type": "object", - "properties": { - "document_type": { "type": "string", "example": "dubai_commercial_license" }, - "country": { "type": "string", "example": "United Arab Emirates" }, - "authority": { "type": "string", "example": "Dubai Economy and Tourism" }, - "license_no": { "type": "string", "example": "1426961" }, - "company_name": { "type": "string", "example": "NEST CLIMATE TECHNICAL SERVICES L.L.C" }, - "business_name": { "type": "string", "example": "NEST CLIMATE TECHNICAL SERVICES L.L.C" }, - "license_category": { "type": "string", "example": "Dep. of Economic Development" }, - "legal_type": { "type": "string", "example": "Limited Liability Company(LLC)" }, - "issue_date": { "type": "string", "example": "22/10/2024" }, - "expiry_date": { "type": "string", "example": "21/10/2026" }, - "main_license_no": { "type": "string", "example": "1426961" }, - "register_no": { "type": "string", "example": "2436760" }, - "dcci_no": { "type": "string", "example": "570232" } - } - }, - "emirates_id": { - "type": "object", - "properties": { - "emirates_id_number": { "type": "string", "example": "784-1988-5310327-2" }, - "name": { "type": "string", "example": "KRISHNA PRASAD" }, - "date_of_birth": { "type": "string", "example": "1988-03-22" }, - "issue_date": { "type": "string", "example": "2023-04-11" }, - "expiry_date": { "type": "string", "example": "2028-04-11" }, - "employer": { "type": "string", "example": "Federal Authority" }, - "issue_place": { "type": "string", "example": "Abu Dhabi" }, - "occupation": { "type": "string", "example": "Manager" }, - "nationality": { "type": "string", "example": "NPL" }, - "gender": { "type": "string", "example": "Male" } - } - }, "created_at": { "type": "string", "format": "date-time" @@ -5356,6 +6659,16 @@ "type": "string", "example": "HI" }, + "languages": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "English", + "Arabic" + ] + }, "fcm_token": { "type": "string", "example": "fcm_token_example" @@ -5380,10 +6693,6 @@ "type": "string", "example": "4 Years" }, - "religion": { - "type": "string", - "example": "Muslim" - }, "bio": { "type": "string", "example": "Certified infant caregiver and housekeeper with excellent cooking skills." @@ -5412,6 +6721,19 @@ "type": "string", "example": "Passport Verified" }, + "document_expiry_days": { + "type": "integer", + "example": -857 + }, + "document_expiry_status": { + "type": "string", + "example": "Visa Expired" + }, + "visa_expiry_date": { + "type": "string", + "format": "date", + "example": "2024-02-15" + }, "preferred_job_type": { "type": "string", "example": "full-time" @@ -5445,6 +6767,19 @@ "type": "string", "example": "Marina" }, + "rating": { + "type": "number", + "example": 4.5 + }, + "reviews_count": { + "type": "integer", + "example": 2 + }, + "photo": { + "type": "string", + "nullable": true, + "example": "https://example.com/photo.jpg" + }, "created_at": { "type": "string", "format": "date-time", @@ -5455,6 +6790,12 @@ "format": "date-time", "example": "2026-05-20T14:54:26.000000Z" }, + "deleted_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, "category": { "type": "object", "properties": { @@ -5480,6 +6821,32 @@ "name": { "type": "string", "example": "Cleaning" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "image_path": { + "type": "string", + "nullable": true, + "example": "skills/cleaning.png" + }, + "pivot": { + "type": "object", + "properties": { + "worker_id": { + "type": "integer", + "example": 136 + }, + "skill_id": { + "type": "integer", + "example": 6 + } + } } } } @@ -5527,7 +6894,21 @@ }, "file_path": { "type": "string", + "nullable": true, "example": "uploads/documents/1716200000_passport_my_file.jpg" + }, + "ocr_data": { + "type": "object", + "nullable": true, + "description": "OCR raw parsed data or confidence data" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" } } }, diff --git a/resources/js/Layouts/AdminLayout.jsx b/resources/js/Layouts/AdminLayout.jsx index 1c1dc68..13f255d 100644 --- a/resources/js/Layouts/AdminLayout.jsx +++ b/resources/js/Layouts/AdminLayout.jsx @@ -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 }, diff --git a/resources/js/Pages/Admin/Auth/Login.jsx b/resources/js/Pages/Admin/Auth/Login.jsx index 4a370bd..e3fb573 100644 --- a/resources/js/Pages/Admin/Auth/Login.jsx +++ b/resources/js/Pages/Admin/Auth/Login.jsx @@ -54,7 +54,7 @@ export default function Login() {
{/* Form */} -
+
- -
- {emp.employer_profile?.company_name || 'N/A'} -
-
- - {emp.employer_profile?.verification_status || 'pending'} + + {emp.employer_profile?.country || 'United Arab Emirates'} @@ -410,7 +402,7 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors, )) ) : ( - + No employers found. @@ -478,10 +470,6 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors, Employer Profile
-
- Company / Household - {selectedEmployer?.employer_profile?.company_name || 'N/A'} -
Email Address {selectedEmployer?.email || 'N/A'} @@ -535,28 +523,52 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,

- Emirates ID & Document Extracted Details + Emirates ID Details

- Emirates ID Number - {selectedEmployer?.employer_profile?.emirates_id_number || 'Not Extracted / Pending'} + ID Number + {selectedEmployer?.employer_profile?.emirates_id_number || 'N/A'}
- Emirates ID Expiry - {formatDate(selectedEmployer?.employer_profile?.emirates_id_expiry)} + Card Number + {selectedEmployer?.employer_profile?.emirates_id_card_number || 'N/A'}
- Verification Status - - {selectedEmployer?.employer_profile?.verification_status || 'Pending'} - + Full Name + {selectedEmployer?.employer_profile?.emirates_id_name || 'N/A'}
- Trade License Expiry - {formatDate(selectedEmployer?.sponsor?.license_expiry)} + Date of Birth + {selectedEmployer?.employer_profile?.emirates_id_dob || 'N/A'} +
+
+ Nationality + {selectedEmployer?.employer_profile?.nationality || 'N/A'} +
+
+ Gender + {selectedEmployer?.employer_profile?.emirates_id_gender || 'N/A'} +
+
+ Issue Date + {selectedEmployer?.employer_profile?.emirates_id_issue_date || 'N/A'} +
+
+ Expiry Date + {selectedEmployer?.employer_profile?.emirates_id_expiry || 'N/A'} +
+
+ Occupation + {selectedEmployer?.employer_profile?.emirates_id_occupation || 'N/A'} +
+
+ Employer + {selectedEmployer?.employer_profile?.emirates_id_employer || 'N/A'} +
+
+ Issuing Place + {selectedEmployer?.employer_profile?.emirates_id_issue_place || 'N/A'}
@@ -590,15 +602,7 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors, required />
-
- - setEditForm({ ...editForm, company_name: e.target.value })} - /> -
+
diff --git a/resources/js/Pages/Admin/Subscriptions/Index.jsx b/resources/js/Pages/Admin/Subscriptions/Index.jsx index 3a9934c..c180173 100644 --- a/resources/js/Pages/Admin/Subscriptions/Index.jsx +++ b/resources/js/Pages/Admin/Subscriptions/Index.jsx @@ -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 ( @@ -68,11 +127,13 @@ export default function SubscriptionsIndex({ plans: initialPlans }) { 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" />
- -
+ {filteredPlans.length === 0 ? ( + + + No plans found - ))} + ) : ( + filteredPlans.map((plan) => ( + + +
+
+ {plan.id === 'basic' ? : + plan.id === 'premium' ? : + } +
+ {plan.name} +
+
+ {plan.price} + {plan.duration} + +
+ {plan.features.map((f, i) => ( + + {f} + + ))} +
+
+ + + {plan.status} + + + +
+ + +
+
+
+ )) + )}
- {/* Plan Modal (Placeholder UI) */} + {/* Plan Modal */} @@ -163,44 +232,70 @@ export default function SubscriptionsIndex({ plans: initialPlans }) { -
-
-
- - + +
+
+
+ + 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" + /> +
+
+ + 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" + /> +
- - + + +
+
+ +