language ? array_map('trim', explode(',', $w->language)) : ['English']; // Preferred job types: full-time / part-time / live-in / live-out $jobTypes = ['full-time', 'part-time', 'live-in', 'live-out']; $preferredJobType = $w->preferred_job_type ?? $jobTypes[$w->id % 4]; // Emirates ID verification status $emiratesIdStatus = $w->emirates_id_status; // Visa status $visaStatus = $w->visa_status; $photo = null; $dbReviews = \App\Models\Review::where('worker_id', $w->id)->get(); $reviewsCount = $dbReviews->count(); $rating = $reviewsCount > 0 ? round($dbReviews->avg('rating'), 1) : 0.0; return [ 'id' => $w->id, 'name' => $w->name, 'email' => $w->email, 'phone' => $w->phone, 'nationality' => $w->nationality, 'age' => $w->age, 'salary' => $w->salary, 'experience' => $w->experience, 'verified' => (bool)$w->verified, 'status' => $w->status, 'created_at' => $w->created_at?->toISOString(), 'updated_at' => $w->updated_at?->toISOString(), 'deleted_at' => $w->deleted_at?->toISOString(), 'language' => $w->language, 'languages' => $langs, 'preferred_location' => $w->preferred_location, 'country' => $w->country, 'city' => $w->city, 'area' => $w->area, 'live_in_out' => $w->live_in_out, 'gender' => $w->gender, 'in_country' => (bool)$w->in_country, 'visa_status' => $visaStatus, 'preferred_job_type' => $preferredJobType, 'fcm_token' => $w->fcm_token, 'passport_status' => $w->passport_status, 'emirates_id_status' => $emiratesIdStatus, 'document_expiry_days' => $w->document_expiry_days, 'document_expiry_status' => $w->document_expiry_status, 'visa_expiry_date' => $w->visa_expiry_date, 'bio' => $w->bio, 'photo' => $photo, 'rating' => $rating, 'reviews_count' => $reviewsCount, 'skills' => $w->skills->map(function ($s) { return [ 'id' => $s->id, 'name' => $s->name, 'created_at' => $s->created_at?->toISOString(), 'updated_at' => $s->updated_at?->toISOString(), 'image_path' => $s->image_path, 'pivot' => [ 'worker_id' => $s->pivot->worker_id, 'skill_id' => $s->pivot->skill_id, ] ]; })->toArray(), 'documents' => $w->documents->map(function ($doc) { $ocrData = $doc->ocr_data; if ($doc->type === 'visa' && is_array($ocrData)) { unset($ocrData['sponsor']); unset($ocrData['valid_until']); } return [ 'id' => $doc->id, 'worker_id' => $doc->worker_id, 'type' => $doc->type, 'number' => $doc->number, 'issue_date' => $doc->issue_date, 'expiry_date' => $doc->expiry_date, 'ocr_accuracy' => $doc->ocr_accuracy, 'file_path' => $doc->file_path ? url($doc->file_path) : null, 'ocr_data' => $ocrData, 'created_at' => $doc->created_at?->toISOString(), 'updated_at' => $doc->updated_at?->toISOString(), ]; })->toArray(), 'category' => [ 'id' => 7, 'name' => 'General Helper', ], ]; } /** * 1. GET /api/employers/workers * List of workers available for hiring. */ public function getWorkers(Request $request) { try { $query = Worker::with(['skills', 'documents']) ->where('status', '!=', 'Hired') ->where('status', '!=', 'hidden'); // Apply search filter if provided if ($request->filled('search')) { $search = $request->search; $query->where(function($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('nationality', 'like', "%{$search}%") ->orWhere('religion', 'like', "%{$search}%"); }); } $dbWorkers = $query->latest()->get(); $formattedWorkers = $dbWorkers->map(function ($w) { $isPending = str_contains(strtolower($w->passport_status), 'pending'); if ($isPending || $w->status === 'hidden' || $w->status === 'Hired') { return null; } return $this->formatWorker($w); })->filter()->values(); $workersArray = $formattedWorkers->toArray(); // Apply filters: gender, skills, language/languages, nationality, preferred_location, job_type, live_in_out, in_country, visa_status if ($request->filled('gender')) { $gender = strtolower($request->gender); $workersArray = array_values(array_filter($workersArray, function ($c) use ($gender) { return isset($c['gender']) && strtolower($c['gender']) === $gender; })); } if ($request->filled('nationality')) { $natInput = $request->nationality; $natsArray = is_array($natInput) ? $natInput : array_filter(array_map('trim', explode(',', $natInput))); $natsArray = array_map('strtolower', $natsArray); $workersArray = array_values(array_filter($workersArray, 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('skills')) { $skills = $request->skills; $skillsArray = is_array($skills) ? $skills : array_filter(array_map('trim', explode(',', $skills))); $skillsArray = array_map('strtolower', $skillsArray); $workersArray = array_values(array_filter($workersArray, function ($c) use ($skillsArray) { if (!isset($c['skills']) || !is_array($c['skills'])) return false; foreach ($c['skills'] as $s) { $skillName = is_array($s) ? ($s['name'] ?? '') : (is_object($s) ? ($s->name ?? '') : $s); if (in_array(strtolower($skillName), $skillsArray)) { return true; } } return false; })); } $langParam = $request->input('language') ?? $request->input('languages'); if ($langParam) { $langsArray = is_array($langParam) ? $langParam : array_filter(array_map('trim', explode(',', $langParam))); $langsArray = array_map('strtolower', $langsArray); $workersArray = array_values(array_filter($workersArray, function ($c) use ($langsArray) { if (!isset($c['languages']) || !is_array($c['languages'])) return false; foreach ($c['languages'] as $l) { if (in_array(strtolower($l), $langsArray)) { return true; } } return false; })); } // Filter: preferred_location 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'] ]; $workersArray = array_values(array_filter($workersArray, 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; })); } // Filter: area if ($request->filled('area')) { $areaParam = $request->area; $areasArray = is_array($areaParam) ? $areaParam : array_filter(array_map('trim', explode(',', $areaParam))); $areasArray = array_map('strtolower', $areasArray); $workersArray = array_values(array_filter($workersArray, function ($c) use ($areasArray) { if (!isset($c['area'])) return false; $wArea = strtolower($c['area']); foreach ($areasArray as $a) { if ($a === 'all') return true; if (str_contains($wArea, $a)) { return true; } } return false; })); } // Filter: job_type / preferred_job_type $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); $workersArray = array_values(array_filter($workersArray, 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; })); } // Filter: accommodation_type / accomadation_type / live_in_out $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); $workersArray = array_values(array_filter($workersArray, 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; })); } // Filter: in country / out country (in_country / out_country) 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) { $workersArray = array_values(array_filter($workersArray, 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) { $workersArray = array_values(array_filter($workersArray, function ($c) use ($isOutCountry) { return isset($c['in_country']) && (bool)$c['in_country'] !== $isOutCountry; })); } } } // Filter: visa_status / next_visa_type / visa_type (applicable when in country) $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); $workersArray = array_values(array_filter($workersArray, 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; })); } $page = (int)$request->input('page', 1); $perPage = (int)$request->input('per_page', 15); $total = count($workersArray); $offset = ($page - 1) * $perPage; $paginatedWorkers = array_slice($workersArray, $offset, $perPage); return response()->json([ 'success' => true, 'data' => [ 'workers' => $paginatedWorkers, 'pagination' => [ 'total' => $total, 'per_page' => $perPage, 'current_page' => $page, 'last_page' => max(1, (int)ceil($total / $perPage)), ] ] ], 200); } catch (\Exception $e) { logger()->error('Mobile API Employer Get Workers Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred while fetching the worker list.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } /** * 2. GET /api/employers/candidates * List of candidates representing standard job applications and direct hiring offers. */ public function getCandidates(Request $request) { /** @var User $employer */ $employer = $request->attributes->get('employer'); try { $employerId = $employer->id; // Fetch Job Applications $jobIds = JobPost::where('employer_id', $employerId)->pluck('id'); $applicationsQuery = JobApplication::whereIn('job_id', $jobIds) ->with(['worker.skills', 'worker.documents', 'jobPost']); // Fetch Direct Hiring Offers $directOffersQuery = JobOffer::where('employer_id', $employerId) ->with('worker.skills', 'worker.documents'); // Apply search filter if provided if ($request->filled('search')) { $search = $request->search; $applicationsQuery->whereHas('worker', function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('nationality', 'like', "%{$search}%") ->orWhere('religion', 'like', "%{$search}%"); }); $directOffersQuery->whereHas('worker', function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('nationality', 'like', "%{$search}%") ->orWhere('religion', 'like', "%{$search}%"); }); } $applications = $applicationsQuery->get(); $selectedWorkers = $applications->map(function ($app) { $w = $app->worker; 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'; else $status = ucfirst($app->status); $formattedWorker = $this->formatWorker($w); return array_merge($formattedWorker, [ 'id' => $app->id, 'worker_id' => $w->id, 'status' => $status, 'applied_at' => $app->created_at->format('Y-m-d H:i:s'), 'type' => 'application', ]); })->filter()->values()->toArray(); // Fetch Direct Hiring Offers $directOffers = $directOffersQuery->get(); $directWorkers = $directOffers->map(function ($offer) { $w = $offer->worker; 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'; $formattedWorker = $this->formatWorker($w); return array_merge($formattedWorker, [ 'id' => 'offer_' . $offer->id, 'worker_id' => $w->id, 'status' => $status, 'applied_at' => $offer->created_at->format('Y-m-d H:i:s'), 'type' => 'direct_offer', ]); })->filter()->values()->toArray(); // Merge and sort $mergedCandidates = array_merge($selectedWorkers, $directWorkers); // Only display Hired candidates in the candidates pipeline (exclude active states) $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) { return $c && $c['status'] === 'Hired'; })); // Apply filters: gender, skills, languages, nationality, availability, preferred_location, job_type, live_in_out, in_country, visa_status if ($request->filled('gender')) { $gender = strtolower($request->gender); $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($gender) { return isset($c['gender']) && strtolower($c['gender']) === $gender; })); } if ($request->filled('nationality')) { $natInput = $request->nationality; $natsArray = is_array($natInput) ? $natInput : array_filter(array_map('trim', explode(',', $natInput))); $natsArray = array_map('strtolower', $natsArray); $mergedCandidates = array_values(array_filter($mergedCandidates, 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('availability')) { $availability = strtolower($request->availability); $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($availability) { return isset($c['availability']) && strtolower($c['availability']) === $availability; })); } if ($request->filled('skills')) { $skills = $request->skills; $skillsArray = is_array($skills) ? $skills : array_filter(array_map('trim', explode(',', $skills))); $skillsArray = array_map('strtolower', $skillsArray); $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($skillsArray) { if (!isset($c['skills']) || !is_array($c['skills'])) return false; foreach ($c['skills'] as $s) { if (in_array(strtolower($s), $skillsArray)) { return true; } } return false; })); } if ($request->filled('languages')) { $languages = $request->languages; $langsArray = is_array($languages) ? $languages : array_filter(array_map('trim', explode(',', $languages))); $langsArray = array_map('strtolower', $langsArray); $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($langsArray) { if (!isset($c['languages']) || !is_array($c['languages'])) return false; foreach ($c['languages'] as $l) { if (in_array(strtolower($l), $langsArray)) { return true; } } return false; })); } // Filter: preferred_location 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); $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($locsArray) { if (!isset($c['preferred_location'])) return false; $wLoc = strtolower($c['preferred_location']); foreach ($locsArray as $l) { if (str_contains($wLoc, $l)) { return true; } } return false; })); } // Filter: area if ($request->filled('area')) { $areaParam = $request->area; $areasArray = is_array($areaParam) ? $areaParam : array_filter(array_map('trim', explode(',', $areaParam))); $areasArray = array_map('strtolower', $areasArray); $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($areasArray) { if (!isset($c['area'])) return false; $wArea = strtolower($c['area']); foreach ($areasArray as $a) { if ($a === 'all') return true; if (str_contains($wArea, $a)) { return true; } } return false; })); } // Filter: job_type / preferred_job_type $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); $mergedCandidates = array_values(array_filter($mergedCandidates, 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; })); } // Filter: accommodation_type / accomadation_type / live_in_out $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); $mergedCandidates = array_values(array_filter($mergedCandidates, 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; })); } // Filter: in country / out country (in_country / out_country) 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) { $mergedCandidates = array_values(array_filter($mergedCandidates, 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) { $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($isOutCountry) { return isset($c['in_country']) && (bool)$c['in_country'] !== $isOutCountry; })); } } } // Filter: visa_status / next_visa_type / visa_type (applicable when in country) $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); $mergedCandidates = array_values(array_filter($mergedCandidates, 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; })); } $page = (int)$request->input('page', 1); $perPage = (int)$request->input('per_page', 15); $total = count($mergedCandidates); $offset = ($page - 1) * $perPage; $paginatedCandidates = array_slice($mergedCandidates, $offset, $perPage); return response()->json([ 'success' => true, 'data' => [ 'candidates' => $paginatedCandidates, 'pagination' => [ 'total' => $total, 'per_page' => $perPage, 'current_page' => $page, 'last_page' => max(1, (int)ceil($total / $perPage)), ] ] ], 200); } catch (\Exception $e) { logger()->error('Mobile API Employer Get Candidates Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred while fetching the candidate list.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } /** * 3. POST /api/employers/candidates/hire or POST /api/employers/candidates/{id}/hire * Update status from active (or reviewing/pending) to hired. */ public function hireCandidate(Request $request, $id = null) { /** @var User $employer */ $employer = $request->attributes->get('employer'); // Let the client pass either route parameter id OR body parameter identifier $targetId = $id ?: $request->input('id') ?: $request->input('worker_id') ?: $request->input('application_id') ?: $request->input('offer_id'); if (!$targetId) { return response()->json([ 'success' => false, 'message' => 'Candidate or Worker identifier is required.' ], 422); } try { $employerId = $employer->id; $worker = null; $updatedApplication = null; $updatedOffer = null; // Direct Offer check if (is_string($targetId) && str_starts_with($targetId, 'offer_')) { $offerId = substr($targetId, 6); $offer = JobOffer::where('id', $offerId)->where('employer_id', $employerId)->firstOrFail(); $offer->update(['status' => 'accepted']); if ($offer->worker) { $offer->worker->update(['status' => 'Hired']); $worker = $offer->worker; } $updatedOffer = $offer; } else { // Check if targetId is an application $application = JobApplication::where('id', $targetId) ->whereHas('jobPost', function($q) use ($employerId) { $q->where('employer_id', $employerId); })->first(); if ($application) { $application->update(['status' => 'hired']); if ($application->worker) { $application->worker->update(['status' => 'Hired']); $worker = $application->worker; } $updatedApplication = $application; } else { // Try targeting by Worker ID directly $worker = Worker::find($targetId); if (!$worker) { return response()->json([ 'success' => false, 'message' => 'Worker, application, or job offer not found with the provided identifier.' ], 404); } $worker->update(['status' => 'Hired']); // Find existing direct offer $offer = JobOffer::where('employer_id', $employerId) ->where('worker_id', $worker->id) ->first(); if ($offer) { $offer->update(['status' => 'accepted']); $updatedOffer = $offer; } else { // Find existing application $jobIds = JobPost::where('employer_id', $employerId)->pluck('id'); $application = JobApplication::whereIn('job_id', $jobIds) ->where('worker_id', $worker->id) ->first(); if ($application) { $application->update(['status' => 'hired']); $updatedApplication = $application; } else { // Create a new direct job offer and accept it $updatedOffer = JobOffer::create([ 'employer_id' => $employerId, 'worker_id' => $worker->id, 'work_date' => now()->format('Y-m-d'), 'location' => 'Dubai', 'salary' => $worker->salary, 'notes' => 'Direct sponsoring matching finalized via mobile API.', 'status' => 'accepted', ]); } } } } // Dispatch push notification to worker if ($worker && $worker->fcm_token) { \App\Services\FCMService::sendPushNotification( $worker->fcm_token, "Congratulations! You've been Hired", "Employer " . ($employer->name ?? "Employer") . " has hired you.", [ 'type' => 'hired', 'employer_id' => $employer->id, ] ); } return response()->json([ 'success' => true, 'message' => 'Candidate successfully marked as Hired!', 'data' => [ 'worker_id' => $worker ? $worker->id : null, 'worker_status' => $worker ? $worker->status : 'Hired', 'application' => $updatedApplication, 'offer' => $updatedOffer, ] ], 200); } catch (\Exception $e) { logger()->error('Mobile API Employer Hire Candidate Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred while marking the candidate as hired.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } /** * 4. GET /api/employers/workers/{id} * Get detailed worker profile, mirroring the web detail view. */ public function getWorkerDetail(Request $request, $id) { /** @var User $employer */ $employer = $request->attributes->get('employer'); $w = Worker::with(['skills', 'documents'])->find($id); if (!$w) { return response()->json([ 'success' => false, 'message' => 'Worker profile not found.' ], 404); } try { $isPending = str_contains(strtolower($w->passport_status), 'pending'); if ($w->status === 'hidden' || $isPending) { return response()->json([ 'success' => false, 'message' => 'Worker profile not found.' ], 404); } // Record employer profile view (Requirement 3) if ($employer) { ProfileView::updateOrCreate( ['employer_id' => $employer->id, 'worker_id' => $w->id], ['updated_at' => now()] ); } // Map languages $langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']); // Preferred job types $jobTypes = ['full-time', 'part-time', 'live-in', 'live-out']; $preferredJobType = $jobTypes[$w->id % 4]; // Emirates ID status $emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Verification Pending'; // Skills mapping $skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening']; $mappedSkills = [ $skillsList[$w->id % 6], $skillsList[($w->id + 2) % 6] ]; // Visa status $visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa']; $visaStatus = $visaStatusesList[$w->id % 5]; $photo = null; // Fetch dynamic database reviews (Requirement 1) $dbReviews = Review::where('worker_id', $w->id)->with('employer')->latest()->get(); $reviews = $dbReviews->map(function ($rev) { return [ 'id' => $rev->id, 'employer_name' => $rev->employer->name ?? 'Employer', 'rating' => $rev->rating, 'date' => $rev->created_at->format('M d, Y'), 'comment' => $rev->comment, ]; })->toArray(); $reviewsCount = count($reviews); if ($reviewsCount > 0) { $rating = round($dbReviews->avg('rating'), 1); } else { $rating = 0.0; $reviewsCount = 0; $reviews = []; } // Similar workers matching web view $simDb = Worker::query() ->where('id', '!=', $w->id) ->where('status', '!=', 'Hired') ->where('status', '!=', 'hidden') ->limit(3) ->get(); $similarWorkers = $simDb->map(function($sw) { $isPending = str_contains(strtolower($sw->passport_status), 'pending'); if ($isPending || $sw->status === 'hidden' || $sw->status === 'Hired') { return null; } return [ 'id' => $sw->id, 'name' => $sw->name, 'nationality' => $sw->nationality, 'rating' => 4.7, 'verified' => (bool)$sw->verified, ]; })->filter()->values()->toArray(); // Check if there is an existing conversation between this employer and this worker $conversation = \App\Models\Conversation::where('employer_id', $employer->id) ->where('worker_id', $w->id) ->first(); $workerProfile = [ 'id' => $w->id, 'name' => $w->name, 'email' => $w->email, 'phone' => $w->phone, 'nationality' => $w->nationality, 'age' => $w->age, 'salary' => $w->salary, 'experience' => $w->experience, 'verified' => (bool)$w->verified, 'status' => $w->status, 'created_at' => $w->created_at?->toISOString(), 'updated_at' => $w->updated_at?->toISOString(), 'deleted_at' => $w->deleted_at?->toISOString(), 'language' => $w->language, 'languages' => $langs, 'preferred_location' => $w->preferred_location, 'country' => $w->country, 'city' => $w->city, 'area' => $w->area, 'live_in_out' => $w->live_in_out, 'gender' => $w->gender, 'in_country' => (bool)$w->in_country, 'visa_status' => $visaStatus, 'preferred_job_type' => $preferredJobType, 'fcm_token' => $w->fcm_token, 'passport_status' => $w->passport_status, 'emirates_id_status' => $emiratesIdStatus, 'document_expiry_days' => $w->document_expiry_days, 'document_expiry_status' => $w->document_expiry_status, 'visa_expiry_date' => $w->visa_expiry_date, 'bio' => $w->bio, 'photo' => $photo, 'rating' => $rating, 'reviews_count' => $reviewsCount, 'reviews' => $reviews, 'similar_workers' => $similarWorkers, 'conversation_id' => $conversation ? $conversation->id : null, 'skills' => $w->skills->map(function ($s) { return [ 'id' => $s->id, 'name' => $s->name, 'created_at' => $s->created_at?->toISOString(), 'updated_at' => $s->updated_at?->toISOString(), 'image_path' => $s->image_path, 'pivot' => [ 'worker_id' => $s->pivot->worker_id, 'skill_id' => $s->pivot->skill_id, ] ]; })->toArray(), 'documents' => $w->documents->map(function ($doc) { $ocrData = $doc->ocr_data; if ($doc->type === 'visa' && is_array($ocrData)) { unset($ocrData['sponsor']); unset($ocrData['valid_until']); } return [ 'id' => $doc->id, 'worker_id' => $doc->worker_id, 'type' => $doc->type, 'number' => $doc->number, 'issue_date' => $doc->issue_date, 'expiry_date' => $doc->expiry_date, 'ocr_accuracy' => $doc->ocr_accuracy, 'file_path' => $doc->file_path ? url($doc->file_path) : null, 'ocr_data' => $ocrData, 'created_at' => $doc->created_at?->toISOString(), 'updated_at' => $doc->updated_at?->toISOString(), ]; })->toArray(), ]; return response()->json([ 'success' => true, 'data' => [ 'worker' => $workerProfile ] ], 200); } catch (\Exception $e) { logger()->error('Mobile API Employer Get Worker Detail Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred while fetching the worker profile.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } /** * GET /api/employers/shortlist * Retrieve the list of shortlisted workers for the authenticated employer. */ public function getShortlist(Request $request) { /** @var User $employer */ $employer = $request->attributes->get('employer'); try { $page = (int)$request->input('page', 1); $perPage = (int)$request->input('per_page', 15); $shortlists = Shortlist::where('employer_id', $employer->id) ->with(['worker.skills', 'worker.documents']) ->get(); $formattedWorkers = $shortlists->map(function ($s) { if ($s->worker) { return $this->formatWorker($s->worker); } return null; })->filter()->values(); $workersArray = $formattedWorkers->toArray(); $total = count($workersArray); $offset = ($page - 1) * $perPage; $paginatedWorkers = array_slice($workersArray, $offset, $perPage); return response()->json([ 'success' => true, 'data' => [ 'workers' => $paginatedWorkers, 'pagination' => [ 'total' => $total, 'per_page' => $perPage, 'current_page' => $page, 'last_page' => max(1, (int)ceil($total / $perPage)), ] ] ], 200); } catch (\Exception $e) { logger()->error('Mobile API Employer Get Shortlist Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred while fetching the shortlist.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } /** * POST /api/employers/shortlist * Toggle a worker's shortlist status for the authenticated employer. */ public function toggleShortlist(Request $request) { /** @var User $employer */ $employer = $request->attributes->get('employer'); $validator = Validator::make($request->all(), [ 'worker_id' => 'required|exists:workers,id', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', 'errors' => $validator->errors() ], 422); } try { $workerId = $request->worker_id; $existing = Shortlist::where('employer_id', $employer->id) ->where('worker_id', $workerId) ->first(); if ($existing) { $existing->delete(); $action = 'removed'; } else { Shortlist::create([ 'employer_id' => $employer->id, 'worker_id' => $workerId, ]); $action = 'added'; } $shortlistedIds = Shortlist::where('employer_id', $employer->id)->pluck('worker_id')->toArray(); return response()->json([ 'success' => true, 'message' => "Worker successfully {$action} from shortlist.", 'data' => [ 'action' => $action, 'shortlisted_ids' => $shortlistedIds ] ], 200); } catch (\Exception $e) { logger()->error('Mobile API Employer Toggle Shortlist Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred while toggling the shortlist.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } }