Compare commits
No commits in common. "52b47cc2ace0fba465321dc00fedcc4320467a74" and "f1ac2c8436639fa192cfc8b67a53dc571ed8e9d7" have entirely different histories.
52b47cc2ac
...
f1ac2c8436
@ -23,7 +23,6 @@ public function getProfile(Request $request)
|
|||||||
$employer = $request->attributes->get('employer');
|
$employer = $request->attributes->get('employer');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$employer->load('employerReviews.worker');
|
|
||||||
$profile = $employer->employerProfile;
|
$profile = $employer->employerProfile;
|
||||||
|
|
||||||
if (!$profile) {
|
if (!$profile) {
|
||||||
@ -48,9 +47,6 @@ public function getProfile(Request $request)
|
|||||||
'card_number' => $profile->emirates_id_card_number,
|
'card_number' => $profile->emirates_id_card_number,
|
||||||
'full_name' => $profile->emirates_id_name,
|
'full_name' => $profile->emirates_id_name,
|
||||||
'gender' => $profile->emirates_id_gender,
|
'gender' => $profile->emirates_id_gender,
|
||||||
'rating' => $employer->rating,
|
|
||||||
'reviews_count' => $employer->reviews_count,
|
|
||||||
'reviews' => $employer->employerReviews,
|
|
||||||
'emirates_id' => [
|
'emirates_id' => [
|
||||||
'emirates_id_number' => $profile->emirates_id_number,
|
'emirates_id_number' => $profile->emirates_id_number,
|
||||||
'name' => $profile->emirates_id_name,
|
'name' => $profile->emirates_id_name,
|
||||||
@ -271,8 +267,6 @@ public function updateProfile(Request $request)
|
|||||||
|
|
||||||
$profile->save();
|
$profile->save();
|
||||||
|
|
||||||
$employer->load('employerReviews.worker');
|
|
||||||
|
|
||||||
$employerProfile = [
|
$employerProfile = [
|
||||||
'name' => $employer->name,
|
'name' => $employer->name,
|
||||||
'email' => $employer->email,
|
'email' => $employer->email,
|
||||||
@ -286,9 +280,6 @@ public function updateProfile(Request $request)
|
|||||||
'card_number' => $profile->emirates_id_card_number,
|
'card_number' => $profile->emirates_id_card_number,
|
||||||
'full_name' => $profile->emirates_id_name,
|
'full_name' => $profile->emirates_id_name,
|
||||||
'gender' => $profile->emirates_id_gender,
|
'gender' => $profile->emirates_id_gender,
|
||||||
'rating' => $employer->rating,
|
|
||||||
'reviews_count' => $employer->reviews_count,
|
|
||||||
'reviews' => $employer->employerReviews,
|
|
||||||
'emirates_id' => [
|
'emirates_id' => [
|
||||||
'emirates_id_number' => $profile->emirates_id_number,
|
'emirates_id_number' => $profile->emirates_id_number,
|
||||||
'name' => $profile->emirates_id_name,
|
'name' => $profile->emirates_id_name,
|
||||||
|
|||||||
@ -266,7 +266,7 @@ public function setupProfile(Request $request)
|
|||||||
// Clear the OTP verification gate
|
// Clear the OTP verification gate
|
||||||
Cache::forget('worker_otp_verified_' . $identifier);
|
Cache::forget('worker_otp_verified_' . $identifier);
|
||||||
|
|
||||||
$worker->load(['skills', 'documents', 'reviews.employer.employerProfile', 'employerReviews.employer.employerProfile']);
|
$worker->load(['skills']);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@ -523,7 +523,7 @@ public function register(Request $request)
|
|||||||
return $worker;
|
return $worker;
|
||||||
});
|
});
|
||||||
|
|
||||||
$result->load(['skills', 'documents', 'reviews.employer.employerProfile', 'employerReviews.employer.employerProfile']);
|
$result->load(['skills', 'documents']);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@ -591,7 +591,7 @@ public function register(Request $request)
|
|||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Worker logged in successfully.',
|
'message' => 'Worker logged in successfully.',
|
||||||
'data' => [
|
'data' => [
|
||||||
'worker' => $worker->load(['skills', 'documents', 'reviews.employer.employerProfile', 'employerReviews.employer.employerProfile']),
|
'worker' => $worker->load(['skills', 'documents']),
|
||||||
'token' => $apiToken
|
'token' => $apiToken
|
||||||
]
|
]
|
||||||
], 200);
|
], 200);
|
||||||
|
|||||||
@ -28,7 +28,7 @@ public function getProfile(Request $request)
|
|||||||
/** @var Worker $worker */
|
/** @var Worker $worker */
|
||||||
$worker = $request->attributes->get('worker');
|
$worker = $request->attributes->get('worker');
|
||||||
|
|
||||||
$worker->load(['skills', 'documents', 'reviews.employer.employerProfile', 'employerReviews.employer.employerProfile']);
|
$worker->load(['skills', 'documents']);
|
||||||
|
|
||||||
$worker->makeHidden(['email', 'religion', 'availability', 'bio', 'country', 'city', 'area']);
|
$worker->makeHidden(['email', 'religion', 'availability', 'bio', 'country', 'city', 'area']);
|
||||||
|
|
||||||
@ -240,7 +240,7 @@ public function updateProfile(Request $request)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$worker->load(['skills', 'documents', 'reviews.employer.employerProfile', 'employerReviews.employer.employerProfile']);
|
$worker->load(['skills', 'documents']);
|
||||||
|
|
||||||
$worker->makeHidden(['email', 'religion', 'availability', 'bio', 'country', 'city', 'area']);
|
$worker->makeHidden(['email', 'religion', 'availability', 'bio', 'country', 'city', 'area']);
|
||||||
|
|
||||||
@ -286,7 +286,7 @@ public function goLive(Request $request)
|
|||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Your profile is now live! Status set to Active.',
|
'message' => 'Your profile is now live! Status set to Active.',
|
||||||
'data' => [
|
'data' => [
|
||||||
'worker' => $worker->load(['skills', 'documents', 'reviews.employer.employerProfile', 'employerReviews.employer.employerProfile'])
|
'worker' => $worker->load(['skills', 'documents'])
|
||||||
]
|
]
|
||||||
], 200);
|
], 200);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
@ -339,7 +339,7 @@ public function toggleAvailability(Request $request)
|
|||||||
'still_looking' => $stillLooking,
|
'still_looking' => $stillLooking,
|
||||||
'status' => $newStatus,
|
'status' => $newStatus,
|
||||||
'data' => [
|
'data' => [
|
||||||
'worker' => $worker->load(['skills', 'documents', 'reviews.employer.employerProfile', 'employerReviews.employer.employerProfile'])
|
'worker' => $worker->load(['skills', 'documents'])
|
||||||
]
|
]
|
||||||
], 200);
|
], 200);
|
||||||
|
|
||||||
@ -373,7 +373,7 @@ public function markHired(Request $request)
|
|||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Congratulations! You have been successfully marked as Hired. Your profile has been automatically removed from active search.',
|
'message' => 'Congratulations! You have been successfully marked as Hired. Your profile has been automatically removed from active search.',
|
||||||
'data' => [
|
'data' => [
|
||||||
'worker' => $worker->load(['skills', 'documents', 'reviews.employer.employerProfile', 'employerReviews.employer.employerProfile'])
|
'worker' => $worker->load(['skills', 'documents'])
|
||||||
]
|
]
|
||||||
], 200);
|
], 200);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
@ -589,7 +589,7 @@ public function getDashboard(Request $request)
|
|||||||
$currentSponsor = null;
|
$currentSponsor = null;
|
||||||
$acceptedOffer = JobOffer::where('worker_id', $worker->id)
|
$acceptedOffer = JobOffer::where('worker_id', $worker->id)
|
||||||
->where('status', 'accepted')
|
->where('status', 'accepted')
|
||||||
->with(['employer.employerProfile', 'employer.employerReviews.worker'])
|
->with('employer.employerProfile')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($acceptedOffer && $acceptedOffer->employer) {
|
if ($acceptedOffer && $acceptedOffer->employer) {
|
||||||
@ -605,9 +605,6 @@ public function getDashboard(Request $request)
|
|||||||
'phone' => $emp->phone,
|
'phone' => $emp->phone,
|
||||||
'hired_at' => $acceptedOffer->updated_at->toIso8601String(),
|
'hired_at' => $acceptedOffer->updated_at->toIso8601String(),
|
||||||
'hired_at_formatted' => $acceptedOffer->updated_at->format('M d, Y'),
|
'hired_at_formatted' => $acceptedOffer->updated_at->format('M d, Y'),
|
||||||
'rating' => $emp->rating,
|
|
||||||
'reviews_count' => $emp->reviews_count,
|
|
||||||
'reviews' => $emp->employerReviews,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,7 +685,7 @@ public function getEmployers(Request $request)
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$query = JobOffer::where('worker_id', $worker->id)
|
$query = JobOffer::where('worker_id', $worker->id)
|
||||||
->with(['employer.employerProfile', 'employer.employerReviews.worker']);
|
->with(['employer.employerProfile']);
|
||||||
|
|
||||||
// Filtering by status
|
// Filtering by status
|
||||||
if ($request->filled('status')) {
|
if ($request->filled('status')) {
|
||||||
@ -748,9 +745,6 @@ public function getEmployers(Request $request)
|
|||||||
'company_name' => $empProfile->company_name ?? 'Private Sponsor',
|
'company_name' => $empProfile->company_name ?? 'Private Sponsor',
|
||||||
'city' => $empProfile->city ?? null,
|
'city' => $empProfile->city ?? null,
|
||||||
'nationality' => $empProfile->nationality ?? null,
|
'nationality' => $empProfile->nationality ?? null,
|
||||||
'rating' => $emp->rating,
|
|
||||||
'reviews_count' => $emp->reviews_count,
|
|
||||||
'reviews' => $emp->employerReviews,
|
|
||||||
] : null
|
] : null
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@ -933,155 +927,4 @@ private function cleanVisaData(array $visaDataInput): array
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get details of an employer profile for worker view.
|
|
||||||
* GET /api/workers/employers/{id}
|
|
||||||
*/
|
|
||||||
public function getEmployerProfile(Request $request, $id)
|
|
||||||
{
|
|
||||||
/** @var Worker $worker */
|
|
||||||
$worker = $request->attributes->get('worker');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Find the employer (role should be 'employer')
|
|
||||||
$employer = \App\Models\User::where('id', $id)->where('role', 'employer')->first();
|
|
||||||
|
|
||||||
if (!$employer) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Employer not found.'
|
|
||||||
], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$empProfile = $employer->employerProfile;
|
|
||||||
|
|
||||||
// 1. Check access permissions for contact details
|
|
||||||
$hasAccess = false;
|
|
||||||
|
|
||||||
// Check if there is an active/previous JobOffer
|
|
||||||
$hasOffer = \App\Models\JobOffer::where('employer_id', $employer->id)
|
|
||||||
->where('worker_id', $worker->id)
|
|
||||||
->exists();
|
|
||||||
|
|
||||||
// Check if the worker has applied to any jobs of this employer
|
|
||||||
$hasApplication = \App\Models\JobApplication::where('worker_id', $worker->id)
|
|
||||||
->whereHas('jobPost', function ($query) use ($employer) {
|
|
||||||
$query->where('employer_id', $employer->id);
|
|
||||||
})
|
|
||||||
->exists();
|
|
||||||
|
|
||||||
// Check if there is an active/previous conversation
|
|
||||||
$hasConversation = \App\Models\Conversation::where('employer_id', $employer->id)
|
|
||||||
->where('worker_id', $worker->id)
|
|
||||||
->exists();
|
|
||||||
|
|
||||||
if ($hasOffer || $hasApplication || $hasConversation) {
|
|
||||||
$hasAccess = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Fetch active job details (if applicable)
|
|
||||||
$activeJobs = \App\Models\JobPost::where('employer_id', $employer->id)
|
|
||||||
->where('status', 'active')
|
|
||||||
->latest()
|
|
||||||
->get()
|
|
||||||
->map(function ($job) {
|
|
||||||
return [
|
|
||||||
'id' => $job->id,
|
|
||||||
'title' => $job->title,
|
|
||||||
'location' => $job->location,
|
|
||||||
'salary' => (int) $job->salary,
|
|
||||||
'job_type' => $job->job_type,
|
|
||||||
'start_date' => $job->start_date ? $job->start_date->format('Y-m-d') : null,
|
|
||||||
'description' => $job->description,
|
|
||||||
'requirements' => $job->requirements,
|
|
||||||
'posted_at' => $job->created_at->toIso8601String(),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Overall Rating and Review Summary
|
|
||||||
$reviewsQuery = \App\Models\EmployerReview::where('employer_id', $employer->id);
|
|
||||||
$totalReviews = $reviewsQuery->count();
|
|
||||||
$avgRating = $totalReviews > 0 ? round($reviewsQuery->avg('rating'), 1) : 0.0;
|
|
||||||
|
|
||||||
$starsBreakdown = [
|
|
||||||
'5' => \App\Models\EmployerReview::where('employer_id', $employer->id)->where('rating', 5)->count(),
|
|
||||||
'4' => \App\Models\EmployerReview::where('employer_id', $employer->id)->where('rating', 4)->count(),
|
|
||||||
'3' => \App\Models\EmployerReview::where('employer_id', $employer->id)->where('rating', 3)->count(),
|
|
||||||
'2' => \App\Models\EmployerReview::where('employer_id', $employer->id)->where('rating', 2)->count(),
|
|
||||||
'1' => \App\Models\EmployerReview::where('employer_id', $employer->id)->where('rating', 1)->count(),
|
|
||||||
];
|
|
||||||
|
|
||||||
// 4. Paginated Reviews list
|
|
||||||
$perPage = (int) $request->input('per_page', 10);
|
|
||||||
$reviewsPaginator = \App\Models\EmployerReview::where('employer_id', $employer->id)
|
|
||||||
->with('worker')
|
|
||||||
->latest()
|
|
||||||
->paginate($perPage);
|
|
||||||
|
|
||||||
$reviews = collect($reviewsPaginator->items())->map(function ($rev) {
|
|
||||||
return [
|
|
||||||
'id' => $rev->id,
|
|
||||||
'rating' => $rev->rating,
|
|
||||||
'title' => $rev->title,
|
|
||||||
'comment' => $rev->comment,
|
|
||||||
'created_at' => $rev->created_at->toIso8601String(),
|
|
||||||
'worker' => $rev->worker ? [
|
|
||||||
'id' => $rev->worker->id,
|
|
||||||
'name' => $rev->worker->name,
|
|
||||||
'nationality' => $rev->worker->nationality,
|
|
||||||
] : null,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5. Structure the response
|
|
||||||
$employerData = [
|
|
||||||
'id' => $employer->id,
|
|
||||||
'name' => $employer->name,
|
|
||||||
'email' => $hasAccess ? $employer->email : null,
|
|
||||||
'phone' => $hasAccess ? ($empProfile->phone ?? $employer->phone) : null,
|
|
||||||
'company_name' => $empProfile->company_name ?? 'Private Sponsor',
|
|
||||||
'city' => $empProfile->city ?? null,
|
|
||||||
'district' => $empProfile->district ?? null,
|
|
||||||
'nationality' => $empProfile->nationality ?? null,
|
|
||||||
'address' => $empProfile->address ?? null,
|
|
||||||
'accommodation' => $empProfile->accommodation ?? null,
|
|
||||||
'language' => $empProfile->language ?? null,
|
|
||||||
'profile_photo_url' => $empProfile->profile_photo_url ?? null,
|
|
||||||
'rating' => $avgRating,
|
|
||||||
'reviews_count' => $totalReviews,
|
|
||||||
'review_summary' => [
|
|
||||||
'total' => $totalReviews,
|
|
||||||
'average' => $avgRating,
|
|
||||||
'stars' => $starsBreakdown,
|
|
||||||
],
|
|
||||||
'active_jobs' => $activeJobs,
|
|
||||||
];
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'data' => [
|
|
||||||
'employer' => $employerData,
|
|
||||||
'reviews' => [
|
|
||||||
'data' => $reviews,
|
|
||||||
'pagination' => [
|
|
||||||
'total' => $reviewsPaginator->total(),
|
|
||||||
'per_page' => $reviewsPaginator->perPage(),
|
|
||||||
'current_page' => $reviewsPaginator->currentPage(),
|
|
||||||
'last_page' => $reviewsPaginator->lastPage(),
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
], 200);
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
logger()->error('Worker View Employer Profile Failure: ' . $e->getMessage());
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'An error occurred while loading the employer profile.',
|
|
||||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,12 +24,6 @@ public function addReview(Request $request)
|
|||||||
/** @var Worker $worker */
|
/** @var Worker $worker */
|
||||||
$worker = $request->attributes->get('worker');
|
$worker = $request->attributes->get('worker');
|
||||||
|
|
||||||
// Sanitize job_id: if not a valid existing JobPost ID, treat it as null (job id is not needed/optional)
|
|
||||||
$jobIdInput = $request->input('job_id');
|
|
||||||
if (empty($jobIdInput) || !is_numeric($jobIdInput) || !\App\Models\JobPost::where('id', $jobIdInput)->exists()) {
|
|
||||||
$request->merge(['job_id' => null]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'employer_id' => 'required|exists:users,id',
|
'employer_id' => 'required|exists:users,id',
|
||||||
'job_id' => 'nullable|exists:job_posts,id',
|
'job_id' => 'nullable|exists:job_posts,id',
|
||||||
|
|||||||
@ -56,21 +56,22 @@ public function index(Request $request)
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map languages from database comma-separated string
|
// Map languages with country names
|
||||||
$langs = $w->language ? array_map('trim', explode(',', $w->language)) : ['English'];
|
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
||||||
|
|
||||||
// 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 = $jobTypes[$w->id % 4];
|
||||||
|
|
||||||
// Emirates ID verification status (dynamic passport status)
|
// Emirates ID verification status (dynamic passport status)
|
||||||
$emiratesIdStatus = $w->emirates_id_status;
|
$emiratesIdStatus = $w->emirates_id_status;
|
||||||
|
|
||||||
// Map skills dynamically from database
|
// Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening
|
||||||
$mappedSkills = $w->skills->pluck('name')->toArray();
|
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
||||||
if (empty($mappedSkills)) {
|
$mappedSkills = [
|
||||||
$mappedSkills = ['cooking', 'cleaning'];
|
$skillsList[$w->id % 6],
|
||||||
}
|
$skillsList[($w->id + 2) % 6]
|
||||||
|
];
|
||||||
|
|
||||||
// Visa status
|
// Visa status
|
||||||
$visaStatus = $w->visa_status;
|
$visaStatus = $w->visa_status;
|
||||||
@ -85,16 +86,13 @@ public function index(Request $request)
|
|||||||
return [
|
return [
|
||||||
'id' => $w->id,
|
'id' => $w->id,
|
||||||
'name' => $w->name,
|
'name' => $w->name,
|
||||||
'phone' => $w->phone,
|
|
||||||
'gender' => $w->gender ?? 'Female',
|
|
||||||
'nationality' => $w->nationality,
|
'nationality' => $w->nationality,
|
||||||
'photo' => $photo,
|
'photo' => $photo,
|
||||||
'emirates_id_status' => $emiratesIdStatus,
|
'emirates_id_status' => $emiratesIdStatus,
|
||||||
'passport_status' => $w->passport_status,
|
'passport_status' => $w->passport_status,
|
||||||
'category' => 'Domestic Worker',
|
|
||||||
'main_profession' => $w->main_profession,
|
|
||||||
'skills' => $mappedSkills,
|
'skills' => $mappedSkills,
|
||||||
'visa_status' => $visaStatus,
|
'visa_status' => $visaStatus,
|
||||||
|
'gender' => $w->gender ?? 'Female',
|
||||||
'experience' => $w->experience,
|
'experience' => $w->experience,
|
||||||
'salary' => (int) $w->salary,
|
'salary' => (int) $w->salary,
|
||||||
'religion' => $w->religion,
|
'religion' => $w->religion,
|
||||||
@ -102,7 +100,6 @@ public function index(Request $request)
|
|||||||
'age' => $w->age,
|
'age' => $w->age,
|
||||||
'verified' => (bool) $w->verified,
|
'verified' => (bool) $w->verified,
|
||||||
'preferred_job_type' => $preferredJobType,
|
'preferred_job_type' => $preferredJobType,
|
||||||
'live_in_out' => $w->live_in_out ?? 'Live-in',
|
|
||||||
'bio' => $w->bio,
|
'bio' => $w->bio,
|
||||||
'rating' => $rating,
|
'rating' => $rating,
|
||||||
'reviews_count' => $reviewsCount,
|
'reviews_count' => $reviewsCount,
|
||||||
@ -114,177 +111,8 @@ public function index(Request $request)
|
|||||||
];
|
];
|
||||||
})->filter()->values()->toArray();
|
})->filter()->values()->toArray();
|
||||||
|
|
||||||
// Apply request filters: preferred_location, job_type, live_in_out, nationality, in_country, visa_status
|
|
||||||
if ($request->filled('preferred_location')) {
|
|
||||||
$prefLoc = $request->preferred_location;
|
|
||||||
$locsArray = is_array($prefLoc) ? $prefLoc : array_filter(array_map('trim', explode(',', $prefLoc)));
|
|
||||||
$locsArray = array_map('strtolower', $locsArray);
|
|
||||||
|
|
||||||
$locationMapping = [
|
|
||||||
'dubai' => ['dubai', 'marina', 'barsha', 'nahda', 'jumeirah', 'deira', 'downtown', 'silicon', 'sports', 'motor', 'jlt', 'jbr', 'meydan', 'ranches', 'bay', 'mirdif', 'quoz'],
|
|
||||||
'abu dhabi' => ['abu dhabi', 'yas', 'khalifa', 'reem', 'saadiyat', 'raha', 'mussafah', 'zahiyah', 'karamah'],
|
|
||||||
'oman' => ['oman', 'muscat', 'salalah', 'sohar', 'nizwa', 'sur', 'ibri', 'rustaq']
|
|
||||||
];
|
|
||||||
|
|
||||||
$shortlistedWorkers = array_values(array_filter($shortlistedWorkers, function ($c) use ($locsArray, $locationMapping) {
|
|
||||||
if (!isset($c['preferred_location'])) return false;
|
|
||||||
$wLoc = strtolower($c['preferred_location']);
|
|
||||||
foreach ($locsArray as $l) {
|
|
||||||
if ($l === 'all') return true;
|
|
||||||
$allowedKeywords = $locationMapping[$l] ?? [$l];
|
|
||||||
foreach ($allowedKeywords as $keyword) {
|
|
||||||
if (str_contains($wLoc, $keyword)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
$jobTypeParam = $request->input('job_type') ?? $request->input('preferred_job_type');
|
|
||||||
if ($jobTypeParam) {
|
|
||||||
$jobTypesArray = is_array($jobTypeParam) ? $jobTypeParam : array_filter(array_map('trim', explode(',', $jobTypeParam)));
|
|
||||||
$jobTypesArray = array_map('strtolower', $jobTypesArray);
|
|
||||||
|
|
||||||
$shortlistedWorkers = array_values(array_filter($shortlistedWorkers, function ($c) use ($jobTypesArray) {
|
|
||||||
if (!isset($c['preferred_job_type'])) return false;
|
|
||||||
$wJobType = strtolower($c['preferred_job_type']);
|
|
||||||
foreach ($jobTypesArray as $jt) {
|
|
||||||
if (str_contains($wJobType, $jt)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
$accParam = $request->input('live_in_out') ?? $request->input('accommodation_type') ?? $request->input('accomadation_type');
|
|
||||||
if ($accParam) {
|
|
||||||
$accsArray = is_array($accParam) ? $accParam : array_filter(array_map('trim', explode(',', $accParam)));
|
|
||||||
$accsArray = array_map(function($val) {
|
|
||||||
return str_replace(['_', '-'], ' ', strtolower($val));
|
|
||||||
}, $accsArray);
|
|
||||||
|
|
||||||
$shortlistedWorkers = array_values(array_filter($shortlistedWorkers, function ($c) use ($accsArray) {
|
|
||||||
if (!isset($c['live_in_out'])) return false;
|
|
||||||
$wAcc = str_replace(['_', '-'], ' ', strtolower($c['live_in_out']));
|
|
||||||
foreach ($accsArray as $a) {
|
|
||||||
if (str_contains($wAcc, $a)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->filled('nationality')) {
|
|
||||||
$natInput = $request->nationality;
|
|
||||||
$natsArray = is_array($natInput) ? $natInput : array_filter(array_map('trim', explode(',', $natInput)));
|
|
||||||
$natsArray = array_map('strtolower', $natsArray);
|
|
||||||
|
|
||||||
$shortlistedWorkers = array_values(array_filter($shortlistedWorkers, function ($c) use ($natsArray) {
|
|
||||||
if (!isset($c['nationality'])) return false;
|
|
||||||
$wNat = strtolower($c['nationality']);
|
|
||||||
foreach ($natsArray as $n) {
|
|
||||||
if (str_contains($wNat, $n) || $wNat === $n) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if ($request->filled('main_profession')) {
|
|
||||||
$mainProfession = strtolower($request->main_profession);
|
|
||||||
$shortlistedWorkers = array_values(array_filter($shortlistedWorkers, function ($c) use ($mainProfession) {
|
|
||||||
return isset($c['main_profession']) && strtolower($c['main_profession']) === $mainProfession;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if ($request->filled('gender')) {
|
|
||||||
$gender = strtolower($request->gender);
|
|
||||||
$shortlistedWorkers = array_values(array_filter($shortlistedWorkers, function ($c) use ($gender) {
|
|
||||||
return isset($c['gender']) && strtolower($c['gender']) === $gender;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if ($request->has('in_country')) {
|
|
||||||
$inCountryVal = $request->input('in_country');
|
|
||||||
if ($inCountryVal !== null && $inCountryVal !== '') {
|
|
||||||
$isInCountry = filter_var($inCountryVal, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
||||||
if ($isInCountry === null) {
|
|
||||||
$strVal = strtolower(trim($inCountryVal));
|
|
||||||
if ($strVal === 'in' || $strVal === 'in_country' || $strVal === 'yes') {
|
|
||||||
$isInCountry = true;
|
|
||||||
} elseif ($strVal === 'out' || $strVal === 'out_country' || $strVal === 'no') {
|
|
||||||
$isInCountry = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($isInCountry !== null) {
|
|
||||||
$shortlistedWorkers = array_values(array_filter($shortlistedWorkers, function ($c) use ($isInCountry) {
|
|
||||||
return isset($c['in_country']) && (bool)$c['in_country'] === $isInCountry;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->has('out_country')) {
|
|
||||||
$outCountryVal = $request->input('out_country');
|
|
||||||
if ($outCountryVal !== null && $outCountryVal !== '') {
|
|
||||||
$isOutCountry = filter_var($outCountryVal, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
||||||
if ($isOutCountry === null) {
|
|
||||||
$strVal = strtolower(trim($outCountryVal));
|
|
||||||
if ($strVal === 'out' || $strVal === 'out_country' || $strVal === 'yes') {
|
|
||||||
$isOutCountry = true;
|
|
||||||
} elseif ($strVal === 'in' || $strVal === 'in_country' || $strVal === 'no') {
|
|
||||||
$isOutCountry = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($isOutCountry !== null) {
|
|
||||||
$shortlistedWorkers = array_values(array_filter($shortlistedWorkers, function ($c) use ($isOutCountry) {
|
|
||||||
return isset($c['in_country']) && (bool)$c['in_country'] !== $isOutCountry;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$visaParam = $request->input('visa_status') ?? $request->input('next_visa_type') ?? $request->input('visa_type');
|
|
||||||
if ($visaParam) {
|
|
||||||
$visasArray = is_array($visaParam) ? $visaParam : array_filter(array_map('trim', explode(',', $visaParam)));
|
|
||||||
$visasArray = array_map('strtolower', $visasArray);
|
|
||||||
|
|
||||||
$shortlistedWorkers = array_values(array_filter($shortlistedWorkers, function ($c) use ($visasArray) {
|
|
||||||
if (!isset($c['visa_status'])) return false;
|
|
||||||
$isInCountry = isset($c['in_country']) && (bool)$c['in_country'];
|
|
||||||
if (!$isInCountry) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$wVisa = strtolower($c['visa_status']);
|
|
||||||
foreach ($visasArray as $v) {
|
|
||||||
if (str_contains($wVisa, $v)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
$nationalitiesResponse = app(\App\Http\Controllers\Api\WorkerAuthController::class)->nationalities(new \Illuminate\Http\Request(['per_page' => 500]));
|
|
||||||
$nationalitiesData = json_decode($nationalitiesResponse->getContent(), true);
|
|
||||||
$dbNationalities = collect($nationalitiesData['data']['nationalities'] ?? [])->pluck('name')->filter()->toArray();
|
|
||||||
|
|
||||||
$filtersMetadata = [
|
|
||||||
'nationalities' => array_merge(['All Nationalities'], $dbNationalities),
|
|
||||||
'professions' => ['All Professions', 'Housemaid', 'Nanny', 'Cook', 'Driver', 'Caregiver'],
|
|
||||||
'experienceLevels' => ['All Experience', '1-2 Years', '3-5 Years', '5+ Years'],
|
|
||||||
'religions' => ['All Religions', 'Christian', 'Muslim', 'Hindu', 'Buddhist'],
|
|
||||||
'languages' => ['All Languages', 'English', 'Arabic', 'Hindi', 'Tagalog'],
|
|
||||||
'workTypes' => ['All Types', 'full-time', 'part-time', 'live-in', 'live-out'],
|
|
||||||
'skills' => ['All Skills', 'cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'],
|
|
||||||
'visaStatuses' => ['All Visa Statuses', 'Residence Visa', 'Tourist Visa', 'Employment Visa'],
|
|
||||||
];
|
|
||||||
|
|
||||||
return Inertia::render('Employer/Shortlist', [
|
return Inertia::render('Employer/Shortlist', [
|
||||||
'shortlistedWorkers' => $shortlistedWorkers,
|
'shortlistedWorkers' => $shortlistedWorkers,
|
||||||
'filtersMetadata' => $filtersMetadata,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,8 +33,6 @@ public function handle(Request $request, Closure $next): Response
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
session()->forget('user');
|
|
||||||
auth()->logout();
|
|
||||||
return redirect()->route('employer.login');
|
return redirect()->route('employer.login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,34 +46,6 @@ public function hasActiveSubscription(): bool
|
|||||||
return $this->subscription_status === 'active' && ($this->subscription_expires_at === null || $this->subscription_expires_at > now());
|
return $this->subscription_status === 'active' && ($this->subscription_expires_at === null || $this->subscription_expires_at > now());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected $appends = [
|
|
||||||
'rating',
|
|
||||||
'reviews_count',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function employerReviews()
|
|
||||||
{
|
|
||||||
return $this->hasMany(EmployerReview::class, 'employer_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRatingAttribute()
|
|
||||||
{
|
|
||||||
if ($this->role !== 'employer') {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
$dbReviews = $this->employerReviews;
|
|
||||||
$count = $dbReviews->count();
|
|
||||||
return $count > 0 ? round($dbReviews->avg('rating'), 1) : 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getReviewsCountAttribute()
|
|
||||||
{
|
|
||||||
if ($this->role !== 'employer') {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return $this->employerReviews()->count();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function subscription()
|
public function subscription()
|
||||||
{
|
{
|
||||||
return $this->hasOne(Subscription::class);
|
return $this->hasOne(Subscription::class);
|
||||||
|
|||||||
@ -59,8 +59,6 @@ class Worker extends Model
|
|||||||
'document_expiry_days',
|
'document_expiry_days',
|
||||||
'document_expiry_status',
|
'document_expiry_status',
|
||||||
'visa_expiry_date',
|
'visa_expiry_date',
|
||||||
'rating',
|
|
||||||
'reviews_count',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function getPassportStatusAttribute()
|
public function getPassportStatusAttribute()
|
||||||
@ -192,25 +190,6 @@ public function reviews()
|
|||||||
return $this->hasMany(Review::class);
|
return $this->hasMany(Review::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function employerReviews()
|
|
||||||
{
|
|
||||||
return $this->hasMany(EmployerReview::class, 'worker_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRatingAttribute()
|
|
||||||
{
|
|
||||||
// Prevent infinite recursion by not using relationships if we want to avoid eager load loops,
|
|
||||||
// but typically $this->reviews is safe as long as we don't eager load recursively.
|
|
||||||
$dbReviews = $this->reviews;
|
|
||||||
$count = $dbReviews->count();
|
|
||||||
return $count > 0 ? round($dbReviews->avg('rating'), 1) : 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getReviewsCountAttribute()
|
|
||||||
{
|
|
||||||
return $this->reviews()->count();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function profileViews()
|
public function profileViews()
|
||||||
{
|
{
|
||||||
return $this->hasMany(ProfileView::class);
|
return $this->hasMany(ProfileView::class);
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
"Sponsor/Auth"
|
"Sponsor/Auth"
|
||||||
],
|
],
|
||||||
"summary": "Register Sponsor Account",
|
"summary": "Register Sponsor Account",
|
||||||
"description": "Registers a new sponsor with basic information and uploads their organization/trade license. Sponsors have restricted access — dashboard and charity events only. No payment required.",
|
"description": "Registers a new sponsor with basic information and uploads their organization/trade license. Sponsors have restricted access \u2014 dashboard and charity events only. No payment required.",
|
||||||
"security": [],
|
"security": [],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@ -51,7 +51,7 @@
|
|||||||
"mobile": {
|
"mobile": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "+971501112233",
|
"example": "+971501112233",
|
||||||
"description": "Mobile phone number — must be unique. (REQUIRED)"
|
"description": "Mobile phone number \u00e2\u20ac\u201d must be unique. (REQUIRED)"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -958,7 +958,7 @@
|
|||||||
"Worker/Auth"
|
"Worker/Auth"
|
||||||
],
|
],
|
||||||
"summary": "Register Worker Account (Step 1)",
|
"summary": "Register Worker Account (Step 1)",
|
||||||
"description": "Creates the worker account and returns a secure Bearer token.\n\nDocument upload is a **separate step** after registration:\n- `POST /workers/register/passport` — upload passport (OCR-processed)\n- `POST /workers/register/visa` — upload visa (OCR-processed)\n\nBoth document endpoints require the Bearer token returned here.",
|
"description": "Creates the worker account and returns a secure Bearer token.\n\nDocument upload is a **separate step** after registration:\n- `POST /workers/register/passport` \u2014 upload passport (OCR-processed)\n- `POST /workers/register/visa` \u2014 upload visa (OCR-processed)\n\nBoth document endpoints require the Bearer token returned here.",
|
||||||
"security": [],
|
"security": [],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@ -1263,7 +1263,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"422": {
|
"422": {
|
||||||
"description": "Validation error — field validation failed or Passport OCR extraction failed.",
|
"description": "Validation error \u2014 field validation failed or Passport OCR extraction failed.",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@ -1550,7 +1550,7 @@
|
|||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Hindi (हिन्दी)"
|
"example": "Hindi (\u0939\u093f\u0928\u094d\u0926\u0940)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5995,20 +5995,6 @@
|
|||||||
"hired_at_formatted": {
|
"hired_at_formatted": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Jun 01, 2026"
|
"example": "Jun 01, 2026"
|
||||||
},
|
|
||||||
"rating": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 4.8
|
|
||||||
},
|
|
||||||
"reviews_count": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 3
|
|
||||||
},
|
|
||||||
"reviews": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -6052,20 +6038,6 @@
|
|||||||
"hired_at_formatted": {
|
"hired_at_formatted": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Jun 01, 2026"
|
"example": "Jun 01, 2026"
|
||||||
},
|
|
||||||
"rating": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 4.8
|
|
||||||
},
|
|
||||||
"reviews_count": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 3
|
|
||||||
},
|
|
||||||
"reviews": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -7305,168 +7277,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/workers/employers/{id}": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"Worker/Employers"
|
|
||||||
],
|
|
||||||
"summary": "Worker View Employer Profile",
|
|
||||||
"description": "Allows authenticated workers to view the complete profile of a specific employer, including company details, ratings, review summaries, active jobs, and a paginated list of worker-submitted reviews. Contact details are conditionally visible depending on whether the worker has a past/present connection with the employer.",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"description": "The unique database ID of the employer.",
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "per_page",
|
|
||||||
"in": "query",
|
|
||||||
"required": false,
|
|
||||||
"description": "Number of reviews to display per page (default: 10).",
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"in": "query",
|
|
||||||
"required": false,
|
|
||||||
"description": "The page number of the reviews list to fetch.",
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Employer profile loaded successfully.",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"success": {
|
|
||||||
"type": "boolean",
|
|
||||||
"example": true
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"employer": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": { "type": "integer", "example": 1 },
|
|
||||||
"name": { "type": "string", "example": "Jane Sponsor" },
|
|
||||||
"email": { "type": "string", "nullable": true, "example": "sponsor@example.com" },
|
|
||||||
"phone": { "type": "string", "nullable": true, "example": "+971501112222" },
|
|
||||||
"company_name": { "type": "string", "example": "Sponsor Co" },
|
|
||||||
"city": { "type": "string", "nullable": true, "example": "Dubai" },
|
|
||||||
"district": { "type": "string", "nullable": true, "example": "Dubai Marina" },
|
|
||||||
"nationality": { "type": "string", "nullable": true, "example": "Emirati" },
|
|
||||||
"address": { "type": "string", "nullable": true, "example": "123 Street" },
|
|
||||||
"accommodation": { "type": "string", "nullable": true, "example": "Provided" },
|
|
||||||
"language": { "type": "string", "nullable": true, "example": "English" },
|
|
||||||
"profile_photo_url": { "type": "string", "nullable": true, "example": "https://example.com/photo.jpg" },
|
|
||||||
"rating": { "type": "number", "format": "float", "example": 4.5 },
|
|
||||||
"reviews_count": { "type": "integer", "example": 12 },
|
|
||||||
"review_summary": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"total": { "type": "integer", "example": 12 },
|
|
||||||
"average": { "type": "number", "example": 4.5 },
|
|
||||||
"stars": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"5": { "type": "integer", "example": 8 },
|
|
||||||
"4": { "type": "integer", "example": 2 },
|
|
||||||
"3": { "type": "integer", "example": 1 },
|
|
||||||
"2": { "type": "integer", "example": 1 },
|
|
||||||
"1": { "type": "integer", "example": 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"active_jobs": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": { "type": "integer", "example": 5 },
|
|
||||||
"title": { "type": "string", "example": "Housekeeper Needed" },
|
|
||||||
"location": { "type": "string", "example": "Dubai Marina" },
|
|
||||||
"salary": { "type": "integer", "example": 2500 },
|
|
||||||
"job_type": { "type": "string", "example": "Full Time" },
|
|
||||||
"start_date": { "type": "string", "format": "date", "example": "2026-08-01" },
|
|
||||||
"description": { "type": "string", "example": "Clean house" },
|
|
||||||
"requirements": { "type": "string", "nullable": true, "example": "Experience required" },
|
|
||||||
"posted_at": { "type": "string", "format": "date-time", "example": "2026-07-04T12:00:00Z" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reviews": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": { "type": "integer", "example": 1 },
|
|
||||||
"rating": { "type": "integer", "example": 5 },
|
|
||||||
"title": { "type": "string", "example": "Great employer" },
|
|
||||||
"comment": { "type": "string", "example": "Highly recommended!" },
|
|
||||||
"created_at": { "type": "string", "format": "date-time", "example": "2026-07-04T12:00:00Z" },
|
|
||||||
"worker": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": { "type": "integer", "example": 2 },
|
|
||||||
"name": { "type": "string", "example": "Worker Name" },
|
|
||||||
"nationality": { "type": "string", "example": "Filipino" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pagination": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"total": { "type": "integer", "example": 1 },
|
|
||||||
"per_page": { "type": "integer", "example": 10 },
|
|
||||||
"current_page": { "type": "integer", "example": 1 },
|
|
||||||
"last_page": { "type": "integer", "example": 1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Employer not found."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/workers/forgot-password": {
|
"/workers/forgot-password": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Workers - Auth"
|
"Workers - Auth"
|
||||||
],
|
],
|
||||||
"summary": "Forgot Password (Send OTP)",
|
"summary": "Forgot Password (Send OTP)",
|
||||||
"description": "Sends a 6-digit OTP to verify the worker's identity using their registered mobile number. Workers do not use email — mobile/phone is the primary identifier. OTP is valid for 10 minutes.",
|
"description": "Sends a 6-digit OTP to verify the worker's identity using their registered mobile number. Workers do not use email \u2014 mobile/phone is the primary identifier. OTP is valid for 10 minutes.",
|
||||||
"operationId": "workerForgotPassword",
|
"operationId": "workerForgotPassword",
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@ -7530,7 +7347,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"422": {
|
"422": {
|
||||||
"description": "Validation error — phone is required"
|
"description": "Validation error \u2014 phone is required"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7758,7 +7575,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"422": {
|
"422": {
|
||||||
"description": "Validation error — email or mobile required"
|
"description": "Validation error \u2014 email or mobile required"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9170,20 +8987,6 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"example": 2
|
"example": 2
|
||||||
},
|
},
|
||||||
"reviews": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"description": "Reviews received by the worker from employers"
|
|
||||||
},
|
|
||||||
"employer_reviews": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"description": "Reviews submitted by the worker about employers"
|
|
||||||
},
|
|
||||||
"photo": {
|
"photo": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
|
|||||||
@ -20,12 +20,8 @@ import {
|
|||||||
HeartHandshake,
|
HeartHandshake,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
MapPin,
|
MapPin,
|
||||||
Calendar,
|
Calendar
|
||||||
Search,
|
|
||||||
RotateCcw,
|
|
||||||
ArrowUpDown
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import FilterDrawer, { FilterSection, FilterSelect, FilterInput, FilterChips, FilterCheckboxList } from '../../components/Employer/FilterDrawer';
|
|
||||||
|
|
||||||
const getLanguageFlag = (lang) => {
|
const getLanguageFlag = (lang) => {
|
||||||
const flags = {
|
const flags = {
|
||||||
@ -40,7 +36,7 @@ const getLanguageFlag = (lang) => {
|
|||||||
return flags[lang] || '🌐';
|
return flags[lang] || '🌐';
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Shortlist({ shortlistedWorkers, filtersMetadata = {} }) {
|
export default function Shortlist({ shortlistedWorkers }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [workers, setWorkers] = useState(shortlistedWorkers || []);
|
const [workers, setWorkers] = useState(shortlistedWorkers || []);
|
||||||
|
|
||||||
@ -48,90 +44,6 @@ export default function Shortlist({ shortlistedWorkers, filtersMetadata = {} })
|
|||||||
const [comparisonIds, setComparisonIds] = useState([]);
|
const [comparisonIds, setComparisonIds] = useState([]);
|
||||||
const [previewWorker, setPreviewWorker] = useState(null);
|
const [previewWorker, setPreviewWorker] = useState(null);
|
||||||
|
|
||||||
// Basic Filters
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const [selectedProfession, setSelectedProfession] = useState('All Professions');
|
|
||||||
const [selectedNationalities, setSelectedNationalities] = useState([]);
|
|
||||||
const [selectedGender, setSelectedGender] = useState('All Genders');
|
|
||||||
const [selectedExperience, setSelectedExperience] = useState('All Experience');
|
|
||||||
const [selectedReligion, setSelectedReligion] = useState('All Religions');
|
|
||||||
const [maxSalary, setMaxSalary] = useState(5000);
|
|
||||||
|
|
||||||
// Advanced Multi-Select Filters & Sorting
|
|
||||||
const [selectedLanguages, setSelectedLanguages] = useState([]);
|
|
||||||
const [selectedVisaStatuses, setSelectedVisaStatuses] = useState([]);
|
|
||||||
const [selectedSkills, setSelectedSkills] = useState([]);
|
|
||||||
const [selectedWorkTypes, setSelectedWorkTypes] = useState([]);
|
|
||||||
const [sortBy, setSortBy] = useState('default');
|
|
||||||
|
|
||||||
// Advanced Filters Toggle & Details
|
|
||||||
const [filterLocation, setFilterLocation] = useState('All');
|
|
||||||
const [filterJobType, setFilterJobType] = useState('All');
|
|
||||||
const [filterAccommodation, setFilterAccommodation] = useState('All');
|
|
||||||
const [filterInCountry, setFilterInCountry] = useState('All');
|
|
||||||
const [filterVisaType, setFilterVisaType] = useState('All');
|
|
||||||
|
|
||||||
// Drawer
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
||||||
|
|
||||||
const resetFilters = () => {
|
|
||||||
setSearchQuery('');
|
|
||||||
setSelectedProfession('All Professions');
|
|
||||||
setSelectedNationalities([]);
|
|
||||||
setSelectedGender('All Genders');
|
|
||||||
setSelectedExperience('All Experience');
|
|
||||||
setSelectedReligion('All Religions');
|
|
||||||
setMaxSalary(5000);
|
|
||||||
setSelectedLanguages([]);
|
|
||||||
setSelectedVisaStatuses([]);
|
|
||||||
setSelectedSkills([]);
|
|
||||||
setSelectedWorkTypes([]);
|
|
||||||
setSortBy('default');
|
|
||||||
setFilterLocation('All');
|
|
||||||
setFilterJobType('All');
|
|
||||||
setFilterAccommodation('All');
|
|
||||||
setFilterInCountry('All');
|
|
||||||
setFilterVisaType('All');
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeFilterCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
if (filterLocation !== 'All' && filterLocation.trim() !== '') count++;
|
|
||||||
if (filterJobType !== 'All') count++;
|
|
||||||
if (filterAccommodation !== 'All') count++;
|
|
||||||
if (selectedNationalities.length > 0) count++;
|
|
||||||
if (selectedGender !== 'All Genders') count++;
|
|
||||||
if (selectedProfession !== 'All Professions') count++;
|
|
||||||
if (filterInCountry !== 'All') count++;
|
|
||||||
if (filterInCountry === 'In Country' && filterVisaType !== 'All') count++;
|
|
||||||
if (selectedSkills.length > 0) count++;
|
|
||||||
if (selectedLanguages.length > 0) count++;
|
|
||||||
if (maxSalary < 5000) count++;
|
|
||||||
return count;
|
|
||||||
}, [filterLocation, filterJobType, filterAccommodation, selectedNationalities, selectedGender, selectedProfession, filterInCountry, filterVisaType, selectedSkills, selectedLanguages, maxSalary]);
|
|
||||||
|
|
||||||
const activeFilterTags = useMemo(() => {
|
|
||||||
const tags = [];
|
|
||||||
if (filterLocation !== 'All' && filterLocation.trim()) tags.push({ key: 'loc', label: `📍 ${filterLocation}`, clear: () => setFilterLocation('All') });
|
|
||||||
if (filterJobType !== 'All') tags.push({ key: 'job', label: `💼 ${filterJobType}`, clear: () => setFilterJobType('All') });
|
|
||||||
if (filterAccommodation !== 'All') tags.push({ key: 'acc', label: `🏠 ${filterAccommodation.replace('_', '-')}`, clear: () => setFilterAccommodation('All') });
|
|
||||||
if (selectedNationalities.length > 0) tags.push({ key: 'nat', label: `🌍 ${selectedNationalities.length} Nat`, clear: () => setSelectedNationalities([]) });
|
|
||||||
if (selectedGender !== 'All Genders') tags.push({ key: 'gender', label: `🚻 ${selectedGender}`, clear: () => setSelectedGender('All Genders') });
|
|
||||||
if (selectedProfession !== 'All Professions') tags.push({ key: 'profession', label: `🧑🔧 ${selectedProfession}`, clear: () => setSelectedProfession('All Professions') });
|
|
||||||
if (filterInCountry !== 'All') tags.push({ key: 'country', label: `✈️ ${filterInCountry}`, clear: () => { setFilterInCountry('All'); setFilterVisaType('All'); } });
|
|
||||||
if (filterInCountry === 'In Country' && filterVisaType !== 'All') tags.push({ key: 'visa', label: `🪪 ${filterVisaType}`, clear: () => setFilterVisaType('All') });
|
|
||||||
if (selectedSkills.length > 0) tags.push({ key: 'skills', label: `🛠 ${selectedSkills.length} skill${selectedSkills.length > 1 ? 's' : ''}`, clear: () => setSelectedSkills([]) });
|
|
||||||
if (selectedLanguages.length > 0) tags.push({ key: 'lang', label: `🗣 ${selectedLanguages.length} lang`, clear: () => setSelectedLanguages([]) });
|
|
||||||
if (maxSalary < 5000) tags.push({ key: 'salary', label: `💰 < ${maxSalary} AED`, clear: () => setMaxSalary(5000) });
|
|
||||||
return tags;
|
|
||||||
}, [filterLocation, filterJobType, filterAccommodation, selectedNationalities, selectedGender, selectedProfession, filterInCountry, filterVisaType, selectedSkills, selectedLanguages, maxSalary]);
|
|
||||||
|
|
||||||
const toggleMultiSelect = (item, setSelectedList) => {
|
|
||||||
setSelectedList(prev =>
|
|
||||||
prev.includes(item) ? prev.filter(x => x !== item) : [...prev, item]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeWorker = (id) => {
|
const removeWorker = (id) => {
|
||||||
// Optimistic UI state update
|
// Optimistic UI state update
|
||||||
setWorkers(workers.filter(w => w.id !== id));
|
setWorkers(workers.filter(w => w.id !== id));
|
||||||
@ -155,114 +67,6 @@ export default function Shortlist({ shortlistedWorkers, filtersMetadata = {} })
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter and Sort Worker List
|
|
||||||
const filteredWorkers = useMemo(() => {
|
|
||||||
let list = [...workers];
|
|
||||||
list = list.filter(worker => {
|
|
||||||
// Search Query
|
|
||||||
if (searchQuery.trim() !== '') {
|
|
||||||
const query = searchQuery.toLowerCase();
|
|
||||||
const matchName = worker.name.toLowerCase().includes(query);
|
|
||||||
const matchBio = worker.bio?.toLowerCase().includes(query) || false;
|
|
||||||
const matchSkills = worker.skills?.some(s => s.toLowerCase().includes(query)) || false;
|
|
||||||
if (!matchName && !matchBio && !matchSkills) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dropdown filters
|
|
||||||
if (selectedProfession !== 'All Professions' && (!worker.main_profession || worker.main_profession.toLowerCase() !== selectedProfession.toLowerCase())) return false;
|
|
||||||
if (selectedNationalities.length > 0 && (!worker.nationality || !selectedNationalities.map(n => n.toLowerCase()).includes(worker.nationality.toLowerCase()))) return false;
|
|
||||||
if (selectedGender !== 'All Genders' && worker.gender && worker.gender.toLowerCase() !== selectedGender.toLowerCase()) return false;
|
|
||||||
if (selectedExperience !== 'All Experience' && worker.experience !== selectedExperience) return false;
|
|
||||||
if (selectedReligion !== 'All Religions' && worker.religion !== selectedReligion) return false;
|
|
||||||
if (maxSalary < 5000 && worker.salary > maxSalary) return false;
|
|
||||||
|
|
||||||
// Multi-select filters
|
|
||||||
if (selectedLanguages.length > 0) {
|
|
||||||
const hasLang = worker.languages?.some(l => selectedLanguages.includes(l));
|
|
||||||
if (!hasLang) return false;
|
|
||||||
}
|
|
||||||
if (selectedSkills.length > 0) {
|
|
||||||
const hasSkill = worker.skills?.some(s => selectedSkills.includes(s.toLowerCase()));
|
|
||||||
if (!hasSkill) return false;
|
|
||||||
}
|
|
||||||
if (selectedVisaStatuses.length > 0) {
|
|
||||||
if (!selectedVisaStatuses.includes(worker.visa_status)) return false;
|
|
||||||
}
|
|
||||||
if (selectedWorkTypes.length > 0 && !selectedWorkTypes.includes(worker.preferred_job_type)) return false;
|
|
||||||
|
|
||||||
// Preferred Location
|
|
||||||
if (filterLocation !== 'All' && filterLocation.trim() !== '') {
|
|
||||||
const locKey = filterLocation.toLowerCase();
|
|
||||||
const locationMapping = {
|
|
||||||
'dubai': ['dubai', 'marina', 'barsha', 'nahda', 'jumeirah', 'deira', 'downtown', 'silicon', 'sports', 'motor', 'jlt', 'jbr', 'meydan', 'ranches', 'bay', 'mirdif', 'quoz'],
|
|
||||||
'abu dhabi': ['abu dhabi', 'yas', 'khalifa', 'reem', 'saadiyat', 'raha', 'mussafah', 'zahiyah', 'karamah'],
|
|
||||||
'oman': ['oman', 'muscat', 'salalah', 'sohar', 'nizwa', 'sur', 'ibri', 'rustaq']
|
|
||||||
};
|
|
||||||
const allowedKeywords = locationMapping[locKey] || [locKey];
|
|
||||||
if (!worker.preferred_location) return false;
|
|
||||||
const wLoc = worker.preferred_location.toLowerCase();
|
|
||||||
const matched = allowedKeywords.some(keyword => wLoc.includes(keyword));
|
|
||||||
if (!matched) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Job Type
|
|
||||||
if (filterJobType !== 'All') {
|
|
||||||
if (!worker.preferred_job_type || worker.preferred_job_type.toLowerCase() !== filterJobType.toLowerCase()) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accommodation
|
|
||||||
if (filterAccommodation !== 'All') {
|
|
||||||
if (!worker.live_in_out || worker.live_in_out.toLowerCase() !== filterAccommodation.toLowerCase()) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In/Out Country
|
|
||||||
if (filterInCountry !== 'All') {
|
|
||||||
const wantInCountry = filterInCountry === 'In Country';
|
|
||||||
const wVal = worker.in_country;
|
|
||||||
if ((wVal === true || wVal === '1' || wVal === 1) !== wantInCountry) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Visa Type (if in country)
|
|
||||||
if (filterInCountry === 'In Country' && filterVisaType !== 'All') {
|
|
||||||
if (!worker.visa_status || !worker.visa_status.toLowerCase().includes(filterVisaType.toLowerCase())) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sorting
|
|
||||||
if (sortBy === 'salary_asc') {
|
|
||||||
list.sort((a, b) => a.salary - b.salary);
|
|
||||||
} else if (sortBy === 'salary_desc') {
|
|
||||||
list.sort((a, b) => b.salary - a.salary);
|
|
||||||
} else if (sortBy === 'rating') {
|
|
||||||
list.sort((a, b) => b.rating - a.rating);
|
|
||||||
} else if (sortBy === 'experience') {
|
|
||||||
list.sort((a, b) => b.age - a.age); // approximate experience by age
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}, [
|
|
||||||
workers,
|
|
||||||
searchQuery,
|
|
||||||
selectedProfession,
|
|
||||||
selectedNationalities,
|
|
||||||
selectedGender,
|
|
||||||
selectedExperience,
|
|
||||||
selectedReligion,
|
|
||||||
maxSalary,
|
|
||||||
selectedLanguages,
|
|
||||||
selectedVisaStatuses,
|
|
||||||
selectedSkills,
|
|
||||||
selectedWorkTypes,
|
|
||||||
sortBy,
|
|
||||||
filterLocation,
|
|
||||||
filterJobType,
|
|
||||||
filterAccommodation,
|
|
||||||
filterInCountry,
|
|
||||||
filterVisaType
|
|
||||||
]);
|
|
||||||
|
|
||||||
const comparedWorkers = useMemo(() => {
|
const comparedWorkers = useMemo(() => {
|
||||||
return workers.filter(w => comparisonIds.includes(w.id));
|
return workers.filter(w => comparisonIds.includes(w.id));
|
||||||
}, [workers, comparisonIds]);
|
}, [workers, comparisonIds]);
|
||||||
@ -286,272 +90,9 @@ export default function Shortlist({ shortlistedWorkers, filtersMetadata = {} })
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Filter Drawer ── */}
|
|
||||||
<FilterDrawer
|
|
||||||
open={drawerOpen}
|
|
||||||
onClose={() => setDrawerOpen(false)}
|
|
||||||
onReset={resetFilters}
|
|
||||||
activeCount={activeFilterCount}
|
|
||||||
title="Filter Saved Workers"
|
|
||||||
>
|
|
||||||
{/* Profession */}
|
|
||||||
{filtersMetadata.professions && (
|
|
||||||
<FilterSection label="Profession">
|
|
||||||
<FilterSelect
|
|
||||||
id="filter_main_profession"
|
|
||||||
value={selectedProfession}
|
|
||||||
onChange={e => setSelectedProfession(e.target.value)}
|
|
||||||
>
|
|
||||||
{filtersMetadata.professions.map(prof => (
|
|
||||||
<option key={prof} value={prof}>
|
|
||||||
{prof === 'All Professions' ? 'All Professions' : prof}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</FilterSelect>
|
|
||||||
</FilterSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Preferred Location */}
|
|
||||||
<FilterSection label="Preferred Location">
|
|
||||||
<FilterSelect
|
|
||||||
id="filter_preferred_location"
|
|
||||||
value={filterLocation}
|
|
||||||
onChange={e => setFilterLocation(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="All">All Locations</option>
|
|
||||||
<option value="Dubai">Dubai</option>
|
|
||||||
<option value="Abu Dhabi">Abu Dhabi</option>
|
|
||||||
<option value="Oman">Oman</option>
|
|
||||||
</FilterSelect>
|
|
||||||
</FilterSection>
|
|
||||||
|
|
||||||
{/* Job Type */}
|
|
||||||
<FilterSection label="Job Type">
|
|
||||||
<FilterChips
|
|
||||||
options={[
|
|
||||||
{ value: 'All', label: 'All' },
|
|
||||||
{ value: 'full-time', label: 'Full-time' },
|
|
||||||
{ value: 'part-time', label: 'Part-time' },
|
|
||||||
]}
|
|
||||||
selected={filterJobType}
|
|
||||||
onChange={setFilterJobType}
|
|
||||||
/>
|
|
||||||
</FilterSection>
|
|
||||||
|
|
||||||
{/* Accommodation */}
|
|
||||||
<FilterSection label="Accommodation Type">
|
|
||||||
<FilterChips
|
|
||||||
options={[
|
|
||||||
{ value: 'All', label: 'All' },
|
|
||||||
{ value: 'live_in', label: 'Live-in' },
|
|
||||||
{ value: 'live_out', label: 'Live-out' },
|
|
||||||
]}
|
|
||||||
selected={filterAccommodation}
|
|
||||||
onChange={setFilterAccommodation}
|
|
||||||
/>
|
|
||||||
</FilterSection>
|
|
||||||
|
|
||||||
{/* Nationality */}
|
|
||||||
{filtersMetadata.nationalities?.length > 1 && (
|
|
||||||
<FilterSection label="Nationality">
|
|
||||||
<FilterCheckboxList
|
|
||||||
items={filtersMetadata.nationalities.filter(n => n !== 'All Nationalities')}
|
|
||||||
selected={selectedNationalities}
|
|
||||||
onToggle={nat => toggleMultiSelect(nat, setSelectedNationalities)}
|
|
||||||
/>
|
|
||||||
</FilterSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Gender */}
|
|
||||||
<FilterSection label="Gender">
|
|
||||||
<FilterChips
|
|
||||||
options={[
|
|
||||||
{ value: 'All Genders', label: 'All' },
|
|
||||||
{ value: 'male', label: 'Male' },
|
|
||||||
{ value: 'female', label: 'Female' },
|
|
||||||
]}
|
|
||||||
selected={selectedGender}
|
|
||||||
onChange={setSelectedGender}
|
|
||||||
/>
|
|
||||||
</FilterSection>
|
|
||||||
|
|
||||||
{/* Country Status */}
|
|
||||||
<FilterSection label="Country Status">
|
|
||||||
<FilterChips
|
|
||||||
options={[
|
|
||||||
{ value: 'All', label: 'All' },
|
|
||||||
{ value: 'In Country', label: 'In Country' },
|
|
||||||
{ value: 'Out of Country', label: 'Out of Country' },
|
|
||||||
]}
|
|
||||||
selected={filterInCountry}
|
|
||||||
onChange={val => {
|
|
||||||
setFilterInCountry(val);
|
|
||||||
if (val !== 'In Country') setFilterVisaType('All');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FilterSection>
|
|
||||||
|
|
||||||
{/* Visa Type */}
|
|
||||||
<FilterSection label="Visa Type">
|
|
||||||
<div className={filterInCountry !== 'In Country' ? 'opacity-40 pointer-events-none' : ''}>
|
|
||||||
<FilterSelect
|
|
||||||
id="filter_visa_status"
|
|
||||||
value={filterVisaType}
|
|
||||||
onChange={e => setFilterVisaType(e.target.value)}
|
|
||||||
disabled={filterInCountry !== 'In Country'}
|
|
||||||
>
|
|
||||||
<option value="All">All Visa Types</option>
|
|
||||||
<option value="Residence Visa">Residence Visa</option>
|
|
||||||
<option value="Tourist Visa">Tourist Visa</option>
|
|
||||||
<option value="Employment Visa">Employment Visa</option>
|
|
||||||
</FilterSelect>
|
|
||||||
{filterInCountry !== 'In Country' && (
|
|
||||||
<p className="text-[11px] text-slate-400 mt-1.5 font-medium">Select "In Country" to enable this filter</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FilterSection>
|
|
||||||
|
|
||||||
{/* Skills */}
|
|
||||||
{filtersMetadata.skills?.length > 1 && (
|
|
||||||
<FilterSection label="Skills">
|
|
||||||
<FilterCheckboxList
|
|
||||||
items={filtersMetadata.skills.slice(1).map(s => s.toLowerCase())}
|
|
||||||
selected={selectedSkills}
|
|
||||||
onToggle={skill => toggleMultiSelect(skill, setSelectedSkills)}
|
|
||||||
/>
|
|
||||||
</FilterSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Languages */}
|
|
||||||
{filtersMetadata.languages?.length > 1 && (
|
|
||||||
<FilterSection label="Languages">
|
|
||||||
<FilterCheckboxList
|
|
||||||
items={filtersMetadata.languages.slice(1)}
|
|
||||||
selected={selectedLanguages}
|
|
||||||
onToggle={lang => toggleMultiSelect(lang, setSelectedLanguages)}
|
|
||||||
/>
|
|
||||||
</FilterSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Salary Range */}
|
|
||||||
<FilterSection label={maxSalary === 5000 ? "Max Salary — Any" : `Max Salary — ${maxSalary} AED`}>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="500"
|
|
||||||
max="5000"
|
|
||||||
step="100"
|
|
||||||
value={maxSalary}
|
|
||||||
onChange={e => setMaxSalary(Number(e.target.value))}
|
|
||||||
className="w-full accent-[#185FA5]"
|
|
||||||
/>
|
|
||||||
<div className="flex justify-between text-[10px] font-bold text-slate-400 mt-1">
|
|
||||||
<span>500 AED</span>
|
|
||||||
<span>5,000 AED</span>
|
|
||||||
</div>
|
|
||||||
</FilterSection>
|
|
||||||
</FilterDrawer>
|
|
||||||
|
|
||||||
{/* Filter and Search Panel */}
|
|
||||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm">
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
|
|
||||||
{/* Search */}
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
placeholder={t('search_by_name_skills_bio', 'Search by name, skills, bio...')}
|
|
||||||
className="w-full pl-11 pr-4 py-3 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{/* Sort */}
|
|
||||||
<div className="flex items-center bg-slate-50 rounded-xl border border-slate-200 px-3 py-2 text-xs font-bold text-slate-700">
|
|
||||||
<ArrowUpDown className="w-4 h-4 text-slate-400 mr-2" />
|
|
||||||
<select
|
|
||||||
value={sortBy}
|
|
||||||
onChange={(e) => setSortBy(e.target.value)}
|
|
||||||
className="bg-transparent focus:outline-none cursor-pointer"
|
|
||||||
>
|
|
||||||
<option value="default">{t('default_match', 'Default Match')}</option>
|
|
||||||
<option value="salary_asc">{t('salary_low_to_high', 'Salary: Low to High')}</option>
|
|
||||||
<option value="salary_desc">{t('salary_high_to_low', 'Salary: High to Low')}</option>
|
|
||||||
<option value="rating">{t('rating_high_to_low', 'Rating: High to Low')}</option>
|
|
||||||
<option value="experience">{t('experience_level', 'Experience Level')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Reset */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={resetFilters}
|
|
||||||
className="flex items-center justify-center space-x-1.5 px-3 py-2.5 rounded-xl border border-slate-200 hover:bg-slate-50 text-xs font-semibold text-slate-600 transition-colors"
|
|
||||||
title="Reset all filters"
|
|
||||||
>
|
|
||||||
<RotateCcw className="w-4 h-4 text-slate-400" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Filter Drawer button */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setDrawerOpen(true)}
|
|
||||||
className={`relative flex items-center space-x-2 px-4 py-2.5 rounded-xl border transition-all text-xs font-bold ${
|
|
||||||
activeFilterCount > 0
|
|
||||||
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-md shadow-blue-200'
|
|
||||||
: 'border-slate-200 hover:bg-slate-50 text-slate-600 bg-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<SlidersHorizontal className="w-4 h-4" />
|
|
||||||
<span>{t('filters', 'Filters')}</span>
|
|
||||||
{activeFilterCount > 0 && (
|
|
||||||
<span className="bg-white/25 text-white text-[10px] font-black px-1.5 py-0.5 rounded-full leading-none">
|
|
||||||
{activeFilterCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Active filter tags */}
|
|
||||||
{activeFilterTags.length > 0 && (
|
|
||||||
<div className="mt-3 pt-3 border-t border-slate-100 flex flex-wrap gap-2 items-center">
|
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest mr-1">Active:</span>
|
|
||||||
{activeFilterTags.map(tag => (
|
|
||||||
<button
|
|
||||||
key={tag.key}
|
|
||||||
onClick={tag.clear}
|
|
||||||
className="inline-flex items-center space-x-1.5 px-2.5 py-1 bg-[#185FA5]/10 text-[#185FA5] text-[11px] font-bold rounded-full border border-[#185FA5]/20 hover:bg-[#185FA5]/20 transition-all"
|
|
||||||
>
|
|
||||||
<span>{tag.label}</span>
|
|
||||||
<span className="text-[#185FA5]/60 font-black">×</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
<button onClick={resetFilters} className="text-[11px] font-bold text-rose-500 hover:text-rose-700 ml-1 transition-colors">Clear all</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Listing metadata bar */}
|
|
||||||
<div className="flex items-center justify-between px-1">
|
|
||||||
<div className="text-xs font-semibold text-slate-500">
|
|
||||||
{t('found_workers_matching', 'Found {count} workers matching your preferences')
|
|
||||||
.split('{count}')[0]}
|
|
||||||
<span className="text-slate-900 font-bold">{filteredWorkers.length}</span>
|
|
||||||
{t('found_workers_matching', 'Found {count} workers matching your preferences')
|
|
||||||
.split('{count}')[1] || ''}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2 text-xs font-medium text-slate-600">
|
|
||||||
<CheckCircle className="w-3.5 h-3.5 text-emerald-600" />
|
|
||||||
<span>{t('direct_sponsoring_active', 'Direct Sponsorship Active')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{workers.length > 0 ? (
|
{workers.length > 0 ? (
|
||||||
filteredWorkers.length > 0 ? (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{filteredWorkers.map((worker) => {
|
{workers.map((worker) => {
|
||||||
const isComparing = comparisonIds.includes(worker.id);
|
const isComparing = comparisonIds.includes(worker.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -727,22 +268,7 @@ export default function Shortlist({ shortlistedWorkers, filtersMetadata = {} })
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-16 bg-white rounded-2xl border border-slate-200 shadow-sm space-y-3 w-full col-span-full">
|
<div className="text-center py-16 bg-white rounded-2xl border border-slate-200 shadow-sm space-y-3">
|
||||||
<SlidersHorizontal className="w-12 h-12 text-slate-300 mx-auto" />
|
|
||||||
<div className="text-base font-bold text-slate-800">{t('no_matching_results', 'No matching saved workers')}</div>
|
|
||||||
<p className="text-xs text-slate-500 max-w-sm mx-auto">
|
|
||||||
{t('no_matching_results_desc', 'Try clearing or modifying your filters to find your saved candidates.')}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={resetFilters}
|
|
||||||
className="inline-block mt-2 bg-[#185FA5] text-white px-6 py-2.5 rounded-xl text-xs font-bold shadow-sm hover:bg-[#144f8a] transition-colors"
|
|
||||||
>
|
|
||||||
{t('clear_all_filters', 'Clear All Filters')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-16 bg-white rounded-2xl border border-slate-200 shadow-sm space-y-3 w-full col-span-full">
|
|
||||||
<SlidersHorizontal className="w-12 h-12 text-slate-300 mx-auto" />
|
<SlidersHorizontal className="w-12 h-12 text-slate-300 mx-auto" />
|
||||||
<div className="text-base font-bold text-slate-800">{t('shortlist_empty', 'Your shortlist is empty')}</div>
|
<div className="text-base font-bold text-slate-800">{t('shortlist_empty', 'Your shortlist is empty')}</div>
|
||||||
<p className="text-xs text-slate-500 max-w-sm mx-auto">
|
<p className="text-xs text-slate-500 max-w-sm mx-auto">
|
||||||
|
|||||||
@ -78,7 +78,6 @@
|
|||||||
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::get('/workers/employers', [WorkerProfileController::class, 'getEmployers']);
|
Route::get('/workers/employers', [WorkerProfileController::class, 'getEmployers']);
|
||||||
Route::get('/workers/employers/{id}', [WorkerProfileController::class, 'getEmployerProfile']);
|
|
||||||
Route::post('/workers/change-password', [WorkerProfileController::class, 'changePassword']);
|
Route::post('/workers/change-password', [WorkerProfileController::class, 'changePassword']);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -85,31 +85,4 @@ public function test_employer_upload_emirates_id_ocr_and_purge()
|
|||||||
$this->assertNotNull($profile->emirates_id_expiry);
|
$this->assertNotNull($profile->emirates_id_expiry);
|
||||||
$this->assertEquals('approved', $profile->verification_status);
|
$this->assertEquals('approved', $profile->verification_status);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that a stale session with an invalid user ID does not cause a redirect loop.
|
|
||||||
*/
|
|
||||||
public function test_stale_session_does_not_cause_redirect_loop()
|
|
||||||
{
|
|
||||||
// 1. Visit with a session user object containing a non-existent ID
|
|
||||||
$staleUser = (object)[
|
|
||||||
'id' => 999999, // Doesn't exist
|
|
||||||
'name' => 'Stale User',
|
|
||||||
'email' => 'stale@example.com',
|
|
||||||
'role' => 'employer',
|
|
||||||
'subscription_status' => 'active',
|
|
||||||
'verification_status' => 'approved',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Accessing dashboard should fail auth, clear session, and redirect to login
|
|
||||||
$response = $this->withSession(['user' => $staleUser])
|
|
||||||
->get('/employer/dashboard');
|
|
||||||
|
|
||||||
$response->assertRedirect(route('employer.login'));
|
|
||||||
$this->assertNull(session('user')); // Session must be cleared
|
|
||||||
|
|
||||||
// Subsequent access to login page should render successfully (no redirect)
|
|
||||||
$loginResponse = $this->get('/employer/login');
|
|
||||||
$loginResponse->assertStatus(200);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,192 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\Worker;
|
|
||||||
use App\Models\Shortlist;
|
|
||||||
use App\Models\WorkerDocument;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class EmployerShortlistWebFilterTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
protected $employer;
|
|
||||||
protected $worker1;
|
|
||||||
protected $worker2;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->employer = User::create([
|
|
||||||
'name' => 'Test Employer',
|
|
||||||
'email' => 'employer@example.com',
|
|
||||||
'password' => bcrypt('password'),
|
|
||||||
'role' => 'employer',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->worker1 = Worker::create([
|
|
||||||
'name' => 'Worker Indian Dubai',
|
|
||||||
'email' => 'worker1@example.com',
|
|
||||||
'phone' => '+971501111111',
|
|
||||||
'password' => bcrypt('password'),
|
|
||||||
'nationality' => 'Indian',
|
|
||||||
'verified' => true,
|
|
||||||
'status' => 'active',
|
|
||||||
'preferred_location' => 'Dubai',
|
|
||||||
'preferred_job_type' => 'full-time',
|
|
||||||
'live_in_out' => 'live_in',
|
|
||||||
'in_country' => true,
|
|
||||||
'visa_status' => 'Residence Visa',
|
|
||||||
'main_profession' => 'Housemaid',
|
|
||||||
'age' => 25,
|
|
||||||
'salary' => 1500,
|
|
||||||
'experience' => '3 Years',
|
|
||||||
'religion' => 'Christian',
|
|
||||||
'language' => 'EN',
|
|
||||||
'availability' => 'Immediate',
|
|
||||||
'bio' => 'Experienced helper',
|
|
||||||
'gender' => 'female',
|
|
||||||
]);
|
|
||||||
|
|
||||||
WorkerDocument::create([
|
|
||||||
'worker_id' => $this->worker1->id,
|
|
||||||
'type' => 'passport',
|
|
||||||
'number' => 'P111111',
|
|
||||||
'file_path' => 'passports/test1.jpg',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->worker2 = Worker::create([
|
|
||||||
'name' => 'Worker Filipino Abu Dhabi',
|
|
||||||
'email' => 'worker2@example.com',
|
|
||||||
'phone' => '+971502222222',
|
|
||||||
'password' => bcrypt('password'),
|
|
||||||
'nationality' => 'Filipino',
|
|
||||||
'verified' => true,
|
|
||||||
'status' => 'active',
|
|
||||||
'preferred_location' => 'Abu Dhabi',
|
|
||||||
'preferred_job_type' => 'part-time',
|
|
||||||
'live_in_out' => 'live_out',
|
|
||||||
'in_country' => true,
|
|
||||||
'visa_status' => 'Tourist Visa',
|
|
||||||
'main_profession' => 'Nanny',
|
|
||||||
'age' => 28,
|
|
||||||
'salary' => 1800,
|
|
||||||
'experience' => '2 Years',
|
|
||||||
'religion' => 'Christian',
|
|
||||||
'language' => 'TL',
|
|
||||||
'availability' => 'Immediate',
|
|
||||||
'bio' => 'Ready to work',
|
|
||||||
'gender' => 'male',
|
|
||||||
]);
|
|
||||||
|
|
||||||
WorkerDocument::create([
|
|
||||||
'worker_id' => $this->worker2->id,
|
|
||||||
'type' => 'passport',
|
|
||||||
'number' => 'P222222',
|
|
||||||
'file_path' => 'passports/test2.jpg',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Shortlist both workers for this employer
|
|
||||||
Shortlist::create([
|
|
||||||
'employer_id' => $this->employer->id,
|
|
||||||
'worker_id' => $this->worker1->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
Shortlist::create([
|
|
||||||
'employer_id' => $this->employer->id,
|
|
||||||
'worker_id' => $this->worker2->id,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test shortlist index page loads correct properties.
|
|
||||||
*/
|
|
||||||
public function test_employer_can_view_shortlist_with_metadata()
|
|
||||||
{
|
|
||||||
$response = $this->actingAs($this->employer)
|
|
||||||
->withSession(['user' => $this->employer])
|
|
||||||
->get(route('employer.shortlist'));
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
$response->assertSee('shortlistedWorkers');
|
|
||||||
$response->assertSee('filtersMetadata');
|
|
||||||
|
|
||||||
// Check if both workers are returned when no filters are applied
|
|
||||||
$shortlisted = $response->viewData('page')['props']['shortlistedWorkers'];
|
|
||||||
$this->assertCount(2, $shortlisted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test shortlist filtering by location.
|
|
||||||
*/
|
|
||||||
public function test_employer_shortlist_filtering_by_location()
|
|
||||||
{
|
|
||||||
$response = $this->actingAs($this->employer)
|
|
||||||
->withSession(['user' => $this->employer])
|
|
||||||
->get(route('employer.shortlist', ['preferred_location' => 'Abu Dhabi']));
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
$shortlisted = $response->viewData('page')['props']['shortlistedWorkers'];
|
|
||||||
|
|
||||||
$this->assertCount(1, $shortlisted);
|
|
||||||
$this->assertEquals('Worker Filipino Abu Dhabi', $shortlisted[0]['name']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test shortlist filtering by nationality.
|
|
||||||
*/
|
|
||||||
public function test_employer_shortlist_filtering_by_nationality()
|
|
||||||
{
|
|
||||||
$response = $this->actingAs($this->employer)
|
|
||||||
->withSession(['user' => $this->employer])
|
|
||||||
->get(route('employer.shortlist', ['nationality' => 'Indian']));
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
$shortlisted = $response->viewData('page')['props']['shortlistedWorkers'];
|
|
||||||
|
|
||||||
$this->assertCount(1, $shortlisted);
|
|
||||||
$this->assertEquals('Worker Indian Dubai', $shortlisted[0]['name']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test shortlist filtering by job type and accommodation.
|
|
||||||
*/
|
|
||||||
public function test_employer_shortlist_filtering_by_job_type_and_accommodation()
|
|
||||||
{
|
|
||||||
$response = $this->actingAs($this->employer)
|
|
||||||
->withSession(['user' => $this->employer])
|
|
||||||
->get(route('employer.shortlist', [
|
|
||||||
'job_type' => 'part-time',
|
|
||||||
'accommodation_type' => 'live_out'
|
|
||||||
]));
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
$shortlisted = $response->viewData('page')['props']['shortlistedWorkers'];
|
|
||||||
|
|
||||||
$this->assertCount(1, $shortlisted);
|
|
||||||
$this->assertEquals('Worker Filipino Abu Dhabi', $shortlisted[0]['name']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test shortlist filtering by visa status and gender.
|
|
||||||
*/
|
|
||||||
public function test_employer_shortlist_filtering_by_visa_status_and_gender()
|
|
||||||
{
|
|
||||||
$response = $this->actingAs($this->employer)
|
|
||||||
->withSession(['user' => $this->employer])
|
|
||||||
->get(route('employer.shortlist', [
|
|
||||||
'visa_status' => 'Residence Visa',
|
|
||||||
'gender' => 'female'
|
|
||||||
]));
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
$shortlisted = $response->viewData('page')['props']['shortlistedWorkers'];
|
|
||||||
|
|
||||||
$this->assertCount(1, $shortlisted);
|
|
||||||
$this->assertEquals('Worker Indian Dubai', $shortlisted[0]['name']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -236,172 +236,4 @@ public function test_worker_employers_list_pagination_and_sorting()
|
|||||||
$this->assertEquals(3, $responsePaginated->json('data.pagination.total'));
|
$this->assertEquals(3, $responsePaginated->json('data.pagination.total'));
|
||||||
$this->assertEquals(2, $responsePaginated->json('data.pagination.per_page'));
|
$this->assertEquals(2, $responsePaginated->json('data.pagination.per_page'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_worker_can_view_employer_profile_with_permissions_and_reviews()
|
|
||||||
{
|
|
||||||
// Create worker
|
|
||||||
$worker = Worker::create([
|
|
||||||
'name' => 'Rahul Sharma',
|
|
||||||
'email' => 'rahul@example.com',
|
|
||||||
'phone' => '+971501234567',
|
|
||||||
'language' => 'HI',
|
|
||||||
'password' => bcrypt('password'),
|
|
||||||
'nationality' => 'Indian',
|
|
||||||
'age' => 25,
|
|
||||||
'salary' => 1500,
|
|
||||||
'availability' => 'Immediate',
|
|
||||||
'experience' => 'Not Specified',
|
|
||||||
'religion' => 'Not Specified',
|
|
||||||
'bio' => 'Test',
|
|
||||||
'verified' => true,
|
|
||||||
'status' => 'active',
|
|
||||||
'api_token' => 'worker-test-token',
|
|
||||||
]);
|
|
||||||
|
|
||||||
WorkerDocument::create([
|
|
||||||
'worker_id' => $worker->id,
|
|
||||||
'type' => 'passport',
|
|
||||||
'number' => '123456',
|
|
||||||
'file_path' => 'passports/test.pdf',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create an employer
|
|
||||||
$employer = User::create([
|
|
||||||
'name' => 'Employer One',
|
|
||||||
'email' => 'emp1@example.com',
|
|
||||||
'password' => bcrypt('password'),
|
|
||||||
'role' => 'employer',
|
|
||||||
]);
|
|
||||||
|
|
||||||
EmployerProfile::create([
|
|
||||||
'user_id' => $employer->id,
|
|
||||||
'company_name' => 'Ahmad Tech Ltd',
|
|
||||||
'phone' => '+971500000001',
|
|
||||||
'nationality' => 'UAE',
|
|
||||||
'city' => 'Dubai',
|
|
||||||
'accommodation' => 'Provided',
|
|
||||||
'language' => 'English',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create active job post
|
|
||||||
\App\Models\JobPost::create([
|
|
||||||
'employer_id' => $employer->id,
|
|
||||||
'title' => 'Housekeeper Needed',
|
|
||||||
'location' => 'Dubai Marina',
|
|
||||||
'salary' => 2500,
|
|
||||||
'workers_needed' => 1,
|
|
||||||
'job_type' => 'Full Time',
|
|
||||||
'start_date' => now()->addDays(5)->toDateString(),
|
|
||||||
'description' => 'Clean house',
|
|
||||||
'status' => 'active',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create an accepted job offer (connection exists)
|
|
||||||
JobOffer::create([
|
|
||||||
'employer_id' => $employer->id,
|
|
||||||
'worker_id' => $worker->id,
|
|
||||||
'work_date' => now()->subDays(10),
|
|
||||||
'location' => 'Dubai',
|
|
||||||
'salary' => 2500,
|
|
||||||
'status' => 'accepted',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create a review submitted by another worker
|
|
||||||
$otherWorker = Worker::create([
|
|
||||||
'name' => 'John Doe',
|
|
||||||
'email' => 'john@example.com',
|
|
||||||
'phone' => '+971501234568',
|
|
||||||
'password' => bcrypt('password'),
|
|
||||||
'nationality' => 'Filipino',
|
|
||||||
'age' => 25,
|
|
||||||
'salary' => 1500,
|
|
||||||
'availability' => 'Immediate',
|
|
||||||
'experience' => 'Not Specified',
|
|
||||||
'religion' => 'Not Specified',
|
|
||||||
'bio' => 'Test',
|
|
||||||
'passport_status' => 'valid',
|
|
||||||
]);
|
|
||||||
|
|
||||||
\App\Models\EmployerReview::create([
|
|
||||||
'worker_id' => $otherWorker->id,
|
|
||||||
'employer_id' => $employer->id,
|
|
||||||
'rating' => 5,
|
|
||||||
'title' => 'Excellent sponsor',
|
|
||||||
'comment' => 'Very polite and pays on time.',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Hit the new API endpoint
|
|
||||||
$response = $this->withHeaders([
|
|
||||||
'Authorization' => 'Bearer worker-test-token',
|
|
||||||
])->getJson("/api/workers/employers/{$employer->id}");
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
$response->assertJsonPath('success', true);
|
|
||||||
$response->assertJsonPath('data.employer.name', 'Employer One');
|
|
||||||
$response->assertJsonPath('data.employer.email', 'emp1@example.com'); // Contact info visible because of JobOffer connection
|
|
||||||
$response->assertJsonPath('data.employer.phone', '+971500000001');
|
|
||||||
$response->assertJsonPath('data.employer.company_name', 'Ahmad Tech Ltd');
|
|
||||||
$response->assertJsonPath('data.employer.rating', 5);
|
|
||||||
$response->assertJsonPath('data.employer.reviews_count', 1);
|
|
||||||
$response->assertJsonPath('data.employer.review_summary.stars.5', 1);
|
|
||||||
$response->assertJsonCount(1, 'data.employer.active_jobs');
|
|
||||||
$response->assertJsonPath('data.employer.active_jobs.0.title', 'Housekeeper Needed');
|
|
||||||
$response->assertJsonCount(1, 'data.reviews.data');
|
|
||||||
$response->assertJsonPath('data.reviews.data.0.worker.name', 'John Doe');
|
|
||||||
$response->assertJsonPath('data.reviews.data.0.title', 'Excellent sponsor');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_worker_can_view_employer_profile_without_permissions_masks_contact()
|
|
||||||
{
|
|
||||||
// Create worker
|
|
||||||
$worker = Worker::create([
|
|
||||||
'name' => 'Rahul Sharma',
|
|
||||||
'email' => 'rahul@example.com',
|
|
||||||
'phone' => '+971501234567',
|
|
||||||
'language' => 'HI',
|
|
||||||
'password' => bcrypt('password'),
|
|
||||||
'nationality' => 'Indian',
|
|
||||||
'age' => 25,
|
|
||||||
'salary' => 1500,
|
|
||||||
'availability' => 'Immediate',
|
|
||||||
'experience' => 'Not Specified',
|
|
||||||
'religion' => 'Not Specified',
|
|
||||||
'bio' => 'Test',
|
|
||||||
'verified' => true,
|
|
||||||
'status' => 'active',
|
|
||||||
'api_token' => 'worker-test-token',
|
|
||||||
]);
|
|
||||||
|
|
||||||
WorkerDocument::create([
|
|
||||||
'worker_id' => $worker->id,
|
|
||||||
'type' => 'passport',
|
|
||||||
'number' => '123456',
|
|
||||||
'file_path' => 'passports/test.pdf',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create an employer
|
|
||||||
$employer = User::create([
|
|
||||||
'name' => 'Employer One',
|
|
||||||
'email' => 'emp1@example.com',
|
|
||||||
'password' => bcrypt('password'),
|
|
||||||
'role' => 'employer',
|
|
||||||
]);
|
|
||||||
|
|
||||||
EmployerProfile::create([
|
|
||||||
'user_id' => $employer->id,
|
|
||||||
'company_name' => 'Ahmad Tech Ltd',
|
|
||||||
'phone' => '+971500000001',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Hit the API (no connection exists)
|
|
||||||
$response = $this->withHeaders([
|
|
||||||
'Authorization' => 'Bearer worker-test-token',
|
|
||||||
])->getJson("/api/workers/employers/{$employer->id}");
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
$response->assertJsonPath('success', true);
|
|
||||||
$response->assertJsonPath('data.employer.name', 'Employer One');
|
|
||||||
$response->assertJsonPath('data.employer.email', null); // Masked/null since no connection exists
|
|
||||||
$response->assertJsonPath('data.employer.phone', null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,211 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\Worker;
|
|
||||||
use App\Models\WorkerDocument;
|
|
||||||
use App\Models\JobPost;
|
|
||||||
use App\Models\JobApplication;
|
|
||||||
use App\Models\Review;
|
|
||||||
use App\Models\EmployerReview;
|
|
||||||
use App\Models\JobOffer;
|
|
||||||
use App\Models\EmployerProfile;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class WorkerLoginReviewDataTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
protected $worker;
|
|
||||||
protected $employer;
|
|
||||||
protected $job;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
// 1. Create worker
|
|
||||||
$this->worker = Worker::create([
|
|
||||||
'name' => 'Rahul Sharma',
|
|
||||||
'email' => 'rahul@example.com',
|
|
||||||
'phone' => '+971501234567',
|
|
||||||
'password' => bcrypt('password'),
|
|
||||||
'nationality' => 'Indian',
|
|
||||||
'age' => 25,
|
|
||||||
'salary' => 1500,
|
|
||||||
'availability' => 'Immediate',
|
|
||||||
'experience' => 'Not Specified',
|
|
||||||
'religion' => 'Not Specified',
|
|
||||||
'bio' => 'Test',
|
|
||||||
'verified' => true,
|
|
||||||
'status' => 'active',
|
|
||||||
'api_token' => 'worker-test-token',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create passport document to prevent pending 404
|
|
||||||
WorkerDocument::create([
|
|
||||||
'worker_id' => $this->worker->id,
|
|
||||||
'type' => 'passport',
|
|
||||||
'number' => '123456',
|
|
||||||
'file_path' => 'passports/test.pdf',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 2. Create an employer
|
|
||||||
$this->employer = User::create([
|
|
||||||
'name' => 'Employer One',
|
|
||||||
'email' => 'emp1@example.com',
|
|
||||||
'password' => bcrypt('password'),
|
|
||||||
'role' => 'employer',
|
|
||||||
'api_token' => 'employer-test-token',
|
|
||||||
]);
|
|
||||||
|
|
||||||
EmployerProfile::create([
|
|
||||||
'user_id' => $this->employer->id,
|
|
||||||
'company_name' => 'Ahmad Tech Ltd',
|
|
||||||
'nationality' => 'UAE',
|
|
||||||
'city' => 'Dubai',
|
|
||||||
'phone' => '+971 50 123 4567',
|
|
||||||
'address' => 'Dubai, UAE',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 3. Create job post
|
|
||||||
$this->job = JobPost::create([
|
|
||||||
'employer_id' => $this->employer->id,
|
|
||||||
'title' => 'Test Job',
|
|
||||||
'location' => 'Dubai',
|
|
||||||
'salary' => 2500,
|
|
||||||
'workers_needed' => 1,
|
|
||||||
'job_type' => 'Full Time',
|
|
||||||
'start_date' => now()->subMonths(4)->toDateString(),
|
|
||||||
'description' => 'Test Job Description',
|
|
||||||
'status' => 'active',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_worker_login_and_profile_returns_review_data()
|
|
||||||
{
|
|
||||||
// Create an Employer review of this Worker
|
|
||||||
Review::create([
|
|
||||||
'employer_id' => $this->employer->id,
|
|
||||||
'worker_id' => $this->worker->id,
|
|
||||||
'rating' => 4,
|
|
||||||
'comment' => 'Good worker',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create a Worker review of this Employer
|
|
||||||
EmployerReview::create([
|
|
||||||
'worker_id' => $this->worker->id,
|
|
||||||
'employer_id' => $this->employer->id,
|
|
||||||
'job_id' => $this->job->id,
|
|
||||||
'rating' => 5,
|
|
||||||
'title' => 'Nice place',
|
|
||||||
'comment' => 'Great experience',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Act: Worker login
|
|
||||||
$response = $this->postJson('/api/workers/login', [
|
|
||||||
'phone' => '+971501234567',
|
|
||||||
'password' => 'password'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
$workerData = $response->json('data.worker');
|
|
||||||
|
|
||||||
$this->assertEquals(4.0, $workerData['rating']);
|
|
||||||
$this->assertEquals(1, $workerData['reviews_count']);
|
|
||||||
$this->assertCount(1, $workerData['reviews']);
|
|
||||||
$this->assertEquals('Good worker', $workerData['reviews'][0]['comment']);
|
|
||||||
$this->assertEquals('Employer One', $workerData['reviews'][0]['employer']['name']);
|
|
||||||
|
|
||||||
$this->assertCount(1, $workerData['employer_reviews']);
|
|
||||||
$this->assertEquals('Great experience', $workerData['employer_reviews'][0]['comment']);
|
|
||||||
$this->assertEquals('Employer One', $workerData['employer_reviews'][0]['employer']['name']);
|
|
||||||
|
|
||||||
// Act: Get worker profile
|
|
||||||
$token = $response->json('data.token');
|
|
||||||
$profileResponse = $this->withHeaders([
|
|
||||||
'Authorization' => 'Bearer ' . $token,
|
|
||||||
])->getJson('/api/workers/profile');
|
|
||||||
|
|
||||||
$profileResponse->assertStatus(200);
|
|
||||||
$profileWorker = $profileResponse->json('data.worker');
|
|
||||||
$this->assertEquals(4.0, $profileWorker['rating']);
|
|
||||||
$this->assertEquals(1, $profileWorker['reviews_count']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_worker_dashboard_and_employers_list_includes_employer_review_stats()
|
|
||||||
{
|
|
||||||
// Hired job offer to establish relationship
|
|
||||||
JobOffer::create([
|
|
||||||
'employer_id' => $this->employer->id,
|
|
||||||
'worker_id' => $this->worker->id,
|
|
||||||
'work_date' => now()->subDays(10),
|
|
||||||
'location' => 'Dubai',
|
|
||||||
'salary' => 2500,
|
|
||||||
'status' => 'accepted',
|
|
||||||
'notes' => 'Current Active Job',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Submit worker review of employer
|
|
||||||
EmployerReview::create([
|
|
||||||
'worker_id' => $this->worker->id,
|
|
||||||
'employer_id' => $this->employer->id,
|
|
||||||
'job_id' => $this->job->id,
|
|
||||||
'rating' => 5,
|
|
||||||
'title' => 'Fantastic',
|
|
||||||
'comment' => 'Very good',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Act: Get dashboard
|
|
||||||
$dashboardResponse = $this->withHeaders([
|
|
||||||
'Authorization' => 'Bearer worker-test-token',
|
|
||||||
])->getJson('/api/workers/dashboard');
|
|
||||||
|
|
||||||
$dashboardResponse->assertStatus(200);
|
|
||||||
$currentEmp = $dashboardResponse->json('data.current_employer');
|
|
||||||
$this->assertEquals(5.0, $currentEmp['rating']);
|
|
||||||
$this->assertEquals(1, $currentEmp['reviews_count']);
|
|
||||||
$this->assertCount(1, $currentEmp['reviews']);
|
|
||||||
$this->assertEquals('Very good', $currentEmp['reviews'][0]['comment']);
|
|
||||||
|
|
||||||
// Act: Get employers list
|
|
||||||
$employersResponse = $this->withHeaders([
|
|
||||||
'Authorization' => 'Bearer worker-test-token',
|
|
||||||
])->getJson('/api/workers/employers');
|
|
||||||
|
|
||||||
$employersResponse->assertStatus(200);
|
|
||||||
$listEmp = $employersResponse->json('data.employers.0.employer');
|
|
||||||
$this->assertEquals(5.0, $listEmp['rating']);
|
|
||||||
$this->assertEquals(1, $listEmp['reviews_count']);
|
|
||||||
$this->assertCount(1, $listEmp['reviews']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_employer_profile_returns_review_data()
|
|
||||||
{
|
|
||||||
// Submit review of employer
|
|
||||||
EmployerReview::create([
|
|
||||||
'worker_id' => $this->worker->id,
|
|
||||||
'employer_id' => $this->employer->id,
|
|
||||||
'job_id' => $this->job->id,
|
|
||||||
'rating' => 5,
|
|
||||||
'title' => 'Nice place',
|
|
||||||
'comment' => 'Great experience',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Act: Get employer profile
|
|
||||||
$response = $this->withHeaders([
|
|
||||||
'Authorization' => 'Bearer employer-test-token',
|
|
||||||
])->getJson('/api/employers/profile');
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
$profile = $response->json('data.profile');
|
|
||||||
|
|
||||||
$this->assertEquals(5.0, $profile['rating']);
|
|
||||||
$this->assertEquals(1, $profile['reviews_count']);
|
|
||||||
$this->assertCount(1, $profile['reviews']);
|
|
||||||
$this->assertEquals('Great experience', $profile['reviews'][0]['comment']);
|
|
||||||
$this->assertEquals('Rahul Sharma', $profile['reviews'][0]['worker']['name']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -268,37 +268,4 @@ public function test_view_and_list_reviews()
|
|||||||
$response->assertJsonCount(1, 'data.reviews');
|
$response->assertJsonCount(1, 'data.reviews');
|
||||||
$response->assertJsonPath('data.reviews.0.title', 'Awesome place');
|
$response->assertJsonPath('data.reviews.0.title', 'Awesome place');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_review_with_invalid_or_missing_job_id_passes_and_saves_null()
|
|
||||||
{
|
|
||||||
// Setup job application with hired status and joining_confirmed_at 3.5 months ago
|
|
||||||
JobApplication::create([
|
|
||||||
'job_id' => $this->job->id,
|
|
||||||
'worker_id' => $this->worker->id,
|
|
||||||
'status' => 'hired',
|
|
||||||
'joining_confirmed_at' => now()->subMonths(3)->subDays(15)->toDateString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Act: Post review with an invalid job_id (e.g. 'invalid-id-or-empty')
|
|
||||||
$response = $this->postJson('/api/workers/reviews', [
|
|
||||||
'employer_id' => $this->employer->id,
|
|
||||||
'job_id' => 'invalid-id-or-empty',
|
|
||||||
'rating' => 4,
|
|
||||||
'title' => 'Fair Employer',
|
|
||||||
'comment' => 'Very good experience overall.',
|
|
||||||
], [
|
|
||||||
'Authorization' => 'Bearer worker_api_token'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->assertStatus(201);
|
|
||||||
$response->assertJsonPath('success', true);
|
|
||||||
$response->assertJsonPath('data.review.job_id', null); // Resolved to null
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('employer_reviews', [
|
|
||||||
'worker_id' => $this->worker->id,
|
|
||||||
'employer_id' => $this->employer->id,
|
|
||||||
'job_id' => null,
|
|
||||||
'rating' => 4,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user