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