migrant-web/app/Http/Controllers/Api/EmployerWorkerController.php

1202 lines
53 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Worker;
use App\Models\Shortlist;
use App\Models\JobOffer;
use App\Models\JobApplication;
use App\Models\JobPost;
use App\Models\Review;
use App\Models\ProfileView;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class EmployerWorkerController extends Controller
{
/**
* Helper to map worker DB models to API presentation format.
*/
private function formatWorker(Worker $w)
{
// Map languages from database comma-separated string
$langs = $w->language ? array_map('trim', explode(',', $w->language)) : ['English'];
// Preferred job types: full-time / part-time / live-in / live-out
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
$preferredJobType = $w->preferred_job_type ?? $jobTypes[$w->id % 4];
// Emirates ID verification status
$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,
'main_profession' => $w->main_profession,
'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(),
'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('main_profession')) {
$mainProfession = strtolower($request->main_profession);
$workersArray = array_values(array_filter($workersArray, function ($c) use ($mainProfession) {
return isset($c['main_profession']) && strtolower($c['main_profession']) === $mainProfession;
}));
}
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';
$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);
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';
$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);
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 (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 && strtolower($c['status'] ?? '') === 'hired';
}));
// Apply filters: gender, skills, languages, nationality, availability, preferred_location, job_type, live_in_out, in_country, visa_status
if ($request->filled('main_profession')) {
$mainProfession = strtolower($request->main_profession);
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($mainProfession) {
return isset($c['main_profession']) && strtolower($c['main_profession']) === $mainProfession;
}));
}
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;
// Resolve the worker ID to perform limit check
$resolvedWorkerId = null;
if (is_string($targetId) && str_starts_with($targetId, 'offer_')) {
$offerId = substr($targetId, 6);
$offer = JobOffer::where('id', $offerId)->where('employer_id', $employerId)->first();
if ($offer) {
$resolvedWorkerId = $offer->worker_id;
}
} else {
$application = JobApplication::where('id', $targetId)
->whereHas('jobPost', function($q) use ($employerId) {
$q->where('employer_id', $employerId);
})->first();
if ($application) {
$resolvedWorkerId = $application->worker_id;
} else {
$resolvedWorkerId = $targetId;
}
}
if ($resolvedWorkerId && !\App\Models\User::canContactWorker($employerId, $resolvedWorkerId)) {
$activeSub = \App\Models\Subscription::where('user_id', $employerId)->where('status', 'active')->first();
$planId = $activeSub ? $activeSub->plan_id : 'basic';
$plan = \App\Models\Plan::find($planId);
$maxContacts = $plan ? $plan->max_contact_people : 10;
return response()->json([
'success' => false,
'message' => 'You have reached your contacted workers limit of ' . $maxContacts . ' under your active plan. Please upgrade or renew your subscription to contact more workers.'
], 403);
}
// 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;
// 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);
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;
// 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([
'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'];
$visaStatus = $visaStatusesList[$w->id % 3];
$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(),
];
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);
}
}
}