Compare commits
14 Commits
48a7703436
...
25510d1b5e
| Author | SHA1 | Date | |
|---|---|---|---|
| 25510d1b5e | |||
| 72da3c5b8d | |||
| ac4cc64cb6 | |||
| 8fb9b9dc26 | |||
| 8444347fb1 | |||
| 9d8b35326d | |||
| 22f82b52a6 | |||
| 9b30985d23 | |||
| 46ff3a2c3f | |||
| cf71073368 | |||
| f733384fa2 | |||
| 55f49acca2 | |||
| 3fcd8d54a0 | |||
| 1a037567ab |
@ -559,15 +559,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([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Password created successfully. Registration finalized.',
|
'message' => 'Password created successfully. Registration finalized.',
|
||||||
|
|||||||
@ -28,8 +28,8 @@ public function getProfile(Request $request)
|
|||||||
if (!$profile) {
|
if (!$profile) {
|
||||||
$profile = EmployerProfile::create([
|
$profile = EmployerProfile::create([
|
||||||
'user_id' => $employer->id,
|
'user_id' => $employer->id,
|
||||||
'company_name' => 'Al Mansoor Household',
|
|
||||||
'phone' => '+971 50 123 4567',
|
'phone' => '+971 50 123 4567',
|
||||||
|
'address' => 'Dubai, UAE',
|
||||||
'verification_status' => 'approved',
|
'verification_status' => 'approved',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -37,13 +37,23 @@ public function getProfile(Request $request)
|
|||||||
$employerProfile = [
|
$employerProfile = [
|
||||||
'name' => $employer->name,
|
'name' => $employer->name,
|
||||||
'email' => $employer->email,
|
'email' => $employer->email,
|
||||||
'company_name' => $profile->company_name,
|
|
||||||
'phone' => $profile->phone,
|
'phone' => $profile->phone,
|
||||||
'language' => $profile->language ?? 'English',
|
'address' => $profile->address,
|
||||||
'notifications' => (bool)($profile->notifications ?? true),
|
'notifications' => (bool)($profile->notifications ?? true),
|
||||||
'verification_status' => $profile->verification_status ?? 'approved',
|
'verification_status' => $profile->verification_status ?? 'approved',
|
||||||
'emirates_id_number' => $profile->emirates_id_number,
|
'emirates_id_number' => $profile->emirates_id_number,
|
||||||
'emirates_id_expiry' => $profile->emirates_id_expiry,
|
'emirates_id_expiry' => $profile->emirates_id_expiry,
|
||||||
|
'id_number' => $profile->emirates_id_number,
|
||||||
|
'card_number' => $profile->emirates_id_card_number,
|
||||||
|
'full_name' => $profile->emirates_id_name,
|
||||||
|
'date_of_birth' => $profile->emirates_id_dob,
|
||||||
|
'nationality' => $profile->nationality,
|
||||||
|
'gender' => $profile->emirates_id_gender,
|
||||||
|
'issue_date' => $profile->emirates_id_issue_date,
|
||||||
|
'expiry_date' => $profile->emirates_id_expiry,
|
||||||
|
'occupation' => $profile->emirates_id_occupation,
|
||||||
|
'employer' => $profile->emirates_id_employer,
|
||||||
|
'issuing_place' => $profile->emirates_id_issue_place,
|
||||||
];
|
];
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@ -94,12 +104,22 @@ public function updateProfile(Request $request)
|
|||||||
'unique:employer_profiles,phone,' . ($employer->employerProfile ? $employer->employerProfile->id : 'NULL'),
|
'unique:employer_profiles,phone,' . ($employer->employerProfile ? $employer->employerProfile->id : 'NULL'),
|
||||||
$sponsor ? 'unique:sponsors,mobile,' . $sponsor->id : 'unique:sponsors,mobile',
|
$sponsor ? 'unique:sponsors,mobile,' . $sponsor->id : 'unique:sponsors,mobile',
|
||||||
],
|
],
|
||||||
'company_name' => 'required|string|max:255',
|
'address' => 'required|string|max:255',
|
||||||
'language' => 'required|string|in:English,Arabic,english,arabic',
|
'notifications' => 'nullable|boolean',
|
||||||
'notifications' => 'required|boolean',
|
|
||||||
'current_password' => 'nullable|required_with:new_password|string',
|
|
||||||
'new_password' => 'nullable|string|min:8|confirmed',
|
|
||||||
'fcm_token' => 'nullable|string|max:255',
|
'fcm_token' => 'nullable|string|max:255',
|
||||||
|
|
||||||
|
// Emirates ID optional fields
|
||||||
|
'id_number' => 'nullable|string|max:255',
|
||||||
|
'card_number' => 'nullable|string|max:255',
|
||||||
|
'full_name' => 'nullable|string|max:255',
|
||||||
|
'date_of_birth' => 'nullable|string|max:255',
|
||||||
|
'nationality' => 'nullable|string|max:255',
|
||||||
|
'gender' => 'nullable|string|max:255',
|
||||||
|
'issue_date' => 'nullable|string|max:255',
|
||||||
|
'expiry_date' => 'nullable|string|max:255',
|
||||||
|
'occupation' => 'nullable|string|max:255',
|
||||||
|
'employer' => 'nullable|string|max:255',
|
||||||
|
'issuing_place' => 'nullable|string|max:255',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
@ -111,14 +131,15 @@ public function updateProfile(Request $request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check current password if new password is provided
|
// Validate Emirates ID immutability — once set, it cannot be changed
|
||||||
if ($request->filled('new_password')) {
|
if ($request->filled('id_number')) {
|
||||||
if (!Hash::check($request->current_password, $employer->password)) {
|
$existingProfile = $employer->employerProfile;
|
||||||
|
if ($existingProfile && $existingProfile->emirates_id_number && $existingProfile->emirates_id_number !== $request->id_number) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'The provided current password does not match.',
|
'message' => 'Emirates ID mismatch. You cannot change your registered Emirates ID number.',
|
||||||
'errors' => [
|
'errors' => [
|
||||||
'current_password' => ['The provided password does not match your current password.']
|
'id_number' => ['Emirates ID mismatch.']
|
||||||
]
|
]
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
@ -140,11 +161,48 @@ public function updateProfile(Request $request)
|
|||||||
// Sync with corresponding sponsor record if found
|
// Sync with corresponding sponsor record if found
|
||||||
$matchingSponsor = \App\Models\Sponsor::where('email', $oldEmail)->first();
|
$matchingSponsor = \App\Models\Sponsor::where('email', $oldEmail)->first();
|
||||||
if ($matchingSponsor) {
|
if ($matchingSponsor) {
|
||||||
$matchingSponsor->update([
|
$sponsorData = [
|
||||||
'full_name' => $request->name,
|
'full_name' => $request->name,
|
||||||
'email' => $request->email,
|
'email' => $request->email,
|
||||||
'mobile' => $request->phone,
|
'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
|
// Update employer_profiles table
|
||||||
@ -152,29 +210,66 @@ public function updateProfile(Request $request)
|
|||||||
if (!$profile) {
|
if (!$profile) {
|
||||||
$profile = new EmployerProfile(['user_id' => $employer->id]);
|
$profile = new EmployerProfile(['user_id' => $employer->id]);
|
||||||
}
|
}
|
||||||
$profile->company_name = $request->company_name;
|
$profile->address = $request->address;
|
||||||
$profile->phone = $request->phone;
|
$profile->phone = $request->phone;
|
||||||
$profile->language = ucfirst(strtolower($request->language));
|
$profile->notifications = $request->has('notifications') ? $request->notifications : ($profile->notifications ?? true);
|
||||||
$profile->notifications = $request->notifications;
|
|
||||||
$profile->save();
|
|
||||||
|
|
||||||
// Update password if present
|
if ($request->has('id_number')) {
|
||||||
if ($request->filled('new_password')) {
|
$profile->emirates_id_number = $request->id_number;
|
||||||
$employer->update([
|
|
||||||
'password' => Hash::make($request->new_password),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
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 = [
|
$employerProfile = [
|
||||||
'name' => $employer->name,
|
'name' => $employer->name,
|
||||||
'email' => $employer->email,
|
'email' => $employer->email,
|
||||||
'company_name' => $profile->company_name,
|
|
||||||
'phone' => $profile->phone,
|
'phone' => $profile->phone,
|
||||||
'language' => $profile->language,
|
'address' => $profile->address,
|
||||||
'notifications' => (bool)$profile->notifications,
|
'notifications' => (bool)$profile->notifications,
|
||||||
'verification_status' => $profile->verification_status ?? 'approved',
|
'verification_status' => $profile->verification_status ?? 'approved',
|
||||||
'emirates_id_number' => $profile->emirates_id_number,
|
'emirates_id_number' => $profile->emirates_id_number,
|
||||||
'emirates_id_expiry' => $profile->emirates_id_expiry,
|
'emirates_id_expiry' => $profile->emirates_id_expiry,
|
||||||
|
'id_number' => $profile->emirates_id_number,
|
||||||
|
'card_number' => $profile->emirates_id_card_number,
|
||||||
|
'full_name' => $profile->emirates_id_name,
|
||||||
|
'date_of_birth' => $profile->emirates_id_dob,
|
||||||
|
'nationality' => $profile->nationality,
|
||||||
|
'gender' => $profile->emirates_id_gender,
|
||||||
|
'issue_date' => $profile->emirates_id_issue_date,
|
||||||
|
'expiry_date' => $profile->emirates_id_expiry,
|
||||||
|
'occupation' => $profile->emirates_id_occupation,
|
||||||
|
'employer' => $profile->emirates_id_employer,
|
||||||
|
'issuing_place' => $profile->emirates_id_issue_place,
|
||||||
];
|
];
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@ -214,6 +309,8 @@ public function getDashboard(Request $request)
|
|||||||
|
|
||||||
$savedCandidates = \App\Models\Shortlist::where('employer_id', $employer->id)->count();
|
$savedCandidates = \App\Models\Shortlist::where('employer_id', $employer->id)->count();
|
||||||
|
|
||||||
|
$totalWorkers = \App\Models\Worker::where('status', 'active')->count();
|
||||||
|
|
||||||
// Resolve plan
|
// Resolve plan
|
||||||
$sub = \Illuminate\Support\Facades\DB::table('subscriptions')
|
$sub = \Illuminate\Support\Facades\DB::table('subscriptions')
|
||||||
->where('user_id', $employer->id)
|
->where('user_id', $employer->id)
|
||||||
@ -280,6 +377,7 @@ public function getDashboard(Request $request)
|
|||||||
'contacted_workers_count' => $contactedWorkersCount,
|
'contacted_workers_count' => $contactedWorkersCount,
|
||||||
'total_hired_workers' => $totalHiredWorkers,
|
'total_hired_workers' => $totalHiredWorkers,
|
||||||
'saved_candidates' => $savedCandidates,
|
'saved_candidates' => $savedCandidates,
|
||||||
|
'total_workers' => $totalWorkers,
|
||||||
],
|
],
|
||||||
'current_plan' => $currentPlan,
|
'current_plan' => $currentPlan,
|
||||||
'recent_announcements' => $recentAnnouncements,
|
'recent_announcements' => $recentAnnouncements,
|
||||||
|
|||||||
@ -22,23 +22,16 @@ class EmployerWorkerController extends Controller
|
|||||||
*/
|
*/
|
||||||
private function formatWorker(Worker $w)
|
private function formatWorker(Worker $w)
|
||||||
{
|
{
|
||||||
// Map languages with country names
|
// Map languages from database comma-separated string
|
||||||
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
$langs = $w->language ? array_map('trim', explode(',', $w->language)) : ['English'];
|
||||||
|
|
||||||
// Preferred job types: full-time / part-time / live-in / live-out
|
// Preferred job types: full-time / part-time / live-in / live-out
|
||||||
$jobTypes = ['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];
|
$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;
|
$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
|
// Visa status
|
||||||
$visaStatus = $w->visa_status;
|
$visaStatus = $w->visa_status;
|
||||||
|
|
||||||
@ -51,28 +44,71 @@ private function formatWorker(Worker $w)
|
|||||||
return [
|
return [
|
||||||
'id' => $w->id,
|
'id' => $w->id,
|
||||||
'name' => $w->name,
|
'name' => $w->name,
|
||||||
|
'email' => $w->email,
|
||||||
|
'phone' => $w->phone,
|
||||||
'nationality' => $w->nationality,
|
'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,
|
'age' => $w->age,
|
||||||
'gender' => $w->gender,
|
'salary' => $w->salary,
|
||||||
|
'experience' => $w->experience,
|
||||||
'verified' => (bool)$w->verified,
|
'verified' => (bool)$w->verified,
|
||||||
'preferred_job_type' => $preferredJobType,
|
'status' => $w->status,
|
||||||
'bio' => $w->bio,
|
'created_at' => $w->created_at?->toISOString(),
|
||||||
'rating' => $rating,
|
'updated_at' => $w->updated_at?->toISOString(),
|
||||||
'reviews_count' => $reviewsCount,
|
'deleted_at' => $w->deleted_at?->toISOString(),
|
||||||
|
'language' => $w->language,
|
||||||
|
'languages' => $langs,
|
||||||
'preferred_location' => $w->preferred_location,
|
'preferred_location' => $w->preferred_location,
|
||||||
|
'country' => $w->country,
|
||||||
|
'city' => $w->city,
|
||||||
'area' => $w->area,
|
'area' => $w->area,
|
||||||
'live_in_out' => $w->live_in_out,
|
'live_in_out' => $w->live_in_out,
|
||||||
|
'gender' => $w->gender,
|
||||||
'in_country' => (bool)$w->in_country,
|
'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_days' => $w->document_expiry_days,
|
||||||
'document_expiry_status' => $w->document_expiry_status,
|
'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['sponsor']);
|
||||||
|
unset($ocrData['valid_until']);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'id' => $doc->id,
|
||||||
|
'worker_id' => $doc->worker_id,
|
||||||
|
'type' => $doc->type,
|
||||||
|
'number' => $doc->number,
|
||||||
|
'issue_date' => $doc->issue_date,
|
||||||
|
'expiry_date' => $doc->expiry_date,
|
||||||
|
'ocr_accuracy' => $doc->ocr_accuracy,
|
||||||
|
'file_path' => $doc->file_path ? url($doc->file_path) : null,
|
||||||
|
'ocr_data' => $ocrData,
|
||||||
|
'created_at' => $doc->created_at?->toISOString(),
|
||||||
|
'updated_at' => $doc->updated_at?->toISOString(),
|
||||||
|
];
|
||||||
|
})->toArray(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +178,8 @@ public function getWorkers(Request $request)
|
|||||||
$workersArray = array_values(array_filter($workersArray, function ($c) use ($skillsArray) {
|
$workersArray = array_values(array_filter($workersArray, function ($c) use ($skillsArray) {
|
||||||
if (!isset($c['skills']) || !is_array($c['skills'])) return false;
|
if (!isset($c['skills']) || !is_array($c['skills'])) return false;
|
||||||
foreach ($c['skills'] as $s) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -314,10 +351,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([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
'workers' => $workersArray
|
'workers' => $paginatedWorkers,
|
||||||
|
'pagination' => [
|
||||||
|
'total' => $total,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'current_page' => $page,
|
||||||
|
'last_page' => max(1, (int)ceil($total / $perPage)),
|
||||||
|
]
|
||||||
]
|
]
|
||||||
], 200);
|
], 200);
|
||||||
|
|
||||||
@ -934,25 +983,74 @@ public function getWorkerDetail(Request $request, $id)
|
|||||||
$workerProfile = [
|
$workerProfile = [
|
||||||
'id' => $w->id,
|
'id' => $w->id,
|
||||||
'name' => $w->name,
|
'name' => $w->name,
|
||||||
|
'email' => $w->email,
|
||||||
|
'phone' => $w->phone,
|
||||||
'nationality' => $w->nationality,
|
'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,
|
'age' => $w->age,
|
||||||
'gender' => $w->gender,
|
'salary' => $w->salary,
|
||||||
|
'experience' => $w->experience,
|
||||||
'verified' => (bool)$w->verified,
|
'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,
|
'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,
|
'bio' => $w->bio,
|
||||||
|
'photo' => $photo,
|
||||||
'rating' => $rating,
|
'rating' => $rating,
|
||||||
'reviews_count' => $reviewsCount,
|
'reviews_count' => $reviewsCount,
|
||||||
'reviews' => $reviews,
|
'reviews' => $reviews,
|
||||||
'similar_workers' => $similarWorkers,
|
'similar_workers' => $similarWorkers,
|
||||||
'conversation_id' => $conversation ? $conversation->id : null,
|
'conversation_id' => $conversation ? $conversation->id : null,
|
||||||
|
'skills' => $w->skills->map(function ($s) {
|
||||||
|
return [
|
||||||
|
'id' => $s->id,
|
||||||
|
'name' => $s->name,
|
||||||
|
'created_at' => $s->created_at?->toISOString(),
|
||||||
|
'updated_at' => $s->updated_at?->toISOString(),
|
||||||
|
'image_path' => $s->image_path,
|
||||||
|
'pivot' => [
|
||||||
|
'worker_id' => $s->pivot->worker_id,
|
||||||
|
'skill_id' => $s->pivot->skill_id,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
})->toArray(),
|
||||||
|
'documents' => $w->documents->map(function ($doc) {
|
||||||
|
$ocrData = $doc->ocr_data;
|
||||||
|
if ($doc->type === 'visa' && is_array($ocrData)) {
|
||||||
|
unset($ocrData['sponsor']);
|
||||||
|
unset($ocrData['valid_until']);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'id' => $doc->id,
|
||||||
|
'worker_id' => $doc->worker_id,
|
||||||
|
'type' => $doc->type,
|
||||||
|
'number' => $doc->number,
|
||||||
|
'issue_date' => $doc->issue_date,
|
||||||
|
'expiry_date' => $doc->expiry_date,
|
||||||
|
'ocr_accuracy' => $doc->ocr_accuracy,
|
||||||
|
'file_path' => $doc->file_path ? url($doc->file_path) : null,
|
||||||
|
'ocr_data' => $ocrData,
|
||||||
|
'created_at' => $doc->created_at?->toISOString(),
|
||||||
|
'updated_at' => $doc->updated_at?->toISOString(),
|
||||||
|
];
|
||||||
|
})->toArray(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@ -987,7 +1085,7 @@ public function getShortlist(Request $request)
|
|||||||
$perPage = (int)$request->input('per_page', 15);
|
$perPage = (int)$request->input('per_page', 15);
|
||||||
|
|
||||||
$shortlists = Shortlist::where('employer_id', $employer->id)
|
$shortlists = Shortlist::where('employer_id', $employer->id)
|
||||||
->with(['worker.skills'])
|
->with(['worker.skills', 'worker.documents'])
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$formattedWorkers = $shortlists->map(function ($s) {
|
$formattedWorkers = $shortlists->map(function ($s) {
|
||||||
|
|||||||
@ -317,12 +317,11 @@ public function forgotPassword(Request $request)
|
|||||||
$sponsor = Sponsor::where('mobile', $request->mobile)->first();
|
$sponsor = Sponsor::where('mobile', $request->mobile)->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent email enumeration — always return success
|
|
||||||
if (!$sponsor || !$sponsor->email) {
|
if (!$sponsor || !$sponsor->email) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => false,
|
||||||
'message' => 'If an account with that contact exists, a reset OTP has been sent.',
|
'message' => 'user not found this email id',
|
||||||
]);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$otp = (string) mt_rand(100000, 999999);
|
$otp = (string) mt_rand(100000, 999999);
|
||||||
|
|||||||
@ -261,14 +261,6 @@ public function setupProfile(Request $request)
|
|||||||
return $worker;
|
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
|
// Clear the OTP verification gate
|
||||||
Cache::forget('worker_otp_verified_' . $identifier);
|
Cache::forget('worker_otp_verified_' . $identifier);
|
||||||
|
|
||||||
@ -338,7 +330,34 @@ public function register(Request $request)
|
|||||||
return $query->where('type', 'passport');
|
return $query->where('type', 'passport');
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
'passport.document_type' => 'nullable|string|max:10',
|
||||||
|
'passport.issuing_country' => 'nullable|string|max:10',
|
||||||
|
'passport.surname' => 'nullable|string|max:255',
|
||||||
|
'passport.given_names' => 'nullable|string|max:255',
|
||||||
|
'passport.nationality' => 'nullable|string|max:100',
|
||||||
|
'passport.date_of_birth' => 'nullable|string',
|
||||||
|
'passport.date_of_expiry' => 'nullable|string',
|
||||||
|
'passport.place_of_birth' => 'nullable|string|max:255',
|
||||||
|
'passport.authority' => 'nullable|string|max:255',
|
||||||
|
'passport.sex' => 'nullable|string|max:10',
|
||||||
|
'passport.date_of_issue' => 'nullable|string',
|
||||||
|
'passport.ocr_accuracy' => 'nullable|numeric',
|
||||||
'visa' => 'nullable|array',
|
'visa' => '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.',
|
'phone.unique' => 'This mobile number is already registered.',
|
||||||
'passport.passport_number.unique' => 'This passport 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,
|
'number' => $passportNum,
|
||||||
'issue_date' => $issueDateFormatted,
|
'issue_date' => $issueDateFormatted,
|
||||||
'expiry_date' => $expiryDateFormatted,
|
'expiry_date' => $expiryDateFormatted,
|
||||||
'ocr_accuracy' => 99.0,
|
'ocr_accuracy' => isset($passportDataInput['ocr_accuracy']) ? (float)$passportDataInput['ocr_accuracy'] : 99.0,
|
||||||
'file_path' => null,
|
'file_path' => null,
|
||||||
'ocr_data' => $passportDataInput,
|
'ocr_data' => $passportDataInput,
|
||||||
]);
|
]);
|
||||||
@ -478,8 +497,8 @@ public function register(Request $request)
|
|||||||
|
|
||||||
// Create visa document if provided
|
// Create visa document if provided
|
||||||
if ($visaDataInput) {
|
if ($visaDataInput) {
|
||||||
$visaNum = $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999));
|
$visaNum = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999));
|
||||||
$expiryDate = $visaDataInput['expiry_date'] ?? null;
|
$expiryDate = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
|
||||||
$issueDate = $visaDataInput['issue_date'] ?? null;
|
$issueDate = $visaDataInput['issue_date'] ?? null;
|
||||||
|
|
||||||
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(2)->toDateString();
|
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(2)->toDateString();
|
||||||
@ -490,7 +509,7 @@ public function register(Request $request)
|
|||||||
'number' => $visaNum,
|
'number' => $visaNum,
|
||||||
'issue_date' => $issueDateFormatted,
|
'issue_date' => $issueDateFormatted,
|
||||||
'expiry_date' => $expiryDateFormatted,
|
'expiry_date' => $expiryDateFormatted,
|
||||||
'ocr_accuracy' => 99.0,
|
'ocr_accuracy' => isset($visaDataInput['ocr_accuracy']) ? (float)$visaDataInput['ocr_accuracy'] : 99.0,
|
||||||
'file_path' => null,
|
'file_path' => null,
|
||||||
'ocr_data' => $visaDataInput,
|
'ocr_data' => $visaDataInput,
|
||||||
]);
|
]);
|
||||||
@ -499,14 +518,6 @@ public function register(Request $request)
|
|||||||
return $worker;
|
return $worker;
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($request->filled('fcm_token')) {
|
|
||||||
\App\Services\FCMService::sendPushNotification(
|
|
||||||
$request->fcm_token,
|
|
||||||
'Welcome to Migrant',
|
|
||||||
'Worker registered successfully.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result->load(['skills', 'documents']);
|
$result->load(['skills', 'documents']);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@ -1250,12 +1261,11 @@ public function forgotPassword(Request $request)
|
|||||||
|
|
||||||
$worker = Worker::where('phone', $request->phone)->first();
|
$worker = Worker::where('phone', $request->phone)->first();
|
||||||
|
|
||||||
// Always return success to prevent phone enumeration
|
|
||||||
if (!$worker) {
|
if (!$worker) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => false,
|
||||||
'message' => 'If an account with that mobile number exists, a reset OTP has been sent.',
|
'message' => 'user not found this mobile number',
|
||||||
]);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$otp = (string) mt_rand(100000, 999999);
|
$otp = (string) mt_rand(100000, 999999);
|
||||||
|
|||||||
@ -290,17 +290,35 @@ public function uploadEmiratesId(Request $request)
|
|||||||
|
|
||||||
$extractedIdNumber = null;
|
$extractedIdNumber = null;
|
||||||
$extractedExpiry = 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')) {
|
if ($request->hasFile('emirates_id_front')) {
|
||||||
$frontFile = $request->file('emirates_id_front');
|
$frontFile = $request->file('emirates_id_front');
|
||||||
$frontOcr = \App\Services\OcrDocumentService::extractEmiratesIdFrontData($frontFile);
|
$frontOcr = \App\Services\OcrDocumentService::extractEmiratesIdFrontData($frontFile);
|
||||||
$extractedIdNumber = $frontOcr['emirates_id_number'];
|
$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')) {
|
if ($request->hasFile('emirates_id_back')) {
|
||||||
$backFile = $request->file('emirates_id_back');
|
$backFile = $request->file('emirates_id_back');
|
||||||
$backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($backFile);
|
$backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($backFile);
|
||||||
$extractedExpiry = $backOcr['expiry_date'];
|
$extractedExpiry = $backOcr['expiry_date'];
|
||||||
|
$extractedIssueDate = $backOcr['issue_date'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->hasFile('emirates_id_file')) {
|
if ($request->hasFile('emirates_id_file')) {
|
||||||
@ -309,12 +327,30 @@ public function uploadEmiratesId(Request $request)
|
|||||||
$backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($file);
|
$backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($file);
|
||||||
$extractedIdNumber = $frontOcr['emirates_id_number'];
|
$extractedIdNumber = $frontOcr['emirates_id_number'];
|
||||||
$extractedExpiry = $backOcr['expiry_date'];
|
$extractedExpiry = $backOcr['expiry_date'];
|
||||||
|
$extractedName = $frontOcr['name'] ?? null;
|
||||||
|
$extractedDob = $frontOcr['date_of_birth'] ?? null;
|
||||||
|
$extractedNationality = $frontOcr['nationality'] ?? null;
|
||||||
|
$extractedCardNumber = $frontOcr['card_number'] ?? null;
|
||||||
|
$extractedGender = $frontOcr['gender'] ?? null;
|
||||||
|
$extractedOccupation = $frontOcr['occupation'] ?? null;
|
||||||
|
$extractedEmployer = $frontOcr['employer'] ?? null;
|
||||||
|
$extractedIssuePlace = $frontOcr['issue_place'] ?? null;
|
||||||
|
$extractedIssueDate = $backOcr['issue_date'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$pending = session('pending_employer_registration');
|
$pending = session('pending_employer_registration');
|
||||||
$pending['emirates_id_file'] = null;
|
$pending['emirates_id_file'] = null;
|
||||||
$pending['emirates_id_number'] = $extractedIdNumber;
|
$pending['emirates_id_number'] = $extractedIdNumber;
|
||||||
$pending['emirates_id_expiry'] = $extractedExpiry;
|
$pending['emirates_id_expiry'] = $extractedExpiry;
|
||||||
|
$pending['emirates_id_name'] = $extractedName;
|
||||||
|
$pending['emirates_id_dob'] = $extractedDob;
|
||||||
|
$pending['emirates_id_nationality'] = $extractedNationality;
|
||||||
|
$pending['emirates_id_card_number'] = $extractedCardNumber;
|
||||||
|
$pending['emirates_id_gender'] = $extractedGender;
|
||||||
|
$pending['emirates_id_occupation'] = $extractedOccupation;
|
||||||
|
$pending['emirates_id_employer'] = $extractedEmployer;
|
||||||
|
$pending['emirates_id_issue_place'] = $extractedIssuePlace;
|
||||||
|
$pending['emirates_id_issue_date'] = $extractedIssueDate;
|
||||||
session(['pending_employer_registration' => $pending]);
|
session(['pending_employer_registration' => $pending]);
|
||||||
session(['employer_emirates_id_uploaded' => true]);
|
session(['employer_emirates_id_uploaded' => true]);
|
||||||
|
|
||||||
@ -438,6 +474,15 @@ public function createPassword(Request $request)
|
|||||||
'emirates_id_front' => null, // We did not store the file
|
'emirates_id_front' => null, // We did not store the file
|
||||||
'emirates_id_number' => $pending['emirates_id_number'] ?? null,
|
'emirates_id_number' => $pending['emirates_id_number'] ?? null,
|
||||||
'emirates_id_expiry' => $pending['emirates_id_expiry'] ?? 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,
|
'address' => $pending['address'] ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -459,6 +504,17 @@ public function createPassword(Request $request)
|
|||||||
'last_login_at' => now(),
|
'last_login_at' => now(),
|
||||||
'emirates_id_file' => null, // We did not store the file
|
'emirates_id_file' => null, // We did not store the file
|
||||||
'address' => $pending['address'] ?? null,
|
'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
|
// Create active subscription in database
|
||||||
|
|||||||
@ -42,15 +42,17 @@ private function buildProfileData($user, $profile)
|
|||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
'phone' => $profile->phone ?? '+971509990001',
|
'phone' => $profile->phone ?? '+971509990001',
|
||||||
'emirates_id' => [
|
'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,
|
'name' => $profile->emirates_id_name ?? $user->name,
|
||||||
'date_of_birth' => $profile->emirates_id_dob ?? '1990-01-01',
|
'date_of_birth' => $profile->emirates_id_dob ?? '1987-04-14',
|
||||||
'nationality' => $profile->nationality ?? 'Emirati',
|
'nationality' => $profile->nationality ?? 'Bangladesh',
|
||||||
'issue_date' => $profile->emirates_id_issue_date ?? '2023-04-11',
|
'issue_date' => $profile->emirates_id_issue_date ?? '2025-12-12',
|
||||||
'expiry_date' => $profile->emirates_id_expiry ?? '2028-04-11',
|
'expiry_date' => $profile->emirates_id_expiry ?? '2027-12-11',
|
||||||
'employer' => $profile->emirates_id_employer ?? 'Federal Authority',
|
'employer' => $profile->emirates_id_employer ?? 'Msj International Technical Services L.L.C UAE',
|
||||||
'issue_place' => $profile->emirates_id_issue_place ?? 'Abu Dhabi',
|
'issue_place' => $profile->emirates_id_issue_place ?? 'Dubai',
|
||||||
'occupation' => $profile->emirates_id_occupation ?? 'Manager',
|
'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',
|
'fcm_token' => $user->fcm_token ?? 'fcm_token_example',
|
||||||
'address' => $profile->address ?? 'Villa 45, Street 12',
|
'address' => $profile->address ?? 'Villa 45, Street 12',
|
||||||
@ -74,15 +76,17 @@ public function index(Request $request)
|
|||||||
$profile = EmployerProfile::create([
|
$profile = EmployerProfile::create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'phone' => '+971509990001',
|
'phone' => '+971509990001',
|
||||||
'emirates_id_number' => '784-1988-5310327-2',
|
'emirates_id_number' => '784-1987-5493842-5',
|
||||||
'emirates_id_name' => 'Ahmad Bin Ahmed',
|
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
'emirates_id_dob' => '1990-01-01',
|
'emirates_id_dob' => '1987-04-14',
|
||||||
'nationality' => 'Emirati',
|
'nationality' => 'Bangladesh',
|
||||||
'emirates_id_issue_date' => '2023-04-11',
|
'emirates_id_issue_date' => '2025-12-12',
|
||||||
'emirates_id_expiry' => '2028-04-11',
|
'emirates_id_expiry' => '2027-12-11',
|
||||||
'emirates_id_employer' => 'Federal Authority',
|
'emirates_id_employer' => 'Msj International Technical Services L.L.C UAE',
|
||||||
'emirates_id_issue_place' => 'Abu Dhabi',
|
'emirates_id_issue_place' => 'Dubai',
|
||||||
'emirates_id_occupation' => 'Manager',
|
'emirates_id_occupation' => 'Electrician',
|
||||||
|
'emirates_id_card_number' => '151023946',
|
||||||
|
'emirates_id_gender' => 'M',
|
||||||
'address' => 'Villa 45, Street 12',
|
'address' => 'Villa 45, Street 12',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -106,15 +110,17 @@ public function edit(Request $request)
|
|||||||
$profile = EmployerProfile::create([
|
$profile = EmployerProfile::create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'phone' => '+971509990001',
|
'phone' => '+971509990001',
|
||||||
'emirates_id_number' => '784-1988-5310327-2',
|
'emirates_id_number' => '784-1987-5493842-5',
|
||||||
'emirates_id_name' => 'Ahmad Bin Ahmed',
|
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
'emirates_id_dob' => '1990-01-01',
|
'emirates_id_dob' => '1987-04-14',
|
||||||
'nationality' => 'Emirati',
|
'nationality' => 'Bangladesh',
|
||||||
'emirates_id_issue_date' => '2023-04-11',
|
'emirates_id_issue_date' => '2025-12-12',
|
||||||
'emirates_id_expiry' => '2028-04-11',
|
'emirates_id_expiry' => '2027-12-11',
|
||||||
'emirates_id_employer' => 'Federal Authority',
|
'emirates_id_employer' => 'Msj International Technical Services L.L.C UAE',
|
||||||
'emirates_id_issue_place' => 'Abu Dhabi',
|
'emirates_id_issue_place' => 'Dubai',
|
||||||
'emirates_id_occupation' => 'Manager',
|
'emirates_id_occupation' => 'Electrician',
|
||||||
|
'emirates_id_card_number' => '151023946',
|
||||||
|
'emirates_id_gender' => 'M',
|
||||||
'address' => 'Villa 45, Street 12',
|
'address' => 'Villa 45, Street 12',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -370,7 +370,6 @@ public function show($id)
|
|||||||
'skills' => $mappedSkills,
|
'skills' => $mappedSkills,
|
||||||
'visa_status' => $visaStatus,
|
'visa_status' => $visaStatus,
|
||||||
'experience' => $w->experience,
|
'experience' => $w->experience,
|
||||||
'experience_years' => 5,
|
|
||||||
'salary' => (int) $w->salary,
|
'salary' => (int) $w->salary,
|
||||||
'religion' => $w->religion,
|
'religion' => $w->religion,
|
||||||
'languages' => $langs,
|
'languages' => $langs,
|
||||||
|
|||||||
@ -29,6 +29,8 @@ class EmployerProfile extends Model
|
|||||||
'emirates_id_employer',
|
'emirates_id_employer',
|
||||||
'emirates_id_issue_place',
|
'emirates_id_issue_place',
|
||||||
'emirates_id_occupation',
|
'emirates_id_occupation',
|
||||||
|
'emirates_id_card_number',
|
||||||
|
'emirates_id_gender',
|
||||||
'address',
|
'address',
|
||||||
'property_type',
|
'property_type',
|
||||||
'profile_photo_url',
|
'profile_photo_url',
|
||||||
|
|||||||
@ -43,6 +43,8 @@ class Sponsor extends Authenticatable
|
|||||||
'emirates_id_employer',
|
'emirates_id_employer',
|
||||||
'emirates_id_issue_place',
|
'emirates_id_issue_place',
|
||||||
'emirates_id_occupation',
|
'emirates_id_occupation',
|
||||||
|
'emirates_id_card_number',
|
||||||
|
'emirates_id_gender',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
|
|||||||
@ -316,6 +316,11 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
|
|||||||
'name' => null,
|
'name' => null,
|
||||||
'nationality' => null,
|
'nationality' => null,
|
||||||
'date_of_birth' => null,
|
'date_of_birth' => null,
|
||||||
|
'card_number' => null,
|
||||||
|
'gender' => null,
|
||||||
|
'occupation' => null,
|
||||||
|
'employer' => null,
|
||||||
|
'issue_place' => null,
|
||||||
'ocr_accuracy' => 0.0,
|
'ocr_accuracy' => 0.0,
|
||||||
'success' => false,
|
'success' => false,
|
||||||
];
|
];
|
||||||
@ -376,6 +381,12 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
|
|||||||
if (!empty($data['date_of_birth'])) {
|
if (!empty($data['date_of_birth'])) {
|
||||||
$extracted['date_of_birth'] = self::normaliseDate($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 {
|
} else {
|
||||||
Log::warning('Emirates ID Front OCR API success=false', ['response' => $json]);
|
Log::warning('Emirates ID Front OCR API success=false', ['response' => $json]);
|
||||||
}
|
}
|
||||||
@ -392,13 +403,31 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
|
|||||||
|
|
||||||
// Fallbacks
|
// Fallbacks
|
||||||
if (empty($extracted['emirates_id_number'])) {
|
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'])) {
|
if (empty($extracted['name'])) {
|
||||||
$extracted['name'] = 'KRISHNA PRASAD';
|
$extracted['name'] = 'Mohammad Jobaier Mohammad Abul Kalam';
|
||||||
}
|
}
|
||||||
if (empty($extracted['nationality'])) {
|
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;
|
return $extracted;
|
||||||
@ -517,10 +546,15 @@ private static function emiratesIdFrontTestFallback(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'emirates_id_number' => '784-1988-5310327-2',
|
'emirates_id_number' => '784-1987-5493842-5',
|
||||||
'name' => 'KRISHNA PRASAD',
|
'name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
'nationality' => 'NPL',
|
'nationality' => 'Bangladesh',
|
||||||
'date_of_birth' => '1988-03-22',
|
'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,
|
'ocr_accuracy' => 98.5,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -529,8 +563,8 @@ private static function emiratesIdBackTestFallback(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'expiry_date' => '2028-04-11',
|
'expiry_date' => '2027-12-11',
|
||||||
'issue_date' => '2023-04-11',
|
'issue_date' => '2025-12-12',
|
||||||
'ocr_accuracy' => 98.5,
|
'ocr_accuracy' => 98.5,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||||
|
$table->string('company_name')->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||||
|
$table->string('company_name')->nullable(false)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||||
|
$table->string('emirates_id_card_number')->nullable();
|
||||||
|
$table->string('emirates_id_gender')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('sponsors', function (Blueprint $table) {
|
||||||
|
$table->string('emirates_id_card_number')->nullable();
|
||||||
|
$table->string('emirates_id_gender')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['emirates_id_card_number', 'emirates_id_gender']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('sponsors', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['emirates_id_card_number', 'emirates_id_gender']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"openapi": "3.0.0",
|
"openapi": "3.0.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Migrant Mobile API",
|
"title": "Migrant Mobile API",
|
||||||
@ -23,7 +23,7 @@
|
|||||||
"Sponsor/Auth"
|
"Sponsor/Auth"
|
||||||
],
|
],
|
||||||
"summary": "Register Sponsor Account",
|
"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": [],
|
"security": [],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@ -51,7 +51,7 @@
|
|||||||
"mobile": {
|
"mobile": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "+971501112233",
|
"example": "+971501112233",
|
||||||
"description": "Mobile phone number — must be unique. (REQUIRED)"
|
"description": "Mobile phone number \u00e2\u20ac\u201d must be unique. (REQUIRED)"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -600,7 +600,7 @@
|
|||||||
"Worker/Auth"
|
"Worker/Auth"
|
||||||
],
|
],
|
||||||
"summary": "Register Worker Account (Step 1)",
|
"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": [],
|
"security": [],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@ -743,6 +743,11 @@
|
|||||||
"nationality": {
|
"nationality": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ocr_accuracy": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 99
|
||||||
|
},
|
||||||
"passport_number": {
|
"passport_number": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -767,6 +772,22 @@
|
|||||||
"accompanied_by": {
|
"accompanied_by": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"country": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "United Arab Emirates"
|
||||||
|
},
|
||||||
|
"date_of_birth": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "25-JUN-1999"
|
||||||
|
},
|
||||||
|
"document_type": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "uae_visa"
|
||||||
|
},
|
||||||
|
"entry_permit_no": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "77003098 / 2019 / 204"
|
||||||
|
},
|
||||||
"expiry_date": {
|
"expiry_date": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "2024-02-15"
|
"example": "2024-02-15"
|
||||||
@ -774,6 +795,10 @@
|
|||||||
"file_number": {
|
"file_number": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"full_name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Mr.MUHAMMAD NADEEM RASHEED"
|
||||||
|
},
|
||||||
"id_number": {
|
"id_number": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -784,17 +809,50 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"nationality": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "PAKISTAN"
|
||||||
|
},
|
||||||
|
"ocr_accuracy": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"example": 99
|
||||||
|
},
|
||||||
|
"passport_no": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "EN9458281"
|
||||||
|
},
|
||||||
"passport_number": {
|
"passport_number": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"place_of_birth": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "SIALKOT PAK"
|
||||||
|
},
|
||||||
"place_of_issue": {
|
"place_of_issue": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"profession": {
|
"profession": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"example": "SALES REPRESENTATIVE"
|
||||||
},
|
},
|
||||||
"sponsor": {
|
"sponsor": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sponsor_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"uid_no": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "207404887"
|
||||||
|
},
|
||||||
|
"valid_until": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "04-MAR-2019"
|
||||||
|
},
|
||||||
|
"visa_type": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "ENTRY PERMIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -837,7 +895,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"422": {
|
"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": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@ -1124,7 +1182,7 @@
|
|||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Hindi (हिन्दी)"
|
"example": "Hindi (\u0939\u093f\u0928\u094d\u0926\u0940)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3580,11 +3638,110 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Employer/Profile"
|
"Employer/Profile"
|
||||||
],
|
],
|
||||||
"summary": "Get Profile details (Employer)",
|
"summary": "Get Employer Profile Details",
|
||||||
"description": "Retrieves the authenticated employer's profile details including their selected company name, phone, language preference, and notification settings.",
|
"description": "Retrieves the authenticated employer's profile details.",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3594,8 +3751,8 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Employer/Profile"
|
"Employer/Profile"
|
||||||
],
|
],
|
||||||
"summary": "Update Profile details (Employer)",
|
"summary": "Update Employer Profile Details",
|
||||||
"description": "Allows employers to update their profile details (company name, phone number, name, email, language preference, and notification settings) and change their account password.",
|
"description": "Updates the authenticated employer's profile details including optional Emirates ID metadata. Password changes are not supported via this endpoint.",
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
"content": {
|
"content": {
|
||||||
@ -3606,9 +3763,7 @@
|
|||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
"phone",
|
"phone",
|
||||||
"company_name",
|
"address"
|
||||||
"language",
|
|
||||||
"notifications"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
@ -3623,40 +3778,75 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "+971509990001"
|
"example": "+971509990001"
|
||||||
},
|
},
|
||||||
"company_name": {
|
"address": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Ahmad Tech Ltd"
|
"example": "Villa 12, Jumeirah 2"
|
||||||
},
|
|
||||||
"language": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"English",
|
|
||||||
"Arabic",
|
|
||||||
"english",
|
|
||||||
"arabic"
|
|
||||||
],
|
|
||||||
"example": "English"
|
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"example": true
|
"example": true,
|
||||||
},
|
"description": "Optional. Enable/disable push notifications. Defaults to true if not provided.",
|
||||||
"current_password": {
|
"default": true
|
||||||
"type": "string",
|
|
||||||
"example": "Password@123"
|
|
||||||
},
|
|
||||||
"new_password": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "NewSecurePassword@123"
|
|
||||||
},
|
|
||||||
"new_password_confirmation": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "NewSecurePassword@123"
|
|
||||||
},
|
},
|
||||||
"fcm_token": {
|
"fcm_token": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "fcm_token_example",
|
"example": "fcm_token_example",
|
||||||
"description": "Firebase Cloud Messaging token for push notifications."
|
"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)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3665,7 +3855,113 @@
|
|||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation error."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3818,7 +4114,52 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3857,105 +4198,13 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"worker": {
|
"worker": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Worker"
|
||||||
|
},
|
||||||
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 1
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Jane Doe"
|
|
||||||
},
|
|
||||||
"nationality": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Filipino"
|
|
||||||
},
|
|
||||||
"photo": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200"
|
|
||||||
},
|
|
||||||
"emirates_id_status": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Passport Verified",
|
|
||||||
"description": "Deprecated - Use passport_status instead"
|
|
||||||
},
|
|
||||||
"passport_status": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Passport Verified"
|
|
||||||
},
|
|
||||||
"category": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Domestic Worker"
|
|
||||||
},
|
|
||||||
"skills": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"example": [
|
|
||||||
"childcare",
|
|
||||||
"cooking"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"availability_status": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Active"
|
|
||||||
},
|
|
||||||
"visa_status": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Residence Visa"
|
|
||||||
},
|
|
||||||
"experience": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "5 Years"
|
|
||||||
},
|
|
||||||
"experience_years": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 5
|
|
||||||
},
|
|
||||||
"salary": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 2500
|
|
||||||
},
|
|
||||||
"religion": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Christian"
|
|
||||||
},
|
|
||||||
"languages": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"example": [
|
|
||||||
"English",
|
|
||||||
"Tagalog"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"age": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 28
|
|
||||||
},
|
|
||||||
"verified": {
|
|
||||||
"type": "boolean",
|
|
||||||
"example": true
|
|
||||||
},
|
|
||||||
"preferred_job_type": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "live-in"
|
|
||||||
},
|
|
||||||
"bio": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Experienced and caring domestic worker specialing in childcare and housekeeping..."
|
|
||||||
},
|
|
||||||
"rating": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 4.3
|
|
||||||
},
|
|
||||||
"reviews_count": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 6
|
|
||||||
},
|
|
||||||
"reviews": {
|
"reviews": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@ -3975,6 +4224,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4915,7 +5166,7 @@
|
|||||||
"Workers - Auth"
|
"Workers - Auth"
|
||||||
],
|
],
|
||||||
"summary": "Forgot Password (Send OTP)",
|
"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",
|
"operationId": "workerForgotPassword",
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@ -4939,7 +5190,7 @@
|
|||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OTP sent successfully (response is identical whether account exists or not, to prevent enumeration)",
|
"description": "OTP sent successfully.",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@ -4958,8 +5209,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": {
|
"422": {
|
||||||
"description": "Validation error — phone is required"
|
"description": "Validation error \u2014 phone is required"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5147,10 +5418,47 @@
|
|||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"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": {
|
"422": {
|
||||||
"description": "Validation error — email or mobile required"
|
"description": "Validation error \u2014 email or mobile required"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5306,6 +5614,16 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "HI"
|
"example": "HI"
|
||||||
},
|
},
|
||||||
|
"languages": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"English",
|
||||||
|
"Arabic"
|
||||||
|
]
|
||||||
|
},
|
||||||
"fcm_token": {
|
"fcm_token": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "fcm_token_example"
|
"example": "fcm_token_example"
|
||||||
@ -5330,10 +5648,6 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "4 Years"
|
"example": "4 Years"
|
||||||
},
|
},
|
||||||
"religion": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Muslim"
|
|
||||||
},
|
|
||||||
"bio": {
|
"bio": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Certified infant caregiver and housekeeper with excellent cooking skills."
|
"example": "Certified infant caregiver and housekeeper with excellent cooking skills."
|
||||||
@ -5362,6 +5676,19 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Passport Verified"
|
"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": {
|
"preferred_job_type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "full-time"
|
"example": "full-time"
|
||||||
@ -5395,6 +5722,19 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Marina"
|
"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": {
|
"created_at": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
@ -5405,6 +5745,12 @@
|
|||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"example": "2026-05-20T14:54:26.000000Z"
|
"example": "2026-05-20T14:54:26.000000Z"
|
||||||
},
|
},
|
||||||
|
"deleted_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"example": null
|
||||||
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -5430,6 +5776,32 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Cleaning"
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5477,7 +5849,21 @@
|
|||||||
},
|
},
|
||||||
"file_path": {
|
"file_path": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
"example": "uploads/documents/1716200000_passport_my_file.jpg"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export default function AdminLayout({ children, title = 'Dashboard' }) {
|
|||||||
{ name: 'Support Tickets', href: '/admin/tickets', icon: LifeBuoy },
|
{ name: 'Support Tickets', href: '/admin/tickets', icon: LifeBuoy },
|
||||||
{ name: 'Frequently Asked Questions', href: '/admin/faqs', icon: HelpCircle },
|
{ name: 'Frequently Asked Questions', href: '/admin/faqs', icon: HelpCircle },
|
||||||
{ name: 'Payments', href: '/admin/payments', icon: BadgeDollarSign },
|
{ name: 'Payments', href: '/admin/payments', icon: BadgeDollarSign },
|
||||||
|
{ name: 'Plan Configuration', href: '/admin/subscriptions', icon: CreditCard },
|
||||||
{ name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 },
|
{ name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 },
|
||||||
{ name: 'System Audit Logs', href: '/admin/audit-logs', icon: History },
|
{ name: 'System Audit Logs', href: '/admin/audit-logs', icon: History },
|
||||||
{ name: 'Charity Events', href: '/admin/events', icon: Heart },
|
{ name: 'Charity Events', href: '/admin/events', icon: Heart },
|
||||||
|
|||||||
@ -286,9 +286,8 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
<TableHead onClick={() => handleSort('name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none">
|
<TableHead onClick={() => handleSort('name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none">
|
||||||
Employer {sortField === 'name' && (sortOrder === 'desc' ? '↓' : '↑')}
|
Employer {sortField === 'name' && (sortOrder === 'desc' ? '↓' : '↑')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Company Name</TableHead>
|
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Subscription Plan</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Subscription Plan</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Verification Status</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Country</TableHead>
|
||||||
<TableHead onClick={() => handleSort('status')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
|
<TableHead onClick={() => handleSort('status')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
|
||||||
Status {sortField === 'status' && (sortOrder === 'desc' ? '↓' : '↑')}
|
Status {sortField === 'status' && (sortOrder === 'desc' ? '↓' : '↑')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@ -310,11 +309,6 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
|
||||||
<div className="text-xs font-bold text-slate-700">
|
|
||||||
{emp.employer_profile?.company_name || 'N/A'}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className={`px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-tight ${
|
<span className={`px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-tight ${
|
||||||
emp.subscription_status === 'active' ? 'bg-blue-50 text-blue-600 border border-blue-100' : 'bg-slate-100 text-slate-500'
|
emp.subscription_status === 'active' ? 'bg-blue-50 text-blue-600 border border-blue-100' : 'bg-slate-100 text-slate-500'
|
||||||
@ -323,10 +317,8 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className={`text-xs font-bold uppercase ${
|
<span className="text-xs font-bold text-slate-700">
|
||||||
emp.employer_profile?.verification_status === 'approved' ? 'text-emerald-600' : 'text-amber-500'
|
{emp.employer_profile?.country || 'United Arab Emirates'}
|
||||||
}`}>
|
|
||||||
{emp.employer_profile?.verification_status || 'pending'}
|
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -410,7 +402,7 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="py-12 text-center text-slate-400 font-semibold text-sm">
|
<TableCell colSpan={5} className="py-12 text-center text-slate-400 font-semibold text-sm">
|
||||||
No employers found.
|
No employers found.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -478,10 +470,6 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
Employer Profile
|
Employer Profile
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-2 gap-4 text-xs">
|
<div className="grid grid-cols-2 gap-4 text-xs">
|
||||||
<div>
|
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Company / Household</span>
|
|
||||||
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.company_name || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Email Address</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Email Address</span>
|
||||||
<span className="font-bold text-slate-800">{selectedEmployer?.email || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{selectedEmployer?.email || 'N/A'}</span>
|
||||||
@ -535,28 +523,52 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-xs space-y-4">
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-xs space-y-4">
|
||||||
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest border-b border-slate-100 pb-2 flex items-center gap-2">
|
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest border-b border-slate-100 pb-2 flex items-center gap-2">
|
||||||
<ShieldCheck className="w-4 h-4 text-[#0F6E56]" />
|
<ShieldCheck className="w-4 h-4 text-[#0F6E56]" />
|
||||||
Emirates ID & Document Extracted Details
|
Emirates ID Details
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-2 gap-4 text-xs">
|
<div className="grid grid-cols-2 gap-4 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Emirates ID Number</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">ID Number</span>
|
||||||
<span className="font-mono font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_number || 'Not Extracted / Pending'}</span>
|
<span className="font-mono font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_number || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Emirates ID Expiry</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Card Number</span>
|
||||||
<span className="font-bold text-slate-800">{formatDate(selectedEmployer?.employer_profile?.emirates_id_expiry)}</span>
|
<span className="font-mono font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_card_number || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Verification Status</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Full Name</span>
|
||||||
<span className={`font-bold uppercase ${
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_name || 'N/A'}</span>
|
||||||
selectedEmployer?.employer_profile?.verification_status === 'approved' ? 'text-emerald-600' : 'text-amber-500'
|
|
||||||
}`}>
|
|
||||||
{selectedEmployer?.employer_profile?.verification_status || 'Pending'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Trade License Expiry</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Date of Birth</span>
|
||||||
<span className="font-bold text-slate-800">{formatDate(selectedEmployer?.sponsor?.license_expiry)}</span>
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_dob || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Nationality</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.nationality || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Gender</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_gender || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Issue Date</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_issue_date || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Expiry Date</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_expiry || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Occupation</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_occupation || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Employer</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_employer || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Issuing Place</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_issue_place || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -590,15 +602,7 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Company Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
|
||||||
value={editForm.company_name}
|
|
||||||
onChange={e => setEditForm({ ...editForm, company_name: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Email Address</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Email Address</label>
|
||||||
|
|||||||
@ -43,19 +43,78 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
|
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [editingPlan, setEditingPlan] = useState(null);
|
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) => {
|
const handleEdit = (plan) => {
|
||||||
setEditingPlan(plan);
|
setEditingPlan(plan);
|
||||||
|
setName(plan.name);
|
||||||
|
setPrice(plan.price);
|
||||||
|
setDuration(plan.duration);
|
||||||
|
setFeatures(plan.features.join('\n'));
|
||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (id) => {
|
const handleDelete = (id) => {
|
||||||
if (confirm('Are you sure you want to delete this plan?')) {
|
if (confirm('Are you sure you want to delete this plan?')) {
|
||||||
// router.delete(`/admin/subscriptions/plans/${id}`);
|
|
||||||
setPlans(plans.filter(p => p.id !== 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 (
|
return (
|
||||||
<AdminLayout title="Subscription Management">
|
<AdminLayout title="Subscription Management">
|
||||||
<Head title="Plans Management" />
|
<Head title="Plans Management" />
|
||||||
@ -68,11 +127,13 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search plans..."
|
placeholder="Search plans..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500/10 focus:border-teal-500 outline-none transition-all"
|
className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500/10 focus:border-teal-500 outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setEditingPlan(null); setIsDialogOpen(true); }}
|
onClick={handleCreateClick}
|
||||||
className="bg-[#0F6E56] text-white px-4 py-2 rounded-lg text-sm font-semibold flex items-center justify-center space-x-2 hover:bg-[#085041] transition-colors shadow-sm"
|
className="bg-[#0F6E56] text-white px-4 py-2 rounded-lg text-sm font-semibold flex items-center justify-center space-x-2 hover:bg-[#085041] transition-colors shadow-sm"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
@ -94,7 +155,14 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{plans.map((plan) => (
|
{filteredPlans.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="text-center py-8 text-slate-400 text-sm">
|
||||||
|
No plans found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
filteredPlans.map((plan) => (
|
||||||
<TableRow key={plan.id} className="hover:bg-slate-50/50 transition-colors">
|
<TableRow key={plan.id} className="hover:bg-slate-50/50 transition-colors">
|
||||||
<TableCell className="py-4">
|
<TableCell className="py-4">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
@ -145,13 +213,14 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Plan Modal (Placeholder UI) */}
|
{/* Plan Modal */}
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogContent className="sm:max-w-[500px] rounded-[24px]">
|
<DialogContent className="sm:max-w-[500px] rounded-[24px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@ -163,44 +232,70 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={handleSave}>
|
||||||
<div className="grid gap-6 py-4">
|
<div className="grid gap-6 py-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Plan Name</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Plan Name</label>
|
||||||
<input className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none" placeholder="e.g. Pro Search" />
|
<input
|
||||||
|
required
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
|
placeholder="e.g. Pro Search"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Price (AED)</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Price (AED)</label>
|
||||||
<input type="number" className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none" placeholder="299" />
|
<input
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
value={price}
|
||||||
|
onChange={(e) => setPrice(e.target.value)}
|
||||||
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
|
placeholder="299"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label>
|
||||||
<select className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none">
|
<select
|
||||||
<option>Monthly</option>
|
value={duration}
|
||||||
<option>Quarterly</option>
|
onChange={(e) => setDuration(e.target.value)}
|
||||||
<option>Yearly</option>
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||||
|
>
|
||||||
|
<option value="Monthly">Monthly</option>
|
||||||
|
<option value="Quarterly">Quarterly</option>
|
||||||
|
<option value="Yearly">Yearly</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Features (one per line)</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Features (one per line)</label>
|
||||||
<textarea className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none min-h-[100px]" placeholder="Unlimited search Verified badges" />
|
<textarea
|
||||||
|
value={features}
|
||||||
|
onChange={(e) => setFeatures(e.target.value)}
|
||||||
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none min-h-[100px]"
|
||||||
|
placeholder="Unlimited search Verified badges"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="sm:justify-end gap-3">
|
<DialogFooter className="sm:justify-end gap-3 pt-4 border-t border-slate-100">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setIsDialogOpen(false)}
|
onClick={() => setIsDialogOpen(false)}
|
||||||
className="px-6 py-2.5 rounded-xl text-sm font-bold text-slate-400 uppercase tracking-widest hover:bg-slate-50 transition-colors"
|
className="px-6 py-2.5 rounded-xl text-sm font-bold text-slate-400 uppercase tracking-widest hover:bg-slate-50 transition-colors"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="submit"
|
||||||
className="bg-[#0F6E56] text-white px-8 py-2.5 rounded-xl text-sm font-bold uppercase tracking-widest shadow-lg shadow-teal-500/20 active:scale-95 transition-all"
|
className="bg-[#0F6E56] text-white px-8 py-2.5 rounded-xl text-sm font-bold uppercase tracking-widest shadow-lg shadow-teal-500/20 active:scale-95 transition-all"
|
||||||
>
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
|
|||||||
@ -208,7 +208,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
})).filter(worker => {
|
})).filter(worker => {
|
||||||
const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
worker.email.toLowerCase().includes(searchTerm.toLowerCase());
|
worker.email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
const matchesStatus = statusFilter === 'all' || worker.status === statusFilter;
|
const matchesStatus = statusFilter === 'all' || worker.status?.toLowerCase() === statusFilter.toLowerCase();
|
||||||
return matchesSearch && matchesStatus;
|
return matchesSearch && matchesStatus;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -250,15 +250,13 @@ export default function WorkerManagement({ workers }) {
|
|||||||
className="w-full pl-10 pr-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm focus:bg-white focus:ring-4 focus:ring-[#0F6E56]/10 outline-none transition-all font-medium"
|
className="w-full pl-10 pr-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm focus:bg-white focus:ring-4 focus:ring-[#0F6E56]/10 outline-none transition-all font-medium"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button className="p-2.5 bg-white border border-slate-200 rounded-xl text-slate-400 hover:bg-slate-50 transition-colors">
|
|
||||||
<Filter className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 w-full md:w-auto">
|
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mr-2">Lifecycle Filter:</span>
|
<span className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mr-2">Lifecycle Filter:</span>
|
||||||
<div className="flex bg-slate-100 p-1 rounded-xl">
|
<div className="flex bg-slate-100 p-1 rounded-xl">
|
||||||
{['all', 'active', 'inactive', 'suspended'].map((f) => (
|
{['all', 'active', 'hired', 'suspended'].map((f) => (
|
||||||
<button
|
<button
|
||||||
key={f}
|
key={f}
|
||||||
onClick={() => setStatusFilter(f)}
|
onClick={() => setStatusFilter(f)}
|
||||||
@ -377,9 +375,13 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`px-2 py-0.5 rounded-full font-black text-[9px] uppercase tracking-wider block w-fit ${
|
className={`px-2 py-0.5 rounded-full font-black text-[9px] uppercase tracking-wider block w-fit ${
|
||||||
worker.status === 'active'
|
worker.status?.toLowerCase() === 'active'
|
||||||
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
||||||
: worker.status === 'suspended' ? 'bg-red-100 text-red-800 border-none' : 'bg-slate-50 text-slate-700 border-slate-200'
|
: worker.status?.toLowerCase() === 'hired'
|
||||||
|
? 'bg-blue-50 text-blue-700 border-blue-200'
|
||||||
|
: worker.status?.toLowerCase() === 'suspended'
|
||||||
|
? 'bg-red-100 text-red-800 border-none'
|
||||||
|
: 'bg-slate-50 text-slate-700 border-slate-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{worker.status}
|
{worker.status}
|
||||||
@ -526,15 +528,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="text-[10px] font-bold text-slate-500">Religion</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={editForm.religion}
|
|
||||||
onChange={e => setEditForm({ ...editForm, religion: e.target.value })}
|
|
||||||
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -716,10 +710,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Visa Status</label>
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Visa Status</label>
|
||||||
<span className="text-slate-800 font-extrabold">{selectedWorker?.visa_status || 'N/A'}</span>
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.visa_status || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Religion</label>
|
|
||||||
<span className="text-slate-800 font-extrabold">{selectedWorker?.religion || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Age</label>
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Age</label>
|
||||||
<span className="text-slate-800 font-extrabold">{selectedWorker?.age || 'N/A'}</span>
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.age || 'N/A'}</span>
|
||||||
@ -760,14 +751,26 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
|
||||||
<span className="font-mono font-bold text-slate-800">{passportDoc.ocr_data?.passport_number || passportDoc.number || 'N/A'}</span>
|
<span className="font-mono font-bold text-slate-800">{passportDoc.ocr_data?.passport_number || passportDoc.number || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Surname</span>
|
||||||
|
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.surname || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Given Names</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Given Names</span>
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.given_names || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.given_names || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sex</span>
|
||||||
|
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.sex || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Birth</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Birth</span>
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_birth || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_birth || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Birth</span>
|
||||||
|
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.place_of_birth || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Nationality</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Nationality</span>
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.nationality || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.nationality || 'N/A'}</span>
|
||||||
@ -776,10 +779,6 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issuing Country</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issuing Country</span>
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.issuing_country || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.issuing_country || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Birth</span>
|
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.place_of_birth || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Issue</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Issue</span>
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_issue || passportDoc.issue_date || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_issue || passportDoc.issue_date || 'N/A'}</span>
|
||||||
@ -788,6 +787,18 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Expiry</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Expiry</span>
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_expiry || passportDoc.expiry_date || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_expiry || passportDoc.expiry_date || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Authority</span>
|
||||||
|
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.authority || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Document Type</span>
|
||||||
|
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.document_type || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Personal Number</span>
|
||||||
|
<span className="font-mono font-bold text-slate-800">{passportDoc.ocr_data?.personal_number || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{passportDoc.file_path && (
|
{passportDoc.file_path && (
|
||||||
<div className="pt-2 border-t border-slate-200/60 flex justify-end">
|
<div className="pt-2 border-t border-slate-200/60 flex justify-end">
|
||||||
@ -822,6 +833,14 @@ export default function WorkerManagement({ workers }) {
|
|||||||
return (
|
return (
|
||||||
<div className="p-4 bg-slate-50 rounded-xl border border-slate-100 space-y-3">
|
<div className="p-4 bg-slate-50 rounded-xl border border-slate-100 space-y-3">
|
||||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Entry Permit No</span>
|
||||||
|
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.entry_permit_no || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">UID No</span>
|
||||||
|
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.uid_no || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">File Number</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">File Number</span>
|
||||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.file_number || visaDoc.number || 'N/A'}</span>
|
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.file_number || visaDoc.number || 'N/A'}</span>
|
||||||
@ -830,6 +849,55 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">ID Number</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">ID Number</span>
|
||||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.id_number || 'N/A'}</span>
|
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.id_number || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Full Name</span>
|
||||||
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.full_name || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Name</span>
|
||||||
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.name || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Visa Type</span>
|
||||||
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.visa_type || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Profession</span>
|
||||||
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.profession || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sponsor Name</span>
|
||||||
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.sponsor_name || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Nationality</span>
|
||||||
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.nationality || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Country</span>
|
||||||
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.country || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport No</span>
|
||||||
|
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.passport_no || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
|
||||||
|
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.passport_number || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Birth</span>
|
||||||
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.date_of_birth || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Birth</span>
|
||||||
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.place_of_birth || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Issue</span>
|
||||||
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.place_of_issue || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issue Date</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issue Date</span>
|
||||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.issue_date || visaDoc.issue_date || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.issue_date || visaDoc.issue_date || 'N/A'}</span>
|
||||||
@ -838,17 +906,18 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Expiry Date</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Expiry Date</span>
|
||||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.expiry_date || visaDoc.expiry_date || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.expiry_date || visaDoc.expiry_date || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Accompanied By</span>
|
||||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.passport_number || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.accompanied_by || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Issue</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Document Type</span>
|
||||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.place_of_issue || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.document_type || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sponsor</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">OCR Accuracy</span>
|
||||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.sponsor || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.ocr_accuracy || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{visaDoc.file_path && (
|
{visaDoc.file_path && (
|
||||||
@ -869,50 +938,18 @@ export default function WorkerManagement({ workers }) {
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Compliance Notes */}
|
|
||||||
<div className="space-y-2 bg-yellow-50/60 p-4 border border-yellow-200 rounded-2xl">
|
|
||||||
<label className="text-[10px] font-black text-yellow-800 uppercase tracking-widest flex items-center gap-1">
|
|
||||||
<FileText className="w-3.5 h-3.5" />
|
|
||||||
<span>Internal Admin Compliance Notes Logs</span>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
rows="3"
|
|
||||||
placeholder="Write admin logs regarding candidate verification reviews or user complaints..."
|
|
||||||
className="w-full bg-white border border-yellow-200 rounded-xl p-3 text-xs font-bold text-slate-700 focus:ring-2 focus:ring-yellow-500/20 outline-none"
|
|
||||||
value={adminNotes}
|
|
||||||
onChange={e => setAdminNotes(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column (1/3) */}
|
{/* Right Column (1/3) */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Contact & Location */}
|
{/* Contact Detail */}
|
||||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm space-y-4">
|
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm space-y-4">
|
||||||
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Contact & Location</h3>
|
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Contact Detail</h3>
|
||||||
<div className="space-y-3.5 text-xs font-bold text-slate-700">
|
<div className="space-y-3.5 text-xs font-bold text-slate-700">
|
||||||
<div className="flex items-center space-x-2 bg-slate-50 p-2.5 rounded-xl border border-slate-100">
|
<div className="flex items-center space-x-2 bg-slate-50 p-2.5 rounded-xl border border-slate-100">
|
||||||
<Phone className="w-4 h-4 text-teal-600 flex-shrink-0" />
|
<Phone className="w-4 h-4 text-teal-600 flex-shrink-0" />
|
||||||
<span className="text-slate-800 font-extrabold">{selectedWorker?.phone || '+971 52 489 1209'}</span>
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.phone || '+971 52 489 1209'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3 pt-1">
|
|
||||||
<div>
|
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Country</label>
|
|
||||||
<span className="text-slate-800 font-extrabold block">{selectedWorker?.country || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">City</label>
|
|
||||||
<span className="text-slate-800 font-extrabold block">{selectedWorker?.city || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2 border-t border-slate-100 pt-2">
|
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Area</label>
|
|
||||||
<span className="text-slate-800 font-extrabold block">{selectedWorker?.area || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2 border-t border-slate-100 pt-2">
|
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Preferred Location</label>
|
|
||||||
<span className="text-slate-800 font-extrabold block text-xs leading-normal">{selectedWorker?.preferred_location || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -952,31 +989,6 @@ export default function WorkerManagement({ workers }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Fraud Flagging */}
|
|
||||||
<div className="border-t border-slate-100 pt-4">
|
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-2">Trustworthiness</label>
|
|
||||||
{selectedWorker?.is_fraud ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center space-x-1.5 text-red-600 bg-red-50 border border-red-100 px-3 py-2 rounded-xl text-xs font-extrabold">
|
|
||||||
<ShieldAlert className="w-4 h-4" />
|
|
||||||
<span>Flagged as Fraud</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => handleFlagFraud(selectedWorker.id, false, '')}
|
|
||||||
className="w-full py-2 bg-slate-900 hover:bg-slate-800 text-white rounded-xl text-[10px] font-black uppercase tracking-wider transition-all"
|
|
||||||
>
|
|
||||||
Clear Fraud Flag
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={() => handleFlagFraud(selectedWorker.id, true, 'Flagged by system admin review.')}
|
|
||||||
className="w-full py-2 border border-red-200 hover:bg-red-50 text-red-600 rounded-xl text-[10px] font-black uppercase tracking-wider transition-all"
|
|
||||||
>
|
|
||||||
Flag as Fraud
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -351,13 +351,6 @@ export default function Show({ worker }) {
|
|||||||
<span className="font-black text-slate-900 text-xs uppercase">{worker.live_in_out}</span>
|
<span className="font-black text-slate-900 text-xs uppercase">{worker.live_in_out}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
|
||||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
|
||||||
<MapPin className="w-4 h-4 text-slate-500" />
|
|
||||||
<span>{t('preferred_location', 'Preferred Location')}</span>
|
|
||||||
</div>
|
|
||||||
<span className="font-extrabold text-slate-900 text-xs capitalize">{worker.preferred_location || t('any', 'Any')}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
||||||
|
|||||||
@ -30,10 +30,9 @@ protected function setUp(): void
|
|||||||
|
|
||||||
EmployerProfile::create([
|
EmployerProfile::create([
|
||||||
'user_id' => $this->employer->id,
|
'user_id' => $this->employer->id,
|
||||||
'company_name' => 'Original Company',
|
'address' => 'Original Address',
|
||||||
'phone' => '+971501112222',
|
'phone' => '+971501112222',
|
||||||
'verification_status' => 'approved',
|
'verification_status' => 'approved',
|
||||||
'language' => 'English',
|
|
||||||
'notifications' => true,
|
'notifications' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -54,9 +53,8 @@ public function test_employer_can_get_profile()
|
|||||||
'profile' => [
|
'profile' => [
|
||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
'company_name',
|
|
||||||
'phone',
|
'phone',
|
||||||
'language',
|
'address',
|
||||||
'notifications',
|
'notifications',
|
||||||
'verification_status',
|
'verification_status',
|
||||||
]
|
]
|
||||||
@ -64,7 +62,7 @@ public function test_employer_can_get_profile()
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals('Original Name', $response->json('data.profile.name'));
|
$this->assertEquals('Original Name', $response->json('data.profile.name'));
|
||||||
$this->assertEquals('Original Company', $response->json('data.profile.company_name'));
|
$this->assertEquals('Original Address', $response->json('data.profile.address'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,8 +76,7 @@ public function test_employer_can_update_profile()
|
|||||||
'name' => 'Updated Name',
|
'name' => 'Updated Name',
|
||||||
'email' => 'employer_updated@example.com',
|
'email' => 'employer_updated@example.com',
|
||||||
'phone' => '+971509999999',
|
'phone' => '+971509999999',
|
||||||
'company_name' => 'Updated Company Corp',
|
'address' => 'Updated Address Info',
|
||||||
'language' => 'Arabic',
|
|
||||||
'notifications' => false,
|
'notifications' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -91,9 +88,8 @@ public function test_employer_can_update_profile()
|
|||||||
'profile' => [
|
'profile' => [
|
||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
'company_name',
|
|
||||||
'phone',
|
'phone',
|
||||||
'language',
|
'address',
|
||||||
'notifications',
|
'notifications',
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -107,67 +103,94 @@ public function test_employer_can_update_profile()
|
|||||||
|
|
||||||
$this->assertDatabaseHas('employer_profiles', [
|
$this->assertDatabaseHas('employer_profiles', [
|
||||||
'user_id' => $this->employer->id,
|
'user_id' => $this->employer->id,
|
||||||
'company_name' => 'Updated Company Corp',
|
'address' => 'Updated Address Info',
|
||||||
'language' => 'Arabic',
|
|
||||||
'notifications' => false,
|
'notifications' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test employer can update password when providing correct current password.
|
* Test employer can update profile with optional Emirates ID fields.
|
||||||
*/
|
*/
|
||||||
public function test_employer_can_update_password()
|
public function test_employer_can_update_profile_with_emirates_id_fields()
|
||||||
{
|
{
|
||||||
$response = $this->withHeaders([
|
$response = $this->withHeaders([
|
||||||
'Authorization' => 'Bearer ' . $this->token,
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
])->postJson('/api/employers/profile/update', [
|
])->postJson('/api/employers/profile/update', [
|
||||||
'name' => 'Original Name',
|
'name' => 'Ahmad',
|
||||||
'email' => 'employer@example.com',
|
'email' => 'ahmad@example.com',
|
||||||
'phone' => '+971501112222',
|
'phone' => '+971509990001',
|
||||||
'company_name' => 'Original Company',
|
'address' => 'Villa 12, Jumeirah 2',
|
||||||
'language' => 'English',
|
|
||||||
'notifications' => true,
|
'notifications' => true,
|
||||||
'current_password' => 'Password@123',
|
'id_number' => '784-1987-5493842-5',
|
||||||
'new_password' => 'NewSecurePassword@123',
|
'card_number' => '151023946',
|
||||||
'new_password_confirmation' => 'NewSecurePassword@123',
|
'full_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
|
'date_of_birth' => '14/04/1987',
|
||||||
|
'nationality' => 'Bangladesh',
|
||||||
|
'gender' => 'M',
|
||||||
|
'issue_date' => '12/12/2025',
|
||||||
|
'expiry_date' => '11/12/2027',
|
||||||
|
'occupation' => 'Electrician',
|
||||||
|
'employer' => 'Msj International Technical Services L.L.C UAE',
|
||||||
|
'issuing_place' => 'Dubai',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
// Verify password hash updated
|
'success' => true,
|
||||||
$this->assertTrue(Hash::check('NewSecurePassword@123', $this->employer->fresh()->password));
|
'data' => [
|
||||||
}
|
'profile' => [
|
||||||
|
'name' => 'Ahmad',
|
||||||
/**
|
'email' => 'ahmad@example.com',
|
||||||
* Test employer password update fails with incorrect current password.
|
'phone' => '+971509990001',
|
||||||
*/
|
'address' => 'Villa 12, Jumeirah 2',
|
||||||
public function test_employer_password_update_fails_with_incorrect_current_password()
|
'id_number' => '784-1987-5493842-5',
|
||||||
{
|
'card_number' => '151023946',
|
||||||
$response = $this->withHeaders([
|
'full_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
'Authorization' => 'Bearer ' . $this->token,
|
'gender' => 'M',
|
||||||
])->postJson('/api/employers/profile/update', [
|
]
|
||||||
'name' => 'Original Name',
|
|
||||||
'email' => 'employer@example.com',
|
|
||||||
'phone' => '+971501112222',
|
|
||||||
'company_name' => 'Original Company',
|
|
||||||
'language' => 'English',
|
|
||||||
'notifications' => true,
|
|
||||||
'current_password' => 'WrongCurrentPassword',
|
|
||||||
'new_password' => 'NewSecurePassword@123',
|
|
||||||
'new_password_confirmation' => 'NewSecurePassword@123',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->assertStatus(422)
|
|
||||||
->assertJsonStructure([
|
|
||||||
'success',
|
|
||||||
'message',
|
|
||||||
'errors' => [
|
|
||||||
'current_password'
|
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Verify password was NOT changed
|
$this->assertDatabaseHas('employer_profiles', [
|
||||||
$this->assertTrue(Hash::check('Password@123', $this->employer->fresh()->password));
|
'user_id' => $this->employer->id,
|
||||||
|
'emirates_id_number' => '784-1987-5493842-5',
|
||||||
|
'emirates_id_card_number' => '151023946',
|
||||||
|
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
|
'emirates_id_gender' => 'M',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test employer cannot change an existing Emirates ID number.
|
||||||
|
*/
|
||||||
|
public function test_employer_cannot_change_existing_emirates_id_number()
|
||||||
|
{
|
||||||
|
// First, set the Emirates ID
|
||||||
|
$profile = $this->employer->employerProfile;
|
||||||
|
$profile->emirates_id_number = '784-1987-5493842-5';
|
||||||
|
$profile->save();
|
||||||
|
|
||||||
|
// Attempt to change to a different ID
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
|
])->postJson('/api/employers/profile/update', [
|
||||||
|
'name' => 'Original Name',
|
||||||
|
'email' => 'employer@example.com',
|
||||||
|
'phone' => '+971501112222',
|
||||||
|
'address' => 'Original Address',
|
||||||
|
'id_number' => '784-9999-0000000-0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'id_number' => ['Emirates ID mismatch.']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify ID was NOT changed
|
||||||
|
$this->assertEquals('784-1987-5493842-5', $profile->fresh()->emirates_id_number);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -218,4 +241,51 @@ public function test_employer_dashboard_returns_only_two_charity_drives()
|
|||||||
$this->assertFalse($titles->contains('Charity Drive 1'));
|
$this->assertFalse($titles->contains('Charity Drive 1'));
|
||||||
$this->assertFalse($titles->contains('Pending Charity Drive'));
|
$this->assertFalse($titles->contains('Pending Charity Drive'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_employer_dashboard_returns_total_workers_active()
|
||||||
|
{
|
||||||
|
// Delete any existing workers seeded or created to make count precise
|
||||||
|
\App\Models\Worker::query()->delete();
|
||||||
|
|
||||||
|
// Create 3 active workers
|
||||||
|
\App\Models\Worker::create([
|
||||||
|
'name' => 'Active 1', 'email' => 'a1@test.com', 'phone' => '+971501110001',
|
||||||
|
'password' => bcrypt('password'), 'status' => 'active', 'nationality' => 'Indian',
|
||||||
|
'preferred_location' => 'Dubai', 'age' => 25, 'salary' => 1500, 'experience' => '3 Years',
|
||||||
|
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
|
||||||
|
]);
|
||||||
|
\App\Models\Worker::create([
|
||||||
|
'name' => 'Active 2', 'email' => 'a2@test.com', 'phone' => '+971501110002',
|
||||||
|
'password' => bcrypt('password'), 'status' => 'active', 'nationality' => 'Indian',
|
||||||
|
'preferred_location' => 'Dubai', 'age' => 26, 'salary' => 1500, 'experience' => '3 Years',
|
||||||
|
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
|
||||||
|
]);
|
||||||
|
\App\Models\Worker::create([
|
||||||
|
'name' => 'Active 3', 'email' => 'a3@test.com', 'phone' => '+971501110003',
|
||||||
|
'password' => bcrypt('password'), 'status' => 'active', 'nationality' => 'Indian',
|
||||||
|
'preferred_location' => 'Dubai', 'age' => 27, 'salary' => 1500, 'experience' => '3 Years',
|
||||||
|
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 2 inactive/hidden workers
|
||||||
|
\App\Models\Worker::create([
|
||||||
|
'name' => 'Inactive 1', 'email' => 'i1@test.com', 'phone' => '+971502220001',
|
||||||
|
'password' => bcrypt('password'), 'status' => 'hidden', 'nationality' => 'Indian',
|
||||||
|
'preferred_location' => 'Dubai', 'age' => 28, 'salary' => 1500, 'experience' => '3 Years',
|
||||||
|
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
|
||||||
|
]);
|
||||||
|
\App\Models\Worker::create([
|
||||||
|
'name' => 'Inactive 2', 'email' => 'i2@test.com', 'phone' => '+971502220002',
|
||||||
|
'password' => bcrypt('password'), 'status' => 'Hired', 'nationality' => 'Indian',
|
||||||
|
'preferred_location' => 'Dubai', 'age' => 29, 'salary' => 1500, 'experience' => '3 Years',
|
||||||
|
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
|
])->getJson('/api/employers/dashboard');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$this->assertEquals(3, $response->json('data.stats.total_workers'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -435,11 +435,184 @@ public function test_worker_list_includes_document_expiry_status()
|
|||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
$workers = $response->json('data.workers');
|
$workers = $response->json('data.workers');
|
||||||
|
|
||||||
// Find the worker we just created in the response
|
|
||||||
$matchingWorker = collect($workers)->firstWhere('id', $worker->id);
|
$matchingWorker = collect($workers)->firstWhere('id', $worker->id);
|
||||||
|
|
||||||
$this->assertNotNull($matchingWorker);
|
$this->assertNotNull($matchingWorker);
|
||||||
$this->assertEquals(45, $matchingWorker['document_expiry_days']);
|
$this->assertEquals(45, $matchingWorker['document_expiry_days']);
|
||||||
$this->assertEquals('Visa expires in 45 days', $matchingWorker['document_expiry_status']);
|
$this->assertEquals('Visa expires in 45 days', $matchingWorker['document_expiry_status']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_worker_list_api_has_full_details_and_pagination()
|
||||||
|
{
|
||||||
|
$worker = Worker::create([
|
||||||
|
'name' => 'Full Detail Worker',
|
||||||
|
'email' => 'full_detail@example.com',
|
||||||
|
'phone' => '+971509999888',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
'preferred_location' => 'Dubai Marina',
|
||||||
|
'live_in_out' => 'live_out',
|
||||||
|
'gender' => 'male',
|
||||||
|
'in_country' => true,
|
||||||
|
'visa_status' => 'Tourist Visa',
|
||||||
|
'preferred_job_type' => 'full-time',
|
||||||
|
'fcm_token' => 'fcm_token_example',
|
||||||
|
'age' => 36,
|
||||||
|
'salary' => 1500.00,
|
||||||
|
'experience' => '4 Years',
|
||||||
|
'language' => 'English, Arabic',
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => 'Experienced helper',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$doc1 = WorkerDocument::create([
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'type' => 'passport',
|
||||||
|
'number' => 'YB98239279327',
|
||||||
|
'issue_date' => '2017-07-05',
|
||||||
|
'expiry_date' => '2022-07-05',
|
||||||
|
'ocr_accuracy' => 99,
|
||||||
|
'ocr_data' => ['authority' => 'FMP LLC'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$doc2 = WorkerDocument::create([
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'type' => 'visa',
|
||||||
|
'number' => '343434',
|
||||||
|
'issue_date' => '2022-02-16',
|
||||||
|
'expiry_date' => '2024-02-15',
|
||||||
|
'ocr_accuracy' => 99,
|
||||||
|
'ocr_data' => ['sponsor' => 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add a skill
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Ironing']);
|
||||||
|
$worker->skills()->attach($skill->id);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
|
])->getJson('/api/employers/workers?page=1&per_page=15');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonStructure([
|
||||||
|
'success',
|
||||||
|
'data' => [
|
||||||
|
'workers',
|
||||||
|
'pagination' => [
|
||||||
|
'total',
|
||||||
|
'per_page',
|
||||||
|
'current_page',
|
||||||
|
'last_page',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$workers = $response->json('data.workers');
|
||||||
|
$matchingWorker = collect($workers)->firstWhere('id', $worker->id);
|
||||||
|
|
||||||
|
$this->assertNotNull($matchingWorker);
|
||||||
|
$this->assertEquals('+971509999888', $matchingWorker['phone']);
|
||||||
|
$this->assertEquals('1500.00', $matchingWorker['salary']);
|
||||||
|
$this->assertEquals('active', $matchingWorker['status']);
|
||||||
|
$this->assertEquals('English, Arabic', $matchingWorker['language']);
|
||||||
|
$this->assertEquals('fcm_token_example', $matchingWorker['fcm_token']);
|
||||||
|
$this->assertEquals('2024-02-15', $matchingWorker['visa_expiry_date']);
|
||||||
|
|
||||||
|
// Check skills object array
|
||||||
|
$this->assertIsArray($matchingWorker['skills']);
|
||||||
|
$this->assertCount(1, $matchingWorker['skills']);
|
||||||
|
$this->assertEquals('Ironing', $matchingWorker['skills'][0]['name']);
|
||||||
|
|
||||||
|
// Check documents object array
|
||||||
|
$this->assertIsArray($matchingWorker['documents']);
|
||||||
|
$this->assertCount(2, $matchingWorker['documents']);
|
||||||
|
$this->assertEquals('passport', $matchingWorker['documents'][0]['type']);
|
||||||
|
$this->assertEquals('visa', $matchingWorker['documents'][1]['type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_worker_detail_api_has_full_details_without_religion_and_cleaned_ocr_data()
|
||||||
|
{
|
||||||
|
$worker = Worker::create([
|
||||||
|
'name' => 'Detail Worker Test',
|
||||||
|
'email' => 'detail_test@example.com',
|
||||||
|
'phone' => '+971509999777',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
'preferred_location' => 'Dubai Marina',
|
||||||
|
'live_in_out' => 'live_out',
|
||||||
|
'gender' => 'male',
|
||||||
|
'in_country' => true,
|
||||||
|
'visa_status' => 'Tourist Visa',
|
||||||
|
'preferred_job_type' => 'full-time',
|
||||||
|
'fcm_token' => 'fcm_token_example',
|
||||||
|
'age' => 36,
|
||||||
|
'salary' => 1500.00,
|
||||||
|
'experience' => '4 Years',
|
||||||
|
'language' => 'English, Arabic',
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => 'Experienced helper',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$doc1 = WorkerDocument::create([
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'type' => 'passport',
|
||||||
|
'number' => 'YB98239279327',
|
||||||
|
'issue_date' => '2017-07-05',
|
||||||
|
'expiry_date' => '2022-07-05',
|
||||||
|
'ocr_accuracy' => 99,
|
||||||
|
'ocr_data' => ['authority' => 'FMP LLC'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$doc2 = WorkerDocument::create([
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'type' => 'visa',
|
||||||
|
'number' => '343434',
|
||||||
|
'issue_date' => '2022-02-16',
|
||||||
|
'expiry_date' => '2024-02-15',
|
||||||
|
'ocr_accuracy' => 99,
|
||||||
|
'ocr_data' => [
|
||||||
|
'sponsor' => 'some sponsor',
|
||||||
|
'sponsor_name' => 'some sponsor name',
|
||||||
|
'valid_until' => '2024-02-15',
|
||||||
|
'expiry_date' => '2024-02-15'
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
|
])->getJson('/api/employers/workers/' . $worker->id);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$wData = $response->json('data.worker');
|
||||||
|
|
||||||
|
$this->assertNotNull($wData);
|
||||||
|
$this->assertEquals('+971509999777', $wData['phone']);
|
||||||
|
$this->assertEquals('detail_test@example.com', $wData['email']);
|
||||||
|
$this->assertEquals('1500.00', $wData['salary']);
|
||||||
|
$this->assertEquals('active', $wData['status']);
|
||||||
|
$this->assertEquals('English, Arabic', $wData['language']);
|
||||||
|
$this->assertEquals('fcm_token_example', $wData['fcm_token']);
|
||||||
|
$this->assertEquals('2024-02-15', $wData['visa_expiry_date']);
|
||||||
|
|
||||||
|
// Assert religion is removed
|
||||||
|
$this->assertArrayNotHasKey('religion', $wData);
|
||||||
|
|
||||||
|
// Check documents object array
|
||||||
|
$this->assertIsArray($wData['documents']);
|
||||||
|
$this->assertCount(2, $wData['documents']);
|
||||||
|
|
||||||
|
// Assert visa ocr_data is cleaned up
|
||||||
|
$visaDoc = collect($wData['documents'])->firstWhere('type', 'visa');
|
||||||
|
$this->assertNotNull($visaDoc);
|
||||||
|
$this->assertArrayNotHasKey('sponsor', $visaDoc['ocr_data']);
|
||||||
|
$this->assertArrayNotHasKey('valid_until', $visaDoc['ocr_data']);
|
||||||
|
$this->assertEquals('some sponsor name', $visaDoc['ocr_data']['sponsor_name']);
|
||||||
|
$this->assertEquals('2024-02-15', $visaDoc['ocr_data']['expiry_date']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -571,4 +571,36 @@ public function test_sponsor_registration_with_emirates_id_and_separate_license(
|
|||||||
$sponsor->refresh();
|
$sponsor->refresh();
|
||||||
$this->assertEquals('2028-06-12 00:00:00', $sponsor->license_expiry->toDateTimeString());
|
$this->assertEquals('2028-06-12 00:00:00', $sponsor->license_expiry->toDateTimeString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test worker forgot password user not found error.
|
||||||
|
*/
|
||||||
|
public function test_worker_forgot_password_user_not_found()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/workers/forgot-password', [
|
||||||
|
'phone' => '+971501111111',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(404)
|
||||||
|
->assertJson([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'user not found this mobile number',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test sponsor forgot password user not found error.
|
||||||
|
*/
|
||||||
|
public function test_sponsor_forgot_password_user_not_found()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/sponsors/forgot-password', [
|
||||||
|
'email' => 'nonexistent.sponsor@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(404)
|
||||||
|
->assertJson([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'user not found this email id',
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,14 +90,30 @@ public function test_employer_web_registration_with_emirates_id()
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$profile = EmployerProfile::where('phone', '501234567')->first();
|
$profile = EmployerProfile::where('phone', '501234567')->first();
|
||||||
$this->assertNotNull($profile->emirates_id_number);
|
$this->assertEquals('784-1987-5493842-5', $profile->emirates_id_number);
|
||||||
$this->assertNotNull($profile->emirates_id_expiry);
|
$this->assertEquals('2027-12-11', $profile->emirates_id_expiry);
|
||||||
|
$this->assertEquals('Mohammad Jobaier Mohammad Abul Kalam', $profile->emirates_id_name);
|
||||||
|
$this->assertEquals('1987-04-14', $profile->emirates_id_dob);
|
||||||
|
$this->assertEquals('Bangladesh', $profile->nationality);
|
||||||
|
$this->assertEquals('2025-12-12', $profile->emirates_id_issue_date);
|
||||||
|
$this->assertEquals('Msj International Technical Services L.L.C UAE', $profile->emirates_id_employer);
|
||||||
|
$this->assertEquals('Dubai', $profile->emirates_id_issue_place);
|
||||||
|
$this->assertEquals('Electrician', $profile->emirates_id_occupation);
|
||||||
|
|
||||||
$this->assertDatabaseHas('sponsors', [
|
$this->assertDatabaseHas('sponsors', [
|
||||||
'email' => 'abdullah@example.com',
|
'email' => 'abdullah@example.com',
|
||||||
'mobile' => '501234567',
|
'mobile' => '501234567',
|
||||||
'emirates_id_file' => null, // File is not stored
|
'emirates_id_file' => null, // File is not stored
|
||||||
'address' => 'Villa 14, Al Safa, Dubai',
|
'address' => 'Villa 14, Al Safa, Dubai',
|
||||||
|
'emirates_id' => '784-1987-5493842-5',
|
||||||
|
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
|
'emirates_id_dob' => '1987-04-14',
|
||||||
|
'nationality' => 'Bangladesh',
|
||||||
|
'emirates_id_issue_date' => '2025-12-12',
|
||||||
|
'emirates_id_expiry' => '2027-12-11',
|
||||||
|
'emirates_id_employer' => 'Msj International Technical Services L.L.C UAE',
|
||||||
|
'emirates_id_issue_place' => 'Dubai',
|
||||||
|
'emirates_id_occupation' => 'Electrician',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,8 +160,8 @@ public function test_employer_web_registration_with_two_sided_emirates_id()
|
|||||||
$this->assertTrue(session('employer_emirates_id_uploaded'));
|
$this->assertTrue(session('employer_emirates_id_uploaded'));
|
||||||
|
|
||||||
$pendingRegAfterId = session('pending_employer_registration');
|
$pendingRegAfterId = session('pending_employer_registration');
|
||||||
$this->assertEquals('784-1988-5310327-2', $pendingRegAfterId['emirates_id_number']); // Mock fallback value
|
$this->assertEquals('784-1987-5493842-5', $pendingRegAfterId['emirates_id_number']); // Mock fallback value
|
||||||
$this->assertEquals('2028-04-11', $pendingRegAfterId['emirates_id_expiry']); // Mock fallback value
|
$this->assertEquals('2027-12-11', $pendingRegAfterId['emirates_id_expiry']); // Mock fallback value
|
||||||
|
|
||||||
// Step 4: Payment Choice
|
// Step 4: Payment Choice
|
||||||
$responseStep4 = $this->post('/employer/register-payment', [
|
$responseStep4 = $this->post('/employer/register-payment', [
|
||||||
@ -172,8 +188,15 @@ public function test_employer_web_registration_with_two_sided_emirates_id()
|
|||||||
|
|
||||||
$profile = EmployerProfile::where('phone', '509876543')->first();
|
$profile = EmployerProfile::where('phone', '509876543')->first();
|
||||||
$this->assertNotNull($profile);
|
$this->assertNotNull($profile);
|
||||||
$this->assertEquals('784-1988-5310327-2', $profile->emirates_id_number);
|
$this->assertEquals('784-1987-5493842-5', $profile->emirates_id_number);
|
||||||
$this->assertEquals('2028-04-11', $profile->emirates_id_expiry);
|
$this->assertEquals('2027-12-11', $profile->emirates_id_expiry);
|
||||||
|
$this->assertEquals('Mohammad Jobaier Mohammad Abul Kalam', $profile->emirates_id_name);
|
||||||
|
$this->assertEquals('1987-04-14', $profile->emirates_id_dob);
|
||||||
|
$this->assertEquals('Bangladesh', $profile->nationality);
|
||||||
|
$this->assertEquals('2025-12-12', $profile->emirates_id_issue_date);
|
||||||
|
$this->assertEquals('Msj International Technical Services L.L.C UAE', $profile->emirates_id_employer);
|
||||||
|
$this->assertEquals('Dubai', $profile->emirates_id_issue_place);
|
||||||
|
$this->assertEquals('Electrician', $profile->emirates_id_occupation);
|
||||||
$this->assertEquals('Apartment 402, Al Nahda, Sharjah', $profile->address);
|
$this->assertEquals('Apartment 402, Al Nahda, Sharjah', $profile->address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -536,6 +536,103 @@ public function test_register_passport_and_visa_with_direct_json_payload()
|
|||||||
$this->assertEquals('Sponsor Name', $visaDoc->ocr_data['sponsor']);
|
$this->assertEquals('Sponsor Name', $visaDoc->ocr_data['sponsor']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test worker registration with the specific passport fields and ocr_accuracy.
|
||||||
|
*/
|
||||||
|
public function test_register_passport_with_full_ocr_fields()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/workers/register', [
|
||||||
|
'name' => 'Temp Name',
|
||||||
|
'phone' => '+971507777777',
|
||||||
|
'salary' => 2000,
|
||||||
|
'password' => 'secret123',
|
||||||
|
'language' => 'HI',
|
||||||
|
'passport' => [
|
||||||
|
'document_type' => 'P',
|
||||||
|
'issuing_country' => 'HAS',
|
||||||
|
'surname' => 'KHALED',
|
||||||
|
'given_names' => 'KHALED AL',
|
||||||
|
'passport_number' => 'Y34B67890',
|
||||||
|
'nationality' => 'RE1',
|
||||||
|
'date_of_birth' => '1990-05-14',
|
||||||
|
'date_of_expiry' => '2022-05-07',
|
||||||
|
'place_of_birth' => 'SHARJAH',
|
||||||
|
'authority' => 'MINISTRY OF INTERIOR U.S. 21 is ,,,, is',
|
||||||
|
'sex' => 'M',
|
||||||
|
'date_of_issue' => '2017-07-05',
|
||||||
|
'ocr_accuracy' => 98.7,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201);
|
||||||
|
$workerId = $response->json('data.worker.id');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('worker_documents', [
|
||||||
|
'worker_id' => $workerId,
|
||||||
|
'type' => 'passport',
|
||||||
|
'number' => 'Y34B67890',
|
||||||
|
'expiry_date' => '2022-05-07',
|
||||||
|
'issue_date' => '2017-07-05',
|
||||||
|
'ocr_accuracy' => 98.7,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$passportDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'passport')->first();
|
||||||
|
$this->assertNotNull($passportDoc->ocr_data);
|
||||||
|
$this->assertEquals('KHALED AL', $passportDoc->ocr_data['given_names']);
|
||||||
|
$this->assertEquals('KHALED', $passportDoc->ocr_data['surname']);
|
||||||
|
$this->assertEquals('MINISTRY OF INTERIOR U.S. 21 is ,,,, is', $passportDoc->ocr_data['authority']);
|
||||||
|
$this->assertEquals(98.7, $passportDoc->ocr_data['ocr_accuracy']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test worker registration with the specific visa fields and ocr_accuracy.
|
||||||
|
*/
|
||||||
|
public function test_register_visa_with_full_ocr_fields()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/workers/register', [
|
||||||
|
'name' => 'Temp Name',
|
||||||
|
'phone' => '+971507777778',
|
||||||
|
'salary' => 2000,
|
||||||
|
'password' => 'secret123',
|
||||||
|
'language' => 'HI',
|
||||||
|
'visa' => [
|
||||||
|
'document_type' => 'uae_visa',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'visa_type' => 'ENTRY PERMIT',
|
||||||
|
'entry_permit_no' => '77003098 / 2019 / 204',
|
||||||
|
'issue_date' => '',
|
||||||
|
'valid_until' => '04-MAR-2019',
|
||||||
|
'uid_no' => '207404887',
|
||||||
|
'full_name' => 'Mr.MUHAMMAD NADEEM RASHEED',
|
||||||
|
'nationality' => 'PAKISTAN',
|
||||||
|
'place_of_birth' => 'SIALKOT PAK',
|
||||||
|
'date_of_birth' => '25-JUN-1999',
|
||||||
|
'passport_no' => 'EN9458281',
|
||||||
|
'profession' => 'SALES REPRESENTATIVE',
|
||||||
|
'sponsor_name' => '',
|
||||||
|
'ocr_accuracy' => 97.4,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201);
|
||||||
|
$workerId = $response->json('data.worker.id');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('worker_documents', [
|
||||||
|
'worker_id' => $workerId,
|
||||||
|
'type' => 'visa',
|
||||||
|
'number' => '77003098 / 2019 / 204',
|
||||||
|
'expiry_date' => '2019-03-04',
|
||||||
|
'ocr_accuracy' => 97.4,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$visaDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'visa')->first();
|
||||||
|
$this->assertNotNull($visaDoc->ocr_data);
|
||||||
|
$this->assertEquals('uae_visa', $visaDoc->ocr_data['document_type']);
|
||||||
|
$this->assertEquals('ENTRY PERMIT', $visaDoc->ocr_data['visa_type']);
|
||||||
|
$this->assertEquals('Mr.MUHAMMAD NADEEM RASHEED', $visaDoc->ocr_data['full_name']);
|
||||||
|
$this->assertEquals(97.4, $visaDoc->ocr_data['ocr_accuracy']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test worker passport and visa registration when they are passed as JSON strings.
|
* Test worker passport and visa registration when they are passed as JSON strings.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export default defineConfig({
|
|||||||
port: 5173,
|
port: 5173,
|
||||||
cors: true,
|
cors: true,
|
||||||
hmr: {
|
hmr: {
|
||||||
host: '192.168.29.131',
|
host: '192.168.0.166',
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
ignored: ['**/storage/framework/views/**'],
|
ignored: ['**/storage/framework/views/**'],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user