From e475a7558bc8af9aa8477327fc160222d4d4fdf8 Mon Sep 17 00:00:00 2001 From: mohanmd Date: Mon, 6 Jul 2026 10:36:54 +0530 Subject: [PATCH] worker api changes hiring, worker/employers api list --- .../Api/EmployerWorkerController.php | 107 +++++---- .../Controllers/Api/WorkerJobController.php | 14 ++ .../Api/WorkerProfileController.php | 205 ++++++++++++++---- .../Employer/CandidateController.php | 45 +++- .../js/Pages/Employer/SelectedCandidates.jsx | 4 +- tests/Feature/EmployerWorkerFilterApiTest.php | 20 +- tests/Feature/WorkerEmployersApiTest.php | 192 ++++++++++++++++ tests/Feature/WorkerJobApiTest.php | 97 +++++++++ 8 files changed, 555 insertions(+), 129 deletions(-) diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php index e73f5dd..eedc28f 100644 --- a/app/Http/Controllers/Api/EmployerWorkerController.php +++ b/app/Http/Controllers/Api/EmployerWorkerController.php @@ -90,30 +90,6 @@ private function formatWorker(Worker $w) ] ]; })->toArray(), - 'documents' => $w->documents->map(function ($doc) { - $ocrData = $doc->ocr_data; - if ($doc->type === 'visa' && is_array($ocrData)) { - unset($ocrData['file_number']); - unset($ocrData['name']); - unset($ocrData['passport_number']); - unset($ocrData['accompanied_by']); - unset($ocrData['valid_until']); - unset($ocrData['sponsor']); - } - return [ - 'id' => $doc->id, - 'worker_id' => $doc->worker_id, - 'type' => $doc->type, - 'number' => $doc->number, - 'issue_date' => $doc->issue_date, - 'expiry_date' => $doc->expiry_date, - 'ocr_accuracy' => $doc->ocr_accuracy, - 'file_path' => $doc->file_path ? url($doc->file_path) : null, - 'ocr_data' => $ocrData, - 'created_at' => $doc->created_at?->toISOString(), - 'updated_at' => $doc->updated_at?->toISOString(), - ]; - })->toArray(), 'category' => [ 'id' => 7, 'name' => 'General Helper', @@ -440,10 +416,11 @@ public function getCandidates(Request $request) if (!$w) return null; $status = 'Reviewing'; - if ($app->status === 'hired') $status = 'Hired'; - elseif ($app->status === 'rejected') $status = 'Rejected'; - elseif ($app->status === 'shortlisted' || $app->status === 'offer_sent') $status = 'Offer Sent'; - elseif ($app->status === 'applied') $status = 'Reviewing'; + $dbStatusLower = strtolower($app->status ?? ''); + if ($dbStatusLower === 'hired') $status = 'Hired'; + elseif ($dbStatusLower === 'rejected') $status = 'Rejected'; + elseif ($dbStatusLower === 'shortlisted' || $dbStatusLower === 'offer_sent' || $dbStatusLower === 'offer sent') $status = 'Offer Sent'; + elseif ($dbStatusLower === 'applied') $status = 'Reviewing'; else $status = ucfirst($app->status); $formattedWorker = $this->formatWorker($w); @@ -465,9 +442,10 @@ public function getCandidates(Request $request) if (!$w) return null; $status = 'Offer Sent'; - if ($offer->status === 'accepted') $status = 'Hired'; - elseif ($offer->status === 'rejected') $status = 'Rejected'; - elseif ($offer->status === 'pending') $status = 'Offer Sent'; + $dbOfferStatusLower = strtolower($offer->status ?? ''); + if ($dbOfferStatusLower === 'accepted' || $dbOfferStatusLower === 'hired') $status = 'Hired'; + elseif ($dbOfferStatusLower === 'rejected') $status = 'Rejected'; + elseif ($dbOfferStatusLower === 'pending' || $dbOfferStatusLower === 'offer sent' || $dbOfferStatusLower === 'offer_sent') $status = 'Offer Sent'; $formattedWorker = $this->formatWorker($w); @@ -480,12 +458,21 @@ public function getCandidates(Request $request) ]); })->filter()->values()->toArray(); - // Merge and sort - $mergedCandidates = array_merge($selectedWorkers, $directWorkers); + // Merge and sort (filtering out duplicates for direct offers where already hired via application) + $hiredAppWorkerIds = collect($selectedWorkers) + ->filter(fn($w) => strtolower($w['status'] ?? '') === 'hired') + ->pluck('worker_id') + ->toArray(); + + $filteredDirectWorkers = array_filter($directWorkers, function($dw) use ($hiredAppWorkerIds) { + return $dw && !in_array($dw['worker_id'], $hiredAppWorkerIds); + }); + + $mergedCandidates = array_merge($selectedWorkers, $filteredDirectWorkers); // Only display Hired candidates in the candidates pipeline (exclude active states) $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) { - return $c && $c['status'] === 'Hired'; + return $c && strtolower($c['status'] ?? '') === 'hired'; })); // Apply filters: gender, skills, languages, nationality, availability, preferred_location, job_type, live_in_out, in_country, visa_status @@ -810,6 +797,20 @@ public function hireCandidate(Request $request, $id = null) $worker = $application->worker; } $updatedApplication = $application; + // Create/update the JobOffer record to satisfy the Worker Employer List API + JobOffer::updateOrCreate( + [ + 'employer_id' => $employerId, + 'worker_id' => $application->worker_id, + ], + [ + 'work_date' => $application->jobPost->start_date ?? now(), + 'location' => $application->jobPost->location ?? 'Dubai', + 'salary' => $application->jobPost->salary ?? 0, + 'status' => 'accepted', + 'notes' => $application->notes ?? 'Hired through Job Application', + ] + ); } else { // Try targeting by Worker ID directly $worker = Worker::find($targetId); @@ -841,6 +842,20 @@ public function hireCandidate(Request $request, $id = null) if ($application) { $application->update(['status' => 'hired']); $updatedApplication = $application; + // Create/update the JobOffer record to satisfy the Worker Employer List API + JobOffer::updateOrCreate( + [ + 'employer_id' => $employerId, + 'worker_id' => $application->worker_id, + ], + [ + 'work_date' => $application->jobPost->start_date ?? now(), + 'location' => $application->jobPost->location ?? 'Dubai', + 'salary' => $application->jobPost->salary ?? 0, + 'status' => 'accepted', + 'notes' => $application->notes ?? 'Hired through Job Application', + ] + ); } else { // Create a new direct job offer and accept it $updatedOffer = JobOffer::create([ @@ -1049,30 +1064,6 @@ public function getWorkerDetail(Request $request, $id) ] ]; })->toArray(), - 'documents' => $w->documents->map(function ($doc) { - $ocrData = $doc->ocr_data; - if ($doc->type === 'visa' && is_array($ocrData)) { - unset($ocrData['file_number']); - unset($ocrData['name']); - unset($ocrData['passport_number']); - unset($ocrData['accompanied_by']); - unset($ocrData['valid_until']); - unset($ocrData['sponsor']); - } - return [ - 'id' => $doc->id, - 'worker_id' => $doc->worker_id, - 'type' => $doc->type, - 'number' => $doc->number, - 'issue_date' => $doc->issue_date, - 'expiry_date' => $doc->expiry_date, - 'ocr_accuracy' => $doc->ocr_accuracy, - 'file_path' => $doc->file_path ? url($doc->file_path) : null, - 'ocr_data' => $ocrData, - 'created_at' => $doc->created_at?->toISOString(), - 'updated_at' => $doc->updated_at?->toISOString(), - ]; - })->toArray(), ]; return response()->json([ diff --git a/app/Http/Controllers/Api/WorkerJobController.php b/app/Http/Controllers/Api/WorkerJobController.php index 69ebc2a..5ba1945 100644 --- a/app/Http/Controllers/Api/WorkerJobController.php +++ b/app/Http/Controllers/Api/WorkerJobController.php @@ -643,6 +643,20 @@ public function employerUpdateApplicationStatus(Request $request, $id) if ($application->worker) { $application->worker->update(['status' => 'Hired']); } + // Create/update the JobOffer record to satisfy the Worker Employer List API + \App\Models\JobOffer::updateOrCreate( + [ + 'employer_id' => $employer->id, + 'worker_id' => $application->worker_id, + ], + [ + 'work_date' => $application->jobPost->start_date ?? now(), + 'location' => $application->jobPost->location ?? 'Dubai', + 'salary' => $application->jobPost->salary ?? 0, + 'status' => 'accepted', + 'notes' => $application->notes ?? 'Hired through Job Application', + ] + ); } elseif ($dbStatus === 'rejected') { if ($application->worker) { $application->worker->update(['status' => 'active']); diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index 9f5664c..e339ea2 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Models\JobOffer; +use App\Models\JobApplication; use App\Models\Worker; use App\Models\WorkerDocument; use App\Models\ProfileView; @@ -587,13 +588,29 @@ public function getDashboard(Request $request) // 3. Currently working sponsor/employer $currentSponsor = null; + $emp = null; + $hiredAt = null; + $acceptedOffer = JobOffer::where('worker_id', $worker->id) - ->where('status', 'accepted') + ->whereIn(DB::raw('LOWER(status)'), ['accepted', 'hired']) ->with(['employer.employerProfile', 'employer.employerReviews.worker']) ->first(); - if ($acceptedOffer && $acceptedOffer->employer) { + if ($acceptedOffer) { $emp = $acceptedOffer->employer; + $hiredAt = $acceptedOffer->updated_at; + } else { + $hiredApplication = \App\Models\JobApplication::where('worker_id', $worker->id) + ->whereIn(DB::raw('LOWER(status)'), ['hired']) + ->with(['jobPost.employer.employerProfile', 'jobPost.employer.employerReviews.worker']) + ->first(); + if ($hiredApplication && $hiredApplication->jobPost && $hiredApplication->jobPost->employer) { + $emp = $hiredApplication->jobPost->employer; + $hiredAt = $hiredApplication->updated_at; + } + } + + if ($emp) { $empProfile = $emp->employerProfile; $currentSponsor = [ 'id' => $emp->id, @@ -602,9 +619,9 @@ public function getDashboard(Request $request) 'nationality' => $empProfile->nationality ?? 'UAE', 'city' => $empProfile->city ?? 'Dubai', 'email' => $emp->email, - 'phone' => $emp->phone, - 'hired_at' => $acceptedOffer->updated_at->toIso8601String(), - 'hired_at_formatted' => $acceptedOffer->updated_at->format('M d, Y'), + 'phone' => $empProfile->phone ?? $emp->phone, + 'hired_at' => $hiredAt ? $hiredAt->toIso8601String() : null, + 'hired_at_formatted' => $hiredAt ? $hiredAt->format('M d, Y') : null, 'rating' => $emp->rating, 'reviews_count' => $emp->reviews_count, 'reviews' => $emp->employerReviews, @@ -687,45 +704,22 @@ public function getEmployers(Request $request) $worker = $request->attributes->get('worker'); try { - $query = JobOffer::where('worker_id', $worker->id) - ->with(['employer.employerProfile', 'employer.employerReviews.worker']); + // Fetch JobOffers + $offers = JobOffer::where('worker_id', $worker->id) + ->with(['employer.employerProfile', 'employer.employerReviews.worker']) + ->get(); - // Filtering by status - if ($request->filled('status')) { - $statusInput = strtolower($request->status); - if ($statusInput === 'active') { - $query->where('status', 'accepted'); - } else { - $query->where('status', $statusInput); - } - } + // Fetch hired JobApplications + $applications = JobApplication::where('worker_id', $worker->id) + ->where('status', 'hired') + ->with(['jobPost.employer.employerProfile', 'jobPost.employer.employerReviews.worker']) + ->get(); - // Sorting - $sortBy = $request->input('sort_by', 'joining_date'); - $sortOrder = strtolower($request->input('sort_order', 'desc')) === 'asc' ? 'asc' : 'desc'; - - $sortColumn = 'work_date'; - if ($sortBy === 'salary') { - $sortColumn = 'salary'; - } elseif ($sortBy === 'status') { - $sortColumn = 'status'; - } elseif ($sortBy === 'created_at') { - $sortColumn = 'created_at'; - } - - // Always display the current active employer (status = accepted) at the top of the list, - // followed by other employers ordered by the chosen sort column. - $query->orderByRaw("CASE WHEN status = 'accepted' THEN 0 ELSE 1 END ASC") - ->orderBy($sortColumn, $sortOrder); - - $perPage = (int) $request->input('per_page', 15); - $paginator = $query->paginate($perPage); - - $employers = collect($paginator->items())->map(function ($offer) { + // Map JobOffers + $mappedOffers = $offers->map(function ($offer) { $emp = $offer->employer; $empProfile = $emp ? $emp->employerProfile : null; - // Map 'accepted' -> 'Active' (and others to Title Case for nice presentation) $statusFormatted = $offer->status; if (strtolower($offer->status) === 'accepted') { $statusFormatted = 'Active'; @@ -736,10 +730,13 @@ public function getEmployers(Request $request) return [ 'offer_id' => $offer->id, 'joining_date' => $offer->work_date ? $offer->work_date->format('Y-m-d') : null, + 'joining_date_raw' => $offer->work_date, 'location' => $offer->location, 'salary' => (int) $offer->salary, 'status' => $statusFormatted, + 'status_raw' => $offer->status, 'notes' => $offer->notes, + 'created_at' => $offer->created_at, 'employer' => $emp ? [ 'id' => $emp->id, 'name' => $emp->name, @@ -755,15 +752,120 @@ public function getEmployers(Request $request) ]; }); + // Map JobApplications + $mappedApps = $applications->map(function ($app) { + $job = $app->jobPost; + $emp = $job ? $job->employer : null; + $empProfile = $emp ? $emp->employerProfile : null; + + return [ + 'offer_id' => $app->id, + 'joining_date' => $app->updated_at ? $app->updated_at->format('Y-m-d') : null, + 'joining_date_raw' => $app->updated_at, + 'location' => $job->location ?? null, + 'salary' => $job ? (int) $job->salary : 0, + 'status' => 'Active', + 'status_raw' => 'accepted', + 'notes' => $app->notes, + 'created_at' => $app->created_at, + 'employer' => $emp ? [ + 'id' => $emp->id, + 'name' => $emp->name, + 'email' => $emp->email, + 'phone' => $emp->phone, + 'company_name' => $empProfile->company_name ?? 'Private Sponsor', + 'city' => $empProfile->city ?? null, + 'nationality' => $empProfile->nationality ?? null, + 'rating' => $emp->rating, + 'reviews_count' => $emp->reviews_count, + 'reviews' => $emp->employerReviews, + ] : null + ]; + }); + + // Merge and de-duplicate by employer id + $merged = $mappedOffers->concat($mappedApps)->unique(function ($item) { + return $item['employer']['id'] ?? null; + })->values(); + + // Filtering by status + if ($request->filled('status')) { + $statusInput = strtolower($request->status); + $merged = $merged->filter(function ($item) use ($statusInput) { + $rawStatus = strtolower($item['status_raw'] ?? ''); + if ($statusInput === 'active') { + return $rawStatus === 'accepted' || $rawStatus === 'hired'; + } + return $rawStatus === $statusInput; + }); + } + + // Sorting + $sortBy = $request->input('sort_by', 'joining_date'); + $sortOrder = strtolower($request->input('sort_order', 'desc')) === 'asc' ? 'asc' : 'desc'; + + $merged = $merged->sort(function ($a, $b) use ($sortBy, $sortOrder) { + // Priority: status = 'Active' (accepted/hired) comes first + $aActive = $a['status'] === 'Active' ? 0 : 1; + $bActive = $b['status'] === 'Active' ? 0 : 1; + + if ($aActive !== $bActive) { + return $aActive <=> $bActive; + } + + $valA = null; + $valB = null; + + if ($sortBy === 'salary') { + $valA = $a['salary']; + $valB = $b['salary']; + } elseif ($sortBy === 'status') { + $valA = $a['status']; + $valB = $b['status']; + } elseif ($sortBy === 'created_at') { + $valA = $a['created_at']; + $valB = $b['created_at']; + } else { + $valA = $a['joining_date_raw']; + $valB = $b['joining_date_raw']; + } + + if ($valA == $valB) { + return 0; + } + + if ($sortOrder === 'asc') { + return $valA > $valB ? 1 : -1; + } else { + return $valA < $valB ? 1 : -1; + } + }); + + // Pagination + $total = $merged->count(); + $perPage = (int) $request->input('per_page', 15); + $currentPage = (int) $request->input('page', 1); + $lastPage = max(1, (int) ceil($total / $perPage)); + + $pageItems = $merged->slice(($currentPage - 1) * $perPage, $perPage)->values(); + + // Clean up temporary sorting/filtering keys + $employers = $pageItems->map(function ($item) { + unset($item['joining_date_raw']); + unset($item['status_raw']); + unset($item['created_at']); + return $item; + }); + return response()->json([ 'success' => true, 'data' => [ 'employers' => $employers, 'pagination' => [ - 'total' => $paginator->total(), - 'per_page' => $paginator->perPage(), - 'current_page' => $paginator->currentPage(), - 'last_page' => $paginator->lastPage(), + 'total' => $total, + 'per_page' => $perPage, + 'current_page' => $currentPage, + 'last_page' => $lastPage, ] ] ], 200); @@ -976,7 +1078,22 @@ public function getEmployerProfile(Request $request, $id) ->where('worker_id', $worker->id) ->exists(); - if ($hasOffer || $hasApplication || $hasConversation) { + // If the worker has 'Hired' status (case-insensitive) and is hired by this employer + $isHiredByThisEmployer = false; + if (strtolower($worker->status ?? '') === 'hired') { + $isHiredByThisEmployer = \App\Models\JobOffer::where('employer_id', $employer->id) + ->where('worker_id', $worker->id) + ->whereIn(DB::raw('LOWER(status)'), ['accepted', 'hired']) + ->exists() || + \App\Models\JobApplication::where('worker_id', $worker->id) + ->whereHas('jobPost', function ($query) use ($employer) { + $query->where('employer_id', $employer->id); + }) + ->where(DB::raw('LOWER(status)'), 'hired') + ->exists(); + } + + if ($hasOffer || $hasApplication || $hasConversation || $isHiredByThisEmployer) { $hasAccess = true; } diff --git a/app/Http/Controllers/Employer/CandidateController.php b/app/Http/Controllers/Employer/CandidateController.php index a7f22f6..59d41d2 100644 --- a/app/Http/Controllers/Employer/CandidateController.php +++ b/app/Http/Controllers/Employer/CandidateController.php @@ -55,10 +55,11 @@ public function index(Request $request) // Map DB status to UI friendly status $status = 'Reviewing'; - if ($app->status === 'hired') $status = 'Hired'; - elseif ($app->status === 'rejected') $status = 'Rejected'; - elseif ($app->status === 'shortlisted' || $app->status === 'offer_sent') $status = 'Offer Sent'; - elseif ($app->status === 'applied') $status = 'Reviewing'; + $dbStatusLower = strtolower($app->status ?? ''); + if ($dbStatusLower === 'hired') $status = 'Hired'; + elseif ($dbStatusLower === 'rejected') $status = 'Rejected'; + elseif ($dbStatusLower === 'shortlisted' || $dbStatusLower === 'offer_sent' || $dbStatusLower === 'offer sent') $status = 'Offer Sent'; + elseif ($dbStatusLower === 'applied') $status = 'Reviewing'; else $status = ucfirst($app->status); return [ @@ -83,15 +84,27 @@ public function index(Request $request) ->with('worker') ->get(); - $directWorkers = $directOffers->map(function ($offer) { + // Get worker IDs of all hired job applications for this employer to prevent duplication + $hiredAppWorkerIds = collect($applications) + ->filter(fn($app) => strtolower($app->status ?? '') === 'hired') + ->pluck('worker_id') + ->toArray(); + + $directWorkers = $directOffers->map(function ($offer) use ($hiredAppWorkerIds) { $w = $offer->worker; if (!$w) return null; + // Avoid duplicate hired entries if worker was hired via standard application + $dbOfferStatusLower = strtolower($offer->status ?? ''); + if (($dbOfferStatusLower === 'accepted' || $dbOfferStatusLower === 'hired') && in_array($w->id, $hiredAppWorkerIds)) { + return null; + } + // Map DB offer status to UI friendly status $status = 'Offer Sent'; - if ($offer->status === 'accepted') $status = 'Hired'; - elseif ($offer->status === 'rejected') $status = 'Rejected'; - elseif ($offer->status === 'pending') $status = 'Offer Sent'; + if ($dbOfferStatusLower === 'accepted' || $dbOfferStatusLower === 'hired') $status = 'Hired'; + elseif ($dbOfferStatusLower === 'rejected') $status = 'Rejected'; + elseif ($dbOfferStatusLower === 'pending' || $dbOfferStatusLower === 'offer sent' || $dbOfferStatusLower === 'offer_sent') $status = 'Offer Sent'; return [ 'id' => 'offer_' . $offer->id, // Unique string key prefix to prevent clashes @@ -115,7 +128,7 @@ public function index(Request $request) // Only display Hired candidates in the candidates pipeline $mergedCandidates = array_values(array_filter($mergedCandidates, function ($w) { - return $w && $w['status'] === 'Hired'; + return $w && strtolower($w['status'] ?? '') === 'hired'; })); // Apply filters: preferred_location, job_type, live_in_out, nationality, in_country, visa_status @@ -411,6 +424,20 @@ public function updateStatus(Request $request, $id) if ($app->worker) { $app->worker->update(['status' => 'Hired']); } + // Create/update the JobOffer record to satisfy the Worker Employer List API + JobOffer::updateOrCreate( + [ + 'employer_id' => $user->id, + 'worker_id' => $app->worker_id, + ], + [ + 'work_date' => $app->jobPost->start_date ?? now(), + 'location' => $app->jobPost->location ?? 'Dubai', + 'salary' => $app->jobPost->salary ?? 0, + 'status' => 'accepted', + 'notes' => $app->notes ?? 'Hired through Job Application', + ] + ); } elseif ($dbStatus === 'rejected') { if ($app->worker) { $app->worker->update(['status' => 'active']); diff --git a/resources/js/Pages/Employer/SelectedCandidates.jsx b/resources/js/Pages/Employer/SelectedCandidates.jsx index c8a822a..454e2f8 100644 --- a/resources/js/Pages/Employer/SelectedCandidates.jsx +++ b/resources/js/Pages/Employer/SelectedCandidates.jsx @@ -78,7 +78,7 @@ export default function SelectedCandidates({ // Apply Client-Side Filters const filteredWorkers = useMemo(() => { - let list = (selectedWorkers || []).filter(w => w.status !== 'Searching'); + let list = (workers || []).filter(w => w.status !== 'Searching'); if (searchTerm.trim() !== '') { const query = searchTerm.toLowerCase(); @@ -134,7 +134,7 @@ export default function SelectedCandidates({ } return list; - }, [selectedWorkers, searchTerm, filterProfession, filterLocation, filterJobType, filterAccommodation, filterNationalities, filterGender, filterInCountry, filterVisaType, filterSkills, filterLanguages, maxSalary]); + }, [workers, searchTerm, filterProfession, filterLocation, filterJobType, filterAccommodation, filterNationalities, filterGender, filterInCountry, filterVisaType, filterSkills, filterLanguages, maxSalary]); const resetFilters = () => { setFilterProfession('All Professions'); diff --git a/tests/Feature/EmployerWorkerFilterApiTest.php b/tests/Feature/EmployerWorkerFilterApiTest.php index 627d229..c389a90 100644 --- a/tests/Feature/EmployerWorkerFilterApiTest.php +++ b/tests/Feature/EmployerWorkerFilterApiTest.php @@ -541,11 +541,8 @@ public function test_worker_list_api_has_full_details_and_pagination() $this->assertCount(1, $matchingWorker['skills']); $this->assertEquals('Ironing', $matchingWorker['skills'][0]['name']); - // Check documents object array - $this->assertIsArray($matchingWorker['documents']); - $this->assertCount(2, $matchingWorker['documents']); - $this->assertEquals('passport', $matchingWorker['documents'][0]['type']); - $this->assertEquals('visa', $matchingWorker['documents'][1]['type']); + // Check documents object array is not present + $this->assertArrayNotHasKey('documents', $matchingWorker); } public function test_worker_detail_api_has_full_details_without_religion_and_cleaned_ocr_data() @@ -618,16 +615,7 @@ public function test_worker_detail_api_has_full_details_without_religion_and_cle // Assert religion is removed $this->assertArrayNotHasKey('religion', $wData); - // Check documents object array - $this->assertIsArray($wData['documents']); - $this->assertCount(2, $wData['documents']); - - // Assert visa ocr_data is cleaned up - $visaDoc = collect($wData['documents'])->firstWhere('type', 'visa'); - $this->assertNotNull($visaDoc); - $this->assertArrayNotHasKey('sponsor', $visaDoc['ocr_data']); - $this->assertArrayNotHasKey('valid_until', $visaDoc['ocr_data']); - $this->assertEquals('some sponsor name', $visaDoc['ocr_data']['sponsor_name']); - $this->assertEquals('2024-02-15', $visaDoc['ocr_data']['expiry_date']); + // Check documents object array is not present + $this->assertArrayNotHasKey('documents', $wData); } } diff --git a/tests/Feature/WorkerEmployersApiTest.php b/tests/Feature/WorkerEmployersApiTest.php index 85b130e..f744dbd 100644 --- a/tests/Feature/WorkerEmployersApiTest.php +++ b/tests/Feature/WorkerEmployersApiTest.php @@ -404,4 +404,196 @@ public function test_worker_can_view_employer_profile_without_permissions_masks_ $response->assertJsonPath('data.employer.email', null); // Masked/null since no connection exists $response->assertJsonPath('data.employer.phone', null); } + + public function test_worker_hired_via_job_application_appears_in_employers_list() + { + // 1. Create a worker + $worker = Worker::create([ + 'name' => 'Hired Worker', + 'email' => 'hiredworker@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-hired-token', + ]); + + WorkerDocument::create([ + 'worker_id' => $worker->id, + 'type' => 'passport', + 'number' => '123456', + 'file_path' => 'passports/test.pdf', + ]); + + // 2. Create employer with active subscription + $employer = User::create([ + 'name' => 'Premium Employer', + 'email' => 'premium_emp@example.com', + 'password' => bcrypt('password'), + 'role' => 'employer', + 'subscription_status' => 'active', + 'api_token' => 'premium_employer_token', + ]); + + EmployerProfile::create([ + 'user_id' => $employer->id, + 'company_name' => 'Premium Corp Ltd', + 'nationality' => 'UAE', + 'city' => 'Abu Dhabi', + ]); + + \App\Models\Subscription::create([ + 'user_id' => $employer->id, + 'plan_id' => 'premium', + 'amount_aed' => 100.00, + 'starts_at' => now(), + 'expires_at' => now()->addDays(30), + 'status' => 'active', + ]); + + // 3. Create a JobPost + $job = \App\Models\JobPost::create([ + 'employer_id' => $employer->id, + 'title' => 'Housekeeper Needed', + 'workers_needed' => 1, + 'location' => 'Abu Dhabi', + 'salary' => 2000, + 'job_type' => 'Full Time', + 'start_date' => now()->addDays(5), + 'description' => 'Test job', + 'requirements' => 'None', + 'status' => 'active', + ]); + + // 4. Create JobApplication + $application = \App\Models\JobApplication::create([ + 'job_id' => $job->id, + 'worker_id' => $worker->id, + 'status' => 'applied', + 'notes' => 'Looking forward to work', + ]); + + // Verify employer list is empty for the worker initially + $response = $this->withHeaders([ + 'Authorization' => 'Bearer worker-hired-token', + ])->getJson('/api/workers/employers'); + + $response->assertStatus(200); + $response->assertJsonCount(0, 'data.employers'); + + // 5. Employer hires candidate via API + $response = $this->postJson("/api/employers/applications/{$application->id}/status", [ + 'status' => 'hired', + 'notes' => 'Hired and approved' + ], [ + 'Authorization' => 'Bearer premium_employer_token' + ]); + + $response->assertStatus(200); + + // Verify that the JobOffer with accepted status has been created + $this->assertDatabaseHas('job_offers', [ + 'employer_id' => $employer->id, + 'worker_id' => $worker->id, + 'status' => 'accepted', + ]); + + // 6. Hit the worker employers API and assert that the employer is now listed + $response = $this->withHeaders([ + 'Authorization' => 'Bearer worker-hired-token', + ])->getJson('/api/workers/employers'); + + $response->assertStatus(200); + $response->assertJsonCount(1, 'data.employers'); + $response->assertJsonPath('data.employers.0.employer.name', 'Premium Employer'); + $response->assertJsonPath('data.employers.0.employer.company_name', 'Premium Corp Ltd'); + $response->assertJsonPath('data.employers.0.status', 'Active'); + } + + public function test_worker_hired_via_job_application_appears_without_job_offer() + { + // 1. Create a worker + $worker = Worker::create([ + 'name' => 'Hired Worker Legacy', + 'email' => 'legacyworker@example.com', + 'phone' => '+971501234560', + '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-legacy-token', + ]); + + WorkerDocument::create([ + 'worker_id' => $worker->id, + 'type' => 'passport', + 'number' => '123456', + 'file_path' => 'passports/test.pdf', + ]); + + // 2. Create employer + $employer = User::create([ + 'name' => 'Legacy Employer', + 'email' => 'legacy_emp@example.com', + 'password' => bcrypt('password'), + 'role' => 'employer', + 'subscription_status' => 'active', + 'api_token' => 'legacy_employer_token', + ]); + + EmployerProfile::create([ + 'user_id' => $employer->id, + 'company_name' => 'Legacy Corp Ltd', + 'nationality' => 'UAE', + 'city' => 'Dubai', + ]); + + // 3. Create a JobPost + $job = \App\Models\JobPost::create([ + 'employer_id' => $employer->id, + 'title' => 'Housekeeper Needed Legacy', + 'workers_needed' => 1, + 'location' => 'Dubai', + 'salary' => 2000, + 'job_type' => 'Full Time', + 'start_date' => now()->addDays(5), + 'description' => 'Test job', + 'requirements' => 'None', + 'status' => 'active', + ]); + + // 4. Create JobApplication with status 'hired' directly, and NO JobOffer + $application = \App\Models\JobApplication::create([ + 'job_id' => $job->id, + 'worker_id' => $worker->id, + 'status' => 'hired', + 'notes' => 'Hired directly in DB previously', + ]); + + // 5. Hit the worker employers API and assert that the employer is listed + $response = $this->withHeaders([ + 'Authorization' => 'Bearer worker-legacy-token', + ])->getJson('/api/workers/employers'); + + $response->assertStatus(200); + $response->assertJsonCount(1, 'data.employers'); + $response->assertJsonPath('data.employers.0.employer.name', 'Legacy Employer'); + $response->assertJsonPath('data.employers.0.employer.company_name', 'Legacy Corp Ltd'); + $response->assertJsonPath('data.employers.0.status', 'Active'); + } } diff --git a/tests/Feature/WorkerJobApiTest.php b/tests/Feature/WorkerJobApiTest.php index 5268371..68c8217 100644 --- a/tests/Feature/WorkerJobApiTest.php +++ b/tests/Feature/WorkerJobApiTest.php @@ -507,4 +507,101 @@ public function test_job_hiring_workflow_enhancements() 'id' => $app->id ]); } + + public function test_hired_worker_visibility_and_profile_access() + { + // Create EmployerProfile with a phone number + \App\Models\EmployerProfile::create([ + 'user_id' => $this->employer->id, + 'company_name' => 'Premium Sponsoring Group', + 'phone' => '971501234567', + 'city' => 'Dubai', + 'nationality' => 'UAE', + ]); + + // 1. Create a job post + $job = JobPost::create([ + 'employer_id' => $this->employer->id, + 'title' => 'Special Hired Job', + 'location' => 'Dubai Jumeirah', + 'salary' => 4500, + 'workers_needed' => 1, + 'job_type' => 'Full Time', + 'start_date' => now()->addDays(5)->format('Y-m-d'), + 'description' => 'A special job details description', + 'status' => 'active', + ]); + + // 2. Worker applies for the job + $response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [ + 'Authorization' => 'Bearer worker_api_token' + ]); + $response->assertStatus(201); + $app = JobApplication::first(); + + // 3. Employer hires the worker by setting status to 'hired' (lowercase, as in the database) + $response = $this->postJson("/api/employers/applications/{$app->id}/status", [ + 'status' => 'hired', + 'notes' => 'Hired through job flow' + ], [ + 'Authorization' => 'Bearer employer_api_token' + ]); + $response->assertStatus(200); + + // Verify status updates + $this->assertDatabaseHas('job_applications', [ + 'id' => $app->id, + 'status' => 'hired', + ]); + $this->assertDatabaseHas('workers', [ + 'id' => $this->worker->id, + 'status' => 'Hired', + ]); + + // 4. Verify candidate is visible in Hired list (web: CandidateController::index) + $response = $this->actingAs($this->employer) + ->withSession(['user' => $this->employer]) + ->get('/employer/candidates'); + $response->assertStatus(200); + // Extract the Inertia prop 'selectedWorkers' to check + $inertiaData = $response->original->getData(); + $selectedWorkers = $inertiaData['page']['props']['selectedWorkers'] ?? []; + $hiredList = array_values(array_filter($selectedWorkers, function ($w) { + return $w['worker_id'] === $this->worker->id; + })); + $this->assertCount(1, $hiredList); + $this->assertEquals('Hired', $hiredList[0]['status']); + + // 5. Verify candidate is visible in Hired list (API: EmployerWorkerController::getCandidates) + $response = $this->getJson('/api/employers/candidates', [ + 'Authorization' => 'Bearer employer_api_token' + ]); + $response->assertStatus(200); + $response->assertJsonPath('success', true); + // Find the worker in candidates array + $candidates = $response->json('data.candidates'); + $hiredApiList = array_values(array_filter($candidates, function ($c) { + return $c['worker_id'] === $this->worker->id; + })); + $this->assertCount(1, $hiredApiList); + $this->assertEquals('Hired', $hiredApiList[0]['status']); + + // 6. Verify hired worker can view employer profile details bypassing subscription limits (WorkerProfileController::getEmployerProfile) + $response = $this->getJson("/api/workers/employers/{$this->employer->id}", [ + 'Authorization' => 'Bearer worker_api_token' + ]); + $response->assertStatus(200); + $response->assertJsonPath('success', true); + $response->assertJsonPath('data.employer.email', 'employer@example.com'); + $response->assertJsonPath('data.employer.phone', '971501234567'); // should be exposed/visible because worker is hired + + // 7. Verify hired worker dashboard shows correct currently working sponsor + $response = $this->getJson('/api/workers/dashboard', [ + 'Authorization' => 'Bearer worker_api_token' + ]); + $response->assertStatus(200); + $response->assertJsonPath('success', true); + $response->assertJsonPath('data.current_employer.id', $this->employer->id); + $response->assertJsonPath('data.currently_working_sponsor.id', $this->employer->id); + } }