migrant-web/app/Http/Controllers/Employer/CandidateController.php

519 lines
22 KiB
PHP

<?php
namespace App\Http\Controllers\Employer;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Inertia\Inertia;
use App\Models\User;
use App\Models\JobApplication;
use App\Models\JobPost;
use App\Models\JobOffer;
class CandidateController extends Controller
{
private function resolveCurrentUser()
{
$sess = session('user');
$sessId = is_array($sess) ? ($sess['id'] ?? null) : ($sess->id ?? null);
if (!$sessId) {
$user = User::where('role', 'employer')->first();
if ($user) {
session(['user' => (object)[
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'role' => 'employer',
'subscription_status' => $user->subscription_status ?? 'active',
]]);
return $user;
}
} else {
return User::find($sessId);
}
return null;
}
public function index(Request $request)
{
$user = $this->resolveCurrentUser();
$employerId = $user ? $user->id : 2;
// 1. Get job posts created by this employer
$jobIds = JobPost::where('employer_id', $employerId)->pluck('id');
// Fetch applications for those jobs
$applications = JobApplication::whereIn('job_id', $jobIds)
->with(['worker', 'jobPost'])
->get();
$selectedWorkers = $applications->map(function ($app) {
$w = $app->worker;
if (!$w) return null;
// Map DB status to UI friendly status
$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 [
'id' => $app->id, // standard application id
'worker_id' => $w->id,
'name' => $w->name,
'nationality' => $w->nationality,
'salary' => (int)$w->salary,
'status' => $status,
'applied_at' => $app->created_at->format('M d, Y'),
'preferred_location' => $w->preferred_location,
'preferred_job_type' => $w->preferred_job_type,
'live_in_out' => $w->live_in_out,
'in_country' => (bool)$w->in_country,
'visa_status' => $w->visa_status,
'main_profession' => $w->main_profession,
];
})->filter()->values()->toArray();
// 2. Fetch direct hiring offers sent by this employer
$directOffers = JobOffer::where('employer_id', $employerId)
->with('worker')
->get();
// 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 ($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
'worker_id' => $w->id,
'name' => $w->name,
'nationality' => $w->nationality,
'salary' => (int)$offer->salary,
'status' => $status,
'applied_at' => $offer->created_at->format('M d, Y'),
'preferred_location' => $w->preferred_location,
'preferred_job_type' => $w->preferred_job_type,
'live_in_out' => $w->live_in_out,
'in_country' => (bool)$w->in_country,
'visa_status' => $w->visa_status,
'main_profession' => $w->main_profession,
];
})->filter()->values()->toArray();
// Merge standard job applications with direct hiring offers for unified tracking
$mergedCandidates = array_merge($selectedWorkers, $directWorkers);
// Only display Hired candidates in the candidates pipeline
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($w) {
return $w && strtolower($w['status'] ?? '') === 'hired';
}));
// Apply filters: preferred_location, job_type, live_in_out, nationality, in_country, visa_status
if ($request->filled('preferred_location')) {
$prefLoc = $request->preferred_location;
$locsArray = is_array($prefLoc) ? $prefLoc : array_filter(array_map('trim', explode(',', $prefLoc)));
$locsArray = array_map('strtolower', $locsArray);
$locationMapping = [
'dubai' => ['dubai', 'marina', 'barsha', 'nahda', 'jumeirah', 'deira', 'downtown', 'silicon', 'sports', 'motor', 'jlt', 'jbr', 'meydan', 'ranches', 'bay', 'mirdif', 'quoz'],
'abu dhabi' => ['abu dhabi', 'yas', 'khalifa', 'reem', 'saadiyat', 'raha', 'mussafah', 'zahiyah', 'karamah'],
'oman' => ['oman', 'muscat', 'salalah', 'sohar', 'nizwa', 'sur', 'ibri', 'rustaq']
];
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($locsArray, $locationMapping) {
if (!isset($c['preferred_location'])) return false;
$wLoc = strtolower($c['preferred_location']);
foreach ($locsArray as $l) {
if ($l === 'all') return true;
$allowedKeywords = $locationMapping[$l] ?? [$l];
foreach ($allowedKeywords as $keyword) {
if (str_contains($wLoc, $keyword)) {
return true;
}
}
}
return false;
}));
}
$jobTypeParam = $request->input('job_type') ?? $request->input('preferred_job_type');
if ($jobTypeParam) {
$jobTypesArray = is_array($jobTypeParam) ? $jobTypeParam : array_filter(array_map('trim', explode(',', $jobTypeParam)));
$jobTypesArray = array_map('strtolower', $jobTypesArray);
$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;
}));
}
$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;
}));
}
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('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->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;
}));
}
}
}
$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;
}));
}
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;
}));
}
$nationalitiesResponse = app(\App\Http\Controllers\Api\WorkerAuthController::class)->nationalities(new \Illuminate\Http\Request(['per_page' => 500]));
$nationalitiesData = json_decode($nationalitiesResponse->getContent(), true);
$allNationalities = collect($nationalitiesData['data']['nationalities'] ?? [])->pluck('name')->filter()->toArray();
return Inertia::render('Employer/SelectedCandidates', [
'selectedWorkers' => $mergedCandidates,
'allNationalities' => $allNationalities,
]);
}
private function checkJobAccess($user)
{
if (!$user) {
return false;
}
$sub = \Illuminate\Support\Facades\DB::table('subscriptions')
->where('user_id', $user->id)
->where('status', 'active')
->latest('id')
->first();
$planId = $sub ? $sub->plan_id : 'basic';
return $planId !== 'basic';
}
public function updateStatus(Request $request, $id)
{
$request->validate([
'status' => 'required|string|in:Applied,Reviewing,Shortlisted,Contacted,Interview Scheduled,Selected,Rejected,Hired,Offer Sent,applied,shortlisted,contacted,interview scheduled,interview_scheduled,selected,rejected,hired,offer sent,offer_sent',
'notes' => 'nullable|string',
]);
// If this is a direct hiring offer response
if (is_string($id) && str_starts_with($id, 'offer_')) {
$offerId = substr($id, 6);
$offer = JobOffer::findOrFail($offerId);
$dbStatus = 'pending';
if (in_array(strtolower($request->status), ['hired', 'accepted'])) {
$dbStatus = 'accepted';
$offer->worker->update(['status' => 'Hired']);
} elseif (in_array(strtolower($request->status), ['rejected'])) {
$dbStatus = 'rejected';
$offer->worker->update(['status' => 'active']);
} elseif (in_array(strtolower($request->status), ['offer sent', 'offer_sent', 'pending'])) {
$dbStatus = 'pending';
}
$offer->update(['status' => $dbStatus]);
return back()->with('success', 'Direct candidate hiring offer status updated successfully to ' . $request->status);
}
// Standard job application status update
$app = JobApplication::findOrFail($id);
$user = $this->resolveCurrentUser();
if (!$user) {
return redirect()->route('employer.login');
}
if (!$this->checkJobAccess($user)) {
return redirect()->route('employer.subscription')
->with('error', 'Your current plan does not support the Job Apply module. Please upgrade to a Premium plan to access this feature.');
}
$dbStatus = 'applied';
$statusLower = strtolower($request->status);
if ($statusLower === 'hired') {
$dbStatus = 'hired';
} elseif ($statusLower === 'rejected') {
$dbStatus = 'rejected';
} elseif ($statusLower === 'shortlisted' || $statusLower === 'offer sent' || $statusLower === 'offer_sent') {
$dbStatus = 'shortlisted';
} elseif ($statusLower === 'contacted') {
$dbStatus = 'contacted';
} elseif ($statusLower === 'interview scheduled' || $statusLower === 'interview_scheduled') {
$dbStatus = 'interview_scheduled';
} elseif ($statusLower === 'selected') {
$dbStatus = 'selected';
} elseif ($statusLower === 'reviewing' || $statusLower === 'applied') {
$dbStatus = 'applied';
}
// Contact Limit check: if moving to 'shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired'
$contactStatuses = ['shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired'];
if (in_array($dbStatus, $contactStatuses)) {
if (!User::canContactWorker($user->id, $app->worker_id)) {
$activeSub = \App\Models\Subscription::where('user_id', $user->id)->where('status', 'active')->first();
$planId = $activeSub ? $activeSub->plan_id : 'basic';
$plan = \App\Models\Plan::find($planId);
$maxContacts = $plan ? $plan->max_contact_people : 10;
// Web: contact limit block redirect
return redirect()->route('employer.subscription')
->with('error', 'You have reached your contacted workers limit of ' . $maxContacts . ' under your active plan. Please upgrade or renew your subscription to contact more workers.');
}
}
// Update application status & history
$history = $app->status_history ?: [];
$history[] = [
'status' => $dbStatus,
'timestamp' => now()->toIso8601String(),
'notes' => $request->input('notes'),
];
$updateData = [
'status' => $dbStatus,
'status_history' => $history,
];
if ($request->filled('notes')) {
$updateData['notes'] = $request->notes;
}
$app->update($updateData);
// If shortlisted, automatically add to Saved Workers (Shortlist table) if not already saved
if ($dbStatus === 'shortlisted') {
$exists = \App\Models\Shortlist::where('employer_id', $user->id)
->where('worker_id', $app->worker_id)
->exists();
if (!$exists) {
\App\Models\Shortlist::create([
'employer_id' => $user->id,
'worker_id' => $app->worker_id,
]);
}
}
// If hired, also update worker status
if ($dbStatus === 'hired') {
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']);
}
}
// Trigger notifications
$this->triggerStatusNotification($app, $dbStatus);
return back()->with('success', 'Candidate application status updated successfully to ' . $request->status);
}
private function triggerStatusNotification($application, $status)
{
$worker = $application->worker;
$job = $application->jobPost;
$employer = $job ? $job->employer : null;
// When a new application is submitted: notify employer
if ($status === 'applied') {
if ($employer && $employer->fcm_token) {
\App\Services\FCMService::sendPushNotification(
$employer->fcm_token,
"New Job Application",
"A candidate has applied for your job: " . ($job->title ?? 'Job Post'),
[
'type' => 'new_job_application',
'job_id' => $job->id,
'application_id' => $application->id,
]
);
}
return;
}
// When status is updated: notify worker
if ($worker && $worker->fcm_token) {
$title = "Job Application Update";
$body = "";
switch (strtolower($status)) {
case 'shortlisted':
$body = "You have been shortlisted for the job: " . ($job->title ?? 'Job Post');
break;
case 'contacted':
$body = "An employer wants to contact you regarding the job: " . ($job->title ?? 'Job Post');
break;
case 'interview scheduled':
case 'interview_scheduled':
$body = "An interview has been scheduled for the job: " . ($job->title ?? 'Job Post');
break;
case 'selected':
$body = "Congratulations! You have been selected for the job: " . ($job->title ?? 'Job Post');
break;
case 'rejected':
$body = "Your application for the job: " . ($job->title ?? 'Job Post') . " has been reviewed and rejected.";
break;
case 'hired':
$body = "Congratulations! You have been hired for the job: " . ($job->title ?? 'Job Post');
break;
default:
$body = "Your application status for the job: " . ($job->title ?? 'Job Post') . " has changed to " . ucfirst($status);
break;
}
\App\Services\FCMService::sendPushNotification(
$worker->fcm_token,
$title,
$body,
[
'type' => 'job_application_status_update',
'job_id' => $job->id ?? '',
'application_id' => $application->id,
'status' => $status,
]
);
}
}
}