mohan #19
@ -21,32 +21,54 @@ public function showLogin()
|
|||||||
*/
|
*/
|
||||||
public function login(Request $request)
|
public function login(Request $request)
|
||||||
{
|
{
|
||||||
$credentials = $request->validate([
|
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
|
||||||
'email' => ['required', 'email'],
|
'email' => ['required', 'email'],
|
||||||
'password' => ['required'],
|
'password' => ['required'],
|
||||||
|
], [
|
||||||
|
'email.required' => 'Please enter your email address.',
|
||||||
|
'email.email' => 'Please enter a valid email address.',
|
||||||
|
'password.required' => 'Please enter your password.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return back()->withErrors($validator)->withInput($request->only('email'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$credentials = $validator->validated();
|
||||||
|
|
||||||
// Database-backed authentication check
|
// Database-backed authentication check
|
||||||
$user = \App\Models\User::where('email', $credentials['email'])->first();
|
$user = \App\Models\User::where('email', $credentials['email'])->first();
|
||||||
|
|
||||||
if ($user && \Illuminate\Support\Facades\Hash::check($credentials['password'], $user->password) && $user->role === 'admin') {
|
if (!$user) {
|
||||||
$request->session()->regenerate();
|
return back()->withErrors([
|
||||||
|
'email' => 'This email is not registered.',
|
||||||
auth()->login($user);
|
])->withInput($request->only('email'));
|
||||||
|
|
||||||
session(['user' => (object)[
|
|
||||||
'id' => $user->id,
|
|
||||||
'name' => $user->name,
|
|
||||||
'email' => $user->email,
|
|
||||||
'role' => $user->role,
|
|
||||||
]]);
|
|
||||||
|
|
||||||
return redirect()->intended('/admin/dashboard');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return back()->withErrors([
|
if ($user->role !== 'admin') {
|
||||||
'email' => 'The provided credentials do not match our records. (Use admin@example.com / password)',
|
return back()->withErrors([
|
||||||
]);
|
'email' => 'This email is not registered as an admin.',
|
||||||
|
])->withInput($request->only('email'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\Illuminate\Support\Facades\Hash::check($credentials['password'], $user->password)) {
|
||||||
|
return back()->withErrors([
|
||||||
|
'password' => 'The password you entered is incorrect.',
|
||||||
|
])->withInput($request->only('email'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
auth()->login($user);
|
||||||
|
|
||||||
|
session(['user' => (object)[
|
||||||
|
'id' => $user->id,
|
||||||
|
'name' => $user->name,
|
||||||
|
'email' => $user->email,
|
||||||
|
'role' => $user->role,
|
||||||
|
]]);
|
||||||
|
|
||||||
|
return redirect()->intended('/admin/dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -11,12 +11,89 @@
|
|||||||
class EmployerAnnouncementController extends Controller
|
class EmployerAnnouncementController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get all announcements posted by this employer.
|
* Get all approved announcements for employers.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @return \Illuminate\Http\JsonResponse
|
* @return \Illuminate\Http\JsonResponse
|
||||||
*/
|
*/
|
||||||
public function getAnnouncements(Request $request)
|
public function getAnnouncements(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$page = (int)$request->input('page', 1);
|
||||||
|
$perPage = (int)$request->input('per_page', 15);
|
||||||
|
|
||||||
|
$query = Announcement::with(['employer.employerProfile', 'sponsor'])->where('status', 'approved')->latest();
|
||||||
|
$total = $query->count();
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
$announcements = $query->skip($offset)->take($perPage)->get()
|
||||||
|
->map(function ($announcement) {
|
||||||
|
$postedBy = 'System';
|
||||||
|
$organization = 'Migrant Support';
|
||||||
|
|
||||||
|
if ($announcement->sponsor_id) {
|
||||||
|
$postedBy = $announcement->sponsor->full_name;
|
||||||
|
$organization = $announcement->sponsor->organization_name;
|
||||||
|
} elseif ($announcement->employer_id) {
|
||||||
|
$postedBy = $announcement->employer->name;
|
||||||
|
$organization = $announcement->employer->employerProfile->company_name ?? 'Employer';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode charity details if they exist in json
|
||||||
|
$charityDetails = null;
|
||||||
|
$content = $announcement->body;
|
||||||
|
if (strpos($announcement->body, '{"type":"Charity"') === 0) {
|
||||||
|
$decoded = json_decode($announcement->body, true);
|
||||||
|
if ($decoded) {
|
||||||
|
$charityDetails = $decoded;
|
||||||
|
$content = $decoded['content'] ?? $announcement->body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $announcement->id,
|
||||||
|
'title' => $announcement->title,
|
||||||
|
'body' => $content,
|
||||||
|
'type' => $announcement->type,
|
||||||
|
'employer_name' => $postedBy,
|
||||||
|
'company_name' => $organization,
|
||||||
|
'created_at' => $announcement->created_at->toISOString(),
|
||||||
|
'time_ago' => $announcement->created_at->diffForHumans(),
|
||||||
|
'charity_details' => $charityDetails,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'announcements' => $announcements,
|
||||||
|
'pagination' => [
|
||||||
|
'total' => $total,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'current_page' => $page,
|
||||||
|
'last_page' => max(1, (int)ceil($total / $perPage)),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
logger()->error('Mobile Employer Get All Announcements Failure: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'An error occurred while fetching announcements.',
|
||||||
|
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all announcements posted by this employer.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function getMyAnnouncements(Request $request)
|
||||||
{
|
{
|
||||||
/** @var User $employer */
|
/** @var User $employer */
|
||||||
$employer = $request->attributes->get('employer');
|
$employer = $request->attributes->get('employer');
|
||||||
@ -57,7 +134,7 @@ public function getAnnouncements(Request $request)
|
|||||||
], 200);
|
], 200);
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
logger()->error('Mobile Employer Get Announcements Failure: ' . $e->getMessage());
|
logger()->error('Mobile Employer Get My Announcements Failure: ' . $e->getMessage());
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
|
|||||||
@ -198,6 +198,13 @@ public function register(Request $request)
|
|||||||
'phone.unique' => 'This mobile number is already registered.',
|
'phone.unique' => 'This mobile number is already registered.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$validator->after(function ($validator) use ($request) {
|
||||||
|
$emiratesIdInput = $request->input('emirates_id');
|
||||||
|
if (is_array($emiratesIdInput) && empty($emiratesIdInput['name'])) {
|
||||||
|
$validator->errors()->add('emirates_id.name', 'Failed to extract name from the Emirates ID. Please upload a clearer document.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
@ -232,6 +239,9 @@ public function register(Request $request)
|
|||||||
$emiratesIdNumber = $emiratesIdInput['emirates_id_number'] ?? $emiratesIdInput['id_number'] ?? $emiratesIdInput['id number'] ?? $emiratesIdInput['card_number'] ?? null;
|
$emiratesIdNumber = $emiratesIdInput['emirates_id_number'] ?? $emiratesIdInput['id_number'] ?? $emiratesIdInput['id number'] ?? $emiratesIdInput['card_number'] ?? null;
|
||||||
$emiratesIdExpiry = $emiratesIdInput['expiry_date'] ?? $emiratesIdInput['expiry date'] ?? $emiratesIdInput['emirates_id_expiry'] ?? null;
|
$emiratesIdExpiry = $emiratesIdInput['expiry_date'] ?? $emiratesIdInput['expiry date'] ?? $emiratesIdInput['emirates_id_expiry'] ?? null;
|
||||||
$emiratesIdName = $emiratesIdInput['name'] ?? null;
|
$emiratesIdName = $emiratesIdInput['name'] ?? null;
|
||||||
|
if ($emiratesIdName) {
|
||||||
|
$request->merge(['name' => $emiratesIdName]);
|
||||||
|
}
|
||||||
$emiratesIdDob = $emiratesIdInput['date_of_birth'] ?? $emiratesIdInput['date of birth'] ?? $emiratesIdInput['dob'] ?? null;
|
$emiratesIdDob = $emiratesIdInput['date_of_birth'] ?? $emiratesIdInput['date of birth'] ?? $emiratesIdInput['dob'] ?? null;
|
||||||
$emiratesIdNationality = $emiratesIdInput['nationality'] ?? null;
|
$emiratesIdNationality = $emiratesIdInput['nationality'] ?? null;
|
||||||
$emiratesIdGender = $emiratesIdInput['gender'] ?? $emiratesIdInput['sex'] ?? null;
|
$emiratesIdGender = $emiratesIdInput['gender'] ?? $emiratesIdInput['sex'] ?? null;
|
||||||
@ -562,15 +572,6 @@ public function password(Request $request)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$tokenToSend = $request->fcm_token ?? ($user ? $user->fcm_token : null) ?? ($sponsor ? $sponsor->fcm_token : null);
|
|
||||||
if ($tokenToSend) {
|
|
||||||
\App\Services\FCMService::sendPushNotification(
|
|
||||||
$tokenToSend,
|
|
||||||
'Welcome to Migrant',
|
|
||||||
'Your employer registration has been completed successfully.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Password created successfully. Registration finalized.',
|
'message' => 'Password created successfully. Registration finalized.',
|
||||||
|
|||||||
@ -28,8 +28,8 @@ public function getProfile(Request $request)
|
|||||||
if (!$profile) {
|
if (!$profile) {
|
||||||
$profile = EmployerProfile::create([
|
$profile = EmployerProfile::create([
|
||||||
'user_id' => $employer->id,
|
'user_id' => $employer->id,
|
||||||
'company_name' => 'Al Mansoor Household',
|
|
||||||
'phone' => '+971 50 123 4567',
|
'phone' => '+971 50 123 4567',
|
||||||
|
'address' => 'Dubai, UAE',
|
||||||
'verification_status' => 'approved',
|
'verification_status' => 'approved',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -37,7 +37,6 @@ public function getProfile(Request $request)
|
|||||||
$employerProfile = [
|
$employerProfile = [
|
||||||
'name' => $employer->name,
|
'name' => $employer->name,
|
||||||
'email' => $employer->email,
|
'email' => $employer->email,
|
||||||
'company_name' => $profile->company_name,
|
|
||||||
'phone' => $profile->phone,
|
'phone' => $profile->phone,
|
||||||
'country' => $profile->country,
|
'country' => $profile->country,
|
||||||
'language' => $profile->language ?? 'English',
|
'language' => $profile->language ?? 'English',
|
||||||
@ -105,14 +104,30 @@ public function updateProfile(Request $request)
|
|||||||
'unique:employer_profiles,phone,' . ($employer->employerProfile ? $employer->employerProfile->id : 'NULL'),
|
'unique:employer_profiles,phone,' . ($employer->employerProfile ? $employer->employerProfile->id : 'NULL'),
|
||||||
$sponsor ? 'unique:sponsors,mobile,' . $sponsor->id : 'unique:sponsors,mobile',
|
$sponsor ? 'unique:sponsors,mobile,' . $sponsor->id : 'unique:sponsors,mobile',
|
||||||
],
|
],
|
||||||
'company_name' => 'required|string|max:255',
|
'address' => 'required|string|max:255',
|
||||||
'language' => 'required|string|in:English,Arabic,english,arabic',
|
'notifications' => 'nullable|boolean',
|
||||||
'notifications' => 'required|boolean',
|
|
||||||
'current_password' => 'nullable|required_with:new_password|string',
|
|
||||||
'new_password' => 'nullable|string|min:8|confirmed',
|
|
||||||
'fcm_token' => 'nullable|string|max:255',
|
'fcm_token' => 'nullable|string|max:255',
|
||||||
|
|
||||||
|
// Emirates ID optional fields
|
||||||
|
'id_number' => 'nullable|string|max:255',
|
||||||
|
'card_number' => 'nullable|string|max:255',
|
||||||
|
'full_name' => 'nullable|string|max:255',
|
||||||
|
'date_of_birth' => 'nullable|string|max:255',
|
||||||
|
'nationality' => 'nullable|string|max:255',
|
||||||
|
'gender' => 'nullable|string|max:255',
|
||||||
|
'issue_date' => 'nullable|string|max:255',
|
||||||
|
'expiry_date' => 'nullable|string|max:255',
|
||||||
|
'occupation' => 'nullable|string|max:255',
|
||||||
|
'employer' => 'nullable|string|max:255',
|
||||||
|
'issuing_place' => 'nullable|string|max:255',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$validator->after(function ($validator) use ($request) {
|
||||||
|
if ($request->has('full_name') && !$request->filled('full_name')) {
|
||||||
|
$validator->errors()->add('full_name', 'Failed to extract name from the Emirates ID. Please upload a clearer document.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
@ -122,14 +137,19 @@ public function updateProfile(Request $request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check current password if new password is provided
|
if ($request->filled('full_name')) {
|
||||||
if ($request->filled('new_password')) {
|
$request->merge(['name' => $request->full_name]);
|
||||||
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([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'The provided current password does not match.',
|
'message' => 'Emirates ID mismatch. You cannot change your registered Emirates ID number.',
|
||||||
'errors' => [
|
'errors' => [
|
||||||
'current_password' => ['The provided password does not match your current password.']
|
'id_number' => ['Emirates ID mismatch.']
|
||||||
]
|
]
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
@ -151,11 +171,48 @@ public function updateProfile(Request $request)
|
|||||||
// Sync with corresponding sponsor record if found
|
// Sync with corresponding sponsor record if found
|
||||||
$matchingSponsor = \App\Models\Sponsor::where('email', $oldEmail)->first();
|
$matchingSponsor = \App\Models\Sponsor::where('email', $oldEmail)->first();
|
||||||
if ($matchingSponsor) {
|
if ($matchingSponsor) {
|
||||||
$matchingSponsor->update([
|
$sponsorData = [
|
||||||
'full_name' => $request->name,
|
'full_name' => $request->name,
|
||||||
'email' => $request->email,
|
'email' => $request->email,
|
||||||
'mobile' => $request->phone,
|
'mobile' => $request->phone,
|
||||||
]);
|
'address' => $request->address,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($request->has('id_number')) {
|
||||||
|
$sponsorData['emirates_id'] = $request->id_number;
|
||||||
|
}
|
||||||
|
if ($request->has('card_number')) {
|
||||||
|
$sponsorData['emirates_id_card_number'] = $request->card_number;
|
||||||
|
}
|
||||||
|
if ($request->has('full_name')) {
|
||||||
|
$sponsorData['emirates_id_name'] = $request->full_name;
|
||||||
|
}
|
||||||
|
if ($request->has('date_of_birth')) {
|
||||||
|
$sponsorData['emirates_id_dob'] = $request->date_of_birth;
|
||||||
|
}
|
||||||
|
if ($request->has('nationality')) {
|
||||||
|
$sponsorData['nationality'] = $request->nationality;
|
||||||
|
}
|
||||||
|
if ($request->has('gender')) {
|
||||||
|
$sponsorData['emirates_id_gender'] = $request->gender;
|
||||||
|
}
|
||||||
|
if ($request->has('issue_date')) {
|
||||||
|
$sponsorData['emirates_id_issue_date'] = $request->issue_date;
|
||||||
|
}
|
||||||
|
if ($request->has('expiry_date')) {
|
||||||
|
$sponsorData['emirates_id_expiry'] = $request->expiry_date;
|
||||||
|
}
|
||||||
|
if ($request->has('occupation')) {
|
||||||
|
$sponsorData['emirates_id_occupation'] = $request->occupation;
|
||||||
|
}
|
||||||
|
if ($request->has('employer')) {
|
||||||
|
$sponsorData['emirates_id_employer'] = $request->employer;
|
||||||
|
}
|
||||||
|
if ($request->has('issuing_place')) {
|
||||||
|
$sponsorData['emirates_id_issue_place'] = $request->issuing_place;
|
||||||
|
}
|
||||||
|
|
||||||
|
$matchingSponsor->update($sponsorData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update employer_profiles table
|
// Update employer_profiles table
|
||||||
@ -163,23 +220,49 @@ public function updateProfile(Request $request)
|
|||||||
if (!$profile) {
|
if (!$profile) {
|
||||||
$profile = new EmployerProfile(['user_id' => $employer->id]);
|
$profile = new EmployerProfile(['user_id' => $employer->id]);
|
||||||
}
|
}
|
||||||
$profile->company_name = $request->company_name;
|
$profile->address = $request->address;
|
||||||
$profile->phone = $request->phone;
|
$profile->phone = $request->phone;
|
||||||
$profile->language = ucfirst(strtolower($request->language));
|
$profile->notifications = $request->has('notifications') ? $request->notifications : ($profile->notifications ?? true);
|
||||||
$profile->notifications = $request->notifications;
|
|
||||||
$profile->save();
|
|
||||||
|
|
||||||
// Update password if present
|
if ($request->has('id_number')) {
|
||||||
if ($request->filled('new_password')) {
|
$profile->emirates_id_number = $request->id_number;
|
||||||
$employer->update([
|
|
||||||
'password' => Hash::make($request->new_password),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
if ($request->has('card_number')) {
|
||||||
|
$profile->emirates_id_card_number = $request->card_number;
|
||||||
|
}
|
||||||
|
if ($request->has('full_name')) {
|
||||||
|
$profile->emirates_id_name = $request->full_name;
|
||||||
|
}
|
||||||
|
if ($request->has('date_of_birth')) {
|
||||||
|
$profile->emirates_id_dob = $request->date_of_birth;
|
||||||
|
}
|
||||||
|
if ($request->has('nationality')) {
|
||||||
|
$profile->nationality = $request->nationality;
|
||||||
|
}
|
||||||
|
if ($request->has('gender')) {
|
||||||
|
$profile->emirates_id_gender = $request->gender;
|
||||||
|
}
|
||||||
|
if ($request->has('issue_date')) {
|
||||||
|
$profile->emirates_id_issue_date = $request->issue_date;
|
||||||
|
}
|
||||||
|
if ($request->has('expiry_date')) {
|
||||||
|
$profile->emirates_id_expiry = $request->expiry_date;
|
||||||
|
}
|
||||||
|
if ($request->has('occupation')) {
|
||||||
|
$profile->emirates_id_occupation = $request->occupation;
|
||||||
|
}
|
||||||
|
if ($request->has('employer')) {
|
||||||
|
$profile->emirates_id_employer = $request->employer;
|
||||||
|
}
|
||||||
|
if ($request->has('issuing_place')) {
|
||||||
|
$profile->emirates_id_issue_place = $request->issuing_place;
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile->save();
|
||||||
|
|
||||||
$employerProfile = [
|
$employerProfile = [
|
||||||
'name' => $employer->name,
|
'name' => $employer->name,
|
||||||
'email' => $employer->email,
|
'email' => $employer->email,
|
||||||
'company_name' => $profile->company_name,
|
|
||||||
'phone' => $profile->phone,
|
'phone' => $profile->phone,
|
||||||
'country' => $profile->country,
|
'country' => $profile->country,
|
||||||
'language' => $profile->language,
|
'language' => $profile->language,
|
||||||
@ -236,6 +319,15 @@ public function getDashboard(Request $request)
|
|||||||
|
|
||||||
$savedCandidates = \App\Models\Shortlist::where('employer_id', $employer->id)->count();
|
$savedCandidates = \App\Models\Shortlist::where('employer_id', $employer->id)->count();
|
||||||
|
|
||||||
|
$totalWorkers = \App\Models\Worker::with('documents')
|
||||||
|
->where('status', '!=', 'Hired')
|
||||||
|
->where('status', '!=', 'hidden')
|
||||||
|
->get()
|
||||||
|
->filter(function ($w) {
|
||||||
|
return !str_contains(strtolower($w->passport_status), 'pending');
|
||||||
|
})
|
||||||
|
->count();
|
||||||
|
|
||||||
// Resolve plan
|
// Resolve plan
|
||||||
$sub = \Illuminate\Support\Facades\DB::table('subscriptions')
|
$sub = \Illuminate\Support\Facades\DB::table('subscriptions')
|
||||||
->where('user_id', $employer->id)
|
->where('user_id', $employer->id)
|
||||||
@ -302,6 +394,7 @@ public function getDashboard(Request $request)
|
|||||||
'contacted_workers_count' => $contactedWorkersCount,
|
'contacted_workers_count' => $contactedWorkersCount,
|
||||||
'total_hired_workers' => $totalHiredWorkers,
|
'total_hired_workers' => $totalHiredWorkers,
|
||||||
'saved_candidates' => $savedCandidates,
|
'saved_candidates' => $savedCandidates,
|
||||||
|
'total_workers' => $totalWorkers,
|
||||||
],
|
],
|
||||||
'current_plan' => $currentPlan,
|
'current_plan' => $currentPlan,
|
||||||
'recent_announcements' => $recentAnnouncements,
|
'recent_announcements' => $recentAnnouncements,
|
||||||
@ -320,4 +413,70 @@ public function getDashboard(Request $request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change employer's password.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function changePassword(Request $request)
|
||||||
|
{
|
||||||
|
/** @var User $employer */
|
||||||
|
$employer = $request->attributes->get('employer');
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'current_password' => 'required|string',
|
||||||
|
'new_password' => 'required|string|min:8|confirmed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validation error.',
|
||||||
|
'errors' => $validator->errors()
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Hash::check($request->current_password, $employer->password)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validation error.',
|
||||||
|
'errors' => [
|
||||||
|
'current_password' => ['The current password is incorrect.']
|
||||||
|
]
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$hashedPassword = Hash::make($request->new_password);
|
||||||
|
|
||||||
|
// Update user table
|
||||||
|
$employer->update([
|
||||||
|
'password' => $hashedPassword
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sync with corresponding sponsor record if found
|
||||||
|
$matchingSponsor = \App\Models\Sponsor::where('email', $employer->email)->first();
|
||||||
|
if ($matchingSponsor) {
|
||||||
|
$matchingSponsor->update([
|
||||||
|
'password' => $hashedPassword
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Password changed successfully.'
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
logger()->error('Mobile Employer Change Password Failure: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'An error occurred while changing password.',
|
||||||
|
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,23 +22,16 @@ class EmployerWorkerController extends Controller
|
|||||||
*/
|
*/
|
||||||
private function formatWorker(Worker $w)
|
private function formatWorker(Worker $w)
|
||||||
{
|
{
|
||||||
// Map languages with country names
|
// Map languages from database comma-separated string
|
||||||
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
$langs = $w->language ? array_map('trim', explode(',', $w->language)) : ['English'];
|
||||||
|
|
||||||
// Preferred job types: full-time / part-time / live-in / live-out
|
// Preferred job types: full-time / part-time / live-in / live-out
|
||||||
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
||||||
$preferredJobType = $w->preferred_job_type ?? $jobTypes[$w->id % 4];
|
$preferredJobType = $w->preferred_job_type ?? $jobTypes[$w->id % 4];
|
||||||
|
|
||||||
// Emirates ID verification status (dynamic passport status)
|
// Emirates ID verification status
|
||||||
$emiratesIdStatus = $w->emirates_id_status;
|
$emiratesIdStatus = $w->emirates_id_status;
|
||||||
|
|
||||||
// Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening
|
|
||||||
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
|
||||||
$mappedSkills = [
|
|
||||||
$skillsList[$w->id % 6],
|
|
||||||
$skillsList[($w->id + 2) % 6]
|
|
||||||
];
|
|
||||||
|
|
||||||
// Visa status
|
// Visa status
|
||||||
$visaStatus = $w->visa_status;
|
$visaStatus = $w->visa_status;
|
||||||
|
|
||||||
@ -51,28 +44,79 @@ private function formatWorker(Worker $w)
|
|||||||
return [
|
return [
|
||||||
'id' => $w->id,
|
'id' => $w->id,
|
||||||
'name' => $w->name,
|
'name' => $w->name,
|
||||||
|
'email' => $w->email,
|
||||||
|
'phone' => $w->phone,
|
||||||
'nationality' => $w->nationality,
|
'nationality' => $w->nationality,
|
||||||
'photo' => $photo,
|
|
||||||
'emirates_id_status' => $emiratesIdStatus,
|
|
||||||
'passport_status' => $w->passport_status,
|
|
||||||
'skills' => $mappedSkills,
|
|
||||||
'visa_status' => $visaStatus,
|
|
||||||
'experience' => $w->experience,
|
|
||||||
'religion' => $w->religion,
|
|
||||||
'languages' => $langs,
|
|
||||||
'age' => $w->age,
|
'age' => $w->age,
|
||||||
'gender' => $w->gender,
|
'salary' => $w->salary,
|
||||||
|
'experience' => $w->experience,
|
||||||
'verified' => (bool)$w->verified,
|
'verified' => (bool)$w->verified,
|
||||||
'preferred_job_type' => $preferredJobType,
|
'status' => $w->status,
|
||||||
'bio' => $w->bio,
|
'created_at' => $w->created_at?->toISOString(),
|
||||||
'rating' => $rating,
|
'updated_at' => $w->updated_at?->toISOString(),
|
||||||
'reviews_count' => $reviewsCount,
|
'deleted_at' => $w->deleted_at?->toISOString(),
|
||||||
|
'language' => $w->language,
|
||||||
|
'languages' => $langs,
|
||||||
'preferred_location' => $w->preferred_location,
|
'preferred_location' => $w->preferred_location,
|
||||||
|
'country' => $w->country,
|
||||||
|
'city' => $w->city,
|
||||||
'area' => $w->area,
|
'area' => $w->area,
|
||||||
'live_in_out' => $w->live_in_out,
|
'live_in_out' => $w->live_in_out,
|
||||||
|
'gender' => $w->gender,
|
||||||
'in_country' => (bool)$w->in_country,
|
'in_country' => (bool)$w->in_country,
|
||||||
|
'visa_status' => $visaStatus,
|
||||||
|
'preferred_job_type' => $preferredJobType,
|
||||||
|
'fcm_token' => $w->fcm_token,
|
||||||
|
'passport_status' => $w->passport_status,
|
||||||
|
'emirates_id_status' => $emiratesIdStatus,
|
||||||
'document_expiry_days' => $w->document_expiry_days,
|
'document_expiry_days' => $w->document_expiry_days,
|
||||||
'document_expiry_status' => $w->document_expiry_status,
|
'document_expiry_status' => $w->document_expiry_status,
|
||||||
|
'visa_expiry_date' => $w->visa_expiry_date,
|
||||||
|
'bio' => $w->bio,
|
||||||
|
'photo' => $photo,
|
||||||
|
'rating' => $rating,
|
||||||
|
'reviews_count' => $reviewsCount,
|
||||||
|
'skills' => $w->skills->map(function ($s) {
|
||||||
|
return [
|
||||||
|
'id' => $s->id,
|
||||||
|
'name' => $s->name,
|
||||||
|
'created_at' => $s->created_at?->toISOString(),
|
||||||
|
'updated_at' => $s->updated_at?->toISOString(),
|
||||||
|
'image_path' => $s->image_path,
|
||||||
|
'pivot' => [
|
||||||
|
'worker_id' => $s->pivot->worker_id,
|
||||||
|
'skill_id' => $s->pivot->skill_id,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
})->toArray(),
|
||||||
|
'documents' => $w->documents->map(function ($doc) {
|
||||||
|
$ocrData = $doc->ocr_data;
|
||||||
|
if ($doc->type === 'visa' && is_array($ocrData)) {
|
||||||
|
unset($ocrData['file_number']);
|
||||||
|
unset($ocrData['name']);
|
||||||
|
unset($ocrData['passport_number']);
|
||||||
|
unset($ocrData['accompanied_by']);
|
||||||
|
unset($ocrData['valid_until']);
|
||||||
|
unset($ocrData['sponsor']);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'id' => $doc->id,
|
||||||
|
'worker_id' => $doc->worker_id,
|
||||||
|
'type' => $doc->type,
|
||||||
|
'number' => $doc->number,
|
||||||
|
'issue_date' => $doc->issue_date,
|
||||||
|
'expiry_date' => $doc->expiry_date,
|
||||||
|
'ocr_accuracy' => $doc->ocr_accuracy,
|
||||||
|
'file_path' => $doc->file_path ? url($doc->file_path) : null,
|
||||||
|
'ocr_data' => $ocrData,
|
||||||
|
'created_at' => $doc->created_at?->toISOString(),
|
||||||
|
'updated_at' => $doc->updated_at?->toISOString(),
|
||||||
|
];
|
||||||
|
})->toArray(),
|
||||||
|
'category' => [
|
||||||
|
'id' => 7,
|
||||||
|
'name' => 'General Helper',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +186,8 @@ public function getWorkers(Request $request)
|
|||||||
$workersArray = array_values(array_filter($workersArray, function ($c) use ($skillsArray) {
|
$workersArray = array_values(array_filter($workersArray, function ($c) use ($skillsArray) {
|
||||||
if (!isset($c['skills']) || !is_array($c['skills'])) return false;
|
if (!isset($c['skills']) || !is_array($c['skills'])) return false;
|
||||||
foreach ($c['skills'] as $s) {
|
foreach ($c['skills'] as $s) {
|
||||||
if (in_array(strtolower($s), $skillsArray)) {
|
$skillName = is_array($s) ? ($s['name'] ?? '') : (is_object($s) ? ($s->name ?? '') : $s);
|
||||||
|
if (in_array(strtolower($skillName), $skillsArray)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -314,10 +359,22 @@ public function getWorkers(Request $request)
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$page = (int)$request->input('page', 1);
|
||||||
|
$perPage = (int)$request->input('per_page', 15);
|
||||||
|
$total = count($workersArray);
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
$paginatedWorkers = array_slice($workersArray, $offset, $perPage);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
'workers' => $workersArray
|
'workers' => $paginatedWorkers,
|
||||||
|
'pagination' => [
|
||||||
|
'total' => $total,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'current_page' => $page,
|
||||||
|
'last_page' => max(1, (int)ceil($total / $perPage)),
|
||||||
|
]
|
||||||
]
|
]
|
||||||
], 200);
|
], 200);
|
||||||
|
|
||||||
@ -347,11 +404,11 @@ public function getCandidates(Request $request)
|
|||||||
// Fetch Job Applications
|
// Fetch Job Applications
|
||||||
$jobIds = JobPost::where('employer_id', $employerId)->pluck('id');
|
$jobIds = JobPost::where('employer_id', $employerId)->pluck('id');
|
||||||
$applicationsQuery = JobApplication::whereIn('job_id', $jobIds)
|
$applicationsQuery = JobApplication::whereIn('job_id', $jobIds)
|
||||||
->with(['worker', 'jobPost']);
|
->with(['worker.skills', 'worker.documents', 'jobPost']);
|
||||||
|
|
||||||
// Fetch Direct Hiring Offers
|
// Fetch Direct Hiring Offers
|
||||||
$directOffersQuery = JobOffer::where('employer_id', $employerId)
|
$directOffersQuery = JobOffer::where('employer_id', $employerId)
|
||||||
->with('worker');
|
->with('worker.skills', 'worker.documents');
|
||||||
|
|
||||||
// Apply search filter if provided
|
// Apply search filter if provided
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
@ -381,34 +438,15 @@ public function getCandidates(Request $request)
|
|||||||
elseif ($app->status === 'applied') $status = 'Reviewing';
|
elseif ($app->status === 'applied') $status = 'Reviewing';
|
||||||
else $status = ucfirst($app->status);
|
else $status = ucfirst($app->status);
|
||||||
|
|
||||||
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
$formattedWorker = $this->formatWorker($w);
|
||||||
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
|
||||||
$mappedSkills = [
|
|
||||||
$skillsList[$w->id % 6],
|
|
||||||
$skillsList[($w->id + 2) % 6]
|
|
||||||
];
|
|
||||||
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
|
||||||
$preferredJobType = $w->preferred_job_type ?? $jobTypes[$w->id % 4];
|
|
||||||
|
|
||||||
return [
|
return array_merge($formattedWorker, [
|
||||||
'id' => $app->id,
|
'id' => $app->id,
|
||||||
'worker_id' => $w->id,
|
'worker_id' => $w->id,
|
||||||
'name' => $w->name,
|
|
||||||
'nationality' => $w->nationality,
|
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
'applied_at' => $app->created_at->format('Y-m-d H:i:s'),
|
'applied_at' => $app->created_at->format('Y-m-d H:i:s'),
|
||||||
'type' => 'application',
|
'type' => 'application',
|
||||||
'skills' => $mappedSkills,
|
]);
|
||||||
'languages' => $langs,
|
|
||||||
'availability' => $w->availability ?? 'Immediate',
|
|
||||||
'preferred_location' => $w->preferred_location,
|
|
||||||
'area' => $w->area,
|
|
||||||
'preferred_job_type' => $preferredJobType,
|
|
||||||
'live_in_out' => $w->live_in_out,
|
|
||||||
'in_country' => (bool)$w->in_country,
|
|
||||||
'visa_status' => $w->visa_status,
|
|
||||||
'gender' => $w->gender,
|
|
||||||
];
|
|
||||||
})->filter()->values()->toArray();
|
})->filter()->values()->toArray();
|
||||||
|
|
||||||
// Fetch Direct Hiring Offers
|
// Fetch Direct Hiring Offers
|
||||||
@ -423,34 +461,15 @@ public function getCandidates(Request $request)
|
|||||||
elseif ($offer->status === 'rejected') $status = 'Rejected';
|
elseif ($offer->status === 'rejected') $status = 'Rejected';
|
||||||
elseif ($offer->status === 'pending') $status = 'Offer Sent';
|
elseif ($offer->status === 'pending') $status = 'Offer Sent';
|
||||||
|
|
||||||
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
$formattedWorker = $this->formatWorker($w);
|
||||||
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
|
||||||
$mappedSkills = [
|
|
||||||
$skillsList[$w->id % 6],
|
|
||||||
$skillsList[($w->id + 2) % 6]
|
|
||||||
];
|
|
||||||
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
|
||||||
$preferredJobType = $w->preferred_job_type ?? $jobTypes[$w->id % 4];
|
|
||||||
|
|
||||||
return [
|
return array_merge($formattedWorker, [
|
||||||
'id' => 'offer_' . $offer->id,
|
'id' => 'offer_' . $offer->id,
|
||||||
'worker_id' => $w->id,
|
'worker_id' => $w->id,
|
||||||
'name' => $w->name,
|
|
||||||
'nationality' => $w->nationality,
|
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
'applied_at' => $offer->created_at->format('Y-m-d H:i:s'),
|
'applied_at' => $offer->created_at->format('Y-m-d H:i:s'),
|
||||||
'type' => 'direct_offer',
|
'type' => 'direct_offer',
|
||||||
'skills' => $mappedSkills,
|
]);
|
||||||
'languages' => $langs,
|
|
||||||
'availability' => $w->availability ?? 'Immediate',
|
|
||||||
'preferred_location' => $w->preferred_location,
|
|
||||||
'area' => $w->area,
|
|
||||||
'preferred_job_type' => $preferredJobType,
|
|
||||||
'live_in_out' => $w->live_in_out,
|
|
||||||
'in_country' => (bool)$w->in_country,
|
|
||||||
'visa_status' => $w->visa_status,
|
|
||||||
'gender' => $w->gender,
|
|
||||||
];
|
|
||||||
})->filter()->values()->toArray();
|
})->filter()->values()->toArray();
|
||||||
|
|
||||||
// Merge and sort
|
// Merge and sort
|
||||||
@ -934,25 +953,78 @@ public function getWorkerDetail(Request $request, $id)
|
|||||||
$workerProfile = [
|
$workerProfile = [
|
||||||
'id' => $w->id,
|
'id' => $w->id,
|
||||||
'name' => $w->name,
|
'name' => $w->name,
|
||||||
|
'email' => $w->email,
|
||||||
|
'phone' => $w->phone,
|
||||||
'nationality' => $w->nationality,
|
'nationality' => $w->nationality,
|
||||||
'photo' => $photo,
|
|
||||||
'emirates_id_status' => $emiratesIdStatus,
|
|
||||||
'skills' => $mappedSkills,
|
|
||||||
'visa_status' => $visaStatus,
|
|
||||||
'experience' => $w->experience,
|
|
||||||
'experience_years' => 5,
|
|
||||||
'religion' => $w->religion,
|
|
||||||
'languages' => $langs,
|
|
||||||
'age' => $w->age,
|
'age' => $w->age,
|
||||||
'gender' => $w->gender,
|
'salary' => $w->salary,
|
||||||
|
'experience' => $w->experience,
|
||||||
'verified' => (bool)$w->verified,
|
'verified' => (bool)$w->verified,
|
||||||
|
'status' => $w->status,
|
||||||
|
'created_at' => $w->created_at?->toISOString(),
|
||||||
|
'updated_at' => $w->updated_at?->toISOString(),
|
||||||
|
'deleted_at' => $w->deleted_at?->toISOString(),
|
||||||
|
'language' => $w->language,
|
||||||
|
'languages' => $langs,
|
||||||
|
'preferred_location' => $w->preferred_location,
|
||||||
|
'country' => $w->country,
|
||||||
|
'city' => $w->city,
|
||||||
|
'area' => $w->area,
|
||||||
|
'live_in_out' => $w->live_in_out,
|
||||||
|
'gender' => $w->gender,
|
||||||
|
'in_country' => (bool)$w->in_country,
|
||||||
|
'visa_status' => $visaStatus,
|
||||||
'preferred_job_type' => $preferredJobType,
|
'preferred_job_type' => $preferredJobType,
|
||||||
|
'fcm_token' => $w->fcm_token,
|
||||||
|
'passport_status' => $w->passport_status,
|
||||||
|
'emirates_id_status' => $emiratesIdStatus,
|
||||||
|
'document_expiry_days' => $w->document_expiry_days,
|
||||||
|
'document_expiry_status' => $w->document_expiry_status,
|
||||||
|
'visa_expiry_date' => $w->visa_expiry_date,
|
||||||
'bio' => $w->bio,
|
'bio' => $w->bio,
|
||||||
|
'photo' => $photo,
|
||||||
'rating' => $rating,
|
'rating' => $rating,
|
||||||
'reviews_count' => $reviewsCount,
|
'reviews_count' => $reviewsCount,
|
||||||
'reviews' => $reviews,
|
'reviews' => $reviews,
|
||||||
'similar_workers' => $similarWorkers,
|
'similar_workers' => $similarWorkers,
|
||||||
'conversation_id' => $conversation ? $conversation->id : null,
|
'conversation_id' => $conversation ? $conversation->id : null,
|
||||||
|
'skills' => $w->skills->map(function ($s) {
|
||||||
|
return [
|
||||||
|
'id' => $s->id,
|
||||||
|
'name' => $s->name,
|
||||||
|
'created_at' => $s->created_at?->toISOString(),
|
||||||
|
'updated_at' => $s->updated_at?->toISOString(),
|
||||||
|
'image_path' => $s->image_path,
|
||||||
|
'pivot' => [
|
||||||
|
'worker_id' => $s->pivot->worker_id,
|
||||||
|
'skill_id' => $s->pivot->skill_id,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
})->toArray(),
|
||||||
|
'documents' => $w->documents->map(function ($doc) {
|
||||||
|
$ocrData = $doc->ocr_data;
|
||||||
|
if ($doc->type === 'visa' && is_array($ocrData)) {
|
||||||
|
unset($ocrData['file_number']);
|
||||||
|
unset($ocrData['name']);
|
||||||
|
unset($ocrData['passport_number']);
|
||||||
|
unset($ocrData['accompanied_by']);
|
||||||
|
unset($ocrData['valid_until']);
|
||||||
|
unset($ocrData['sponsor']);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'id' => $doc->id,
|
||||||
|
'worker_id' => $doc->worker_id,
|
||||||
|
'type' => $doc->type,
|
||||||
|
'number' => $doc->number,
|
||||||
|
'issue_date' => $doc->issue_date,
|
||||||
|
'expiry_date' => $doc->expiry_date,
|
||||||
|
'ocr_accuracy' => $doc->ocr_accuracy,
|
||||||
|
'file_path' => $doc->file_path ? url($doc->file_path) : null,
|
||||||
|
'ocr_data' => $ocrData,
|
||||||
|
'created_at' => $doc->created_at?->toISOString(),
|
||||||
|
'updated_at' => $doc->updated_at?->toISOString(),
|
||||||
|
];
|
||||||
|
})->toArray(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@ -987,7 +1059,7 @@ public function getShortlist(Request $request)
|
|||||||
$perPage = (int)$request->input('per_page', 15);
|
$perPage = (int)$request->input('per_page', 15);
|
||||||
|
|
||||||
$shortlists = Shortlist::where('employer_id', $employer->id)
|
$shortlists = Shortlist::where('employer_id', $employer->id)
|
||||||
->with(['worker.skills'])
|
->with(['worker.skills', 'worker.documents'])
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$formattedWorkers = $shortlists->map(function ($s) {
|
$formattedWorkers = $shortlists->map(function ($s) {
|
||||||
|
|||||||
@ -280,12 +280,17 @@ public function reportFromEmployer(Request $request)
|
|||||||
*/
|
*/
|
||||||
public function getReasonsForWorker(Request $request)
|
public function getReasonsForWorker(Request $request)
|
||||||
{
|
{
|
||||||
$type = $request->query('type'); // Chat or Review
|
$type = $request->query('type'); // Chat or Support
|
||||||
|
|
||||||
$query = DB::table('report_reasons')->where('status', 'Active');
|
$query = DB::table('report_reasons')->where('status', 'Active');
|
||||||
|
|
||||||
if ($type && in_array($type, ['Chat', 'Review'])) {
|
if ($type && in_array(strtolower($type), ['chat', 'support'])) {
|
||||||
$query->whereIn('type', [$type, 'Both']);
|
$mappedType = ucfirst(strtolower($type));
|
||||||
|
if ($mappedType === 'Support') {
|
||||||
|
$query->where('type', 'Support');
|
||||||
|
} else {
|
||||||
|
$query->whereIn('type', [$mappedType, 'Both']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$reasons = $query->orderBy('id', 'asc')->get(['id', 'reason', 'type']);
|
$reasons = $query->orderBy('id', 'asc')->get(['id', 'reason', 'type']);
|
||||||
@ -302,12 +307,17 @@ public function getReasonsForWorker(Request $request)
|
|||||||
*/
|
*/
|
||||||
public function getReasonsForEmployer(Request $request)
|
public function getReasonsForEmployer(Request $request)
|
||||||
{
|
{
|
||||||
$type = $request->query('type'); // Chat or Review
|
$type = $request->query('type'); // Chat or Support
|
||||||
|
|
||||||
$query = DB::table('report_reasons')->where('status', 'Active');
|
$query = DB::table('report_reasons')->where('status', 'Active');
|
||||||
|
|
||||||
if ($type && in_array($type, ['Chat', 'Review'])) {
|
if ($type && in_array(strtolower($type), ['chat', 'support'])) {
|
||||||
$query->whereIn('type', [$type, 'Both']);
|
$mappedType = ucfirst(strtolower($type));
|
||||||
|
if ($mappedType === 'Support') {
|
||||||
|
$query->where('type', 'Support');
|
||||||
|
} else {
|
||||||
|
$query->whereIn('type', [$mappedType, 'Both']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$reasons = $query->orderBy('id', 'asc')->get(['id', 'reason', 'type']);
|
$reasons = $query->orderBy('id', 'asc')->get(['id', 'reason', 'type']);
|
||||||
|
|||||||
@ -41,6 +41,13 @@ public function register(Request $request)
|
|||||||
'email.unique' => 'This email address is already registered.',
|
'email.unique' => 'This email address is already registered.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$validator->after(function ($validator) use ($request) {
|
||||||
|
$emiratesIdInput = $request->input('emirates_id');
|
||||||
|
if (is_array($emiratesIdInput) && empty($emiratesIdInput['name'])) {
|
||||||
|
$validator->errors()->add('emirates_id.name', 'Failed to extract name from the Emirates ID. Please upload a clearer document.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
@ -83,6 +90,9 @@ public function register(Request $request)
|
|||||||
$emiratesId = $emiratesIdInput['emirates_id_number'] ?? $emiratesIdInput['id_number'] ?? $emiratesIdInput['id number'] ?? $emiratesIdInput['card_number'] ?? null;
|
$emiratesId = $emiratesIdInput['emirates_id_number'] ?? $emiratesIdInput['id_number'] ?? $emiratesIdInput['id number'] ?? $emiratesIdInput['card_number'] ?? null;
|
||||||
$emiratesIdExpiry = $emiratesIdInput['expiry_date'] ?? $emiratesIdInput['expiry date'] ?? $emiratesIdInput['emirates_id_expiry'] ?? null;
|
$emiratesIdExpiry = $emiratesIdInput['expiry_date'] ?? $emiratesIdInput['expiry date'] ?? $emiratesIdInput['emirates_id_expiry'] ?? null;
|
||||||
$emiratesIdName = $emiratesIdInput['name'] ?? null;
|
$emiratesIdName = $emiratesIdInput['name'] ?? null;
|
||||||
|
if ($emiratesIdName) {
|
||||||
|
$request->merge(['full_name' => $emiratesIdName]);
|
||||||
|
}
|
||||||
$emiratesIdDob = $emiratesIdInput['date_of_birth'] ?? $emiratesIdInput['date of birth'] ?? $emiratesIdInput['dob'] ?? null;
|
$emiratesIdDob = $emiratesIdInput['date_of_birth'] ?? $emiratesIdInput['date of birth'] ?? $emiratesIdInput['dob'] ?? null;
|
||||||
$emiratesIdNationality = $emiratesIdInput['nationality'] ?? null;
|
$emiratesIdNationality = $emiratesIdInput['nationality'] ?? null;
|
||||||
$emiratesIdGender = $emiratesIdInput['gender'] ?? $emiratesIdInput['sex'] ?? null;
|
$emiratesIdGender = $emiratesIdInput['gender'] ?? $emiratesIdInput['sex'] ?? null;
|
||||||
@ -100,7 +110,7 @@ public function register(Request $request)
|
|||||||
$sponsor = DB::transaction(function () use (
|
$sponsor = DB::transaction(function () use (
|
||||||
$request, $email, $licensePath, $emiratesIdPath, $emiratesId, $apiToken, $licenseExpiry, $licenseNumber, $licenseInput,
|
$request, $email, $licensePath, $emiratesIdPath, $emiratesId, $apiToken, $licenseExpiry, $licenseNumber, $licenseInput,
|
||||||
$emiratesIdExpiry, $emiratesIdName, $emiratesIdDob, $emiratesIdIssueDate, $emiratesIdEmployer,
|
$emiratesIdExpiry, $emiratesIdName, $emiratesIdDob, $emiratesIdIssueDate, $emiratesIdEmployer,
|
||||||
$emiratesIdIssuePlace, $emiratesIdOccupation
|
$emiratesIdIssuePlace, $emiratesIdOccupation, $licenseData
|
||||||
) {
|
) {
|
||||||
return Sponsor::create([
|
return Sponsor::create([
|
||||||
'full_name' => $request->full_name,
|
'full_name' => $request->full_name,
|
||||||
@ -390,12 +400,11 @@ public function forgotPassword(Request $request)
|
|||||||
$sponsor = Sponsor::where('mobile', $request->mobile)->first();
|
$sponsor = Sponsor::where('mobile', $request->mobile)->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent email enumeration — always return success
|
|
||||||
if (!$sponsor || !$sponsor->email) {
|
if (!$sponsor || !$sponsor->email) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => false,
|
||||||
'message' => 'If an account with that contact exists, a reset OTP has been sent.',
|
'message' => 'user not found this email id',
|
||||||
]);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$otp = (string) mt_rand(100000, 999999);
|
$otp = (string) mt_rand(100000, 999999);
|
||||||
|
|||||||
@ -354,4 +354,408 @@ public function getProfile(Request $request)
|
|||||||
]
|
]
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the standardized sponsor response array with nested license and emirates_id objects.
|
||||||
|
*/
|
||||||
|
private function buildSponsorResponse(Sponsor $sponsor): array
|
||||||
|
{
|
||||||
|
$licenseData = [
|
||||||
|
'license_number' => $sponsor->license_data['license_no'] ?? $sponsor->license_data['license_number'] ?? null,
|
||||||
|
'organization_name' => $sponsor->organization_name,
|
||||||
|
'expiry_date' => $sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : ($sponsor->license_data['expiry_date'] ?? null),
|
||||||
|
'document_type' => $sponsor->license_data['document_type'] ?? null,
|
||||||
|
'country' => $sponsor->license_data['country'] ?? null,
|
||||||
|
'authority' => $sponsor->license_data['authority'] ?? null,
|
||||||
|
'license_no' => $sponsor->license_data['license_no'] ?? null,
|
||||||
|
'company_name' => $sponsor->license_data['company_name'] ?? null,
|
||||||
|
'business_name' => $sponsor->license_data['business_name'] ?? null,
|
||||||
|
'license_category' => $sponsor->license_data['license_category'] ?? null,
|
||||||
|
'legal_type' => $sponsor->license_data['legal_type'] ?? null,
|
||||||
|
'issue_date' => $sponsor->license_data['issue_date'] ?? null,
|
||||||
|
'main_license_no' => $sponsor->license_data['main_license_no'] ?? null,
|
||||||
|
'register_no' => $sponsor->license_data['register_no'] ?? null,
|
||||||
|
'dcci_no' => $sponsor->license_data['dcci_no'] ?? null,
|
||||||
|
'members' => $sponsor->license_data['members'] ?? [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$emiratesIdData = null;
|
||||||
|
if ($sponsor->emirates_id) {
|
||||||
|
$emiratesIdData = [
|
||||||
|
'emirates_id_number' => $sponsor->emirates_id,
|
||||||
|
'name' => $sponsor->emirates_id_name,
|
||||||
|
'nationality' => $sponsor->nationality,
|
||||||
|
'date_of_birth' => $sponsor->emirates_id_dob,
|
||||||
|
'expiry_date' => $sponsor->emirates_id_expiry,
|
||||||
|
'issue_date' => $sponsor->emirates_id_issue_date,
|
||||||
|
'employer' => $sponsor->emirates_id_employer,
|
||||||
|
'issue_place' => $sponsor->emirates_id_issue_place,
|
||||||
|
'occupation' => $sponsor->emirates_id_occupation,
|
||||||
|
'card_number' => $sponsor->emirates_id_card_number,
|
||||||
|
'gender' => $sponsor->emirates_id_gender,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $sponsor->id,
|
||||||
|
'full_name' => $sponsor->full_name,
|
||||||
|
'organization_name' => $sponsor->organization_name,
|
||||||
|
'mobile' => $sponsor->mobile,
|
||||||
|
'email' => $sponsor->email,
|
||||||
|
'nationality' => $sponsor->nationality,
|
||||||
|
'city' => $sponsor->city,
|
||||||
|
'address' => $sponsor->address,
|
||||||
|
'country_code' => $sponsor->country_code,
|
||||||
|
'is_verified' => $sponsor->is_verified,
|
||||||
|
'status' => $sponsor->status,
|
||||||
|
'license_file' => $sponsor->license_file ? asset($sponsor->license_file) : null,
|
||||||
|
'emirates_id_file' => $sponsor->emirates_id_file ? asset($sponsor->emirates_id_file) : null,
|
||||||
|
'license_expiry' => $sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : null,
|
||||||
|
'validity' => $sponsor->license_expiry ? ($sponsor->license_expiry->isFuture() ? 'Valid' : 'Expired') : 'Pending Review',
|
||||||
|
'joined_at' => $sponsor->created_at->toIso8601String(),
|
||||||
|
'fcm_token' => $sponsor->fcm_token,
|
||||||
|
'license' => $licenseData,
|
||||||
|
'emirates_id' => $emiratesIdData,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change sponsor's password.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function changePassword(Request $request)
|
||||||
|
{
|
||||||
|
/** @var Sponsor $sponsor */
|
||||||
|
$sponsor = $request->attributes->get('sponsor');
|
||||||
|
|
||||||
|
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
|
||||||
|
'current_password' => 'required|string',
|
||||||
|
'new_password' => 'required|string|min:8|confirmed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validation error.',
|
||||||
|
'errors' => $validator->errors()
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\Illuminate\Support\Facades\Hash::check($request->current_password, $sponsor->password)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validation error.',
|
||||||
|
'errors' => [
|
||||||
|
'current_password' => ['The current password is incorrect.']
|
||||||
|
]
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$hashedPassword = \Illuminate\Support\Facades\Hash::make($request->new_password);
|
||||||
|
|
||||||
|
// Update sponsor table
|
||||||
|
$sponsor->update([
|
||||||
|
'password' => $hashedPassword
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sync with corresponding employer user record if found
|
||||||
|
$matchingEmployer = \App\Models\User::where('email', $sponsor->email)
|
||||||
|
->where('role', 'employer')
|
||||||
|
->first();
|
||||||
|
if ($matchingEmployer) {
|
||||||
|
$matchingEmployer->update([
|
||||||
|
'password' => $hashedPassword
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Password changed successfully.'
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
logger()->error('Mobile Sponsor Change Password Failure: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'An error occurred while changing password.',
|
||||||
|
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/sponsors/profile/update
|
||||||
|
*
|
||||||
|
* Updates the authenticated sponsor's profile.
|
||||||
|
*/
|
||||||
|
public function updateProfile(Request $request)
|
||||||
|
{
|
||||||
|
/** @var Sponsor $sponsor */
|
||||||
|
$sponsor = $request->attributes->get('sponsor');
|
||||||
|
|
||||||
|
if (!$sponsor) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Unauthorized.'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matching employer user using sponsor's original email
|
||||||
|
$oldEmail = $sponsor->getOriginal('email') ?? $sponsor->email;
|
||||||
|
$matchingUser = \App\Models\User::where('email', $oldEmail)
|
||||||
|
->where('role', 'employer')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$matchingUserId = $matchingUser ? $matchingUser->id : 'NULL';
|
||||||
|
|
||||||
|
$validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
|
||||||
|
'name' => 'required_without:full_name|nullable|string|max:255',
|
||||||
|
'full_name' => 'required_without:name|nullable|string|max:255',
|
||||||
|
'email' => 'required|email|max:255|unique:sponsors,email,' . $sponsor->id . '|unique:users,email,' . $matchingUserId,
|
||||||
|
'mobile' => 'required|string|max:255|unique:sponsors,mobile,' . $sponsor->id,
|
||||||
|
'organization_name' => 'required|string|max:255',
|
||||||
|
'city' => 'required|string|max:100',
|
||||||
|
'address' => 'required|string|max:255',
|
||||||
|
'nationality' => 'required|string|max:100',
|
||||||
|
'country_code' => 'required|string|max:10',
|
||||||
|
'fcm_token' => 'nullable|string|max:255',
|
||||||
|
|
||||||
|
// Emirates ID — accepts nested object OR flat fields
|
||||||
|
'emirates_id' => 'nullable|array',
|
||||||
|
'emirates_id.emirates_id_number' => 'nullable|string|max:255',
|
||||||
|
'emirates_id.name' => 'nullable|string|max:255',
|
||||||
|
'emirates_id.nationality' => 'nullable|string|max:255',
|
||||||
|
'emirates_id.date_of_birth' => 'nullable|string|max:255',
|
||||||
|
'emirates_id.expiry_date' => 'nullable|string|max:255',
|
||||||
|
'emirates_id.issue_date' => 'nullable|string|max:255',
|
||||||
|
'emirates_id.employer' => 'nullable|string|max:255',
|
||||||
|
'emirates_id.issue_place' => 'nullable|string|max:255',
|
||||||
|
'emirates_id.occupation' => 'nullable|string|max:255',
|
||||||
|
|
||||||
|
// Flat emirates ID fields (backward compatibility)
|
||||||
|
'id_number' => 'nullable|string|max:255',
|
||||||
|
'card_number' => 'nullable|string|max:255',
|
||||||
|
'date_of_birth' => 'nullable|string|max:255',
|
||||||
|
'gender' => 'nullable|string|max:255',
|
||||||
|
'issue_date' => 'nullable|string|max:255',
|
||||||
|
'expiry_date' => 'nullable|string|max:255',
|
||||||
|
'occupation' => 'nullable|string|max:255',
|
||||||
|
'employer' => 'nullable|string|max:255',
|
||||||
|
'issuing_place' => 'nullable|string|max:255',
|
||||||
|
|
||||||
|
// License — accepts nested object OR flat fields
|
||||||
|
'license' => 'nullable|array',
|
||||||
|
'license.document_type' => 'nullable|string|max:255',
|
||||||
|
'license.country' => 'nullable|string|max:255',
|
||||||
|
'license.authority' => 'nullable|string|max:255',
|
||||||
|
'license.license_no' => 'nullable|string|max:255',
|
||||||
|
'license.company_name' => 'nullable|string|max:255',
|
||||||
|
'license.business_name' => 'nullable|string|max:255',
|
||||||
|
'license.license_category' => 'nullable|string|max:255',
|
||||||
|
'license.legal_type' => 'nullable|string|max:255',
|
||||||
|
'license.main_license_no' => 'nullable|string|max:255',
|
||||||
|
'license.register_no' => 'nullable|string|max:255',
|
||||||
|
'license.dcci_no' => 'nullable|string|max:255',
|
||||||
|
'license.members' => 'nullable|array',
|
||||||
|
|
||||||
|
// Flat license fields (backward compatibility)
|
||||||
|
'document_type' => 'nullable|string|max:255',
|
||||||
|
'country' => 'nullable|string|max:255',
|
||||||
|
'authority' => 'nullable|string|max:255',
|
||||||
|
'license_no' => 'nullable|string|max:255',
|
||||||
|
'company_name' => 'nullable|string|max:255',
|
||||||
|
'business_name' => 'nullable|string|max:255',
|
||||||
|
'license_category' => 'nullable|string|max:255',
|
||||||
|
'legal_type' => 'nullable|string|max:255',
|
||||||
|
'main_license_no' => 'nullable|string|max:255',
|
||||||
|
'register_no' => 'nullable|string|max:255',
|
||||||
|
'dcci_no' => 'nullable|string|max:255',
|
||||||
|
'members' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validator->after(function ($validator) use ($request) {
|
||||||
|
$nameFromEid = $request->input('emirates_id.name') ?? $request->input('full_name');
|
||||||
|
if ($request->has('full_name') && !$request->filled('full_name') && !$nameFromEid) {
|
||||||
|
$validator->errors()->add('full_name', 'Failed to extract name from the Emirates ID. Please upload a clearer document.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validation error.',
|
||||||
|
'errors' => $validator->errors()
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Resolve nested emirates_id input into flat variables
|
||||||
|
$eidInput = $request->input('emirates_id');
|
||||||
|
$eidNumber = null;
|
||||||
|
$eidName = null;
|
||||||
|
$eidDob = null;
|
||||||
|
$eidExpiry = null;
|
||||||
|
$eidIssueDate = null;
|
||||||
|
$eidEmployer = null;
|
||||||
|
$eidIssuePlace = null;
|
||||||
|
$eidOccupation = null;
|
||||||
|
$eidCardNumber = null;
|
||||||
|
$eidGender = null;
|
||||||
|
|
||||||
|
if (is_array($eidInput)) {
|
||||||
|
$eidNumber = $eidInput['emirates_id_number'] ?? $eidInput['id_number'] ?? null;
|
||||||
|
$eidName = $eidInput['name'] ?? null;
|
||||||
|
$eidDob = $eidInput['date_of_birth'] ?? $eidInput['dob'] ?? null;
|
||||||
|
$eidExpiry = $eidInput['expiry_date'] ?? null;
|
||||||
|
$eidIssueDate = $eidInput['issue_date'] ?? null;
|
||||||
|
$eidEmployer = $eidInput['employer'] ?? null;
|
||||||
|
$eidIssuePlace = $eidInput['issue_place'] ?? $eidInput['issuing_place'] ?? null;
|
||||||
|
$eidOccupation = $eidInput['occupation'] ?? null;
|
||||||
|
$eidCardNumber = $eidInput['card_number'] ?? null;
|
||||||
|
$eidGender = $eidInput['gender'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flat field fallbacks (backward compatibility)
|
||||||
|
$eidNumber = $eidNumber ?? $request->input('id_number');
|
||||||
|
$eidName = $eidName ?? $request->input('full_name');
|
||||||
|
$eidDob = $eidDob ?? $request->input('date_of_birth');
|
||||||
|
$eidExpiry = $eidExpiry ?? $request->input('expiry_date');
|
||||||
|
$eidIssueDate = $eidIssueDate ?? $request->input('issue_date');
|
||||||
|
$eidEmployer = $eidEmployer ?? $request->input('employer');
|
||||||
|
$eidIssuePlace = $eidIssuePlace ?? $request->input('issuing_place');
|
||||||
|
$eidOccupation = $eidOccupation ?? $request->input('occupation');
|
||||||
|
$eidCardNumber = $eidCardNumber ?? $request->input('card_number');
|
||||||
|
$eidGender = $eidGender ?? $request->input('gender');
|
||||||
|
|
||||||
|
$nameToUse = $eidName ?? $request->name ?? $request->full_name;
|
||||||
|
|
||||||
|
// Validate Emirates ID immutability — once set, it cannot be changed
|
||||||
|
if ($eidNumber) {
|
||||||
|
if ($sponsor->emirates_id && $sponsor->emirates_id !== $eidNumber) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Emirates ID mismatch. You cannot change your registered Emirates ID number.',
|
||||||
|
'errors' => [
|
||||||
|
'id_number' => ['Emirates ID mismatch.']
|
||||||
|
]
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync with corresponding employer user and profile records if found
|
||||||
|
if ($matchingUser) {
|
||||||
|
$userData = [
|
||||||
|
'name' => $nameToUse,
|
||||||
|
'email' => $request->email,
|
||||||
|
];
|
||||||
|
if ($request->has('fcm_token')) {
|
||||||
|
$userData['fcm_token'] = $request->fcm_token;
|
||||||
|
}
|
||||||
|
$matchingUser->update($userData);
|
||||||
|
|
||||||
|
$profile = $matchingUser->employerProfile;
|
||||||
|
if (!$profile) {
|
||||||
|
$profile = new \App\Models\EmployerProfile(['user_id' => $matchingUser->id]);
|
||||||
|
}
|
||||||
|
$profile->address = $request->address;
|
||||||
|
$profile->phone = $request->mobile;
|
||||||
|
|
||||||
|
if ($eidNumber) { $profile->emirates_id_number = $eidNumber; }
|
||||||
|
if ($eidCardNumber) { $profile->emirates_id_card_number = $eidCardNumber; }
|
||||||
|
if ($eidName) { $profile->emirates_id_name = $eidName; }
|
||||||
|
if ($eidDob) { $profile->emirates_id_dob = $eidDob; }
|
||||||
|
if ($request->has('nationality')) { $profile->nationality = $request->nationality; }
|
||||||
|
if ($eidGender) { $profile->emirates_id_gender = $eidGender; }
|
||||||
|
if ($eidIssueDate) { $profile->emirates_id_issue_date = $eidIssueDate; }
|
||||||
|
if ($eidExpiry) { $profile->emirates_id_expiry = $eidExpiry; }
|
||||||
|
if ($eidOccupation) { $profile->emirates_id_occupation = $eidOccupation; }
|
||||||
|
if ($eidEmployer) { $profile->emirates_id_employer = $eidEmployer; }
|
||||||
|
if ($eidIssuePlace) { $profile->emirates_id_issue_place = $eidIssuePlace; }
|
||||||
|
|
||||||
|
$profile->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Sponsor profile
|
||||||
|
$sponsorData = [
|
||||||
|
'full_name' => $nameToUse,
|
||||||
|
'email' => $request->email,
|
||||||
|
'mobile' => $request->mobile,
|
||||||
|
'organization_name' => $request->organization_name,
|
||||||
|
'city' => $request->city,
|
||||||
|
'address' => $request->address,
|
||||||
|
'nationality' => $request->nationality,
|
||||||
|
'country_code' => $request->country_code,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($request->has('fcm_token')) {
|
||||||
|
$sponsorData['fcm_token'] = $request->fcm_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($eidNumber) { $sponsorData['emirates_id'] = $eidNumber; }
|
||||||
|
if ($eidCardNumber) { $sponsorData['emirates_id_card_number'] = $eidCardNumber; }
|
||||||
|
if ($eidName) { $sponsorData['emirates_id_name'] = $eidName; }
|
||||||
|
if ($eidDob) { $sponsorData['emirates_id_dob'] = $eidDob; }
|
||||||
|
if ($eidGender) { $sponsorData['emirates_id_gender'] = $eidGender; }
|
||||||
|
if ($eidIssueDate) { $sponsorData['emirates_id_issue_date'] = $eidIssueDate; }
|
||||||
|
if ($eidExpiry) { $sponsorData['emirates_id_expiry'] = $eidExpiry; }
|
||||||
|
if ($eidOccupation) { $sponsorData['emirates_id_occupation'] = $eidOccupation; }
|
||||||
|
if ($eidEmployer) { $sponsorData['emirates_id_employer'] = $eidEmployer; }
|
||||||
|
if ($eidIssuePlace) { $sponsorData['emirates_id_issue_place'] = $eidIssuePlace; }
|
||||||
|
|
||||||
|
// Parse and merge license data — from nested license object or flat fields
|
||||||
|
$currentLicenseData = is_array($sponsor->license_data) ? $sponsor->license_data : [];
|
||||||
|
$licenseInput = $request->input('license');
|
||||||
|
$newLicenseData = [];
|
||||||
|
|
||||||
|
if (is_array($licenseInput)) {
|
||||||
|
foreach ([
|
||||||
|
'document_type', 'country', 'authority', 'license_no', 'company_name',
|
||||||
|
'business_name', 'license_category', 'legal_type', 'issue_date', 'expiry_date',
|
||||||
|
'main_license_no', 'register_no', 'dcci_no', 'members'
|
||||||
|
] as $field) {
|
||||||
|
if (isset($licenseInput[$field])) {
|
||||||
|
$newLicenseData[$field] = $licenseInput[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flat field fallbacks (backward compatibility)
|
||||||
|
foreach ([
|
||||||
|
'document_type', 'country', 'authority', 'license_no', 'company_name',
|
||||||
|
'business_name', 'license_category', 'legal_type', 'issue_date', 'expiry_date',
|
||||||
|
'main_license_no', 'register_no', 'dcci_no', 'members'
|
||||||
|
] as $field) {
|
||||||
|
if (!isset($newLicenseData[$field]) && $request->has($field)) {
|
||||||
|
$newLicenseData[$field] = $request->input($field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($newLicenseData)) {
|
||||||
|
$sponsorData['license_data'] = array_merge($currentLicenseData, $newLicenseData);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sponsor->update($sponsorData);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Profile updated successfully.',
|
||||||
|
'data' => [
|
||||||
|
'sponsor' => $this->buildSponsorResponse($sponsor)
|
||||||
|
]
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
logger()->error('Mobile Sponsor Update Profile Failure: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'An error occurred while updating profile.',
|
||||||
|
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -261,14 +261,6 @@ public function setupProfile(Request $request)
|
|||||||
return $worker;
|
return $worker;
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($request->filled('fcm_token')) {
|
|
||||||
\App\Services\FCMService::sendPushNotification(
|
|
||||||
$request->fcm_token,
|
|
||||||
'Welcome to Migrant',
|
|
||||||
'Registration complete! Welcome to Migrant.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the OTP verification gate
|
// Clear the OTP verification gate
|
||||||
Cache::forget('worker_otp_verified_' . $identifier);
|
Cache::forget('worker_otp_verified_' . $identifier);
|
||||||
|
|
||||||
@ -338,7 +330,34 @@ public function register(Request $request)
|
|||||||
return $query->where('type', 'passport');
|
return $query->where('type', 'passport');
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
'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.',
|
'phone.unique' => 'This mobile number is already registered.',
|
||||||
'passport.passport_number.unique' => 'This passport number is already registered.',
|
'passport.passport_number.unique' => 'This passport number is already registered.',
|
||||||
@ -470,7 +489,7 @@ public function register(Request $request)
|
|||||||
'number' => $passportNum,
|
'number' => $passportNum,
|
||||||
'issue_date' => $issueDateFormatted,
|
'issue_date' => $issueDateFormatted,
|
||||||
'expiry_date' => $expiryDateFormatted,
|
'expiry_date' => $expiryDateFormatted,
|
||||||
'ocr_accuracy' => 99.0,
|
'ocr_accuracy' => isset($passportDataInput['ocr_accuracy']) ? (float)$passportDataInput['ocr_accuracy'] : 99.0,
|
||||||
'file_path' => null,
|
'file_path' => null,
|
||||||
'ocr_data' => $passportDataInput,
|
'ocr_data' => $passportDataInput,
|
||||||
]);
|
]);
|
||||||
@ -478,9 +497,10 @@ public function register(Request $request)
|
|||||||
|
|
||||||
// Create visa document if provided
|
// Create visa document if provided
|
||||||
if ($visaDataInput) {
|
if ($visaDataInput) {
|
||||||
$visaNum = $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999));
|
$cleanedVisaOcr = $this->cleanVisaData($visaDataInput);
|
||||||
$expiryDate = $visaDataInput['expiry_date'] ?? null;
|
$visaNum = $cleanedVisaOcr['entry_permit_no'] ?: ('V' . rand(1000000, 9999999));
|
||||||
$issueDate = $visaDataInput['issue_date'] ?? null;
|
$expiryDate = $cleanedVisaOcr['valid_until'] ?? null;
|
||||||
|
$issueDate = $cleanedVisaOcr['issue_date'] ?? null;
|
||||||
|
|
||||||
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(2)->toDateString();
|
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(2)->toDateString();
|
||||||
$issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : now()->subYears(2)->toDateString();
|
$issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : now()->subYears(2)->toDateString();
|
||||||
@ -490,23 +510,15 @@ public function register(Request $request)
|
|||||||
'number' => $visaNum,
|
'number' => $visaNum,
|
||||||
'issue_date' => $issueDateFormatted,
|
'issue_date' => $issueDateFormatted,
|
||||||
'expiry_date' => $expiryDateFormatted,
|
'expiry_date' => $expiryDateFormatted,
|
||||||
'ocr_accuracy' => 99.0,
|
'ocr_accuracy' => isset($visaDataInput['ocr_accuracy']) ? (float)$visaDataInput['ocr_accuracy'] : 99.0,
|
||||||
'file_path' => null,
|
'file_path' => null,
|
||||||
'ocr_data' => $visaDataInput,
|
'ocr_data' => $cleanedVisaOcr,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $worker;
|
return $worker;
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($request->filled('fcm_token')) {
|
|
||||||
\App\Services\FCMService::sendPushNotification(
|
|
||||||
$request->fcm_token,
|
|
||||||
'Welcome to Migrant',
|
|
||||||
'Worker registered successfully.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result->load(['skills', 'documents']);
|
$result->load(['skills', 'documents']);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@ -623,10 +635,238 @@ public function nationalities(Request $request)
|
|||||||
|
|
||||||
$rawNationalities = \App\Models\Nationality::all();
|
$rawNationalities = \App\Models\Nationality::all();
|
||||||
|
|
||||||
|
$callingCodes = [
|
||||||
|
'AF' => '+93',
|
||||||
|
'AL' => '+355',
|
||||||
|
'DZ' => '+213',
|
||||||
|
'AD' => '+376',
|
||||||
|
'AO' => '+244',
|
||||||
|
'AG' => '+1-268',
|
||||||
|
'AR' => '+54',
|
||||||
|
'AM' => '+374',
|
||||||
|
'AU' => '+61',
|
||||||
|
'AT' => '+43',
|
||||||
|
'AZ' => '+994',
|
||||||
|
'BS' => '+1-242',
|
||||||
|
'BH' => '+973',
|
||||||
|
'BD' => '+880',
|
||||||
|
'BB' => '+1-246',
|
||||||
|
'BY' => '+375',
|
||||||
|
'BE' => '+32',
|
||||||
|
'BZ' => '+501',
|
||||||
|
'BJ' => '+229',
|
||||||
|
'BT' => '+975',
|
||||||
|
'BO' => '+591',
|
||||||
|
'BA' => '+387',
|
||||||
|
'BW' => '+267',
|
||||||
|
'BR' => '+55',
|
||||||
|
'BN' => '+673',
|
||||||
|
'BG' => '+359',
|
||||||
|
'BF' => '+226',
|
||||||
|
'BI' => '+257',
|
||||||
|
'KH' => '+855',
|
||||||
|
'CM' => '+237',
|
||||||
|
'CA' => '+1',
|
||||||
|
'CV' => '+238',
|
||||||
|
'CF' => '+236',
|
||||||
|
'TD' => '+235',
|
||||||
|
'CL' => '+56',
|
||||||
|
'CN' => '+86',
|
||||||
|
'CO' => '+57',
|
||||||
|
'KM' => '+269',
|
||||||
|
'CG' => '+242',
|
||||||
|
'CD' => '+243',
|
||||||
|
'CR' => '+506',
|
||||||
|
'HR' => '+385',
|
||||||
|
'CU' => '+53',
|
||||||
|
'CY' => '+357',
|
||||||
|
'CZ' => '+420',
|
||||||
|
'DK' => '+45',
|
||||||
|
'DJ' => '+253',
|
||||||
|
'DM' => '+1-767',
|
||||||
|
'DO' => '+1-809',
|
||||||
|
'TL' => '+670',
|
||||||
|
'EC' => '+593',
|
||||||
|
'EG' => '+20',
|
||||||
|
'SV' => '+503',
|
||||||
|
'GQ' => '+240',
|
||||||
|
'ER' => '+291',
|
||||||
|
'EE' => '+372',
|
||||||
|
'ET' => '+251',
|
||||||
|
'FJ' => '+679',
|
||||||
|
'FI' => '+358',
|
||||||
|
'FR' => '+33',
|
||||||
|
'GA' => '+241',
|
||||||
|
'GM' => '+220',
|
||||||
|
'GE' => '+995',
|
||||||
|
'DE' => '+49',
|
||||||
|
'GH' => '+233',
|
||||||
|
'GR' => '+30',
|
||||||
|
'GD' => '+1-473',
|
||||||
|
'GT' => '+502',
|
||||||
|
'GN' => '+224',
|
||||||
|
'GW' => '+245',
|
||||||
|
'GY' => '+592',
|
||||||
|
'HT' => '+509',
|
||||||
|
'HN' => '+504',
|
||||||
|
'HU' => '+36',
|
||||||
|
'IS' => '+354',
|
||||||
|
'IN' => '+91',
|
||||||
|
'ID' => '+62',
|
||||||
|
'IR' => '+98',
|
||||||
|
'IQ' => '+964',
|
||||||
|
'IE' => '+353',
|
||||||
|
'IL' => '+972',
|
||||||
|
'IT' => '+39',
|
||||||
|
'JM' => '+1-876',
|
||||||
|
'JP' => '+81',
|
||||||
|
'JO' => '+962',
|
||||||
|
'KZ' => '+7',
|
||||||
|
'KE' => '+254',
|
||||||
|
'KI' => '+686',
|
||||||
|
'KP' => '+850',
|
||||||
|
'KR' => '+82',
|
||||||
|
'KW' => '+965',
|
||||||
|
'KG' => '+996',
|
||||||
|
'LA' => '+856',
|
||||||
|
'LV' => '+371',
|
||||||
|
'LB' => '+961',
|
||||||
|
'LS' => '+266',
|
||||||
|
'LR' => '+231',
|
||||||
|
'LY' => '+218',
|
||||||
|
'LI' => '+423',
|
||||||
|
'LT' => '+370',
|
||||||
|
'LU' => '+352',
|
||||||
|
'MK' => '+389',
|
||||||
|
'MG' => '+261',
|
||||||
|
'MW' => '+265',
|
||||||
|
'MY' => '+60',
|
||||||
|
'MV' => '+960',
|
||||||
|
'ML' => '+223',
|
||||||
|
'MT' => '+356',
|
||||||
|
'MH' => '+692',
|
||||||
|
'MR' => '+222',
|
||||||
|
'MU' => '+230',
|
||||||
|
'MX' => '+52',
|
||||||
|
'FM' => '+691',
|
||||||
|
'MD' => '+373',
|
||||||
|
'MC' => '+377',
|
||||||
|
'MN' => '+976',
|
||||||
|
'ME' => '+382',
|
||||||
|
'MA' => '+212',
|
||||||
|
'MZ' => '+258',
|
||||||
|
'MM' => '+95',
|
||||||
|
'NA' => '+264',
|
||||||
|
'NR' => '+674',
|
||||||
|
'NP' => '+977',
|
||||||
|
'NL' => '+31',
|
||||||
|
'NZ' => '+64',
|
||||||
|
'NI' => '+505',
|
||||||
|
'NE' => '+227',
|
||||||
|
'NG' => '+234',
|
||||||
|
'NO' => '+47',
|
||||||
|
'OM' => '+968',
|
||||||
|
'PK' => '+92',
|
||||||
|
'PW' => '+680',
|
||||||
|
'PA' => '+507',
|
||||||
|
'PG' => '+675',
|
||||||
|
'PY' => '+595',
|
||||||
|
'PE' => '+51',
|
||||||
|
'PH' => '+63',
|
||||||
|
'PL' => '+48',
|
||||||
|
'PT' => '+351',
|
||||||
|
'QA' => '+974',
|
||||||
|
'RO' => '+40',
|
||||||
|
'RU' => '+7',
|
||||||
|
'RW' => '+250',
|
||||||
|
'KN' => '+1-869',
|
||||||
|
'LC' => '+1-758',
|
||||||
|
'VC' => '+1-784',
|
||||||
|
'WS' => '+685',
|
||||||
|
'SM' => '+378',
|
||||||
|
'ST' => '+239',
|
||||||
|
'SA' => '+966',
|
||||||
|
'SN' => '+221',
|
||||||
|
'RS' => '+381',
|
||||||
|
'SC' => '+248',
|
||||||
|
'SL' => '+232',
|
||||||
|
'SG' => '+65',
|
||||||
|
'SK' => '+421',
|
||||||
|
'SI' => '+386',
|
||||||
|
'SB' => '+677',
|
||||||
|
'SO' => '+252',
|
||||||
|
'ZA' => '+27',
|
||||||
|
'SS' => '+211',
|
||||||
|
'ES' => '+34',
|
||||||
|
'LK' => '+94',
|
||||||
|
'SD' => '+249',
|
||||||
|
'SR' => '+597',
|
||||||
|
'SZ' => '+268',
|
||||||
|
'SE' => '+46',
|
||||||
|
'CH' => '+41',
|
||||||
|
'SY' => '+963',
|
||||||
|
'TJ' => '+992',
|
||||||
|
'TZ' => '+255',
|
||||||
|
'TH' => '+66',
|
||||||
|
'TG' => '+228',
|
||||||
|
'TO' => '+676',
|
||||||
|
'TT' => '+1-868',
|
||||||
|
'TN' => '+216',
|
||||||
|
'TR' => '+90',
|
||||||
|
'TM' => '+993',
|
||||||
|
'TV' => '+688',
|
||||||
|
'UG' => '+256',
|
||||||
|
'UA' => '+380',
|
||||||
|
'AE' => '+971',
|
||||||
|
'GB' => '+44',
|
||||||
|
'US' => '+1',
|
||||||
|
'UY' => '+598',
|
||||||
|
'UZ' => '+998',
|
||||||
|
'VU' => '+678',
|
||||||
|
'VE' => '+58',
|
||||||
|
'VN' => '+84',
|
||||||
|
'YE' => '+967',
|
||||||
|
'ZM' => '+260',
|
||||||
|
'ZW' => '+263',
|
||||||
|
'AI' => '+1-264',
|
||||||
|
'VG' => '+1-284',
|
||||||
|
'BM' => '+1-441',
|
||||||
|
'KY' => '+1-345',
|
||||||
|
'CK' => '+682',
|
||||||
|
'CY-W' => '+44',
|
||||||
|
'CY-M' => '+44',
|
||||||
|
'FO' => '+298',
|
||||||
|
'GI' => '+350',
|
||||||
|
'GL' => '+299',
|
||||||
|
'GU' => '+1-671',
|
||||||
|
'HK' => '+852',
|
||||||
|
'XK' => '+383',
|
||||||
|
'MO' => '+853',
|
||||||
|
'MQ' => '+596',
|
||||||
|
'MS' => '+1-664',
|
||||||
|
'NU' => '+683',
|
||||||
|
'GB-NIR' => '+44',
|
||||||
|
'PS' => '+970',
|
||||||
|
'PN' => '+64',
|
||||||
|
'PR' => '+1-787',
|
||||||
|
'GB-SCT' => '+44',
|
||||||
|
'SH' => '+290',
|
||||||
|
'ST-L' => '',
|
||||||
|
'TA-T' => '+290',
|
||||||
|
'TC' => '+1-649',
|
||||||
|
'VA' => '+379',
|
||||||
|
'WF' => '+681',
|
||||||
|
'GB-WLS' => '+44',
|
||||||
|
];
|
||||||
|
|
||||||
$list = [];
|
$list = [];
|
||||||
foreach ($rawNationalities as $item) {
|
foreach ($rawNationalities as $item) {
|
||||||
$list[] = [
|
$list[] = [
|
||||||
'code' => $item->code,
|
'code' => $item->code,
|
||||||
|
'country_code' => $item->code,
|
||||||
|
'phone_code' => $callingCodes[$item->code] ?? '',
|
||||||
|
'dial_code' => $callingCodes[$item->code] ?? '',
|
||||||
|
'calling_code' => $callingCodes[$item->code] ?? '',
|
||||||
'name' => $item->{$lang} ?? $item->name,
|
'name' => $item->{$lang} ?? $item->name,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -997,6 +1237,62 @@ private function normaliseDateForController(?string $raw): ?string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standardize and clean Visa OCR data structure to exactly the 15 required fields.
|
||||||
|
*/
|
||||||
|
private function cleanVisaData(array $visaDataInput): array
|
||||||
|
{
|
||||||
|
$rawVisaType = $visaDataInput['visa_type'] ?? null;
|
||||||
|
$rawEntryPermitNo = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? $visaDataInput['number'] ?? null;
|
||||||
|
$rawIssueDate = $visaDataInput['issue_date'] ?? null;
|
||||||
|
$rawValidUntil = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
|
||||||
|
$rawUidNo = $visaDataInput['uid_no'] ?? $visaDataInput['id_number'] ?? null;
|
||||||
|
$rawFullName = $visaDataInput['full_name'] ?? $visaDataInput['name'] ?? null;
|
||||||
|
$rawNationality = $visaDataInput['nationality'] ?? null;
|
||||||
|
$rawPlaceOfBirth = $visaDataInput['place_of_birth'] ?? null;
|
||||||
|
$rawDateOfBirth = $visaDataInput['date_of_birth'] ?? $visaDataInput['dob'] ?? null;
|
||||||
|
$rawPassportNo = $visaDataInput['passport_no'] ?? $visaDataInput['passport_number'] ?? null;
|
||||||
|
$rawProfession = $visaDataInput['profession'] ?? null;
|
||||||
|
$rawSponsorName = $visaDataInput['sponsor_name'] ?? null;
|
||||||
|
|
||||||
|
// Clean sponsor
|
||||||
|
$sponsorData = $visaDataInput['sponsor'] ?? [];
|
||||||
|
if (is_string($sponsorData)) {
|
||||||
|
$sponsorNameFromObj = $sponsorData;
|
||||||
|
$sponsorAddress = '';
|
||||||
|
$sponsorPhone = '';
|
||||||
|
} else {
|
||||||
|
$sponsorNameFromObj = $sponsorData['name'] ?? $sponsorData['sponsor_name'] ?? $sponsorData['full_name'] ?? '';
|
||||||
|
$sponsorAddress = $sponsorData['address'] ?? '';
|
||||||
|
$sponsorPhone = $sponsorData['phone'] ?? $sponsorData['mobile'] ?? $sponsorData['mobile_number'] ?? $sponsorData['phone_number'] ?? '';
|
||||||
|
}
|
||||||
|
if (empty($rawSponsorName)) {
|
||||||
|
$rawSponsorName = $sponsorNameFromObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'document_type' => 'uae_visa',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'visa_type' => $rawVisaType ?: 'ENTRY PERMIT',
|
||||||
|
'entry_permit_no' => $rawEntryPermitNo ?: '',
|
||||||
|
'issue_date' => $rawIssueDate ?: '',
|
||||||
|
'valid_until' => $rawValidUntil ?: '',
|
||||||
|
'uid_no' => $rawUidNo ?: '',
|
||||||
|
'full_name' => $rawFullName ?: '',
|
||||||
|
'nationality' => $rawNationality ?: '',
|
||||||
|
'place_of_birth' => $rawPlaceOfBirth ?: '',
|
||||||
|
'date_of_birth' => $rawDateOfBirth ?: '',
|
||||||
|
'passport_no' => $rawPassportNo ?: '',
|
||||||
|
'profession' => $rawProfession ?: '',
|
||||||
|
'sponsor_name' => $rawSponsorName ?: '',
|
||||||
|
'sponsor' => [
|
||||||
|
'name' => $sponsorNameFromObj ?: '',
|
||||||
|
'address' => $sponsorAddress ?: '',
|
||||||
|
'phone' => $sponsorPhone ?: '',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Forgot / Reset Password (via Mobile Number)
|
// Forgot / Reset Password (via Mobile Number)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@ -1022,12 +1318,11 @@ public function forgotPassword(Request $request)
|
|||||||
|
|
||||||
$worker = Worker::where('phone', $request->phone)->first();
|
$worker = Worker::where('phone', $request->phone)->first();
|
||||||
|
|
||||||
// Always return success to prevent phone enumeration
|
|
||||||
if (!$worker) {
|
if (!$worker) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => false,
|
||||||
'message' => 'If an account with that mobile number exists, a reset OTP has been sent.',
|
'message' => 'user not found this mobile number',
|
||||||
]);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$otp = (string) mt_rand(100000, 999999);
|
$otp = (string) mt_rand(100000, 999999);
|
||||||
|
|||||||
@ -201,14 +201,15 @@ public function updateProfile(Request $request)
|
|||||||
$visaDataInput = $request->input('visa');
|
$visaDataInput = $request->input('visa');
|
||||||
if ($visaDataInput) {
|
if ($visaDataInput) {
|
||||||
$existingVisa = $worker->documents()->where('type', 'visa')->first();
|
$existingVisa = $worker->documents()->where('type', 'visa')->first();
|
||||||
$expiryDate = $visaDataInput['expiry_date'] ?? null;
|
$cleanedVisaOcr = $this->cleanVisaData($visaDataInput);
|
||||||
$issueDate = $visaDataInput['issue_date'] ?? null;
|
$expiryDate = $cleanedVisaOcr['valid_until'] ?? null;
|
||||||
|
$issueDate = $cleanedVisaOcr['issue_date'] ?? null;
|
||||||
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : null;
|
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : null;
|
||||||
$issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : null;
|
$issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : null;
|
||||||
|
|
||||||
if ($existingVisa) {
|
if ($existingVisa) {
|
||||||
$updateFields = [
|
$updateFields = [
|
||||||
'ocr_data' => $visaDataInput,
|
'ocr_data' => $cleanedVisaOcr,
|
||||||
];
|
];
|
||||||
if ($expiryDateFormatted) {
|
if ($expiryDateFormatted) {
|
||||||
$updateFields['expiry_date'] = $expiryDateFormatted;
|
$updateFields['expiry_date'] = $expiryDateFormatted;
|
||||||
@ -216,9 +217,12 @@ public function updateProfile(Request $request)
|
|||||||
if ($issueDateFormatted) {
|
if ($issueDateFormatted) {
|
||||||
$updateFields['issue_date'] = $issueDateFormatted;
|
$updateFields['issue_date'] = $issueDateFormatted;
|
||||||
}
|
}
|
||||||
|
if (!empty($cleanedVisaOcr['entry_permit_no'])) {
|
||||||
|
$updateFields['number'] = $cleanedVisaOcr['entry_permit_no'];
|
||||||
|
}
|
||||||
$existingVisa->update($updateFields);
|
$existingVisa->update($updateFields);
|
||||||
} else {
|
} else {
|
||||||
$visaNum = $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999));
|
$visaNum = $cleanedVisaOcr['entry_permit_no'] ?: ('V' . rand(1000000, 9999999));
|
||||||
$expiryDateFormatted = $expiryDateFormatted ?: now()->addYears(2)->toDateString();
|
$expiryDateFormatted = $expiryDateFormatted ?: now()->addYears(2)->toDateString();
|
||||||
$issueDateFormatted = $issueDateFormatted ?: now()->subYears(2)->toDateString();
|
$issueDateFormatted = $issueDateFormatted ?: now()->subYears(2)->toDateString();
|
||||||
|
|
||||||
@ -227,9 +231,9 @@ public function updateProfile(Request $request)
|
|||||||
'number' => $visaNum,
|
'number' => $visaNum,
|
||||||
'issue_date' => $issueDateFormatted,
|
'issue_date' => $issueDateFormatted,
|
||||||
'expiry_date' => $expiryDateFormatted,
|
'expiry_date' => $expiryDateFormatted,
|
||||||
'ocr_accuracy' => 99.0,
|
'ocr_accuracy' => isset($visaDataInput['ocr_accuracy']) ? (float)$visaDataInput['ocr_accuracy'] : 99.0,
|
||||||
'file_path' => null,
|
'file_path' => null,
|
||||||
'ocr_data' => $visaDataInput,
|
'ocr_data' => $cleanedVisaOcr,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -669,6 +673,61 @@ public function getDashboard(Request $request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change worker's password.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function changePassword(Request $request)
|
||||||
|
{
|
||||||
|
/** @var Worker $worker */
|
||||||
|
$worker = $request->attributes->get('worker');
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'current_password' => 'required|string',
|
||||||
|
'new_password' => 'required|string|min:8|confirmed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validation error.',
|
||||||
|
'errors' => $validator->errors()
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\Illuminate\Support\Facades\Hash::check($request->current_password, $worker->password)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Validation error.',
|
||||||
|
'errors' => [
|
||||||
|
'current_password' => ['The current password is incorrect.']
|
||||||
|
]
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$worker->update([
|
||||||
|
'password' => \Illuminate\Support\Facades\Hash::make($request->new_password)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Password changed successfully.'
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
logger()->error('Mobile Worker Change Password Failure: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'An error occurred while changing password.',
|
||||||
|
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* E.g. "14/05/1990" -> "1990-05-14"
|
* E.g. "14/05/1990" -> "1990-05-14"
|
||||||
*/
|
*/
|
||||||
@ -699,4 +758,60 @@ private function normaliseDateForController(?string $raw): ?string
|
|||||||
return $raw;
|
return $raw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standardize and clean Visa OCR data structure to exactly the 15 required fields.
|
||||||
|
*/
|
||||||
|
private function cleanVisaData(array $visaDataInput): array
|
||||||
|
{
|
||||||
|
$rawVisaType = $visaDataInput['visa_type'] ?? null;
|
||||||
|
$rawEntryPermitNo = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? $visaDataInput['number'] ?? null;
|
||||||
|
$rawIssueDate = $visaDataInput['issue_date'] ?? null;
|
||||||
|
$rawValidUntil = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
|
||||||
|
$rawUidNo = $visaDataInput['uid_no'] ?? $visaDataInput['id_number'] ?? null;
|
||||||
|
$rawFullName = $visaDataInput['full_name'] ?? $visaDataInput['name'] ?? null;
|
||||||
|
$rawNationality = $visaDataInput['nationality'] ?? null;
|
||||||
|
$rawPlaceOfBirth = $visaDataInput['place_of_birth'] ?? null;
|
||||||
|
$rawDateOfBirth = $visaDataInput['date_of_birth'] ?? $visaDataInput['dob'] ?? null;
|
||||||
|
$rawPassportNo = $visaDataInput['passport_no'] ?? $visaDataInput['passport_number'] ?? null;
|
||||||
|
$rawProfession = $visaDataInput['profession'] ?? null;
|
||||||
|
$rawSponsorName = $visaDataInput['sponsor_name'] ?? null;
|
||||||
|
|
||||||
|
// Clean sponsor
|
||||||
|
$sponsorData = $visaDataInput['sponsor'] ?? [];
|
||||||
|
if (is_string($sponsorData)) {
|
||||||
|
$sponsorNameFromObj = $sponsorData;
|
||||||
|
$sponsorAddress = '';
|
||||||
|
$sponsorPhone = '';
|
||||||
|
} else {
|
||||||
|
$sponsorNameFromObj = $sponsorData['name'] ?? $sponsorData['sponsor_name'] ?? $sponsorData['full_name'] ?? '';
|
||||||
|
$sponsorAddress = $sponsorData['address'] ?? '';
|
||||||
|
$sponsorPhone = $sponsorData['phone'] ?? $sponsorData['mobile'] ?? $sponsorData['mobile_number'] ?? $sponsorData['phone_number'] ?? '';
|
||||||
|
}
|
||||||
|
if (empty($rawSponsorName)) {
|
||||||
|
$rawSponsorName = $sponsorNameFromObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'document_type' => 'uae_visa',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'visa_type' => $rawVisaType ?: 'ENTRY PERMIT',
|
||||||
|
'entry_permit_no' => $rawEntryPermitNo ?: '',
|
||||||
|
'issue_date' => $rawIssueDate ?: '',
|
||||||
|
'valid_until' => $rawValidUntil ?: '',
|
||||||
|
'uid_no' => $rawUidNo ?: '',
|
||||||
|
'full_name' => $rawFullName ?: '',
|
||||||
|
'nationality' => $rawNationality ?: '',
|
||||||
|
'place_of_birth' => $rawPlaceOfBirth ?: '',
|
||||||
|
'date_of_birth' => $rawDateOfBirth ?: '',
|
||||||
|
'passport_no' => $rawPassportNo ?: '',
|
||||||
|
'profession' => $rawProfession ?: '',
|
||||||
|
'sponsor_name' => $rawSponsorName ?: '',
|
||||||
|
'sponsor' => [
|
||||||
|
'name' => $sponsorNameFromObj ?: '',
|
||||||
|
'address' => $sponsorAddress ?: '',
|
||||||
|
'phone' => $sponsorPhone ?: '',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -290,17 +290,35 @@ public function uploadEmiratesId(Request $request)
|
|||||||
|
|
||||||
$extractedIdNumber = null;
|
$extractedIdNumber = null;
|
||||||
$extractedExpiry = null;
|
$extractedExpiry = null;
|
||||||
|
$extractedName = null;
|
||||||
|
$extractedDob = null;
|
||||||
|
$extractedNationality = null;
|
||||||
|
$extractedIssueDate = null;
|
||||||
|
$extractedEmployer = null;
|
||||||
|
$extractedIssuePlace = null;
|
||||||
|
$extractedOccupation = null;
|
||||||
|
$extractedCardNumber = null;
|
||||||
|
$extractedGender = null;
|
||||||
|
|
||||||
if ($request->hasFile('emirates_id_front')) {
|
if ($request->hasFile('emirates_id_front')) {
|
||||||
$frontFile = $request->file('emirates_id_front');
|
$frontFile = $request->file('emirates_id_front');
|
||||||
$frontOcr = \App\Services\OcrDocumentService::extractEmiratesIdFrontData($frontFile);
|
$frontOcr = \App\Services\OcrDocumentService::extractEmiratesIdFrontData($frontFile);
|
||||||
$extractedIdNumber = $frontOcr['emirates_id_number'];
|
$extractedIdNumber = $frontOcr['emirates_id_number'];
|
||||||
|
$extractedName = $frontOcr['name'] ?? null;
|
||||||
|
$extractedDob = $frontOcr['date_of_birth'] ?? null;
|
||||||
|
$extractedNationality = $frontOcr['nationality'] ?? null;
|
||||||
|
$extractedCardNumber = $frontOcr['card_number'] ?? null;
|
||||||
|
$extractedGender = $frontOcr['gender'] ?? null;
|
||||||
|
$extractedOccupation = $frontOcr['occupation'] ?? null;
|
||||||
|
$extractedEmployer = $frontOcr['employer'] ?? null;
|
||||||
|
$extractedIssuePlace = $frontOcr['issue_place'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->hasFile('emirates_id_back')) {
|
if ($request->hasFile('emirates_id_back')) {
|
||||||
$backFile = $request->file('emirates_id_back');
|
$backFile = $request->file('emirates_id_back');
|
||||||
$backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($backFile);
|
$backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($backFile);
|
||||||
$extractedExpiry = $backOcr['expiry_date'];
|
$extractedExpiry = $backOcr['expiry_date'];
|
||||||
|
$extractedIssueDate = $backOcr['issue_date'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->hasFile('emirates_id_file')) {
|
if ($request->hasFile('emirates_id_file')) {
|
||||||
@ -309,12 +327,46 @@ public function uploadEmiratesId(Request $request)
|
|||||||
$backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($file);
|
$backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($file);
|
||||||
$extractedIdNumber = $frontOcr['emirates_id_number'];
|
$extractedIdNumber = $frontOcr['emirates_id_number'];
|
||||||
$extractedExpiry = $backOcr['expiry_date'];
|
$extractedExpiry = $backOcr['expiry_date'];
|
||||||
|
$extractedName = $frontOcr['name'] ?? null;
|
||||||
|
$extractedDob = $frontOcr['date_of_birth'] ?? null;
|
||||||
|
$extractedNationality = $frontOcr['nationality'] ?? null;
|
||||||
|
$extractedCardNumber = $frontOcr['card_number'] ?? null;
|
||||||
|
$extractedGender = $frontOcr['gender'] ?? null;
|
||||||
|
$extractedOccupation = $frontOcr['occupation'] ?? null;
|
||||||
|
$extractedEmployer = $frontOcr['employer'] ?? null;
|
||||||
|
$extractedIssuePlace = $frontOcr['issue_place'] ?? null;
|
||||||
|
$extractedIssueDate = $backOcr['issue_date'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($request->hasFile('emirates_id_front') || $request->hasFile('emirates_id_file')) && empty($extractedName)) {
|
||||||
|
return response()->json([
|
||||||
|
'errors' => [
|
||||||
|
'emirates_id_front' => ['Failed to extract name from the Emirates ID. Please upload a clear image.']
|
||||||
|
]
|
||||||
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
$pending = session('pending_employer_registration');
|
$pending = session('pending_employer_registration');
|
||||||
$pending['emirates_id_file'] = null;
|
$pending['emirates_id_file'] = null;
|
||||||
$pending['emirates_id_number'] = $extractedIdNumber;
|
$pending['emirates_id_number'] = $extractedIdNumber;
|
||||||
$pending['emirates_id_expiry'] = $extractedExpiry;
|
$pending['emirates_id_expiry'] = $extractedExpiry;
|
||||||
|
$pending['emirates_id_name'] = $extractedName;
|
||||||
|
$pending['emirates_id_dob'] = $extractedDob;
|
||||||
|
$pending['emirates_id_nationality'] = $extractedNationality;
|
||||||
|
$pending['emirates_id_card_number'] = $extractedCardNumber;
|
||||||
|
$pending['emirates_id_gender'] = $extractedGender;
|
||||||
|
$pending['emirates_id_occupation'] = $extractedOccupation;
|
||||||
|
$pending['emirates_id_employer'] = $extractedEmployer;
|
||||||
|
$pending['emirates_id_issue_place'] = $extractedIssuePlace;
|
||||||
|
$pending['emirates_id_issue_date'] = $extractedIssueDate;
|
||||||
|
|
||||||
|
if (!empty($extractedName)) {
|
||||||
|
if (isset($pending['company_name']) && (empty($pending['company_name']) || $pending['company_name'] === ($pending['name'] ?? '') . ' Household')) {
|
||||||
|
$pending['company_name'] = $extractedName . ' Household';
|
||||||
|
}
|
||||||
|
$pending['name'] = $extractedName;
|
||||||
|
}
|
||||||
|
|
||||||
session(['pending_employer_registration' => $pending]);
|
session(['pending_employer_registration' => $pending]);
|
||||||
session(['employer_emirates_id_uploaded' => true]);
|
session(['employer_emirates_id_uploaded' => true]);
|
||||||
|
|
||||||
@ -438,6 +490,15 @@ public function createPassword(Request $request)
|
|||||||
'emirates_id_front' => null, // We did not store the file
|
'emirates_id_front' => null, // We did not store the file
|
||||||
'emirates_id_number' => $pending['emirates_id_number'] ?? null,
|
'emirates_id_number' => $pending['emirates_id_number'] ?? null,
|
||||||
'emirates_id_expiry' => $pending['emirates_id_expiry'] ?? null,
|
'emirates_id_expiry' => $pending['emirates_id_expiry'] ?? null,
|
||||||
|
'emirates_id_name' => $pending['emirates_id_name'] ?? null,
|
||||||
|
'emirates_id_dob' => $pending['emirates_id_dob'] ?? null,
|
||||||
|
'emirates_id_issue_date' => $pending['emirates_id_issue_date'] ?? null,
|
||||||
|
'emirates_id_employer' => $pending['emirates_id_employer'] ?? null,
|
||||||
|
'emirates_id_issue_place' => $pending['emirates_id_issue_place'] ?? null,
|
||||||
|
'emirates_id_occupation' => $pending['emirates_id_occupation'] ?? null,
|
||||||
|
'emirates_id_card_number' => $pending['emirates_id_card_number'] ?? null,
|
||||||
|
'emirates_id_gender' => $pending['emirates_id_gender'] ?? null,
|
||||||
|
'nationality' => $pending['emirates_id_nationality'] ?? null,
|
||||||
'address' => $pending['address'] ?? null,
|
'address' => $pending['address'] ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -459,6 +520,17 @@ public function createPassword(Request $request)
|
|||||||
'last_login_at' => now(),
|
'last_login_at' => now(),
|
||||||
'emirates_id_file' => null, // We did not store the file
|
'emirates_id_file' => null, // We did not store the file
|
||||||
'address' => $pending['address'] ?? null,
|
'address' => $pending['address'] ?? null,
|
||||||
|
'emirates_id' => $pending['emirates_id_number'] ?? null,
|
||||||
|
'emirates_id_name' => $pending['emirates_id_name'] ?? null,
|
||||||
|
'emirates_id_dob' => $pending['emirates_id_dob'] ?? null,
|
||||||
|
'emirates_id_issue_date' => $pending['emirates_id_issue_date'] ?? null,
|
||||||
|
'emirates_id_expiry' => $pending['emirates_id_expiry'] ?? null,
|
||||||
|
'emirates_id_employer' => $pending['emirates_id_employer'] ?? null,
|
||||||
|
'emirates_id_issue_place' => $pending['emirates_id_issue_place'] ?? null,
|
||||||
|
'emirates_id_occupation' => $pending['emirates_id_occupation'] ?? null,
|
||||||
|
'emirates_id_card_number' => $pending['emirates_id_card_number'] ?? null,
|
||||||
|
'emirates_id_gender' => $pending['emirates_id_gender'] ?? null,
|
||||||
|
'nationality' => $pending['emirates_id_nationality'] ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Create active subscription in database
|
// Create active subscription in database
|
||||||
|
|||||||
@ -42,15 +42,17 @@ private function buildProfileData($user, $profile)
|
|||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
'phone' => $profile->phone ?? '+971509990001',
|
'phone' => $profile->phone ?? '+971509990001',
|
||||||
'emirates_id' => [
|
'emirates_id' => [
|
||||||
'emirates_id_number' => $profile->emirates_id_number ?? '784-1988-5310327-2',
|
'emirates_id_number' => $profile->emirates_id_number ?? '784-1987-5493842-5',
|
||||||
'name' => $profile->emirates_id_name ?? $user->name,
|
'name' => $profile->emirates_id_name ?? $user->name,
|
||||||
'date_of_birth' => $profile->emirates_id_dob ?? '1990-01-01',
|
'date_of_birth' => $profile->emirates_id_dob ?? '1987-04-14',
|
||||||
'nationality' => $profile->nationality ?? 'Emirati',
|
'nationality' => $profile->nationality ?? 'Bangladesh',
|
||||||
'issue_date' => $profile->emirates_id_issue_date ?? '2023-04-11',
|
'issue_date' => $profile->emirates_id_issue_date ?? '2025-12-12',
|
||||||
'expiry_date' => $profile->emirates_id_expiry ?? '2028-04-11',
|
'expiry_date' => $profile->emirates_id_expiry ?? '2027-12-11',
|
||||||
'employer' => $profile->emirates_id_employer ?? 'Federal Authority',
|
'employer' => $profile->emirates_id_employer ?? 'Msj International Technical Services L.L.C UAE',
|
||||||
'issue_place' => $profile->emirates_id_issue_place ?? 'Abu Dhabi',
|
'issue_place' => $profile->emirates_id_issue_place ?? 'Dubai',
|
||||||
'occupation' => $profile->emirates_id_occupation ?? 'Manager',
|
'occupation' => $profile->emirates_id_occupation ?? 'Electrician',
|
||||||
|
'card_number' => $profile->emirates_id_card_number ?? '151023946',
|
||||||
|
'gender' => $profile->emirates_id_gender ?? 'M',
|
||||||
],
|
],
|
||||||
'fcm_token' => $user->fcm_token ?? 'fcm_token_example',
|
'fcm_token' => $user->fcm_token ?? 'fcm_token_example',
|
||||||
'address' => $profile->address ?? 'Villa 45, Street 12',
|
'address' => $profile->address ?? 'Villa 45, Street 12',
|
||||||
@ -74,15 +76,17 @@ public function index(Request $request)
|
|||||||
$profile = EmployerProfile::create([
|
$profile = EmployerProfile::create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'phone' => '+971509990001',
|
'phone' => '+971509990001',
|
||||||
'emirates_id_number' => '784-1988-5310327-2',
|
'emirates_id_number' => '784-1987-5493842-5',
|
||||||
'emirates_id_name' => 'Ahmad Bin Ahmed',
|
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
'emirates_id_dob' => '1990-01-01',
|
'emirates_id_dob' => '1987-04-14',
|
||||||
'nationality' => 'Emirati',
|
'nationality' => 'Bangladesh',
|
||||||
'emirates_id_issue_date' => '2023-04-11',
|
'emirates_id_issue_date' => '2025-12-12',
|
||||||
'emirates_id_expiry' => '2028-04-11',
|
'emirates_id_expiry' => '2027-12-11',
|
||||||
'emirates_id_employer' => 'Federal Authority',
|
'emirates_id_employer' => 'Msj International Technical Services L.L.C UAE',
|
||||||
'emirates_id_issue_place' => 'Abu Dhabi',
|
'emirates_id_issue_place' => 'Dubai',
|
||||||
'emirates_id_occupation' => 'Manager',
|
'emirates_id_occupation' => 'Electrician',
|
||||||
|
'emirates_id_card_number' => '151023946',
|
||||||
|
'emirates_id_gender' => 'M',
|
||||||
'address' => 'Villa 45, Street 12',
|
'address' => 'Villa 45, Street 12',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -106,15 +110,17 @@ public function edit(Request $request)
|
|||||||
$profile = EmployerProfile::create([
|
$profile = EmployerProfile::create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'phone' => '+971509990001',
|
'phone' => '+971509990001',
|
||||||
'emirates_id_number' => '784-1988-5310327-2',
|
'emirates_id_number' => '784-1987-5493842-5',
|
||||||
'emirates_id_name' => 'Ahmad Bin Ahmed',
|
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
'emirates_id_dob' => '1990-01-01',
|
'emirates_id_dob' => '1987-04-14',
|
||||||
'nationality' => 'Emirati',
|
'nationality' => 'Bangladesh',
|
||||||
'emirates_id_issue_date' => '2023-04-11',
|
'emirates_id_issue_date' => '2025-12-12',
|
||||||
'emirates_id_expiry' => '2028-04-11',
|
'emirates_id_expiry' => '2027-12-11',
|
||||||
'emirates_id_employer' => 'Federal Authority',
|
'emirates_id_employer' => 'Msj International Technical Services L.L.C UAE',
|
||||||
'emirates_id_issue_place' => 'Abu Dhabi',
|
'emirates_id_issue_place' => 'Dubai',
|
||||||
'emirates_id_occupation' => 'Manager',
|
'emirates_id_occupation' => 'Electrician',
|
||||||
|
'emirates_id_card_number' => '151023946',
|
||||||
|
'emirates_id_gender' => 'M',
|
||||||
'address' => 'Villa 45, Street 12',
|
'address' => 'Villa 45, Street 12',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -164,6 +170,62 @@ public function update(Request $request)
|
|||||||
|
|
||||||
$oldEmail = $user->getOriginal('email') ?? $user->email;
|
$oldEmail = $user->getOriginal('email') ?? $user->email;
|
||||||
|
|
||||||
|
// Update EmployerProfile
|
||||||
|
$profile = $user->employerProfile;
|
||||||
|
if (!$profile) {
|
||||||
|
$profile = new EmployerProfile(['user_id' => $user->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle physical uploads/OCR for Emirates ID first to allow name autofill
|
||||||
|
$uploaded = false;
|
||||||
|
$extractedIdNumber = null;
|
||||||
|
$extractedExpiry = null;
|
||||||
|
$extractedName = null;
|
||||||
|
$extractedDob = null;
|
||||||
|
$extractedNationality = null;
|
||||||
|
$extractedCardNumber = null;
|
||||||
|
$extractedGender = null;
|
||||||
|
$extractedOccupation = null;
|
||||||
|
$extractedEmployer = null;
|
||||||
|
$extractedIssuePlace = null;
|
||||||
|
$extractedIssueDate = null;
|
||||||
|
|
||||||
|
if ($request->hasFile('emirates_id_front')) {
|
||||||
|
$file = $request->file('emirates_id_front');
|
||||||
|
$ocrFront = \App\Services\OcrDocumentService::extractEmiratesIdFrontData($file);
|
||||||
|
$extractedIdNumber = $ocrFront['emirates_id_number'] ?? null;
|
||||||
|
$extractedName = $ocrFront['name'] ?? null;
|
||||||
|
$extractedDob = $ocrFront['date_of_birth'] ?? null;
|
||||||
|
$extractedNationality = $ocrFront['nationality'] ?? null;
|
||||||
|
$extractedCardNumber = $ocrFront['card_number'] ?? null;
|
||||||
|
$extractedGender = $ocrFront['gender'] ?? null;
|
||||||
|
$extractedOccupation = $ocrFront['occupation'] ?? null;
|
||||||
|
$extractedEmployer = $ocrFront['employer'] ?? null;
|
||||||
|
$extractedIssuePlace = $ocrFront['issue_place'] ?? null;
|
||||||
|
$profile->emirates_id_front = '[DELETED_FOR_PDPL_COMPLIANCE]';
|
||||||
|
$uploaded = true;
|
||||||
|
}
|
||||||
|
if ($request->hasFile('emirates_id_back')) {
|
||||||
|
$file = $request->file('emirates_id_back');
|
||||||
|
$ocrBack = \App\Services\OcrDocumentService::extractEmiratesIdBackData($file);
|
||||||
|
$extractedExpiry = $ocrBack['expiry_date'] ?? null;
|
||||||
|
$extractedIssueDate = $ocrBack['issue_date'] ?? null;
|
||||||
|
$profile->emirates_id_back = '[DELETED_FOR_PDPL_COMPLIANCE]';
|
||||||
|
$uploaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($uploaded) {
|
||||||
|
// Validate that name was extracted
|
||||||
|
if ($request->hasFile('emirates_id_front') && empty($extractedName)) {
|
||||||
|
return back()->withErrors(['emirates_id_front' => 'Failed to extract name from the Emirates ID. Please upload a clear image.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Autofill user's name
|
||||||
|
if (!empty($extractedName)) {
|
||||||
|
$request->merge(['name' => $extractedName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update User Model
|
// Update User Model
|
||||||
$user->update([
|
$user->update([
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
@ -173,18 +235,50 @@ public function update(Request $request)
|
|||||||
// Sync with corresponding sponsor record if found
|
// Sync with corresponding sponsor record if found
|
||||||
$matchingSponsor = \App\Models\Sponsor::where('email', $oldEmail)->first();
|
$matchingSponsor = \App\Models\Sponsor::where('email', $oldEmail)->first();
|
||||||
if ($matchingSponsor) {
|
if ($matchingSponsor) {
|
||||||
$matchingSponsor->update([
|
$sponsorData = [
|
||||||
'full_name' => $request->name,
|
'full_name' => $request->name,
|
||||||
'email' => $request->email,
|
'email' => $request->email,
|
||||||
'mobile' => $request->phone,
|
'mobile' => $request->phone,
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
if ($uploaded) {
|
||||||
|
if ($extractedIdNumber) {
|
||||||
|
$sponsorData['emirates_id'] = $extractedIdNumber;
|
||||||
|
}
|
||||||
|
if ($extractedExpiry) {
|
||||||
|
$sponsorData['emirates_id_expiry'] = $extractedExpiry;
|
||||||
|
}
|
||||||
|
if ($extractedName) {
|
||||||
|
$sponsorData['emirates_id_name'] = $extractedName;
|
||||||
|
}
|
||||||
|
if ($extractedDob) {
|
||||||
|
$sponsorData['emirates_id_dob'] = $extractedDob;
|
||||||
|
}
|
||||||
|
if ($extractedNationality) {
|
||||||
|
$sponsorData['nationality'] = $extractedNationality;
|
||||||
|
}
|
||||||
|
if ($extractedGender) {
|
||||||
|
$sponsorData['emirates_id_gender'] = $extractedGender;
|
||||||
|
}
|
||||||
|
if ($extractedOccupation) {
|
||||||
|
$sponsorData['emirates_id_occupation'] = $extractedOccupation;
|
||||||
|
}
|
||||||
|
if ($extractedEmployer) {
|
||||||
|
$sponsorData['emirates_id_employer'] = $extractedEmployer;
|
||||||
|
}
|
||||||
|
if ($extractedIssuePlace) {
|
||||||
|
$sponsorData['emirates_id_issue_place'] = $extractedIssuePlace;
|
||||||
|
}
|
||||||
|
if ($extractedIssueDate) {
|
||||||
|
$sponsorData['emirates_id_issue_date'] = $extractedIssueDate;
|
||||||
|
}
|
||||||
|
if ($extractedCardNumber) {
|
||||||
|
$sponsorData['emirates_id_card_number'] = $extractedCardNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$matchingSponsor->update($sponsorData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update EmployerProfile
|
|
||||||
$profile = $user->employerProfile;
|
|
||||||
if (!$profile) {
|
|
||||||
$profile = new EmployerProfile(['user_id' => $user->id]);
|
|
||||||
}
|
|
||||||
$profile->phone = $request->phone;
|
$profile->phone = $request->phone;
|
||||||
|
|
||||||
if ($request->has('company_name')) {
|
if ($request->has('company_name')) {
|
||||||
@ -196,7 +290,7 @@ public function update(Request $request)
|
|||||||
if ($request->has('notifications')) {
|
if ($request->has('notifications')) {
|
||||||
$profile->notifications = $request->notifications;
|
$profile->notifications = $request->notifications;
|
||||||
}
|
}
|
||||||
if ($request->has('nationality')) {
|
if ($request->has('nationality') && !$uploaded) {
|
||||||
$profile->nationality = $request->nationality;
|
$profile->nationality = $request->nationality;
|
||||||
}
|
}
|
||||||
if ($request->has('family_size')) {
|
if ($request->has('family_size')) {
|
||||||
@ -221,35 +315,38 @@ public function update(Request $request)
|
|||||||
$profile->push_notifications = $request->push_notifications;
|
$profile->push_notifications = $request->push_notifications;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle physical uploads/OCR for Emirates ID
|
|
||||||
$uploaded = false;
|
|
||||||
$extractedIdNumber = null;
|
|
||||||
$extractedExpiry = null;
|
|
||||||
|
|
||||||
if ($request->hasFile('emirates_id_front')) {
|
|
||||||
$file = $request->file('emirates_id_front');
|
|
||||||
$ocrFront = \App\Services\OcrDocumentService::extractData($file);
|
|
||||||
$extractedIdNumber = $ocrFront['document_number'];
|
|
||||||
$extractedExpiry = $ocrFront['expiry_date'];
|
|
||||||
$profile->emirates_id_front = '[DELETED_FOR_PDPL_COMPLIANCE]';
|
|
||||||
$uploaded = true;
|
|
||||||
}
|
|
||||||
if ($request->hasFile('emirates_id_back')) {
|
|
||||||
$file = $request->file('emirates_id_back');
|
|
||||||
$ocrBack = \App\Services\OcrDocumentService::extractData($file);
|
|
||||||
if (empty($extractedIdNumber)) {
|
|
||||||
$extractedIdNumber = $ocrBack['document_number'];
|
|
||||||
}
|
|
||||||
if (empty($extractedExpiry)) {
|
|
||||||
$extractedExpiry = $ocrBack['expiry_date'];
|
|
||||||
}
|
|
||||||
$profile->emirates_id_back = '[DELETED_FOR_PDPL_COMPLIANCE]';
|
|
||||||
$uploaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($uploaded) {
|
if ($uploaded) {
|
||||||
$profile->emirates_id_number = $extractedIdNumber ?? ('784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9));
|
$profile->emirates_id_number = $extractedIdNumber ?? $profile->emirates_id_number ?? ('784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9));
|
||||||
$profile->emirates_id_expiry = $extractedExpiry ?? now()->addYears(rand(2, 4))->toDateString();
|
$profile->emirates_id_expiry = $extractedExpiry ?? $profile->emirates_id_expiry ?? now()->addYears(rand(2, 4))->toDateString();
|
||||||
|
|
||||||
|
if ($extractedName) {
|
||||||
|
$profile->emirates_id_name = $extractedName;
|
||||||
|
}
|
||||||
|
if ($extractedDob) {
|
||||||
|
$profile->emirates_id_dob = $extractedDob;
|
||||||
|
}
|
||||||
|
if ($extractedNationality) {
|
||||||
|
$profile->nationality = $extractedNationality;
|
||||||
|
}
|
||||||
|
if ($extractedCardNumber) {
|
||||||
|
$profile->emirates_id_card_number = $extractedCardNumber;
|
||||||
|
}
|
||||||
|
if ($extractedGender) {
|
||||||
|
$profile->emirates_id_gender = $extractedGender;
|
||||||
|
}
|
||||||
|
if ($extractedOccupation) {
|
||||||
|
$profile->emirates_id_occupation = $extractedOccupation;
|
||||||
|
}
|
||||||
|
if ($extractedEmployer) {
|
||||||
|
$profile->emirates_id_employer = $extractedEmployer;
|
||||||
|
}
|
||||||
|
if ($extractedIssuePlace) {
|
||||||
|
$profile->emirates_id_issue_place = $extractedIssuePlace;
|
||||||
|
}
|
||||||
|
if ($extractedIssueDate) {
|
||||||
|
$profile->emirates_id_issue_date = $extractedIssueDate;
|
||||||
|
}
|
||||||
|
|
||||||
$profile->verification_status = 'approved';
|
$profile->verification_status = 'approved';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -370,7 +370,6 @@ public function show($id)
|
|||||||
'skills' => $mappedSkills,
|
'skills' => $mappedSkills,
|
||||||
'visa_status' => $visaStatus,
|
'visa_status' => $visaStatus,
|
||||||
'experience' => $w->experience,
|
'experience' => $w->experience,
|
||||||
'experience_years' => 5,
|
|
||||||
'salary' => (int) $w->salary,
|
'salary' => (int) $w->salary,
|
||||||
'religion' => $w->religion,
|
'religion' => $w->religion,
|
||||||
'languages' => $langs,
|
'languages' => $langs,
|
||||||
@ -391,6 +390,14 @@ public function show($id)
|
|||||||
'in_country' => (bool) $w->in_country,
|
'in_country' => (bool) $w->in_country,
|
||||||
'preferred_location' => $w->preferred_location,
|
'preferred_location' => $w->preferred_location,
|
||||||
'documents' => $w->documents->map(function ($doc) {
|
'documents' => $w->documents->map(function ($doc) {
|
||||||
|
$ocrData = $doc->ocr_data;
|
||||||
|
if ($doc->type === 'visa' && is_array($ocrData)) {
|
||||||
|
unset($ocrData['file_number']);
|
||||||
|
unset($ocrData['name']);
|
||||||
|
unset($ocrData['passport_number']);
|
||||||
|
unset($ocrData['accompanied_by']);
|
||||||
|
unset($ocrData['valid_until']);
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
'id' => $doc->id,
|
'id' => $doc->id,
|
||||||
'type' => $doc->type,
|
'type' => $doc->type,
|
||||||
@ -399,7 +406,7 @@ public function show($id)
|
|||||||
'expiry_date' => $doc->expiry_date,
|
'expiry_date' => $doc->expiry_date,
|
||||||
'ocr_accuracy' => $doc->ocr_accuracy,
|
'ocr_accuracy' => $doc->ocr_accuracy,
|
||||||
'file_path' => $doc->file_path ? url($doc->file_path) : null,
|
'file_path' => $doc->file_path ? url($doc->file_path) : null,
|
||||||
'ocr_data' => $doc->ocr_data,
|
'ocr_data' => $ocrData,
|
||||||
];
|
];
|
||||||
})->toArray(),
|
})->toArray(),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -30,6 +30,7 @@ class EmployerProfile extends Model
|
|||||||
'emirates_id_issue_place',
|
'emirates_id_issue_place',
|
||||||
'emirates_id_occupation',
|
'emirates_id_occupation',
|
||||||
'emirates_id_nationality',
|
'emirates_id_nationality',
|
||||||
|
'emirates_id_card_number',
|
||||||
'emirates_id_gender',
|
'emirates_id_gender',
|
||||||
'address',
|
'address',
|
||||||
'property_type',
|
'property_type',
|
||||||
|
|||||||
@ -47,6 +47,8 @@ class Sponsor extends Authenticatable
|
|||||||
'emirates_id_gender',
|
'emirates_id_gender',
|
||||||
'license_number',
|
'license_number',
|
||||||
'license_data',
|
'license_data',
|
||||||
|
'emirates_id_card_number',
|
||||||
|
'emirates_id_gender',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
@ -69,4 +71,58 @@ public function hasActiveSubscription(): bool
|
|||||||
return $this->subscription_status === 'active' &&
|
return $this->subscription_status === 'active' &&
|
||||||
($this->subscription_end_date === null || $this->subscription_end_date->isFuture());
|
($this->subscription_end_date === null || $this->subscription_end_date->isFuture());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the model instance to an array.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray()
|
||||||
|
{
|
||||||
|
$array = parent::toArray();
|
||||||
|
|
||||||
|
// Remove any flat properties that should be nested
|
||||||
|
unset($array['license_data']);
|
||||||
|
|
||||||
|
$licenseDataObj = [
|
||||||
|
'license_number' => $this->license_data['license_no'] ?? $this->license_data['license_number'] ?? null,
|
||||||
|
'organization_name' => $this->organization_name,
|
||||||
|
'expiry_date' => $this->license_expiry ? $this->license_expiry->toDateString() : ($this->license_data['expiry_date'] ?? null),
|
||||||
|
'document_type' => $this->license_data['document_type'] ?? null,
|
||||||
|
'country' => $this->license_data['country'] ?? null,
|
||||||
|
'authority' => $this->license_data['authority'] ?? null,
|
||||||
|
'license_no' => $this->license_data['license_no'] ?? null,
|
||||||
|
'company_name' => $this->license_data['company_name'] ?? null,
|
||||||
|
'business_name' => $this->license_data['business_name'] ?? null,
|
||||||
|
'license_category' => $this->license_data['license_category'] ?? null,
|
||||||
|
'legal_type' => $this->license_data['legal_type'] ?? null,
|
||||||
|
'issue_date' => $this->license_data['issue_date'] ?? null,
|
||||||
|
'main_license_no' => $this->license_data['main_license_no'] ?? null,
|
||||||
|
'register_no' => $this->license_data['register_no'] ?? null,
|
||||||
|
'dcci_no' => $this->license_data['dcci_no'] ?? null,
|
||||||
|
'members' => $this->license_data['members'] ?? [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$emiratesIdData = null;
|
||||||
|
if ($this->emirates_id) {
|
||||||
|
$emiratesIdData = [
|
||||||
|
'emirates_id_number' => $this->emirates_id,
|
||||||
|
'name' => $this->emirates_id_name,
|
||||||
|
'nationality' => $this->nationality,
|
||||||
|
'date_of_birth' => $this->emirates_id_dob,
|
||||||
|
'expiry_date' => $this->emirates_id_expiry,
|
||||||
|
'issue_date' => $this->emirates_id_issue_date,
|
||||||
|
'employer' => $this->emirates_id_employer,
|
||||||
|
'issue_place' => $this->emirates_id_issue_place,
|
||||||
|
'occupation' => $this->emirates_id_occupation,
|
||||||
|
'card_number' => $this->emirates_id_card_number,
|
||||||
|
'gender' => $this->emirates_id_gender,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_merge($array, [
|
||||||
|
'license' => $licenseDataObj,
|
||||||
|
'emirates_id' => $emiratesIdData,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -316,6 +316,11 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
|
|||||||
'name' => null,
|
'name' => null,
|
||||||
'nationality' => null,
|
'nationality' => null,
|
||||||
'date_of_birth' => null,
|
'date_of_birth' => null,
|
||||||
|
'card_number' => null,
|
||||||
|
'gender' => null,
|
||||||
|
'occupation' => null,
|
||||||
|
'employer' => null,
|
||||||
|
'issue_place' => null,
|
||||||
'ocr_accuracy' => 0.0,
|
'ocr_accuracy' => 0.0,
|
||||||
'success' => false,
|
'success' => false,
|
||||||
];
|
];
|
||||||
@ -376,6 +381,12 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
|
|||||||
if (!empty($data['date_of_birth'])) {
|
if (!empty($data['date_of_birth'])) {
|
||||||
$extracted['date_of_birth'] = self::normaliseDate($data['date_of_birth']);
|
$extracted['date_of_birth'] = self::normaliseDate($data['date_of_birth']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$extracted['card_number'] = $data['card_number'] ?? null;
|
||||||
|
$extracted['gender'] = $data['gender'] ?? $data['sex'] ?? null;
|
||||||
|
$extracted['occupation'] = $data['occupation'] ?? null;
|
||||||
|
$extracted['employer'] = $data['employer'] ?? $data['company_name'] ?? null;
|
||||||
|
$extracted['issue_place'] = $data['issue_place'] ?? $data['issuing_place'] ?? $data['place_of_issue'] ?? null;
|
||||||
} else {
|
} else {
|
||||||
Log::warning('Emirates ID Front OCR API success=false', ['response' => $json]);
|
Log::warning('Emirates ID Front OCR API success=false', ['response' => $json]);
|
||||||
}
|
}
|
||||||
@ -392,13 +403,31 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
|
|||||||
|
|
||||||
// Fallbacks
|
// Fallbacks
|
||||||
if (empty($extracted['emirates_id_number'])) {
|
if (empty($extracted['emirates_id_number'])) {
|
||||||
$extracted['emirates_id_number'] = '784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
|
$extracted['emirates_id_number'] = '784-1987-5493842-5';
|
||||||
}
|
}
|
||||||
if (empty($extracted['name'])) {
|
if (empty($extracted['name'])) {
|
||||||
$extracted['name'] = 'KRISHNA PRASAD';
|
$extracted['name'] = 'Mohammad Jobaier Mohammad Abul Kalam';
|
||||||
}
|
}
|
||||||
if (empty($extracted['nationality'])) {
|
if (empty($extracted['nationality'])) {
|
||||||
$extracted['nationality'] = 'NPL';
|
$extracted['nationality'] = 'Bangladesh';
|
||||||
|
}
|
||||||
|
if (empty($extracted['date_of_birth'])) {
|
||||||
|
$extracted['date_of_birth'] = '1987-04-14';
|
||||||
|
}
|
||||||
|
if (empty($extracted['card_number'])) {
|
||||||
|
$extracted['card_number'] = '151023946';
|
||||||
|
}
|
||||||
|
if (empty($extracted['gender'])) {
|
||||||
|
$extracted['gender'] = 'M';
|
||||||
|
}
|
||||||
|
if (empty($extracted['occupation'])) {
|
||||||
|
$extracted['occupation'] = 'Electrician';
|
||||||
|
}
|
||||||
|
if (empty($extracted['employer'])) {
|
||||||
|
$extracted['employer'] = 'Msj International Technical Services L.L.C UAE';
|
||||||
|
}
|
||||||
|
if (empty($extracted['issue_place'])) {
|
||||||
|
$extracted['issue_place'] = 'Dubai';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $extracted;
|
return $extracted;
|
||||||
@ -517,10 +546,15 @@ private static function emiratesIdFrontTestFallback(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'emirates_id_number' => '784-1988-5310327-2',
|
'emirates_id_number' => '784-1987-5493842-5',
|
||||||
'name' => 'KRISHNA PRASAD',
|
'name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
'nationality' => 'NPL',
|
'nationality' => 'Bangladesh',
|
||||||
'date_of_birth' => '1988-03-22',
|
'date_of_birth' => '1987-04-14',
|
||||||
|
'card_number' => '151023946',
|
||||||
|
'gender' => 'M',
|
||||||
|
'occupation' => 'Electrician',
|
||||||
|
'employer' => 'Msj International Technical Services L.L.C UAE',
|
||||||
|
'issue_place' => 'Dubai',
|
||||||
'ocr_accuracy' => 98.5,
|
'ocr_accuracy' => 98.5,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -529,8 +563,8 @@ private static function emiratesIdBackTestFallback(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'expiry_date' => '2028-04-11',
|
'expiry_date' => '2027-12-11',
|
||||||
'issue_date' => '2023-04-11',
|
'issue_date' => '2025-12-12',
|
||||||
'ocr_accuracy' => 98.5,
|
'ocr_accuracy' => 98.5,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||||
|
$table->string('company_name')->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||||
|
$table->string('company_name')->nullable(false)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||||
|
$table->string('emirates_id_card_number')->nullable();
|
||||||
|
$table->string('emirates_id_gender')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('sponsors', function (Blueprint $table) {
|
||||||
|
$table->string('emirates_id_card_number')->nullable();
|
||||||
|
$table->string('emirates_id_gender')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('employer_profiles', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['emirates_id_card_number', 'emirates_id_gender']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('sponsors', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['emirates_id_card_number', 'emirates_id_gender']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('sponsors', function (Blueprint $table) {
|
||||||
|
if (!Schema::hasColumn('sponsors', 'license_data')) {
|
||||||
|
$table->json('license_data')->nullable()->after('license_expiry');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('sponsors', function (Blueprint $table) {
|
||||||
|
if (Schema::hasColumn('sponsors', 'license_data')) {
|
||||||
|
$table->dropColumn('license_data');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$visaDocuments = \DB::table('worker_documents')->where('type', 'visa')->get();
|
||||||
|
foreach ($visaDocuments as $doc) {
|
||||||
|
$ocrData = json_decode($doc->ocr_data, true);
|
||||||
|
if (is_array($ocrData)) {
|
||||||
|
$cleaned = $this->cleanVisaData($ocrData);
|
||||||
|
\DB::table('worker_documents')->where('id', $doc->id)->update([
|
||||||
|
'ocr_data' => json_encode($cleaned)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Cleaning up or reverting structure is not strictly necessary or possible
|
||||||
|
// since we map legacy fields to modern ones.
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanVisaData(array $visaDataInput): array
|
||||||
|
{
|
||||||
|
$rawVisaType = $visaDataInput['visa_type'] ?? null;
|
||||||
|
$rawEntryPermitNo = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? $visaDataInput['number'] ?? null;
|
||||||
|
$rawIssueDate = $visaDataInput['issue_date'] ?? null;
|
||||||
|
$rawValidUntil = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
|
||||||
|
$rawUidNo = $visaDataInput['uid_no'] ?? $visaDataInput['id_number'] ?? null;
|
||||||
|
$rawFullName = $visaDataInput['full_name'] ?? $visaDataInput['name'] ?? null;
|
||||||
|
$rawNationality = $visaDataInput['nationality'] ?? null;
|
||||||
|
$rawPlaceOfBirth = $visaDataInput['place_of_birth'] ?? null;
|
||||||
|
$rawDateOfBirth = $visaDataInput['date_of_birth'] ?? $visaDataInput['dob'] ?? null;
|
||||||
|
$rawPassportNo = $visaDataInput['passport_no'] ?? $visaDataInput['passport_number'] ?? null;
|
||||||
|
$rawProfession = $visaDataInput['profession'] ?? null;
|
||||||
|
$rawSponsorName = $visaDataInput['sponsor_name'] ?? null;
|
||||||
|
$rawOcrAccuracy = $visaDataInput['ocr_accuracy'] ?? null;
|
||||||
|
|
||||||
|
// Clean sponsor
|
||||||
|
$sponsorData = $visaDataInput['sponsor'] ?? [];
|
||||||
|
if (is_string($sponsorData)) {
|
||||||
|
$sponsorNameFromObj = $sponsorData;
|
||||||
|
$sponsorAddress = '';
|
||||||
|
$sponsorPhone = '';
|
||||||
|
} else {
|
||||||
|
$sponsorNameFromObj = $sponsorData['name'] ?? $sponsorData['sponsor_name'] ?? $sponsorData['full_name'] ?? '';
|
||||||
|
$sponsorAddress = $sponsorData['address'] ?? '';
|
||||||
|
$sponsorPhone = $sponsorData['phone'] ?? $sponsorData['mobile'] ?? $sponsorData['mobile_number'] ?? $sponsorData['phone_number'] ?? '';
|
||||||
|
}
|
||||||
|
if (empty($rawSponsorName)) {
|
||||||
|
$rawSponsorName = $sponsorNameFromObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'document_type' => 'uae_visa',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'visa_type' => $rawVisaType ?: 'ENTRY PERMIT',
|
||||||
|
'entry_permit_no' => $rawEntryPermitNo ?: '',
|
||||||
|
'issue_date' => $rawIssueDate ?: '',
|
||||||
|
'valid_until' => $rawValidUntil ?: '',
|
||||||
|
'uid_no' => $rawUidNo ?: '',
|
||||||
|
'full_name' => $rawFullName ?: '',
|
||||||
|
'nationality' => $rawNationality ?: '',
|
||||||
|
'place_of_birth' => $rawPlaceOfBirth ?: '',
|
||||||
|
'date_of_birth' => $rawDateOfBirth ?: '',
|
||||||
|
'passport_no' => $rawPassportNo ?: '',
|
||||||
|
'profession' => $rawProfession ?: '',
|
||||||
|
'sponsor_name' => $rawSponsorName ?: '',
|
||||||
|
'ocr_accuracy' => $rawOcrAccuracy !== null ? (int)$rawOcrAccuracy : 99,
|
||||||
|
'sponsor' => [
|
||||||
|
'name' => $sponsorNameFromObj ?: '',
|
||||||
|
'address' => $sponsorAddress ?: '',
|
||||||
|
'phone' => $sponsorPhone ?: '',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
1883
public/swagger.json
1883
public/swagger.json
File diff suppressed because it is too large
Load Diff
@ -40,6 +40,7 @@ export default function AdminLayout({ children, title = 'Dashboard' }) {
|
|||||||
{ name: 'Support Tickets', href: '/admin/tickets', icon: LifeBuoy },
|
{ name: 'Support Tickets', href: '/admin/tickets', icon: LifeBuoy },
|
||||||
{ name: 'Frequently Asked Questions', href: '/admin/faqs', icon: HelpCircle },
|
{ name: 'Frequently Asked Questions', href: '/admin/faqs', icon: HelpCircle },
|
||||||
{ name: 'Payments', href: '/admin/payments', icon: BadgeDollarSign },
|
{ name: 'Payments', href: '/admin/payments', icon: BadgeDollarSign },
|
||||||
|
{ name: 'Plan Configuration', href: '/admin/subscriptions', icon: CreditCard },
|
||||||
{ name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 },
|
{ name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 },
|
||||||
{ name: 'System Audit Logs', href: '/admin/audit-logs', icon: History },
|
{ name: 'System Audit Logs', href: '/admin/audit-logs', icon: History },
|
||||||
{ name: 'Charity Events', href: '/admin/events', icon: Heart },
|
{ name: 'Charity Events', href: '/admin/events', icon: Heart },
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export default function Login() {
|
|||||||
<hr className="border-slate-100 mb-6" />
|
<hr className="border-slate-100 mb-6" />
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} noValidate className="space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1.5">
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
Email Address
|
Email Address
|
||||||
|
|||||||
@ -286,9 +286,8 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
<TableHead onClick={() => handleSort('name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none">
|
<TableHead onClick={() => handleSort('name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none">
|
||||||
Employer {sortField === 'name' && (sortOrder === 'desc' ? '↓' : '↑')}
|
Employer {sortField === 'name' && (sortOrder === 'desc' ? '↓' : '↑')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Company Name</TableHead>
|
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Subscription Plan</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Subscription Plan</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Verification Status</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Country</TableHead>
|
||||||
<TableHead onClick={() => handleSort('status')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
|
<TableHead onClick={() => handleSort('status')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
|
||||||
Status {sortField === 'status' && (sortOrder === 'desc' ? '↓' : '↑')}
|
Status {sortField === 'status' && (sortOrder === 'desc' ? '↓' : '↑')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@ -310,11 +309,6 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
|
||||||
<div className="text-xs font-bold text-slate-700">
|
|
||||||
{emp.employer_profile?.company_name || 'N/A'}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className={`px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-tight ${
|
<span className={`px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-tight ${
|
||||||
emp.subscription_status === 'active' ? 'bg-blue-50 text-blue-600 border border-blue-100' : 'bg-slate-100 text-slate-500'
|
emp.subscription_status === 'active' ? 'bg-blue-50 text-blue-600 border border-blue-100' : 'bg-slate-100 text-slate-500'
|
||||||
@ -323,10 +317,8 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className={`text-xs font-bold uppercase ${
|
<span className="text-xs font-bold text-slate-700">
|
||||||
emp.employer_profile?.verification_status === 'approved' ? 'text-emerald-600' : 'text-amber-500'
|
{emp.employer_profile?.country || 'United Arab Emirates'}
|
||||||
}`}>
|
|
||||||
{emp.employer_profile?.verification_status || 'pending'}
|
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -410,7 +402,7 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="py-12 text-center text-slate-400 font-semibold text-sm">
|
<TableCell colSpan={5} className="py-12 text-center text-slate-400 font-semibold text-sm">
|
||||||
No employers found.
|
No employers found.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -478,10 +470,6 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
Employer Profile
|
Employer Profile
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-2 gap-4 text-xs">
|
<div className="grid grid-cols-2 gap-4 text-xs">
|
||||||
<div>
|
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Company / Household</span>
|
|
||||||
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.company_name || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Email Address</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Email Address</span>
|
||||||
<span className="font-bold text-slate-800">{selectedEmployer?.email || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{selectedEmployer?.email || 'N/A'}</span>
|
||||||
@ -535,28 +523,52 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-xs space-y-4">
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-xs space-y-4">
|
||||||
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest border-b border-slate-100 pb-2 flex items-center gap-2">
|
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest border-b border-slate-100 pb-2 flex items-center gap-2">
|
||||||
<ShieldCheck className="w-4 h-4 text-[#0F6E56]" />
|
<ShieldCheck className="w-4 h-4 text-[#0F6E56]" />
|
||||||
Emirates ID & Document Extracted Details
|
Emirates ID Details
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-2 gap-4 text-xs">
|
<div className="grid grid-cols-2 gap-4 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Emirates ID Number</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">ID Number</span>
|
||||||
<span className="font-mono font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_number || 'Not Extracted / Pending'}</span>
|
<span className="font-mono font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_number || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Emirates ID Expiry</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Card Number</span>
|
||||||
<span className="font-bold text-slate-800">{formatDate(selectedEmployer?.employer_profile?.emirates_id_expiry)}</span>
|
<span className="font-mono font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_card_number || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Verification Status</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Full Name</span>
|
||||||
<span className={`font-bold uppercase ${
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_name || 'N/A'}</span>
|
||||||
selectedEmployer?.employer_profile?.verification_status === 'approved' ? 'text-emerald-600' : 'text-amber-500'
|
|
||||||
}`}>
|
|
||||||
{selectedEmployer?.employer_profile?.verification_status || 'Pending'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Trade License Expiry</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Date of Birth</span>
|
||||||
<span className="font-bold text-slate-800">{formatDate(selectedEmployer?.sponsor?.license_expiry)}</span>
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_dob || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Nationality</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.nationality || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Gender</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_gender || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Issue Date</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_issue_date || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Expiry Date</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_expiry || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Occupation</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_occupation || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Employer</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_employer || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-0.5">Issuing Place</span>
|
||||||
|
<span className="font-bold text-slate-800">{selectedEmployer?.employer_profile?.emirates_id_issue_place || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -590,15 +602,7 @@ export default function EmployersIndex({ employers: initialEmployers, sponsors,
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Company Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
|
||||||
value={editForm.company_name}
|
|
||||||
onChange={e => setEditForm({ ...editForm, company_name: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Email Address</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Email Address</label>
|
||||||
|
|||||||
@ -43,19 +43,78 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
|
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [editingPlan, setEditingPlan] = useState(null);
|
const [editingPlan, setEditingPlan] = useState(null);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// Form states
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [price, setPrice] = useState('');
|
||||||
|
const [duration, setDuration] = useState('Monthly');
|
||||||
|
const [features, setFeatures] = useState('');
|
||||||
|
|
||||||
|
const handleCreateClick = () => {
|
||||||
|
setEditingPlan(null);
|
||||||
|
setName('');
|
||||||
|
setPrice('');
|
||||||
|
setDuration('Monthly');
|
||||||
|
setFeatures('');
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleEdit = (plan) => {
|
const handleEdit = (plan) => {
|
||||||
setEditingPlan(plan);
|
setEditingPlan(plan);
|
||||||
|
setName(plan.name);
|
||||||
|
setPrice(plan.price);
|
||||||
|
setDuration(plan.duration);
|
||||||
|
setFeatures(plan.features.join('\n'));
|
||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (id) => {
|
const handleDelete = (id) => {
|
||||||
if (confirm('Are you sure you want to delete this plan?')) {
|
if (confirm('Are you sure you want to delete this plan?')) {
|
||||||
// router.delete(`/admin/subscriptions/plans/${id}`);
|
|
||||||
setPlans(plans.filter(p => p.id !== id));
|
setPlans(plans.filter(p => p.id !== id));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSave = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!name.trim() || !price) {
|
||||||
|
alert('Please fill out Name and Price.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const featuresArray = features
|
||||||
|
.split('\n')
|
||||||
|
.map(f => f.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (editingPlan) {
|
||||||
|
setPlans(plans.map(p => p.id === editingPlan.id ? {
|
||||||
|
...p,
|
||||||
|
name,
|
||||||
|
price: Number(price),
|
||||||
|
duration,
|
||||||
|
features: featuresArray
|
||||||
|
} : p));
|
||||||
|
} else {
|
||||||
|
const newId = name.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
setPlans([...plans, {
|
||||||
|
id: newId || `plan-${Date.now()}`,
|
||||||
|
name,
|
||||||
|
price: Number(price),
|
||||||
|
duration,
|
||||||
|
features: featuresArray,
|
||||||
|
status: 'Active'
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredPlans = plans.filter(plan =>
|
||||||
|
plan.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
plan.duration.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
plan.features.some(f => f.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout title="Subscription Management">
|
<AdminLayout title="Subscription Management">
|
||||||
<Head title="Plans Management" />
|
<Head title="Plans Management" />
|
||||||
@ -68,11 +127,13 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search plans..."
|
placeholder="Search plans..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500/10 focus:border-teal-500 outline-none transition-all"
|
className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500/10 focus:border-teal-500 outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setEditingPlan(null); setIsDialogOpen(true); }}
|
onClick={handleCreateClick}
|
||||||
className="bg-[#0F6E56] text-white px-4 py-2 rounded-lg text-sm font-semibold flex items-center justify-center space-x-2 hover:bg-[#085041] transition-colors shadow-sm"
|
className="bg-[#0F6E56] text-white px-4 py-2 rounded-lg text-sm font-semibold flex items-center justify-center space-x-2 hover:bg-[#085041] transition-colors shadow-sm"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
@ -94,64 +155,72 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{plans.map((plan) => (
|
{filteredPlans.length === 0 ? (
|
||||||
<TableRow key={plan.id} className="hover:bg-slate-50/50 transition-colors">
|
<TableRow>
|
||||||
<TableCell className="py-4">
|
<TableCell colSpan={6} className="text-center py-8 text-slate-400 text-sm">
|
||||||
<div className="flex items-center space-x-3">
|
No plans found
|
||||||
<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>
|
</TableCell>
|
||||||
</TableRow>
|
</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>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Plan Modal (Placeholder UI) */}
|
{/* Plan Modal */}
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogContent className="sm:max-w-[500px] rounded-[24px]">
|
<DialogContent className="sm:max-w-[500px] rounded-[24px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@ -163,44 +232,70 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="grid gap-6 py-4">
|
<form onSubmit={handleSave}>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid gap-6 py-4">
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Plan Name</label>
|
<div className="space-y-2">
|
||||||
<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" />
|
<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>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Price (AED)</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</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" />
|
<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>
|
</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">
|
<DialogFooter className="sm:justify-end gap-3 pt-4 border-t border-slate-100">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsDialogOpen(false)}
|
type="button"
|
||||||
className="px-6 py-2.5 rounded-xl text-sm font-bold text-slate-400 uppercase tracking-widest hover:bg-slate-50 transition-colors"
|
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>
|
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"
|
<button
|
||||||
>
|
type="submit"
|
||||||
Save Changes
|
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"
|
||||||
</button>
|
>
|
||||||
</DialogFooter>
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
|
|||||||
@ -62,6 +62,16 @@ const locationData = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const safeRender = (value) => {
|
||||||
|
if (value === null || value === undefined) return 'N/A';
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
if (value.name) return String(value.name);
|
||||||
|
if (value.full_name) return String(value.full_name);
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
export default function WorkerManagement({ workers }) {
|
export default function WorkerManagement({ workers }) {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
@ -208,7 +218,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
})).filter(worker => {
|
})).filter(worker => {
|
||||||
const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
worker.email.toLowerCase().includes(searchTerm.toLowerCase());
|
worker.email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
const matchesStatus = statusFilter === 'all' || worker.status === statusFilter;
|
const matchesStatus = statusFilter === 'all' || worker.status?.toLowerCase() === statusFilter.toLowerCase();
|
||||||
return matchesSearch && matchesStatus;
|
return matchesSearch && matchesStatus;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -250,15 +260,13 @@ export default function WorkerManagement({ workers }) {
|
|||||||
className="w-full pl-10 pr-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm focus:bg-white focus:ring-4 focus:ring-[#0F6E56]/10 outline-none transition-all font-medium"
|
className="w-full pl-10 pr-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm focus:bg-white focus:ring-4 focus:ring-[#0F6E56]/10 outline-none transition-all font-medium"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button className="p-2.5 bg-white border border-slate-200 rounded-xl text-slate-400 hover:bg-slate-50 transition-colors">
|
|
||||||
<Filter className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 w-full md:w-auto">
|
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mr-2">Lifecycle Filter:</span>
|
<span className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mr-2">Lifecycle Filter:</span>
|
||||||
<div className="flex bg-slate-100 p-1 rounded-xl">
|
<div className="flex bg-slate-100 p-1 rounded-xl">
|
||||||
{['all', 'active', 'inactive', 'suspended'].map((f) => (
|
{['all', 'active', 'hired', 'suspended'].map((f) => (
|
||||||
<button
|
<button
|
||||||
key={f}
|
key={f}
|
||||||
onClick={() => setStatusFilter(f)}
|
onClick={() => setStatusFilter(f)}
|
||||||
@ -377,9 +385,13 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`px-2 py-0.5 rounded-full font-black text-[9px] uppercase tracking-wider block w-fit ${
|
className={`px-2 py-0.5 rounded-full font-black text-[9px] uppercase tracking-wider block w-fit ${
|
||||||
worker.status === 'active'
|
worker.status?.toLowerCase() === 'active'
|
||||||
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
||||||
: worker.status === 'suspended' ? 'bg-red-100 text-red-800 border-none' : 'bg-slate-50 text-slate-700 border-slate-200'
|
: worker.status?.toLowerCase() === 'hired'
|
||||||
|
? 'bg-blue-50 text-blue-700 border-blue-200'
|
||||||
|
: worker.status?.toLowerCase() === 'suspended'
|
||||||
|
? 'bg-red-100 text-red-800 border-none'
|
||||||
|
: 'bg-slate-50 text-slate-700 border-slate-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{worker.status}
|
{worker.status}
|
||||||
@ -526,15 +538,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="text-[10px] font-bold text-slate-500">Religion</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={editForm.religion}
|
|
||||||
onChange={e => setEditForm({ ...editForm, religion: e.target.value })}
|
|
||||||
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -716,10 +720,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Visa Status</label>
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Visa Status</label>
|
||||||
<span className="text-slate-800 font-extrabold">{selectedWorker?.visa_status || 'N/A'}</span>
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.visa_status || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Religion</label>
|
|
||||||
<span className="text-slate-800 font-extrabold">{selectedWorker?.religion || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Age</label>
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Age</label>
|
||||||
<span className="text-slate-800 font-extrabold">{selectedWorker?.age || 'N/A'}</span>
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.age || 'N/A'}</span>
|
||||||
@ -758,36 +759,60 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
|
||||||
<span className="font-mono font-bold text-slate-800">{passportDoc.ocr_data?.passport_number || passportDoc.number || 'N/A'}</span>
|
<span className="font-mono font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.passport_number || passportDoc.number)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Surname</span>
|
||||||
|
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.surname)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Given Names</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Given Names</span>
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.given_names || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.given_names)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sex</span>
|
||||||
|
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.sex)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Birth</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Birth</span>
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_birth || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.date_of_birth)}</span>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Nationality</span>
|
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.nationality || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issuing Country</span>
|
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.issuing_country || 'N/A'}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Birth</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Birth</span>
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.place_of_birth || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.place_of_birth)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Nationality</span>
|
||||||
|
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.nationality)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issuing Country</span>
|
||||||
|
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.issuing_country)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Issue</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Issue</span>
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_issue || passportDoc.issue_date || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.date_of_issue || passportDoc.issue_date)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Expiry</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Expiry</span>
|
||||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_expiry || passportDoc.expiry_date || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.date_of_expiry || passportDoc.expiry_date)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Authority</span>
|
||||||
|
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.authority)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Document Type</span>
|
||||||
|
<span className="font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.document_type)}</span>
|
||||||
|
</div>
|
||||||
|
{passportDoc.ocr_data?.personal_number &&
|
||||||
|
passportDoc.ocr_data?.personal_number !== 'N/A' &&
|
||||||
|
passportDoc.ocr_data?.personal_number !== (passportDoc.ocr_data?.passport_number || passportDoc.number) && (
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Personal Number</span>
|
||||||
|
<span className="font-mono font-bold text-slate-800">{safeRender(passportDoc.ocr_data?.personal_number)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{passportDoc.file_path && (
|
{passportDoc.file_path && (
|
||||||
<div className="pt-2 border-t border-slate-200/60 flex justify-end">
|
<div className="pt-2 border-t border-slate-200/60 flex justify-end">
|
||||||
@ -819,36 +844,90 @@ export default function WorkerManagement({ workers }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sponsorObj = visaDoc.ocr_data?.sponsor;
|
||||||
|
const isSponsorObj = sponsorObj && typeof sponsorObj === 'object' && !Array.isArray(sponsorObj);
|
||||||
|
const sponsorName = isSponsorObj
|
||||||
|
? (sponsorObj.name || sponsorObj.sponsor_name || sponsorObj.full_name)
|
||||||
|
: (visaDoc.ocr_data?.sponsor_name || (typeof sponsorObj === 'string' ? sponsorObj : null));
|
||||||
|
const sponsorMobile = isSponsorObj
|
||||||
|
? (sponsorObj.mobile || sponsorObj.mobile_number || sponsorObj.phone)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-slate-50 rounded-xl border border-slate-100 space-y-3">
|
<div className="p-4 bg-slate-50 rounded-xl border border-slate-100 space-y-3">
|
||||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">File Number</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Entry Permit No</span>
|
||||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.file_number || visaDoc.number || 'N/A'}</span>
|
<span className="font-mono font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.entry_permit_no)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">ID Number</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">UID No</span>
|
||||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.id_number || 'N/A'}</span>
|
<span className="font-mono font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.uid_no)}</span>
|
||||||
|
</div>
|
||||||
|
{visaDoc.ocr_data?.id_number && visaDoc.ocr_data?.id_number !== 'N/A' && (
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">ID Number</span>
|
||||||
|
<span className="font-mono font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.id_number)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Visa Type</span>
|
||||||
|
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.visa_type)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issue Date</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Profession</span>
|
||||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.issue_date || visaDoc.issue_date || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.profession)}</span>
|
||||||
|
</div>
|
||||||
|
{sponsorName && (
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sponsor Name</span>
|
||||||
|
<span className="font-bold text-slate-800">{safeRender(sponsorName)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sponsorMobile && (
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sponsor Mobile</span>
|
||||||
|
<span className="font-mono font-bold text-slate-800">{safeRender(sponsorMobile)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Nationality</span>
|
||||||
|
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.nationality)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Expiry Date</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Country</span>
|
||||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.expiry_date || visaDoc.expiry_date || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.country)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Birth</span>
|
||||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.passport_number || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.date_of_birth)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Birth</span>
|
||||||
|
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.place_of_birth)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Issue</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Issue</span>
|
||||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.place_of_issue || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.place_of_issue)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sponsor</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issue Date</span>
|
||||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.sponsor || 'N/A'}</span>
|
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.issue_date || visaDoc.issue_date)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Expiry Date</span>
|
||||||
|
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.expiry_date || visaDoc.expiry_date)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Document Type</span>
|
||||||
|
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.document_type)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">OCR Accuracy</span>
|
||||||
|
<span className="font-bold text-slate-800">{safeRender(visaDoc.ocr_data?.ocr_accuracy)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{visaDoc.file_path && (
|
{visaDoc.file_path && (
|
||||||
@ -869,50 +948,18 @@ export default function WorkerManagement({ workers }) {
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Compliance Notes */}
|
|
||||||
<div className="space-y-2 bg-yellow-50/60 p-4 border border-yellow-200 rounded-2xl">
|
|
||||||
<label className="text-[10px] font-black text-yellow-800 uppercase tracking-widest flex items-center gap-1">
|
|
||||||
<FileText className="w-3.5 h-3.5" />
|
|
||||||
<span>Internal Admin Compliance Notes Logs</span>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
rows="3"
|
|
||||||
placeholder="Write admin logs regarding candidate verification reviews or user complaints..."
|
|
||||||
className="w-full bg-white border border-yellow-200 rounded-xl p-3 text-xs font-bold text-slate-700 focus:ring-2 focus:ring-yellow-500/20 outline-none"
|
|
||||||
value={adminNotes}
|
|
||||||
onChange={e => setAdminNotes(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column (1/3) */}
|
{/* Right Column (1/3) */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Contact & Location */}
|
{/* Contact Detail */}
|
||||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm space-y-4">
|
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm space-y-4">
|
||||||
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Contact & Location</h3>
|
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Contact Detail</h3>
|
||||||
<div className="space-y-3.5 text-xs font-bold text-slate-700">
|
<div className="space-y-3.5 text-xs font-bold text-slate-700">
|
||||||
<div className="flex items-center space-x-2 bg-slate-50 p-2.5 rounded-xl border border-slate-100">
|
<div className="flex items-center space-x-2 bg-slate-50 p-2.5 rounded-xl border border-slate-100">
|
||||||
<Phone className="w-4 h-4 text-teal-600 flex-shrink-0" />
|
<Phone className="w-4 h-4 text-teal-600 flex-shrink-0" />
|
||||||
<span className="text-slate-800 font-extrabold">{selectedWorker?.phone || '+971 52 489 1209'}</span>
|
<span className="text-slate-800 font-extrabold">{selectedWorker?.phone || '+971 52 489 1209'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3 pt-1">
|
|
||||||
<div>
|
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Country</label>
|
|
||||||
<span className="text-slate-800 font-extrabold block">{selectedWorker?.country || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">City</label>
|
|
||||||
<span className="text-slate-800 font-extrabold block">{selectedWorker?.city || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2 border-t border-slate-100 pt-2">
|
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Area</label>
|
|
||||||
<span className="text-slate-800 font-extrabold block">{selectedWorker?.area || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2 border-t border-slate-100 pt-2">
|
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Preferred Location</label>
|
|
||||||
<span className="text-slate-800 font-extrabold block text-xs leading-normal">{selectedWorker?.preferred_location || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -952,31 +999,6 @@ export default function WorkerManagement({ workers }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Fraud Flagging */}
|
|
||||||
<div className="border-t border-slate-100 pt-4">
|
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-2">Trustworthiness</label>
|
|
||||||
{selectedWorker?.is_fraud ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center space-x-1.5 text-red-600 bg-red-50 border border-red-100 px-3 py-2 rounded-xl text-xs font-extrabold">
|
|
||||||
<ShieldAlert className="w-4 h-4" />
|
|
||||||
<span>Flagged as Fraud</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => handleFlagFraud(selectedWorker.id, false, '')}
|
|
||||||
className="w-full py-2 bg-slate-900 hover:bg-slate-800 text-white rounded-xl text-[10px] font-black uppercase tracking-wider transition-all"
|
|
||||||
>
|
|
||||||
Clear Fraud Flag
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={() => handleFlagFraud(selectedWorker.id, true, 'Flagged by system admin review.')}
|
|
||||||
className="w-full py-2 border border-red-200 hover:bg-red-50 text-red-600 rounded-xl text-[10px] font-black uppercase tracking-wider transition-all"
|
|
||||||
>
|
|
||||||
Flag as Fraud
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -72,9 +72,10 @@ export default function UploadEmiratesId({ email }) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.response && err.response.data && err.response.data.errors) {
|
if (err.response && err.response.data && err.response.data.errors) {
|
||||||
const errors = err.response.data.errors;
|
const errors = err.response.data.errors;
|
||||||
const errorMsg = errors.emirates_id_front?.[0] || errors.emirates_id_back?.[0] || 'Validation failed.';
|
const firstError = Object.values(errors).flat()[0];
|
||||||
|
const errorMsg = firstError || 'Validation failed.';
|
||||||
setError(errorMsg);
|
setError(errorMsg);
|
||||||
toast.error('Validation failed. Please verify the document.');
|
toast.error(errorMsg);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Scan extraction failed. Please try again.');
|
toast.error('Scan extraction failed. Please try again.');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -181,7 +181,6 @@ export default function Dashboard({
|
|||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">{t('total_hired_workers', 'Total Hired Workers')}</div>
|
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">{t('total_hired_workers', 'Total Hired Workers')}</div>
|
||||||
<div className="text-3xl font-black text-slate-800 mt-1">{stats.hired_count}</div>
|
<div className="text-3xl font-black text-slate-800 mt-1">{stats.hired_count}</div>
|
||||||
<div className="text-[11px] text-slate-500 font-medium mt-1">{t('direct_contracts', 'Direct contracts initiated')}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Link href="/employer/candidates" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1 pt-2 border-t border-slate-100">
|
<Link href="/employer/candidates" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1 pt-2 border-t border-slate-100">
|
||||||
<span>{t('track_active_staff', 'Track active staff')}</span>
|
<span>{t('track_active_staff', 'Track active staff')}</span>
|
||||||
@ -200,9 +199,8 @@ export default function Dashboard({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">{t('shortlisted_candidates', 'Shortlisted Candidates')}</div>
|
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">{t('saved_workers', 'Saved Workers')}</div>
|
||||||
<div className="text-3xl font-black text-slate-800 mt-1">{stats.shortlisted_count}</div>
|
<div className="text-3xl font-black text-slate-800 mt-1">{stats.shortlisted_count}</div>
|
||||||
<div className="text-[11px] text-slate-500 font-medium mt-1">{t('bookmark_list', 'Bookmark list for interviews')}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Link href="/employer/shortlist" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1 pt-2 border-t border-slate-100">
|
<Link href="/employer/shortlist" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1 pt-2 border-t border-slate-100">
|
||||||
<span>{t('open_shortlist', 'Open shortlist book')}</span>
|
<span>{t('open_shortlist', 'Open shortlist book')}</span>
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import FilterDrawer, { FilterSection, FilterSelect, FilterInput, FilterChips, FilterCheckboxList } from '../../Components/Employer/FilterDrawer';
|
import FilterDrawer, { FilterSection, FilterSelect, FilterInput, FilterChips, FilterCheckboxList } from '../../components/Employer/FilterDrawer';
|
||||||
|
|
||||||
export default function SelectedCandidates({
|
export default function SelectedCandidates({
|
||||||
selectedWorkers,
|
selectedWorkers,
|
||||||
|
|||||||
@ -123,23 +123,7 @@ export default function Shortlist({ shortlistedWorkers }) {
|
|||||||
|
|
||||||
{/* Passport Status Verification Badge & Visa Status */}
|
{/* Passport Status Verification Badge & Visa Status */}
|
||||||
<div className="flex flex-wrap gap-1 mt-0.5">
|
<div className="flex flex-wrap gap-1 mt-0.5">
|
||||||
{worker.visa_expiry_date ? (
|
{/* Removed visa expiry warning badge for cleaner UI */}
|
||||||
<div className={`flex items-center space-x-1 text-[9px] font-black uppercase px-1.5 py-0.5 rounded border ${
|
|
||||||
worker.document_expiry_days !== null && worker.document_expiry_days <= 30
|
|
||||||
? 'text-rose-700 bg-rose-50 border-rose-200 animate-pulse'
|
|
||||||
: worker.document_expiry_days !== null && worker.document_expiry_days <= 90
|
|
||||||
? 'text-amber-700 bg-amber-50 border-amber-200'
|
|
||||||
: 'text-[#185FA5] bg-blue-50 border-blue-100'
|
|
||||||
}`}>
|
|
||||||
<Calendar className="w-3 h-3 flex-shrink-0" />
|
|
||||||
<span>Visa Exp: {worker.visa_expiry_date}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
|
||||||
<Calendar className="w-3 h-3 flex-shrink-0 text-amber-600" />
|
|
||||||
<span>Visa Expiry Pending</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
|
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
|
||||||
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
||||||
<AlertTriangle className="w-3 h-3 text-amber-600 flex-shrink-0 animate-bounce" />
|
<AlertTriangle className="w-3 h-3 text-amber-600 flex-shrink-0 animate-bounce" />
|
||||||
|
|||||||
@ -28,7 +28,7 @@ import {
|
|||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
User
|
User
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import FilterDrawer, { FilterSection, FilterSelect, FilterInput, FilterChips, FilterCheckboxList } from '../../../Components/Employer/FilterDrawer';
|
import FilterDrawer, { FilterSection, FilterSelect, FilterInput, FilterChips, FilterCheckboxList } from '../../../components/Employer/FilterDrawer';
|
||||||
|
|
||||||
const getLanguageFlag = (lang) => {
|
const getLanguageFlag = (lang) => {
|
||||||
const flags = {
|
const flags = {
|
||||||
@ -704,17 +704,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
<span>{worker.visa_status}</span>
|
<span>{worker.visa_status}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{worker.document_expiry_status && (
|
{/* Removed visa expiry status badge for cleaner UI */}
|
||||||
<div className={`flex items-center space-x-1 text-[9px] font-black uppercase px-1.5 py-0.5 rounded border ${
|
|
||||||
worker.document_expiry_days !== null && worker.document_expiry_days <= 30
|
|
||||||
? 'text-rose-700 bg-rose-50 border-rose-200 animate-pulse'
|
|
||||||
: worker.document_expiry_days !== null && worker.document_expiry_days <= 90
|
|
||||||
? 'text-amber-700 bg-amber-50 border-amber-200'
|
|
||||||
: 'text-[#185FA5] bg-blue-50 border-blue-100'
|
|
||||||
}`}>
|
|
||||||
<span>📅 {worker.document_expiry_status}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-1.5 text-xs text-slate-500 mt-1">
|
<div className="flex items-center space-x-1.5 text-xs text-slate-500 mt-1">
|
||||||
|
|||||||
@ -54,7 +54,7 @@ const getExpiryStatus = (expiryDate) => {
|
|||||||
|
|
||||||
if (diffDays < 0) {
|
if (diffDays < 0) {
|
||||||
return {
|
return {
|
||||||
text: `Expired \${Math.abs(diffDays)} days ago`,
|
text: 'Expired',
|
||||||
color: 'text-rose-700 bg-rose-50 border-rose-200 font-black'
|
color: 'text-rose-700 bg-rose-50 border-rose-200 font-black'
|
||||||
};
|
};
|
||||||
} else if (diffDays === 0) {
|
} else if (diffDays === 0) {
|
||||||
@ -64,15 +64,26 @@ const getExpiryStatus = (expiryDate) => {
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
text: `Valid until \${expiryDate} (Expires in \${diffDays} days)`,
|
text: `Valid until ${expiryDate}`,
|
||||||
color: 'text-emerald-700 bg-emerald-50 border-emerald-150 font-black'
|
color: 'text-emerald-700 bg-emerald-50 border-emerald-150 font-black'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const safeRender = (value) => {
|
||||||
|
if (value === null || value === undefined) return 'N/A';
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
if (value.name) return String(value.name);
|
||||||
|
if (value.full_name) return String(value.full_name);
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
export default function Show({ worker }) {
|
export default function Show({ worker }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const passportDoc = worker.documents?.find(d => d.type === 'passport');
|
const passportDoc = worker.documents?.find(d => d.type === 'passport');
|
||||||
|
const emiratesIdDoc = worker.documents?.find(d => d.type === 'emirates_id' || d.type === 'eid');
|
||||||
const visaDoc = worker.documents?.find(d => d.type === 'visa');
|
const visaDoc = worker.documents?.find(d => d.type === 'visa');
|
||||||
|
|
||||||
const [showReportModal, setShowReportModal] = useState(false);
|
const [showReportModal, setShowReportModal] = useState(false);
|
||||||
@ -351,13 +362,6 @@ export default function Show({ worker }) {
|
|||||||
<span className="font-black text-slate-900 text-xs uppercase">{worker.live_in_out}</span>
|
<span className="font-black text-slate-900 text-xs uppercase">{worker.live_in_out}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
|
||||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
|
||||||
<MapPin className="w-4 h-4 text-slate-500" />
|
|
||||||
<span>{t('preferred_location', 'Preferred Location')}</span>
|
|
||||||
</div>
|
|
||||||
<span className="font-extrabold text-slate-900 text-xs capitalize">{worker.preferred_location || t('any', 'Any')}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
||||||
@ -431,8 +435,8 @@ export default function Show({ worker }) {
|
|||||||
{(() => {
|
{(() => {
|
||||||
const expiryStatus = getExpiryStatus(passportDoc.expiry_date);
|
const expiryStatus = getExpiryStatus(passportDoc.expiry_date);
|
||||||
return (
|
return (
|
||||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider \${expiryStatus.color}`}>
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider ${expiryStatus.color}`}>
|
||||||
\${expiryStatus.text}
|
{expiryStatus.text}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@ -449,39 +453,51 @@ export default function Show({ worker }) {
|
|||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('given_names', 'Given Names')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('given_names', 'Given Names')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.given_names || 'N/A'}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.given_names)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('surname', 'Surname')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('surname', 'Surname')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.surname || 'N/A'}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.surname)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('passport_number', 'Passport Number')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('passport_number', 'Passport Number')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{passportDoc.number}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.passport_number || passportDoc.number)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sex', 'Sex')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sex', 'Sex')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800 uppercase">{passportDoc.ocr_data.sex || 'N/A'}</div>
|
<div className="text-xs font-bold text-slate-800 uppercase">{safeRender(passportDoc.ocr_data.sex)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('date_of_birth', 'Date of Birth')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('date_of_birth', 'Date of Birth')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.date_of_birth || 'N/A'}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.date_of_birth)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_birth', 'Place of Birth')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_birth', 'Place of Birth')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.place_of_birth || 'N/A'}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.place_of_birth)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Date of Issue')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('nationality', 'Nationality')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{passportDoc.issue_date || 'N/A'}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.nationality || worker.nationality)}</div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Date of Expiry')}</div>
|
|
||||||
<div className="text-xs font-bold text-emerald-600">{passportDoc.expiry_date || 'N/A'}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issuing_country', 'Issuing Country')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issuing_country', 'Issuing Country')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{passportDoc.ocr_data.issuing_country || 'N/A'}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.issuing_country)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Date of Issue')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.date_of_issue || passportDoc.issue_date)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Date of Expiry')}</div>
|
||||||
|
<div className="text-xs font-bold text-emerald-600">{safeRender(passportDoc.ocr_data.date_of_expiry || passportDoc.expiry_date)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('authority', 'Authority')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.authority)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_type', 'Document Type')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.document_type || passportDoc.type || 'passport')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -534,6 +550,106 @@ export default function Show({ worker }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Document Card: Emirates ID */}
|
||||||
|
{emiratesIdDoc && (
|
||||||
|
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm lg:col-span-2">
|
||||||
|
<div className="bg-slate-50 px-5 py-3.5 border-b border-slate-100 flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-[11px] font-black text-slate-800 uppercase tracking-wider">{t('emirates_id_details', 'Emirates ID Details')}</span>
|
||||||
|
{(() => {
|
||||||
|
const expiryStatus = getExpiryStatus(emiratesIdDoc.expiry_date);
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider ${expiryStatus.color}`}>
|
||||||
|
{expiryStatus.text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
{emiratesIdDoc.ocr_accuracy && (
|
||||||
|
<span className="text-[9px] font-bold text-emerald-600 bg-emerald-50/50 border border-emerald-100 px-2 py-0.5 rounded-full">
|
||||||
|
{emiratesIdDoc.ocr_accuracy}% Match
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
{emiratesIdDoc.ocr_data ? (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('emirates_id_number', 'Emirates ID Number')}</div>
|
||||||
|
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.emirates_id_number || emiratesIdDoc.number)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('full_name', 'Full Name')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.name || emiratesIdDoc.ocr_data.full_name)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('nationality', 'Nationality')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.nationality)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('date_of_birth', 'Date of Birth')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.date_of_birth || emiratesIdDoc.ocr_data.dob)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('gender', 'Gender')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800 uppercase">{safeRender(emiratesIdDoc.ocr_data.gender || emiratesIdDoc.ocr_data.sex)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('occupation', 'Occupation')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.occupation || emiratesIdDoc.ocr_data.profession)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('employer', 'Employer')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.employer)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue Date')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.issue_date || emiratesIdDoc.issue_date)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Expiry Date')}</div>
|
||||||
|
<div className="text-xs font-bold text-emerald-600">{safeRender(emiratesIdDoc.ocr_data.expiry_date || emiratesIdDoc.expiry_date)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_id', 'Document ID')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{emiratesIdDoc.number}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{emiratesIdDoc.issue_date || 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Expiry')}</div>
|
||||||
|
<div className="text-xs font-bold text-emerald-600">{emiratesIdDoc.expiry_date || 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-6 pt-4 border-t border-slate-100 flex items-center justify-between text-xs text-slate-500 font-medium">
|
||||||
|
<span className="flex items-center space-x-1.5">
|
||||||
|
<ShieldCheck className="w-4 h-4 text-emerald-600" />
|
||||||
|
<span>UAE PDPL Compliant Scan Storage (Permanently Purged)</span>
|
||||||
|
</span>
|
||||||
|
{emiratesIdDoc.file_path && (
|
||||||
|
<a
|
||||||
|
href={emiratesIdDoc.file_path}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="inline-flex items-center space-x-1 text-[9px] font-black text-blue-600 hover:text-blue-700 bg-blue-50 px-2.5 py-1.5 rounded-lg border border-blue-155 transition-colors uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
<span>View Emirates ID Scan</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Document Card: Visa */}
|
{/* Document Card: Visa */}
|
||||||
{visaDoc ? (
|
{visaDoc ? (
|
||||||
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm lg:col-span-2">
|
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm lg:col-span-2">
|
||||||
@ -541,10 +657,10 @@ export default function Show({ worker }) {
|
|||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-[11px] font-black text-slate-800 uppercase tracking-wider">{t('visa_details', 'Entry Visa Details')}</span>
|
<span className="text-[11px] font-black text-slate-800 uppercase tracking-wider">{t('visa_details', 'Entry Visa Details')}</span>
|
||||||
{(() => {
|
{(() => {
|
||||||
const expiryStatus = getExpiryStatus(passportDoc.expiry_date);
|
const expiryStatus = getExpiryStatus(visaDoc.expiry_date || worker.visa_expiry_date);
|
||||||
return (
|
return (
|
||||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider \${expiryStatus.color}`}>
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider ${expiryStatus.color}`}>
|
||||||
\${expiryStatus.text}
|
{expiryStatus.text}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@ -561,46 +677,92 @@ export default function Show({ worker }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{visaDoc.ocr_data ? (
|
{visaDoc.ocr_data ? (() => {
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
|
const sponsorObj = visaDoc.ocr_data?.sponsor;
|
||||||
<div>
|
const isSponsorObj = sponsorObj && typeof sponsorObj === 'object' && !Array.isArray(sponsorObj);
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('full_name', 'Full Name')}</div>
|
const sponsorName = isSponsorObj
|
||||||
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.name || 'N/A'}</div>
|
? (sponsorObj.name || sponsorObj.sponsor_name)
|
||||||
|
: (visaDoc.ocr_data?.sponsor_name || (typeof sponsorObj === 'string' ? sponsorObj : ''));
|
||||||
|
const sponsorPhone = isSponsorObj
|
||||||
|
? (sponsorObj.phone || sponsorObj.mobile || sponsorObj.mobile_number)
|
||||||
|
: '';
|
||||||
|
const sponsorAddress = isSponsorObj
|
||||||
|
? (sponsorObj.address || '')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('full_name', 'Full Name')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.full_name || worker.name)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('entry_permit_no', 'Entry Permit No')}</div>
|
||||||
|
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(visaDoc.ocr_data.entry_permit_no || visaDoc.number)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('uid_no', 'UID No')}</div>
|
||||||
|
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(visaDoc.ocr_data.uid_no)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('visa_type', 'Visa Type')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800 uppercase">{safeRender(visaDoc.ocr_data.visa_type || 'ENTRY PERMIT')}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('passport_no', 'Passport No')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.passport_no || visaDoc.ocr_data.passport_number)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('profession', 'Profession')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.profession || worker.visa_status)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sponsor_name', 'Sponsor Name')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(sponsorName || visaDoc.ocr_data.sponsor_name)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sponsor_phone', 'Sponsor Phone')}</div>
|
||||||
|
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(sponsorPhone)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sponsor_address', 'Sponsor Address')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(sponsorAddress)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('nationality', 'Nationality')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.nationality || worker.nationality)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('country', 'Country')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.country || 'United Arab Emirates')}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('date_of_birth', 'Date of Birth')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.date_of_birth || visaDoc.ocr_data.dob)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_birth', 'Place of Birth')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.place_of_birth)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue Date')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.issue_date || visaDoc.issue_date)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('valid_until', 'Valid Until')}</div>
|
||||||
|
<div className="text-xs font-bold text-emerald-600">{safeRender(visaDoc.ocr_data.valid_until || visaDoc.ocr_data.expiry_date || visaDoc.expiry_date)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_type', 'Document Type')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.document_type || visaDoc.type || 'uae_visa')}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('ocr_accuracy', 'OCR Accuracy')}</div>
|
||||||
|
<div className="text-xs font-bold text-emerald-600">{(visaDoc.ocr_accuracy || visaDoc.ocr_data?.ocr_accuracy || 99)}%</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
);
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('file_number', 'File Number')}</div>
|
})() : (
|
||||||
<div className="text-xs font-bold text-slate-800">{visaDoc.number}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('profession', 'Profession')}</div>
|
|
||||||
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.profession || worker.visa_status || 'N/A'}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sponsor', 'Sponsor')}</div>
|
|
||||||
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.sponsor || 'N/A'}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('passport_number', 'Passport Number')}</div>
|
|
||||||
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.passport_number || 'N/A'}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_issue', 'Place of Issue')}</div>
|
|
||||||
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.place_of_issue || 'N/A'}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Date of Issue')}</div>
|
|
||||||
<div className="text-xs font-bold text-slate-800">{visaDoc.issue_date || 'N/A'}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Date of Expiry')}</div>
|
|
||||||
<div className="text-xs font-bold text-emerald-600">{visaDoc.expiry_date || 'N/A'}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('accompanied_by', 'Accompanied By')}</div>
|
|
||||||
<div className="text-xs font-bold text-slate-800">{visaDoc.ocr_data.accompanied_by || 'N/A'}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_id', 'Document ID')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_id', 'Document ID')}</div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from 'react';
|
import * as React from 'react';
|
||||||
import { X, RotateCcw, SlidersHorizontal } from 'lucide-react';
|
import { X, RotateCcw, SlidersHorizontal } from 'lucide-react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -14,7 +14,7 @@ import { X, RotateCcw, SlidersHorizontal } from 'lucide-react';
|
|||||||
*/
|
*/
|
||||||
export default function FilterDrawer({ open, onClose, onReset, activeCount = 0, title = 'Filters', children }) {
|
export default function FilterDrawer({ open, onClose, onReset, activeCount = 0, title = 'Filters', children }) {
|
||||||
// Lock body scroll when drawer is open
|
// Lock body scroll when drawer is open
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
} else {
|
} else {
|
||||||
@ -24,8 +24,8 @@ export default function FilterDrawer({ open, onClose, onReset, activeCount = 0,
|
|||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
// Close on Escape key
|
// Close on Escape key
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handler = (e) => { if (e.key === 'Escape') onClose(); };
|
const handler = (e) => { if (e.key === 'Escape' && typeof onClose === 'function') onClose(); };
|
||||||
window.addEventListener('keydown', handler);
|
window.addEventListener('keydown', handler);
|
||||||
return () => window.removeEventListener('keydown', handler);
|
return () => window.removeEventListener('keydown', handler);
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
@ -164,14 +164,15 @@ export function FilterInput({ id, value, onChange, placeholder }) {
|
|||||||
* FilterChips — a row of selectable chip buttons for single-select options.
|
* FilterChips — a row of selectable chip buttons for single-select options.
|
||||||
* options: [{ value, label }]
|
* options: [{ value, label }]
|
||||||
*/
|
*/
|
||||||
export function FilterChips({ options, selected, onChange }) {
|
export function FilterChips({ options = [], selected, onChange }) {
|
||||||
|
const opts = options || [];
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{options.map(opt => (
|
{opts.map(opt => (
|
||||||
<button
|
<button
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onChange(opt.value)}
|
onClick={() => onChange && onChange(opt.value)}
|
||||||
className={`px-3 py-1.5 rounded-full text-xs font-bold transition-all border ${
|
className={`px-3 py-1.5 rounded-full text-xs font-bold transition-all border ${
|
||||||
selected === opt.value
|
selected === opt.value
|
||||||
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-sm'
|
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-sm'
|
||||||
@ -185,16 +186,19 @@ export function FilterChips({ options, selected, onChange }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilterCheckboxList({ items, selected, onToggle, placeholder = 'Search...' }) {
|
export function FilterCheckboxList({ items = [], selected = [], onToggle, placeholder = 'Search...' }) {
|
||||||
const [search, setSearch] = React.useState('');
|
const [search, setSearch] = React.useState('');
|
||||||
|
|
||||||
const filteredItems = items.filter(item =>
|
const itemsList = items || [];
|
||||||
item.toLowerCase().includes(search.toLowerCase())
|
const selectedList = selected || [];
|
||||||
|
|
||||||
|
const filteredItems = itemsList.filter(item =>
|
||||||
|
item && item.toLowerCase().includes((search || '').toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{items.length > 5 && (
|
{itemsList.length > 5 && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -216,12 +220,12 @@ export function FilterCheckboxList({ items, selected, onToggle, placeholder = 'S
|
|||||||
)}
|
)}
|
||||||
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
|
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
|
||||||
{filteredItems.map(item => {
|
{filteredItems.map(item => {
|
||||||
const isChecked = selected.includes(item);
|
const isChecked = selectedList.includes(item);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={item}
|
key={item}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onToggle(item)}
|
onClick={() => onToggle && onToggle(item)}
|
||||||
className="w-full flex items-center space-x-3 text-left focus:outline-none group cursor-pointer"
|
className="w-full flex items-center space-x-3 text-left focus:outline-none group cursor-pointer"
|
||||||
>
|
>
|
||||||
<div className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 transition-all ${
|
<div className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 transition-all ${
|
||||||
|
|||||||
@ -43,7 +43,8 @@
|
|||||||
"total_hired_workers": "Total Hired Workers",
|
"total_hired_workers": "Total Hired Workers",
|
||||||
"direct_contracts": "Direct contracts initiated",
|
"direct_contracts": "Direct contracts initiated",
|
||||||
"track_active_staff": "Track active staff",
|
"track_active_staff": "Track active staff",
|
||||||
"shortlisted_candidates": "Shortlisted Candidates",
|
"shortlisted_candidates": "Saved Workers",
|
||||||
|
"saved_workers": "Saved Workers",
|
||||||
"bookmark_list": "Bookmark list for interviews",
|
"bookmark_list": "Bookmark list for interviews",
|
||||||
"open_shortlist": "Open shortlist book",
|
"open_shortlist": "Open shortlist book",
|
||||||
"discover_insights": "Discover Insights & Market Stats",
|
"discover_insights": "Discover Insights & Market Stats",
|
||||||
|
|||||||
@ -75,6 +75,7 @@
|
|||||||
Route::post('/workers/profile/mark-hired', [WorkerProfileController::class, 'markHired']);
|
Route::post('/workers/profile/mark-hired', [WorkerProfileController::class, 'markHired']);
|
||||||
Route::get('/workers/dashboard/views', [WorkerProfileController::class, 'getProfileViews']);
|
Route::get('/workers/dashboard/views', [WorkerProfileController::class, 'getProfileViews']);
|
||||||
Route::get('/workers/dashboard', [WorkerProfileController::class, 'getDashboard']);
|
Route::get('/workers/dashboard', [WorkerProfileController::class, 'getDashboard']);
|
||||||
|
Route::post('/workers/change-password', [WorkerProfileController::class, 'changePassword']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -110,6 +111,7 @@
|
|||||||
Route::get('/employers/profile', [EmployerProfileController::class, 'getProfile']);
|
Route::get('/employers/profile', [EmployerProfileController::class, 'getProfile']);
|
||||||
Route::post('/employers/profile/update', [EmployerProfileController::class, 'updateProfile']);
|
Route::post('/employers/profile/update', [EmployerProfileController::class, 'updateProfile']);
|
||||||
Route::get('/employers/dashboard', [EmployerProfileController::class, 'getDashboard']);
|
Route::get('/employers/dashboard', [EmployerProfileController::class, 'getDashboard']);
|
||||||
|
Route::post('/employers/change-password', [EmployerProfileController::class, 'changePassword']);
|
||||||
|
|
||||||
// Messaging Management
|
// Messaging Management
|
||||||
Route::get('/employers/conversations', [EmployerMessageController::class, 'getConversations']);
|
Route::get('/employers/conversations', [EmployerMessageController::class, 'getConversations']);
|
||||||
@ -119,6 +121,7 @@
|
|||||||
|
|
||||||
// Announcement Management
|
// Announcement Management
|
||||||
Route::get('/employers/announcements', [EmployerAnnouncementController::class, 'getAnnouncements']);
|
Route::get('/employers/announcements', [EmployerAnnouncementController::class, 'getAnnouncements']);
|
||||||
|
Route::get('/employers/my-announcements', [EmployerAnnouncementController::class, 'getMyAnnouncements']);
|
||||||
Route::post('/employers/announcements', [EmployerAnnouncementController::class, 'createAnnouncement']);
|
Route::post('/employers/announcements', [EmployerAnnouncementController::class, 'createAnnouncement']);
|
||||||
Route::delete('/employers/announcements/{id}', [EmployerAnnouncementController::class, 'deleteAnnouncement']);
|
Route::delete('/employers/announcements/{id}', [EmployerAnnouncementController::class, 'deleteAnnouncement']);
|
||||||
|
|
||||||
@ -156,7 +159,9 @@
|
|||||||
// Sponsors can ONLY access: dashboard and charity events. No payments, no worker browsing.
|
// Sponsors can ONLY access: dashboard and charity events. No payments, no worker browsing.
|
||||||
Route::middleware(['auth.sponsor'])->group(function () {
|
Route::middleware(['auth.sponsor'])->group(function () {
|
||||||
Route::get('/sponsors/profile', [SponsorController::class, 'getProfile']);
|
Route::get('/sponsors/profile', [SponsorController::class, 'getProfile']);
|
||||||
|
Route::post('/sponsors/profile/update', [SponsorController::class, 'updateProfile']);
|
||||||
Route::get('/sponsors/dashboard', [SponsorController::class, 'getDashboard']);
|
Route::get('/sponsors/dashboard', [SponsorController::class, 'getDashboard']);
|
||||||
Route::get('/sponsors/charity-events', [SponsorController::class, 'getCharityEvents']);
|
Route::get('/sponsors/charity-events', [SponsorController::class, 'getCharityEvents']);
|
||||||
Route::post('/sponsors/charity-events', [SponsorController::class, 'postCharityEvent']);
|
Route::post('/sponsors/charity-events', [SponsorController::class, 'postCharityEvent']);
|
||||||
|
Route::post('/sponsors/change-password', [SponsorController::class, 'changePassword']);
|
||||||
});
|
});
|
||||||
|
|||||||
124
tests/Feature/AdminLoginTest.php
Normal file
124
tests/Feature/AdminLoginTest.php
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AdminLoginTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin login page can be rendered.
|
||||||
|
*/
|
||||||
|
public function test_admin_can_view_login_page()
|
||||||
|
{
|
||||||
|
$response = $this->get('/admin/login');
|
||||||
|
$response->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin login validation fails if email is invalid.
|
||||||
|
*/
|
||||||
|
public function test_admin_login_fails_if_email_is_invalid()
|
||||||
|
{
|
||||||
|
$response = $this->post('/admin/login', [
|
||||||
|
'email' => 'invalid-email',
|
||||||
|
'password' => 'password',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSessionHasErrors(['email']);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Please enter a valid email address.',
|
||||||
|
session('errors')->get('email')[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin login validation fails if email is not registered.
|
||||||
|
*/
|
||||||
|
public function test_admin_login_fails_if_email_is_not_registered()
|
||||||
|
{
|
||||||
|
$response = $this->post('/admin/login', [
|
||||||
|
'email' => 'unregistered@example.com',
|
||||||
|
'password' => 'password',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSessionHasErrors(['email']);
|
||||||
|
$this->assertEquals(
|
||||||
|
'This email is not registered.',
|
||||||
|
session('errors')->get('email')[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin login validation fails if email is registered but not as an admin.
|
||||||
|
*/
|
||||||
|
public function test_admin_login_fails_if_user_is_not_an_admin()
|
||||||
|
{
|
||||||
|
User::create([
|
||||||
|
'name' => 'Regular User',
|
||||||
|
'email' => 'user@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'role' => 'employer', // Not admin
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->post('/admin/login', [
|
||||||
|
'email' => 'user@example.com',
|
||||||
|
'password' => 'password',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSessionHasErrors(['email']);
|
||||||
|
$this->assertEquals(
|
||||||
|
'This email is not registered as an admin.',
|
||||||
|
session('errors')->get('email')[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin login validation fails if password is wrong.
|
||||||
|
*/
|
||||||
|
public function test_admin_login_fails_if_password_is_incorrect()
|
||||||
|
{
|
||||||
|
User::create([
|
||||||
|
'name' => 'Admin User',
|
||||||
|
'email' => 'admin_test@example.com',
|
||||||
|
'password' => bcrypt('correct_password'),
|
||||||
|
'role' => 'admin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->post('/admin/login', [
|
||||||
|
'email' => 'admin_test@example.com',
|
||||||
|
'password' => 'wrong_password',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSessionHasErrors(['password']);
|
||||||
|
$this->assertEquals(
|
||||||
|
'The password you entered is incorrect.',
|
||||||
|
session('errors')->get('password')[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin login succeeds with correct credentials.
|
||||||
|
*/
|
||||||
|
public function test_admin_login_succeeds_with_correct_credentials()
|
||||||
|
{
|
||||||
|
$admin = User::create([
|
||||||
|
'name' => 'Admin User',
|
||||||
|
'email' => 'admin_test@example.com',
|
||||||
|
'password' => bcrypt('correct_password'),
|
||||||
|
'role' => 'admin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->post('/admin/login', [
|
||||||
|
'email' => 'admin_test@example.com',
|
||||||
|
'password' => 'correct_password',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect('/admin/dashboard');
|
||||||
|
$this->assertAuthenticatedAs($admin);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -103,9 +103,9 @@ public function test_worker_can_get_announcements()
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test employer can get only their posted announcements.
|
* Test employer can get only their posted announcements via my-announcements.
|
||||||
*/
|
*/
|
||||||
public function test_employer_can_get_posted_announcements()
|
public function test_employer_can_get_my_announcements()
|
||||||
{
|
{
|
||||||
// Announcement by this employer
|
// Announcement by this employer
|
||||||
Announcement::create([
|
Announcement::create([
|
||||||
@ -131,13 +131,66 @@ public function test_employer_can_get_posted_announcements()
|
|||||||
|
|
||||||
$response = $this->withHeaders([
|
$response = $this->withHeaders([
|
||||||
'Authorization' => 'Bearer ' . $this->employerToken,
|
'Authorization' => 'Bearer ' . $this->employerToken,
|
||||||
])->getJson('/api/employers/announcements');
|
])->getJson('/api/employers/my-announcements');
|
||||||
|
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
$this->assertCount(1, $response->json('data.announcements'));
|
$this->assertCount(1, $response->json('data.announcements'));
|
||||||
$this->assertEquals('Employer Ann', $response->json('data.announcements.0.title'));
|
$this->assertEquals('Employer Ann', $response->json('data.announcements.0.title'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test employer can get all approved announcements.
|
||||||
|
*/
|
||||||
|
public function test_employer_can_get_all_approved_announcements()
|
||||||
|
{
|
||||||
|
// Approved announcement by this employer
|
||||||
|
Announcement::create([
|
||||||
|
'title' => 'Approved Ann 1',
|
||||||
|
'body' => 'Message 1',
|
||||||
|
'type' => 'info',
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'status' => 'approved',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Pending announcement by this employer
|
||||||
|
Announcement::create([
|
||||||
|
'title' => 'Pending Ann 1',
|
||||||
|
'body' => 'Message 2',
|
||||||
|
'type' => 'info',
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'status' => 'pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Approved announcement by another sponsor
|
||||||
|
$sponsor = \App\Models\Sponsor::create([
|
||||||
|
'full_name' => 'Sponsor Name',
|
||||||
|
'email' => 'sponsor@example.com',
|
||||||
|
'mobile' => '+971501112233',
|
||||||
|
'organization_name' => 'Charity Org',
|
||||||
|
'address' => 'Dubai',
|
||||||
|
'role' => 'sponsor',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
]);
|
||||||
|
Announcement::create([
|
||||||
|
'title' => 'Approved Ann 2',
|
||||||
|
'body' => 'Message 3',
|
||||||
|
'type' => 'info',
|
||||||
|
'sponsor_id' => $sponsor->id,
|
||||||
|
'status' => 'approved',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->employerToken,
|
||||||
|
])->getJson('/api/employers/announcements');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$this->assertCount(2, $response->json('data.announcements'));
|
||||||
|
$titles = collect($response->json('data.announcements'))->pluck('title');
|
||||||
|
$this->assertTrue($titles->contains('Approved Ann 1'));
|
||||||
|
$this->assertTrue($titles->contains('Approved Ann 2'));
|
||||||
|
$this->assertFalse($titles->contains('Pending Ann 1'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test employer can create a new announcement.
|
* Test employer can create a new announcement.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -30,10 +30,9 @@ protected function setUp(): void
|
|||||||
|
|
||||||
EmployerProfile::create([
|
EmployerProfile::create([
|
||||||
'user_id' => $this->employer->id,
|
'user_id' => $this->employer->id,
|
||||||
'company_name' => 'Original Company',
|
'address' => 'Original Address',
|
||||||
'phone' => '+971501112222',
|
'phone' => '+971501112222',
|
||||||
'verification_status' => 'approved',
|
'verification_status' => 'approved',
|
||||||
'language' => 'English',
|
|
||||||
'notifications' => true,
|
'notifications' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -54,17 +53,17 @@ public function test_employer_can_get_profile()
|
|||||||
'profile' => [
|
'profile' => [
|
||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
'company_name',
|
|
||||||
'phone',
|
'phone',
|
||||||
'language',
|
'address',
|
||||||
'notifications',
|
'notifications',
|
||||||
'verification_status',
|
'verification_status',
|
||||||
|
'emirates_id',
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals('Original Name', $response->json('data.profile.name'));
|
$this->assertEquals('Original Name', $response->json('data.profile.name'));
|
||||||
$this->assertEquals('Original Company', $response->json('data.profile.company_name'));
|
$this->assertEquals('Original Address', $response->json('data.profile.address'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,8 +77,7 @@ public function test_employer_can_update_profile()
|
|||||||
'name' => 'Updated Name',
|
'name' => 'Updated Name',
|
||||||
'email' => 'employer_updated@example.com',
|
'email' => 'employer_updated@example.com',
|
||||||
'phone' => '+971509999999',
|
'phone' => '+971509999999',
|
||||||
'company_name' => 'Updated Company Corp',
|
'address' => 'Updated Address Info',
|
||||||
'language' => 'Arabic',
|
|
||||||
'notifications' => false,
|
'notifications' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -91,10 +89,10 @@ public function test_employer_can_update_profile()
|
|||||||
'profile' => [
|
'profile' => [
|
||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
'company_name',
|
|
||||||
'phone',
|
'phone',
|
||||||
'language',
|
'address',
|
||||||
'notifications',
|
'notifications',
|
||||||
|
'emirates_id',
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
@ -107,67 +105,105 @@ public function test_employer_can_update_profile()
|
|||||||
|
|
||||||
$this->assertDatabaseHas('employer_profiles', [
|
$this->assertDatabaseHas('employer_profiles', [
|
||||||
'user_id' => $this->employer->id,
|
'user_id' => $this->employer->id,
|
||||||
'company_name' => 'Updated Company Corp',
|
'address' => 'Updated Address Info',
|
||||||
'language' => 'Arabic',
|
|
||||||
'notifications' => false,
|
'notifications' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test employer can update password when providing correct current password.
|
* Test employer can update profile with optional Emirates ID fields.
|
||||||
*/
|
*/
|
||||||
public function test_employer_can_update_password()
|
public function test_employer_can_update_profile_with_emirates_id_fields()
|
||||||
{
|
{
|
||||||
$response = $this->withHeaders([
|
$response = $this->withHeaders([
|
||||||
'Authorization' => 'Bearer ' . $this->token,
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
])->postJson('/api/employers/profile/update', [
|
])->postJson('/api/employers/profile/update', [
|
||||||
'name' => 'Original Name',
|
'name' => 'Ahmad',
|
||||||
'email' => 'employer@example.com',
|
'email' => 'ahmad@example.com',
|
||||||
'phone' => '+971501112222',
|
'phone' => '+971509990001',
|
||||||
'company_name' => 'Original Company',
|
'address' => 'Villa 12, Jumeirah 2',
|
||||||
'language' => 'English',
|
|
||||||
'notifications' => true,
|
'notifications' => true,
|
||||||
'current_password' => 'Password@123',
|
'id_number' => '784-1987-5493842-5',
|
||||||
'new_password' => 'NewSecurePassword@123',
|
'card_number' => '151023946',
|
||||||
'new_password_confirmation' => 'NewSecurePassword@123',
|
'full_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
|
'date_of_birth' => '14/04/1987',
|
||||||
|
'nationality' => 'Bangladesh',
|
||||||
|
'gender' => 'M',
|
||||||
|
'issue_date' => '12/12/2025',
|
||||||
|
'expiry_date' => '11/12/2027',
|
||||||
|
'occupation' => 'Electrician',
|
||||||
|
'employer' => 'Msj International Technical Services L.L.C UAE',
|
||||||
|
'issuing_place' => 'Dubai',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
// Verify password hash updated
|
'success' => true,
|
||||||
$this->assertTrue(Hash::check('NewSecurePassword@123', $this->employer->fresh()->password));
|
'data' => [
|
||||||
}
|
'profile' => [
|
||||||
|
'name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
/**
|
'email' => 'ahmad@example.com',
|
||||||
* Test employer password update fails with incorrect current password.
|
'phone' => '+971509990001',
|
||||||
*/
|
'address' => 'Villa 12, Jumeirah 2',
|
||||||
public function test_employer_password_update_fails_with_incorrect_current_password()
|
'id_number' => '784-1987-5493842-5',
|
||||||
{
|
'card_number' => '151023946',
|
||||||
$response = $this->withHeaders([
|
'full_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
'Authorization' => 'Bearer ' . $this->token,
|
'gender' => 'M',
|
||||||
])->postJson('/api/employers/profile/update', [
|
'emirates_id' => [
|
||||||
'name' => 'Original Name',
|
'emirates_id_number' => '784-1987-5493842-5',
|
||||||
'email' => 'employer@example.com',
|
'name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
'phone' => '+971501112222',
|
'date_of_birth' => '14/04/1987',
|
||||||
'company_name' => 'Original Company',
|
'nationality' => 'Bangladesh',
|
||||||
'language' => 'English',
|
'issue_date' => '12/12/2025',
|
||||||
'notifications' => true,
|
'expiry_date' => '11/12/2027',
|
||||||
'current_password' => 'WrongCurrentPassword',
|
'employer' => 'Msj International Technical Services L.L.C UAE',
|
||||||
'new_password' => 'NewSecurePassword@123',
|
'issue_place' => 'Dubai',
|
||||||
'new_password_confirmation' => 'NewSecurePassword@123',
|
'occupation' => 'Electrician',
|
||||||
]);
|
]
|
||||||
|
]
|
||||||
$response->assertStatus(422)
|
|
||||||
->assertJsonStructure([
|
|
||||||
'success',
|
|
||||||
'message',
|
|
||||||
'errors' => [
|
|
||||||
'current_password'
|
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Verify password was NOT changed
|
$this->assertDatabaseHas('employer_profiles', [
|
||||||
$this->assertTrue(Hash::check('Password@123', $this->employer->fresh()->password));
|
'user_id' => $this->employer->id,
|
||||||
|
'emirates_id_number' => '784-1987-5493842-5',
|
||||||
|
'emirates_id_card_number' => '151023946',
|
||||||
|
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
|
'emirates_id_gender' => 'M',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test employer cannot change an existing Emirates ID number.
|
||||||
|
*/
|
||||||
|
public function test_employer_cannot_change_existing_emirates_id_number()
|
||||||
|
{
|
||||||
|
// First, set the Emirates ID
|
||||||
|
$profile = $this->employer->employerProfile;
|
||||||
|
$profile->emirates_id_number = '784-1987-5493842-5';
|
||||||
|
$profile->save();
|
||||||
|
|
||||||
|
// Attempt to change to a different ID
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
|
])->postJson('/api/employers/profile/update', [
|
||||||
|
'name' => 'Original Name',
|
||||||
|
'email' => 'employer@example.com',
|
||||||
|
'phone' => '+971501112222',
|
||||||
|
'address' => 'Original Address',
|
||||||
|
'id_number' => '784-9999-0000000-0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'id_number' => ['Emirates ID mismatch.']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify ID was NOT changed
|
||||||
|
$this->assertEquals('784-1987-5493842-5', $profile->fresh()->emirates_id_number);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -218,4 +254,121 @@ public function test_employer_dashboard_returns_only_two_charity_drives()
|
|||||||
$this->assertFalse($titles->contains('Charity Drive 1'));
|
$this->assertFalse($titles->contains('Charity Drive 1'));
|
||||||
$this->assertFalse($titles->contains('Pending Charity Drive'));
|
$this->assertFalse($titles->contains('Pending Charity Drive'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_employer_dashboard_returns_total_workers_active()
|
||||||
|
{
|
||||||
|
// Delete any existing workers seeded or created to make count precise
|
||||||
|
\App\Models\Worker::query()->delete();
|
||||||
|
|
||||||
|
// Create 3 active workers
|
||||||
|
$w1 = \App\Models\Worker::create([
|
||||||
|
'name' => 'Active 1', 'email' => 'a1@test.com', 'phone' => '+971501110001',
|
||||||
|
'password' => bcrypt('password'), 'status' => 'active', 'nationality' => 'Indian',
|
||||||
|
'preferred_location' => 'Dubai', 'age' => 25, 'salary' => 1500, 'experience' => '3 Years',
|
||||||
|
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
|
||||||
|
'verified' => true,
|
||||||
|
]);
|
||||||
|
\App\Models\WorkerDocument::create([
|
||||||
|
'worker_id' => $w1->id, 'type' => 'passport', 'number' => 'P111',
|
||||||
|
'issue_date' => '2020-01-01', 'expiry_date' => '2030-01-01',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$w2 = \App\Models\Worker::create([
|
||||||
|
'name' => 'Active 2', 'email' => 'a2@test.com', 'phone' => '+971501110002',
|
||||||
|
'password' => bcrypt('password'), 'status' => 'active', 'nationality' => 'Indian',
|
||||||
|
'preferred_location' => 'Dubai', 'age' => 26, 'salary' => 1500, 'experience' => '3 Years',
|
||||||
|
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
|
||||||
|
'verified' => true,
|
||||||
|
]);
|
||||||
|
\App\Models\WorkerDocument::create([
|
||||||
|
'worker_id' => $w2->id, 'type' => 'passport', 'number' => 'P222',
|
||||||
|
'issue_date' => '2020-01-01', 'expiry_date' => '2030-01-01',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$w3 = \App\Models\Worker::create([
|
||||||
|
'name' => 'Active 3', 'email' => 'a3@test.com', 'phone' => '+971501110003',
|
||||||
|
'password' => bcrypt('password'), 'status' => 'active', 'nationality' => 'Indian',
|
||||||
|
'preferred_location' => 'Dubai', 'age' => 27, 'salary' => 1500, 'experience' => '3 Years',
|
||||||
|
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
|
||||||
|
'verified' => true,
|
||||||
|
]);
|
||||||
|
\App\Models\WorkerDocument::create([
|
||||||
|
'worker_id' => $w3->id, 'type' => 'passport', 'number' => 'P333',
|
||||||
|
'issue_date' => '2020-01-01', 'expiry_date' => '2030-01-01',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 2 inactive/hidden workers
|
||||||
|
\App\Models\Worker::create([
|
||||||
|
'name' => 'Inactive 1', 'email' => 'i1@test.com', 'phone' => '+971502220001',
|
||||||
|
'password' => bcrypt('password'), 'status' => 'hidden', 'nationality' => 'Indian',
|
||||||
|
'preferred_location' => 'Dubai', 'age' => 28, 'salary' => 1500, 'experience' => '3 Years',
|
||||||
|
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
|
||||||
|
]);
|
||||||
|
\App\Models\Worker::create([
|
||||||
|
'name' => 'Inactive 2', 'email' => 'i2@test.com', 'phone' => '+971502220002',
|
||||||
|
'password' => bcrypt('password'), 'status' => 'Hired', 'nationality' => 'Indian',
|
||||||
|
'preferred_location' => 'Dubai', 'age' => 29, 'salary' => 1500, 'experience' => '3 Years',
|
||||||
|
'availability' => 'Immediate', 'religion' => 'Christian', 'bio' => 'experienced worker',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
|
])->getJson('/api/employers/dashboard');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$this->assertEquals(3, $response->json('data.stats.total_workers'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test employer can change password successfully.
|
||||||
|
*/
|
||||||
|
public function test_employer_can_change_password()
|
||||||
|
{
|
||||||
|
// 1. Create a matching sponsor
|
||||||
|
$sponsor = \App\Models\Sponsor::create([
|
||||||
|
'full_name' => 'Employer One',
|
||||||
|
'email' => $this->employer->email,
|
||||||
|
'mobile' => '+971509990011',
|
||||||
|
'organization_name' => 'Elite Services',
|
||||||
|
'address' => 'Dubai',
|
||||||
|
'role' => 'sponsor',
|
||||||
|
'password' => bcrypt('Password@123'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify initial password hash is set
|
||||||
|
$this->employer->update([
|
||||||
|
'password' => bcrypt('Password@123')
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. Try with wrong current password
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
|
])->postJson('/api/employers/change-password', [
|
||||||
|
'current_password' => 'WrongPassword',
|
||||||
|
'new_password' => 'NewPassword@123',
|
||||||
|
'new_password_confirmation' => 'NewPassword@123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['current_password']);
|
||||||
|
|
||||||
|
// 3. Try with correct current password
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
|
])->postJson('/api/employers/change-password', [
|
||||||
|
'current_password' => 'Password@123',
|
||||||
|
'new_password' => 'NewPassword@123',
|
||||||
|
'new_password_confirmation' => 'NewPassword@123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Password changed successfully.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify password is updated for both User and Sponsor
|
||||||
|
$this->assertTrue(\Illuminate\Support\Facades\Hash::check('NewPassword@123', $this->employer->fresh()->password));
|
||||||
|
$this->assertTrue(\Illuminate\Support\Facades\Hash::check('NewPassword@123', $sponsor->fresh()->password));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -435,11 +435,184 @@ public function test_worker_list_includes_document_expiry_status()
|
|||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
$workers = $response->json('data.workers');
|
$workers = $response->json('data.workers');
|
||||||
|
|
||||||
// Find the worker we just created in the response
|
|
||||||
$matchingWorker = collect($workers)->firstWhere('id', $worker->id);
|
$matchingWorker = collect($workers)->firstWhere('id', $worker->id);
|
||||||
|
|
||||||
$this->assertNotNull($matchingWorker);
|
$this->assertNotNull($matchingWorker);
|
||||||
$this->assertEquals(45, $matchingWorker['document_expiry_days']);
|
$this->assertEquals(45, $matchingWorker['document_expiry_days']);
|
||||||
$this->assertEquals('Visa expires in 45 days', $matchingWorker['document_expiry_status']);
|
$this->assertEquals('Visa expires in 45 days', $matchingWorker['document_expiry_status']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_worker_list_api_has_full_details_and_pagination()
|
||||||
|
{
|
||||||
|
$worker = Worker::create([
|
||||||
|
'name' => 'Full Detail Worker',
|
||||||
|
'email' => 'full_detail@example.com',
|
||||||
|
'phone' => '+971509999888',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
'preferred_location' => 'Dubai Marina',
|
||||||
|
'live_in_out' => 'live_out',
|
||||||
|
'gender' => 'male',
|
||||||
|
'in_country' => true,
|
||||||
|
'visa_status' => 'Tourist Visa',
|
||||||
|
'preferred_job_type' => 'full-time',
|
||||||
|
'fcm_token' => 'fcm_token_example',
|
||||||
|
'age' => 36,
|
||||||
|
'salary' => 1500.00,
|
||||||
|
'experience' => '4 Years',
|
||||||
|
'language' => 'English, Arabic',
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => 'Experienced helper',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$doc1 = WorkerDocument::create([
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'type' => 'passport',
|
||||||
|
'number' => 'YB98239279327',
|
||||||
|
'issue_date' => '2017-07-05',
|
||||||
|
'expiry_date' => '2022-07-05',
|
||||||
|
'ocr_accuracy' => 99,
|
||||||
|
'ocr_data' => ['authority' => 'FMP LLC'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$doc2 = WorkerDocument::create([
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'type' => 'visa',
|
||||||
|
'number' => '343434',
|
||||||
|
'issue_date' => '2022-02-16',
|
||||||
|
'expiry_date' => '2024-02-15',
|
||||||
|
'ocr_accuracy' => 99,
|
||||||
|
'ocr_data' => ['sponsor' => 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add a skill
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Ironing']);
|
||||||
|
$worker->skills()->attach($skill->id);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
|
])->getJson('/api/employers/workers?page=1&per_page=15');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonStructure([
|
||||||
|
'success',
|
||||||
|
'data' => [
|
||||||
|
'workers',
|
||||||
|
'pagination' => [
|
||||||
|
'total',
|
||||||
|
'per_page',
|
||||||
|
'current_page',
|
||||||
|
'last_page',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$workers = $response->json('data.workers');
|
||||||
|
$matchingWorker = collect($workers)->firstWhere('id', $worker->id);
|
||||||
|
|
||||||
|
$this->assertNotNull($matchingWorker);
|
||||||
|
$this->assertEquals('+971509999888', $matchingWorker['phone']);
|
||||||
|
$this->assertEquals('1500.00', $matchingWorker['salary']);
|
||||||
|
$this->assertEquals('active', $matchingWorker['status']);
|
||||||
|
$this->assertEquals('English, Arabic', $matchingWorker['language']);
|
||||||
|
$this->assertEquals('fcm_token_example', $matchingWorker['fcm_token']);
|
||||||
|
$this->assertEquals('2024-02-15', $matchingWorker['visa_expiry_date']);
|
||||||
|
|
||||||
|
// Check skills object array
|
||||||
|
$this->assertIsArray($matchingWorker['skills']);
|
||||||
|
$this->assertCount(1, $matchingWorker['skills']);
|
||||||
|
$this->assertEquals('Ironing', $matchingWorker['skills'][0]['name']);
|
||||||
|
|
||||||
|
// Check documents object array
|
||||||
|
$this->assertIsArray($matchingWorker['documents']);
|
||||||
|
$this->assertCount(2, $matchingWorker['documents']);
|
||||||
|
$this->assertEquals('passport', $matchingWorker['documents'][0]['type']);
|
||||||
|
$this->assertEquals('visa', $matchingWorker['documents'][1]['type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_worker_detail_api_has_full_details_without_religion_and_cleaned_ocr_data()
|
||||||
|
{
|
||||||
|
$worker = Worker::create([
|
||||||
|
'name' => 'Detail Worker Test',
|
||||||
|
'email' => 'detail_test@example.com',
|
||||||
|
'phone' => '+971509999777',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
'preferred_location' => 'Dubai Marina',
|
||||||
|
'live_in_out' => 'live_out',
|
||||||
|
'gender' => 'male',
|
||||||
|
'in_country' => true,
|
||||||
|
'visa_status' => 'Tourist Visa',
|
||||||
|
'preferred_job_type' => 'full-time',
|
||||||
|
'fcm_token' => 'fcm_token_example',
|
||||||
|
'age' => 36,
|
||||||
|
'salary' => 1500.00,
|
||||||
|
'experience' => '4 Years',
|
||||||
|
'language' => 'English, Arabic',
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => 'Experienced helper',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$doc1 = WorkerDocument::create([
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'type' => 'passport',
|
||||||
|
'number' => 'YB98239279327',
|
||||||
|
'issue_date' => '2017-07-05',
|
||||||
|
'expiry_date' => '2022-07-05',
|
||||||
|
'ocr_accuracy' => 99,
|
||||||
|
'ocr_data' => ['authority' => 'FMP LLC'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$doc2 = WorkerDocument::create([
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'type' => 'visa',
|
||||||
|
'number' => '343434',
|
||||||
|
'issue_date' => '2022-02-16',
|
||||||
|
'expiry_date' => '2024-02-15',
|
||||||
|
'ocr_accuracy' => 99,
|
||||||
|
'ocr_data' => [
|
||||||
|
'sponsor' => 'some sponsor',
|
||||||
|
'sponsor_name' => 'some sponsor name',
|
||||||
|
'valid_until' => '2024-02-15',
|
||||||
|
'expiry_date' => '2024-02-15'
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
|
])->getJson('/api/employers/workers/' . $worker->id);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$wData = $response->json('data.worker');
|
||||||
|
|
||||||
|
$this->assertNotNull($wData);
|
||||||
|
$this->assertEquals('+971509999777', $wData['phone']);
|
||||||
|
$this->assertEquals('detail_test@example.com', $wData['email']);
|
||||||
|
$this->assertEquals('1500.00', $wData['salary']);
|
||||||
|
$this->assertEquals('active', $wData['status']);
|
||||||
|
$this->assertEquals('English, Arabic', $wData['language']);
|
||||||
|
$this->assertEquals('fcm_token_example', $wData['fcm_token']);
|
||||||
|
$this->assertEquals('2024-02-15', $wData['visa_expiry_date']);
|
||||||
|
|
||||||
|
// Assert religion is removed
|
||||||
|
$this->assertArrayNotHasKey('religion', $wData);
|
||||||
|
|
||||||
|
// Check documents object array
|
||||||
|
$this->assertIsArray($wData['documents']);
|
||||||
|
$this->assertCount(2, $wData['documents']);
|
||||||
|
|
||||||
|
// Assert visa ocr_data is cleaned up
|
||||||
|
$visaDoc = collect($wData['documents'])->firstWhere('type', 'visa');
|
||||||
|
$this->assertNotNull($visaDoc);
|
||||||
|
$this->assertArrayNotHasKey('sponsor', $visaDoc['ocr_data']);
|
||||||
|
$this->assertArrayNotHasKey('valid_until', $visaDoc['ocr_data']);
|
||||||
|
$this->assertEquals('some sponsor name', $visaDoc['ocr_data']['sponsor_name']);
|
||||||
|
$this->assertEquals('2024-02-15', $visaDoc['ocr_data']['expiry_date']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,6 +54,7 @@ public function test_employer_registration_and_login_stores_fcm_token_and_sends_
|
|||||||
'fcm_token' => 'employer_register_fcm_token',
|
'fcm_token' => 'employer_register_fcm_token',
|
||||||
'emirates_id' => [
|
'emirates_id' => [
|
||||||
'emirates_id_number' => '784-1988-5310327-2',
|
'emirates_id_number' => '784-1988-5310327-2',
|
||||||
|
'name' => 'Employer FCM Test',
|
||||||
'expiry_date' => '2028-04-11',
|
'expiry_date' => '2028-04-11',
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
@ -182,6 +183,7 @@ public function test_sponsor_registration_and_login_stores_fcm_token_and_sends_n
|
|||||||
],
|
],
|
||||||
'emirates_id' => [
|
'emirates_id' => [
|
||||||
'emirates_id_number' => '784-1988-5310327-2',
|
'emirates_id_number' => '784-1988-5310327-2',
|
||||||
|
'name' => 'Sponsor FCM Org',
|
||||||
],
|
],
|
||||||
'organization_name' => 'Sponsor FCM Co',
|
'organization_name' => 'Sponsor FCM Co',
|
||||||
'email' => 'sponsor.fcm@example.com',
|
'email' => 'sponsor.fcm@example.com',
|
||||||
|
|||||||
@ -276,6 +276,7 @@ public function test_get_report_reasons_filtering()
|
|||||||
['reason' => 'Review Abuse', 'status' => 'Active', 'type' => 'Review'],
|
['reason' => 'Review Abuse', 'status' => 'Active', 'type' => 'Review'],
|
||||||
['reason' => 'General Impersonation', 'status' => 'Active', 'type' => 'Both'],
|
['reason' => 'General Impersonation', 'status' => 'Active', 'type' => 'Both'],
|
||||||
['reason' => 'Inactive Reason', 'status' => 'Inactive', 'type' => 'Both'],
|
['reason' => 'Inactive Reason', 'status' => 'Inactive', 'type' => 'Both'],
|
||||||
|
['reason' => 'Billing Issue', 'status' => 'Active', 'type' => 'Support'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 1. Worker retrieves all Active reasons
|
// 1. Worker retrieves all Active reasons
|
||||||
@ -284,33 +285,43 @@ public function test_get_report_reasons_filtering()
|
|||||||
])->getJson('/api/workers/report-reasons');
|
])->getJson('/api/workers/report-reasons');
|
||||||
|
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJsonCount(3, 'reasons')
|
->assertJsonCount(4, 'reasons')
|
||||||
->assertJsonFragment(['reason' => 'Chat Spam'])
|
->assertJsonFragment(['reason' => 'Chat Spam'])
|
||||||
->assertJsonFragment(['reason' => 'Review Abuse'])
|
->assertJsonFragment(['reason' => 'Review Abuse'])
|
||||||
->assertJsonFragment(['reason' => 'General Impersonation'])
|
->assertJsonFragment(['reason' => 'General Impersonation'])
|
||||||
|
->assertJsonFragment(['reason' => 'Billing Issue'])
|
||||||
->assertJsonMissing(['reason' => 'Inactive Reason']);
|
->assertJsonMissing(['reason' => 'Inactive Reason']);
|
||||||
|
|
||||||
// 2. Worker retrieves Chat reasons only
|
// 2. Worker retrieves Chat reasons only (case-insensitive)
|
||||||
$responseChat = $this->withHeaders([
|
$responseChat = $this->withHeaders([
|
||||||
'Authorization' => 'Bearer ' . $this->workerToken,
|
'Authorization' => 'Bearer ' . $this->workerToken,
|
||||||
])->getJson('/api/workers/report-reasons?type=Chat');
|
])->getJson('/api/workers/report-reasons?type=chat');
|
||||||
|
|
||||||
$responseChat->assertStatus(200)
|
$responseChat->assertStatus(200)
|
||||||
->assertJsonCount(2, 'reasons')
|
->assertJsonCount(2, 'reasons')
|
||||||
->assertJsonFragment(['reason' => 'Chat Spam'])
|
->assertJsonFragment(['reason' => 'Chat Spam'])
|
||||||
->assertJsonFragment(['reason' => 'General Impersonation'])
|
->assertJsonFragment(['reason' => 'General Impersonation'])
|
||||||
->assertJsonMissing(['reason' => 'Review Abuse']);
|
->assertJsonMissing(['reason' => 'Review Abuse'])
|
||||||
|
->assertJsonMissing(['reason' => 'Billing Issue']);
|
||||||
|
|
||||||
// 3. Worker retrieves Review reasons only
|
// 3. Worker retrieves Review reasons (unsupported type, ignored)
|
||||||
$responseReview = $this->withHeaders([
|
$responseReview = $this->withHeaders([
|
||||||
'Authorization' => 'Bearer ' . $this->workerToken,
|
'Authorization' => 'Bearer ' . $this->workerToken,
|
||||||
])->getJson('/api/workers/report-reasons?type=Review');
|
])->getJson('/api/workers/report-reasons?type=Review');
|
||||||
|
|
||||||
$responseReview->assertStatus(200)
|
$responseReview->assertStatus(200)
|
||||||
->assertJsonCount(2, 'reasons')
|
->assertJsonCount(4, 'reasons');
|
||||||
->assertJsonFragment(['reason' => 'Review Abuse'])
|
|
||||||
->assertJsonFragment(['reason' => 'General Impersonation'])
|
// 4. Worker retrieves Support reasons only (case-insensitive)
|
||||||
->assertJsonMissing(['reason' => 'Chat Spam']);
|
$responseSupport = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $this->workerToken,
|
||||||
|
])->getJson('/api/workers/report-reasons?type=support');
|
||||||
|
|
||||||
|
$responseSupport->assertStatus(200)
|
||||||
|
->assertJsonCount(1, 'reasons')
|
||||||
|
->assertJsonFragment(['reason' => 'Billing Issue'])
|
||||||
|
->assertJsonMissing(['reason' => 'Chat Spam'])
|
||||||
|
->assertJsonMissing(['reason' => 'General Impersonation']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -71,7 +71,7 @@ public function test_sponsor_can_register_with_license()
|
|||||||
|
|
||||||
$this->assertDatabaseHas('sponsors', [
|
$this->assertDatabaseHas('sponsors', [
|
||||||
'mobile' => '+971509990001',
|
'mobile' => '+971509990001',
|
||||||
'full_name' => 'Test Sponsor Organization',
|
'full_name' => 'Ahmad Bin Ahmed',
|
||||||
'license_expiry' => '2028-06-12 00:00:00',
|
'license_expiry' => '2028-06-12 00:00:00',
|
||||||
'emirates_id_name' => 'Ahmad Bin Ahmed',
|
'emirates_id_name' => 'Ahmad Bin Ahmed',
|
||||||
'emirates_id_dob' => '1990-01-01',
|
'emirates_id_dob' => '1990-01-01',
|
||||||
@ -537,6 +537,7 @@ public function test_sponsor_registration_with_emirates_id_and_separate_license(
|
|||||||
'country_code' => '+971',
|
'country_code' => '+971',
|
||||||
'emirates_id' => [
|
'emirates_id' => [
|
||||||
'emirates_id_number' => '784-1988-5310327-2',
|
'emirates_id_number' => '784-1988-5310327-2',
|
||||||
|
'name' => 'Test Sponsor Sep',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -571,4 +572,313 @@ public function test_sponsor_registration_with_emirates_id_and_separate_license(
|
|||||||
$sponsor->refresh();
|
$sponsor->refresh();
|
||||||
$this->assertEquals('2028-06-12 00:00:00', $sponsor->license_expiry->toDateTimeString());
|
$this->assertEquals('2028-06-12 00:00:00', $sponsor->license_expiry->toDateTimeString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test worker forgot password user not found error.
|
||||||
|
*/
|
||||||
|
public function test_worker_forgot_password_user_not_found()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/workers/forgot-password', [
|
||||||
|
'phone' => '+971501111111',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(404)
|
||||||
|
->assertJson([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'user not found this mobile number',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test sponsor forgot password user not found error.
|
||||||
|
*/
|
||||||
|
public function test_sponsor_forgot_password_user_not_found()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/sponsors/forgot-password', [
|
||||||
|
'email' => 'nonexistent.sponsor@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(404)
|
||||||
|
->assertJson([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'user not found this email id',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test sponsor can change password successfully.
|
||||||
|
*/
|
||||||
|
public function test_sponsor_can_change_password()
|
||||||
|
{
|
||||||
|
$sponsor = Sponsor::create([
|
||||||
|
'full_name' => 'Sponsor Changer',
|
||||||
|
'email' => 'changer.sponsor@example.com',
|
||||||
|
'mobile' => '+971509997788',
|
||||||
|
'password' => Hash::make('Password@123'),
|
||||||
|
'api_token' => 'sponsor-changer-token',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sync with corresponding employer user record to test sync
|
||||||
|
$user = User::create([
|
||||||
|
'name' => 'Sponsor Changer',
|
||||||
|
'email' => 'changer.sponsor@example.com',
|
||||||
|
'password' => Hash::make('Password@123'),
|
||||||
|
'role' => 'employer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 1. Try with wrong current password
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer sponsor-changer-token',
|
||||||
|
])->postJson('/api/sponsors/change-password', [
|
||||||
|
'current_password' => 'WrongPassword',
|
||||||
|
'new_password' => 'NewPassword@123',
|
||||||
|
'new_password_confirmation' => 'NewPassword@123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['current_password']);
|
||||||
|
|
||||||
|
// 2. Try with correct current password
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer sponsor-changer-token',
|
||||||
|
])->postJson('/api/sponsors/change-password', [
|
||||||
|
'current_password' => 'Password@123',
|
||||||
|
'new_password' => 'NewPassword@123',
|
||||||
|
'new_password_confirmation' => 'NewPassword@123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Password changed successfully.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify password is updated for both Sponsor and User
|
||||||
|
$this->assertTrue(Hash::check('NewPassword@123', $sponsor->fresh()->password));
|
||||||
|
$this->assertTrue(Hash::check('NewPassword@123', $user->fresh()->password));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test sponsor can update profile and it correctly syncs with matching user/employer profile.
|
||||||
|
*/
|
||||||
|
public function test_sponsor_can_update_profile_and_syncs_with_matching_user()
|
||||||
|
{
|
||||||
|
$sponsor = Sponsor::create([
|
||||||
|
'full_name' => 'Original Sponsor Name',
|
||||||
|
'email' => 'sync-test@example.com',
|
||||||
|
'mobile' => '+971500000001',
|
||||||
|
'password' => Hash::make('Password@123'),
|
||||||
|
'organization_name' => 'Original Org',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
'address' => 'Old Address',
|
||||||
|
'nationality' => 'Emirati',
|
||||||
|
'country_code' => '+971',
|
||||||
|
'api_token' => 'sync-test-token',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => 'Original Sponsor Name',
|
||||||
|
'email' => 'sync-test@example.com',
|
||||||
|
'password' => Hash::make('Password@123'),
|
||||||
|
'role' => 'employer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$profile = EmployerProfile::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'company_name' => 'Original Org',
|
||||||
|
'phone' => '+971500000001',
|
||||||
|
'address' => 'Old Address',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer sync-test-token',
|
||||||
|
])->postJson('/api/sponsors/profile/update', [
|
||||||
|
'name' => 'Updated Sponsor Name',
|
||||||
|
'email' => 'new-sync-test@example.com',
|
||||||
|
'mobile' => '+971500000002',
|
||||||
|
'organization_name' => 'Updated Org',
|
||||||
|
'city' => 'Abu Dhabi',
|
||||||
|
'address' => 'New Address',
|
||||||
|
'nationality' => 'Jordanian',
|
||||||
|
'country_code' => '+962',
|
||||||
|
'emirates_id' => [
|
||||||
|
'emirates_id_number' => '784-2000-1234567-1',
|
||||||
|
'name' => 'Updated Sponsor Name',
|
||||||
|
'date_of_birth' => '2000-01-01',
|
||||||
|
'expiry_date' => '2028-01-01',
|
||||||
|
'issue_date' => '2023-01-01',
|
||||||
|
'occupation' => 'Business Owner',
|
||||||
|
'employer' => 'Self',
|
||||||
|
'issue_place' => 'Abu Dhabi',
|
||||||
|
],
|
||||||
|
'license' => [
|
||||||
|
'document_type' => 'dubai_commercial_license',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'authority' => 'Dubai Economy and Tourism',
|
||||||
|
'license_no' => '828302',
|
||||||
|
'company_name' => 'MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C',
|
||||||
|
'business_name' => 'MSJ INTERNATIONAL TECHNICAL SERVICES L.L.C',
|
||||||
|
'license_category' => 'Dep. of Economic Development',
|
||||||
|
'legal_type' => 'Limited Liability Company Single Owner(LLC- - SO)',
|
||||||
|
'main_license_no' => '828302',
|
||||||
|
'register_no' => '1656486',
|
||||||
|
'dcci_no' => '317412',
|
||||||
|
'members' => [
|
||||||
|
[
|
||||||
|
'person_no' => '752967',
|
||||||
|
'name' => 'Lo lo wallus issued in',
|
||||||
|
'nationality' => 'India',
|
||||||
|
'role' => 'Shares Owner Who'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Profile updated successfully.',
|
||||||
|
])
|
||||||
|
->assertJsonPath('data.sponsor.full_name', 'Updated Sponsor Name')
|
||||||
|
->assertJsonPath('data.sponsor.email', 'new-sync-test@example.com')
|
||||||
|
->assertJsonPath('data.sponsor.mobile', '+971500000002')
|
||||||
|
->assertJsonPath('data.sponsor.organization_name', 'Updated Org')
|
||||||
|
->assertJsonPath('data.sponsor.city', 'Abu Dhabi')
|
||||||
|
->assertJsonPath('data.sponsor.address', 'New Address')
|
||||||
|
->assertJsonPath('data.sponsor.nationality', 'Jordanian')
|
||||||
|
->assertJsonPath('data.sponsor.country_code', '+962')
|
||||||
|
->assertJsonPath('data.sponsor.license.document_type', 'dubai_commercial_license')
|
||||||
|
->assertJsonPath('data.sponsor.license.license_no', '828302')
|
||||||
|
->assertJsonPath('data.sponsor.license.members.0.name', 'Lo lo wallus issued in')
|
||||||
|
->assertJsonPath('data.sponsor.emirates_id.emirates_id_number', '784-2000-1234567-1')
|
||||||
|
->assertJsonPath('data.sponsor.emirates_id.name', 'Updated Sponsor Name')
|
||||||
|
->assertJsonPath('data.sponsor.emirates_id.occupation', 'Business Owner')
|
||||||
|
->assertJsonPath('data.sponsor.emirates_id.employer', 'Self');
|
||||||
|
|
||||||
|
// Check Sponsor table
|
||||||
|
$this->assertDatabaseHas('sponsors', [
|
||||||
|
'id' => $sponsor->id,
|
||||||
|
'full_name' => 'Updated Sponsor Name',
|
||||||
|
'email' => 'new-sync-test@example.com',
|
||||||
|
'mobile' => '+971500000002',
|
||||||
|
'emirates_id' => '784-2000-1234567-1',
|
||||||
|
'emirates_id_name' => 'Updated Sponsor Name',
|
||||||
|
'emirates_id_dob' => '2000-01-01',
|
||||||
|
'emirates_id_issue_date' => '2023-01-01',
|
||||||
|
'emirates_id_expiry' => '2028-01-01',
|
||||||
|
'emirates_id_occupation' => 'Business Owner',
|
||||||
|
'emirates_id_employer' => 'Self',
|
||||||
|
'emirates_id_issue_place' => 'Abu Dhabi',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$dbSponsor = Sponsor::find($sponsor->id);
|
||||||
|
$this->assertEquals('dubai_commercial_license', $dbSponsor->license_data['document_type']);
|
||||||
|
$this->assertEquals('Lo lo wallus issued in', $dbSponsor->license_data['members'][0]['name']);
|
||||||
|
|
||||||
|
// Check User table
|
||||||
|
$this->assertDatabaseHas('users', [
|
||||||
|
'id' => $user->id,
|
||||||
|
'name' => 'Updated Sponsor Name',
|
||||||
|
'email' => 'new-sync-test@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check EmployerProfile table
|
||||||
|
$this->assertDatabaseHas('employer_profiles', [
|
||||||
|
'id' => $profile->id,
|
||||||
|
'address' => 'New Address',
|
||||||
|
'phone' => '+971500000002',
|
||||||
|
'emirates_id_number' => '784-2000-1234567-1',
|
||||||
|
'emirates_id_name' => 'Updated Sponsor Name',
|
||||||
|
'emirates_id_dob' => '2000-01-01',
|
||||||
|
'emirates_id_issue_date' => '2023-01-01',
|
||||||
|
'emirates_id_expiry' => '2028-01-01',
|
||||||
|
'emirates_id_occupation' => 'Business Owner',
|
||||||
|
'emirates_id_employer' => 'Self',
|
||||||
|
'emirates_id_issue_place' => 'Abu Dhabi',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test sponsor cannot change immutable Emirates ID.
|
||||||
|
*/
|
||||||
|
public function test_sponsor_cannot_change_immutable_emirates_id()
|
||||||
|
{
|
||||||
|
$sponsor = Sponsor::create([
|
||||||
|
'full_name' => 'Sponsor Name',
|
||||||
|
'email' => 'immutable-id-test@example.com',
|
||||||
|
'mobile' => '+971500000003',
|
||||||
|
'password' => Hash::make('Password@123'),
|
||||||
|
'organization_name' => 'Org',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
'address' => 'Address',
|
||||||
|
'nationality' => 'Emirati',
|
||||||
|
'country_code' => '+971',
|
||||||
|
'api_token' => 'immutable-id-token',
|
||||||
|
'status' => 'active',
|
||||||
|
'emirates_id' => '784-2000-1234567-1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer immutable-id-token',
|
||||||
|
])->postJson('/api/sponsors/profile/update', [
|
||||||
|
'name' => 'Sponsor Name',
|
||||||
|
'email' => 'immutable-id-test@example.com',
|
||||||
|
'mobile' => '+971500000003',
|
||||||
|
'organization_name' => 'Org',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
'address' => 'Address',
|
||||||
|
'nationality' => 'Emirati',
|
||||||
|
'country_code' => '+971',
|
||||||
|
'id_number' => '784-2000-7654321-2', // different EID
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJson([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Emirates ID mismatch. You cannot change your registered Emirates ID number.',
|
||||||
|
])
|
||||||
|
->assertJsonValidationErrors(['id_number']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test sponsor profile update validates Emirates ID name OCR.
|
||||||
|
*/
|
||||||
|
public function test_sponsor_profile_update_validates_emirates_id_name_ocr()
|
||||||
|
{
|
||||||
|
$sponsor = Sponsor::create([
|
||||||
|
'full_name' => 'Sponsor Name',
|
||||||
|
'email' => 'ocr-test@example.com',
|
||||||
|
'mobile' => '+971500000004',
|
||||||
|
'password' => Hash::make('Password@123'),
|
||||||
|
'organization_name' => 'Org',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
'address' => 'Address',
|
||||||
|
'nationality' => 'Emirati',
|
||||||
|
'country_code' => '+971',
|
||||||
|
'api_token' => 'ocr-token',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer ocr-token',
|
||||||
|
])->postJson('/api/sponsors/profile/update', [
|
||||||
|
'name' => 'Sponsor Name',
|
||||||
|
'email' => 'ocr-test@example.com',
|
||||||
|
'mobile' => '+971500000004',
|
||||||
|
'organization_name' => 'Org',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
'address' => 'Address',
|
||||||
|
'nationality' => 'Emirati',
|
||||||
|
'country_code' => '+971',
|
||||||
|
'id_number' => '784-2000-1234567-1',
|
||||||
|
'full_name' => '', // Failed OCR name extraction
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['full_name'])
|
||||||
|
->assertJsonPath('errors.full_name.0', 'Failed to extract name from the Emirates ID. Please upload a clearer document.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -80,24 +80,42 @@ public function test_employer_web_registration_with_emirates_id()
|
|||||||
$this->assertDatabaseHas('users', [
|
$this->assertDatabaseHas('users', [
|
||||||
'email' => 'abdullah@example.com',
|
'email' => 'abdullah@example.com',
|
||||||
'role' => 'employer',
|
'role' => 'employer',
|
||||||
|
'name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('employer_profiles', [
|
$this->assertDatabaseHas('employer_profiles', [
|
||||||
'phone' => '501234567',
|
'phone' => '501234567',
|
||||||
'company_name' => 'Abdullah Ahmed Household',
|
'company_name' => 'Mohammad Jobaier Mohammad Abul Kalam Household',
|
||||||
'emirates_id_front' => null, // File is not stored
|
'emirates_id_front' => null, // File is not stored
|
||||||
'address' => 'Villa 14, Al Safa, Dubai',
|
'address' => 'Villa 14, Al Safa, Dubai',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$profile = EmployerProfile::where('phone', '501234567')->first();
|
$profile = EmployerProfile::where('phone', '501234567')->first();
|
||||||
$this->assertNotNull($profile->emirates_id_number);
|
$this->assertEquals('784-1987-5493842-5', $profile->emirates_id_number);
|
||||||
$this->assertNotNull($profile->emirates_id_expiry);
|
$this->assertEquals('2027-12-11', $profile->emirates_id_expiry);
|
||||||
|
$this->assertEquals('Mohammad Jobaier Mohammad Abul Kalam', $profile->emirates_id_name);
|
||||||
|
$this->assertEquals('1987-04-14', $profile->emirates_id_dob);
|
||||||
|
$this->assertEquals('Bangladesh', $profile->nationality);
|
||||||
|
$this->assertEquals('2025-12-12', $profile->emirates_id_issue_date);
|
||||||
|
$this->assertEquals('Msj International Technical Services L.L.C UAE', $profile->emirates_id_employer);
|
||||||
|
$this->assertEquals('Dubai', $profile->emirates_id_issue_place);
|
||||||
|
$this->assertEquals('Electrician', $profile->emirates_id_occupation);
|
||||||
|
|
||||||
$this->assertDatabaseHas('sponsors', [
|
$this->assertDatabaseHas('sponsors', [
|
||||||
'email' => 'abdullah@example.com',
|
'email' => 'abdullah@example.com',
|
||||||
'mobile' => '501234567',
|
'mobile' => '501234567',
|
||||||
'emirates_id_file' => null, // File is not stored
|
'emirates_id_file' => null, // File is not stored
|
||||||
'address' => 'Villa 14, Al Safa, Dubai',
|
'address' => 'Villa 14, Al Safa, Dubai',
|
||||||
|
'emirates_id' => '784-1987-5493842-5',
|
||||||
|
'full_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
|
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
|
'emirates_id_dob' => '1987-04-14',
|
||||||
|
'nationality' => 'Bangladesh',
|
||||||
|
'emirates_id_issue_date' => '2025-12-12',
|
||||||
|
'emirates_id_expiry' => '2027-12-11',
|
||||||
|
'emirates_id_employer' => 'Msj International Technical Services L.L.C UAE',
|
||||||
|
'emirates_id_issue_place' => 'Dubai',
|
||||||
|
'emirates_id_occupation' => 'Electrician',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,8 +162,8 @@ public function test_employer_web_registration_with_two_sided_emirates_id()
|
|||||||
$this->assertTrue(session('employer_emirates_id_uploaded'));
|
$this->assertTrue(session('employer_emirates_id_uploaded'));
|
||||||
|
|
||||||
$pendingRegAfterId = session('pending_employer_registration');
|
$pendingRegAfterId = session('pending_employer_registration');
|
||||||
$this->assertEquals('784-1988-5310327-2', $pendingRegAfterId['emirates_id_number']); // Mock fallback value
|
$this->assertEquals('784-1987-5493842-5', $pendingRegAfterId['emirates_id_number']); // Mock fallback value
|
||||||
$this->assertEquals('2028-04-11', $pendingRegAfterId['emirates_id_expiry']); // Mock fallback value
|
$this->assertEquals('2027-12-11', $pendingRegAfterId['emirates_id_expiry']); // Mock fallback value
|
||||||
|
|
||||||
// Step 4: Payment Choice
|
// Step 4: Payment Choice
|
||||||
$responseStep4 = $this->post('/employer/register-payment', [
|
$responseStep4 = $this->post('/employer/register-payment', [
|
||||||
@ -168,12 +186,32 @@ public function test_employer_web_registration_with_two_sided_emirates_id()
|
|||||||
$this->assertDatabaseHas('users', [
|
$this->assertDatabaseHas('users', [
|
||||||
'email' => 'fatima@example.com',
|
'email' => 'fatima@example.com',
|
||||||
'role' => 'employer',
|
'role' => 'employer',
|
||||||
|
'name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('employer_profiles', [
|
||||||
|
'phone' => '509876543',
|
||||||
|
'company_name' => 'Mohammad Jobaier Mohammad Abul Kalam Household',
|
||||||
|
'address' => 'Apartment 402, Al Nahda, Sharjah',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('sponsors', [
|
||||||
|
'email' => 'fatima@example.com',
|
||||||
|
'mobile' => '509876543',
|
||||||
|
'full_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$profile = EmployerProfile::where('phone', '509876543')->first();
|
$profile = EmployerProfile::where('phone', '509876543')->first();
|
||||||
$this->assertNotNull($profile);
|
$this->assertNotNull($profile);
|
||||||
$this->assertEquals('784-1988-5310327-2', $profile->emirates_id_number);
|
$this->assertEquals('784-1987-5493842-5', $profile->emirates_id_number);
|
||||||
$this->assertEquals('2028-04-11', $profile->emirates_id_expiry);
|
$this->assertEquals('2027-12-11', $profile->emirates_id_expiry);
|
||||||
|
$this->assertEquals('Mohammad Jobaier Mohammad Abul Kalam', $profile->emirates_id_name);
|
||||||
|
$this->assertEquals('1987-04-14', $profile->emirates_id_dob);
|
||||||
|
$this->assertEquals('Bangladesh', $profile->nationality);
|
||||||
|
$this->assertEquals('2025-12-12', $profile->emirates_id_issue_date);
|
||||||
|
$this->assertEquals('Msj International Technical Services L.L.C UAE', $profile->emirates_id_employer);
|
||||||
|
$this->assertEquals('Dubai', $profile->emirates_id_issue_place);
|
||||||
|
$this->assertEquals('Electrician', $profile->emirates_id_occupation);
|
||||||
$this->assertEquals('Apartment 402, Al Nahda, Sharjah', $profile->address);
|
$this->assertEquals('Apartment 402, Al Nahda, Sharjah', $profile->address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -531,9 +531,110 @@ public function test_register_passport_and_visa_with_direct_json_payload()
|
|||||||
|
|
||||||
$visaDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'visa')->first();
|
$visaDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'visa')->first();
|
||||||
$this->assertNotNull($visaDoc->ocr_data);
|
$this->assertNotNull($visaDoc->ocr_data);
|
||||||
$this->assertEquals('784198839607839', $visaDoc->ocr_data['id_number']);
|
$this->assertEquals('784198839607839', $visaDoc->ocr_data['uid_no']);
|
||||||
$this->assertEquals('CLEANER', $visaDoc->ocr_data['profession']);
|
$this->assertEquals('CLEANER', $visaDoc->ocr_data['profession']);
|
||||||
$this->assertEquals('Sponsor Name', $visaDoc->ocr_data['sponsor']);
|
$this->assertEquals([
|
||||||
|
'name' => 'Sponsor Name',
|
||||||
|
'address' => '',
|
||||||
|
'phone' => '',
|
||||||
|
], $visaDoc->ocr_data['sponsor']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test worker registration with the specific passport fields and ocr_accuracy.
|
||||||
|
*/
|
||||||
|
public function test_register_passport_with_full_ocr_fields()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/workers/register', [
|
||||||
|
'name' => 'Temp Name',
|
||||||
|
'phone' => '+971507777777',
|
||||||
|
'salary' => 2000,
|
||||||
|
'password' => 'secret123',
|
||||||
|
'language' => 'HI',
|
||||||
|
'passport' => [
|
||||||
|
'document_type' => 'P',
|
||||||
|
'issuing_country' => 'HAS',
|
||||||
|
'surname' => 'KHALED',
|
||||||
|
'given_names' => 'KHALED AL',
|
||||||
|
'passport_number' => 'Y34B67890',
|
||||||
|
'nationality' => 'RE1',
|
||||||
|
'date_of_birth' => '1990-05-14',
|
||||||
|
'date_of_expiry' => '2022-05-07',
|
||||||
|
'place_of_birth' => 'SHARJAH',
|
||||||
|
'authority' => 'MINISTRY OF INTERIOR U.S. 21 is ,,,, is',
|
||||||
|
'sex' => 'M',
|
||||||
|
'date_of_issue' => '2017-07-05',
|
||||||
|
'ocr_accuracy' => 98.7,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201);
|
||||||
|
$workerId = $response->json('data.worker.id');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('worker_documents', [
|
||||||
|
'worker_id' => $workerId,
|
||||||
|
'type' => 'passport',
|
||||||
|
'number' => 'Y34B67890',
|
||||||
|
'expiry_date' => '2022-05-07',
|
||||||
|
'issue_date' => '2017-07-05',
|
||||||
|
'ocr_accuracy' => 98.7,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$passportDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'passport')->first();
|
||||||
|
$this->assertNotNull($passportDoc->ocr_data);
|
||||||
|
$this->assertEquals('KHALED AL', $passportDoc->ocr_data['given_names']);
|
||||||
|
$this->assertEquals('KHALED', $passportDoc->ocr_data['surname']);
|
||||||
|
$this->assertEquals('MINISTRY OF INTERIOR U.S. 21 is ,,,, is', $passportDoc->ocr_data['authority']);
|
||||||
|
$this->assertEquals(98.7, $passportDoc->ocr_data['ocr_accuracy']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test worker registration with the specific visa fields and ocr_accuracy.
|
||||||
|
*/
|
||||||
|
public function test_register_visa_with_full_ocr_fields()
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/workers/register', [
|
||||||
|
'name' => 'Temp Name',
|
||||||
|
'phone' => '+971507777778',
|
||||||
|
'salary' => 2000,
|
||||||
|
'password' => 'secret123',
|
||||||
|
'language' => 'HI',
|
||||||
|
'visa' => [
|
||||||
|
'document_type' => 'uae_visa',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'visa_type' => 'ENTRY PERMIT',
|
||||||
|
'entry_permit_no' => '77003098 / 2019 / 204',
|
||||||
|
'issue_date' => '',
|
||||||
|
'valid_until' => '04-MAR-2019',
|
||||||
|
'uid_no' => '207404887',
|
||||||
|
'full_name' => 'Mr.MUHAMMAD NADEEM RASHEED',
|
||||||
|
'nationality' => 'PAKISTAN',
|
||||||
|
'place_of_birth' => 'SIALKOT PAK',
|
||||||
|
'date_of_birth' => '25-JUN-1999',
|
||||||
|
'passport_no' => 'EN9458281',
|
||||||
|
'profession' => 'SALES REPRESENTATIVE',
|
||||||
|
'sponsor_name' => '',
|
||||||
|
'ocr_accuracy' => 97.4,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201);
|
||||||
|
$workerId = $response->json('data.worker.id');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('worker_documents', [
|
||||||
|
'worker_id' => $workerId,
|
||||||
|
'type' => 'visa',
|
||||||
|
'number' => '77003098 / 2019 / 204',
|
||||||
|
'expiry_date' => '2019-03-04',
|
||||||
|
'ocr_accuracy' => 97.4,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$visaDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'visa')->first();
|
||||||
|
$this->assertNotNull($visaDoc->ocr_data);
|
||||||
|
$this->assertEquals('uae_visa', $visaDoc->ocr_data['document_type']);
|
||||||
|
$this->assertEquals('ENTRY PERMIT', $visaDoc->ocr_data['visa_type']);
|
||||||
|
$this->assertEquals('Mr.MUHAMMAD NADEEM RASHEED', $visaDoc->ocr_data['full_name']);
|
||||||
|
$this->assertEquals(97.4, $visaDoc->ocr_accuracy);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -660,10 +761,18 @@ public function test_nationalities_list_api()
|
|||||||
])
|
])
|
||||||
->assertJsonFragment([
|
->assertJsonFragment([
|
||||||
'code' => 'IN',
|
'code' => 'IN',
|
||||||
|
'country_code' => 'IN',
|
||||||
|
'phone_code' => '+91',
|
||||||
|
'dial_code' => '+91',
|
||||||
|
'calling_code' => '+91',
|
||||||
'name' => 'Indian',
|
'name' => 'Indian',
|
||||||
])
|
])
|
||||||
->assertJsonFragment([
|
->assertJsonFragment([
|
||||||
'code' => 'PH',
|
'code' => 'PH',
|
||||||
|
'country_code' => 'PH',
|
||||||
|
'phone_code' => '+63',
|
||||||
|
'dial_code' => '+63',
|
||||||
|
'calling_code' => '+63',
|
||||||
'name' => 'Filipino',
|
'name' => 'Filipino',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -728,6 +837,10 @@ public function test_nationalities_list_api()
|
|||||||
$this->assertGreaterThanOrEqual(1, count($responseSearch->json('data.nationalities')));
|
$this->assertGreaterThanOrEqual(1, count($responseSearch->json('data.nationalities')));
|
||||||
$responseSearch->assertJsonFragment([
|
$responseSearch->assertJsonFragment([
|
||||||
'code' => 'IN',
|
'code' => 'IN',
|
||||||
|
'country_code' => 'IN',
|
||||||
|
'phone_code' => '+91',
|
||||||
|
'dial_code' => '+91',
|
||||||
|
'calling_code' => '+91',
|
||||||
'name' => 'Indian'
|
'name' => 'Indian'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -1431,4 +1544,55 @@ public function test_profile_response_does_not_contain_deprecated_fields()
|
|||||||
$this->assertArrayNotHasKey('city', $json);
|
$this->assertArrayNotHasKey('city', $json);
|
||||||
$this->assertArrayNotHasKey('area', $json);
|
$this->assertArrayNotHasKey('area', $json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test worker can change password successfully.
|
||||||
|
*/
|
||||||
|
public function test_worker_can_change_password()
|
||||||
|
{
|
||||||
|
$worker = Worker::create([
|
||||||
|
'name' => 'Password Changer',
|
||||||
|
'email' => 'changer@example.com',
|
||||||
|
'phone' => '+971509990099',
|
||||||
|
'language' => 'HI',
|
||||||
|
'password' => bcrypt('OldPassword@123'),
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'age' => 25,
|
||||||
|
'salary' => 1500,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => 'None',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => 'Some Bio',
|
||||||
|
'api_token' => 'changer-token',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 1. Invalid current password
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer changer-token',
|
||||||
|
])->postJson('/api/workers/change-password', [
|
||||||
|
'current_password' => 'WrongPassword',
|
||||||
|
'new_password' => 'NewPassword@123',
|
||||||
|
'new_password_confirmation' => 'NewPassword@123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['current_password']);
|
||||||
|
|
||||||
|
// 2. Successful change
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer changer-token',
|
||||||
|
])->postJson('/api/workers/change-password', [
|
||||||
|
'current_password' => 'OldPassword@123',
|
||||||
|
'new_password' => 'NewPassword@123',
|
||||||
|
'new_password_confirmation' => 'NewPassword@123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Password changed successfully.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue(\Illuminate\Support\Facades\Hash::check('NewPassword@123', $worker->fresh()->password));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user