diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php index 031608e..903a654 100644 --- a/app/Http/Controllers/Api/EmployerWorkerController.php +++ b/app/Http/Controllers/Api/EmployerWorkerController.php @@ -772,14 +772,14 @@ public function hireCandidate(Request $request, $id = null) ], 403); } - // Direct Offer check + // Find existing offer or application if targeting by Worker ID directly + $offer = null; + $application = null; + 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' => 'pending']); $worker = $offer->worker; - $updatedOffer = $offer; } else { // Check if targetId is an application $application = JobApplication::where('id', $targetId) @@ -788,9 +788,7 @@ public function hireCandidate(Request $request, $id = null) })->first(); if ($application) { - $application->update(['status' => 'hire_requested']); $worker = $application->worker; - $updatedApplication = $application; } else { // Try targeting by Worker ID directly $worker = Worker::find($targetId); @@ -802,40 +800,83 @@ public function hireCandidate(Request $request, $id = null) ], 404); } - // Find existing direct offer or create a new one with status 'pending' + // Find existing direct offer $offer = JobOffer::where('employer_id', $employerId) ->where('worker_id', $worker->id) ->first(); - if ($offer) { - $offer->update(['status' => 'pending']); - $updatedOffer = $offer; - } else { + if (!$offer) { // 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' => 'hire_requested']); - $updatedApplication = $application; - } else { - // Create a new direct job offer in 'pending' status - $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 initiated via API.', - 'status' => 'pending', - ]); - } } } } + // Determine if this is first click ("Hire") or second click ("Confirm Hire") + $isWaitingForReview = false; + if ($offer && $offer->status === 'waiting_for_review') { + $isWaitingForReview = true; + } elseif ($application && $application->status === 'waiting_for_review') { + $isWaitingForReview = true; + } + + if (!$isWaitingForReview) { + // Step 1: Transition to 'waiting_for_review' + if ($offer) { + $offer->update(['status' => 'waiting_for_review']); + } elseif ($application) { + $application->update(['status' => 'waiting_for_review']); + $history = $application->status_history ?: []; + $history[] = [ + 'status' => 'waiting_for_review', + 'timestamp' => now()->toIso8601String(), + 'notes' => 'Candidate status moved to waiting for review via API.', + ]; + $application->update(['status_history' => $history]); + } else { + // Create new offer in 'waiting_for_review' status + $offer = 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 initiated via API.', + 'status' => 'waiting_for_review', + ]); + } + + return response()->json([ + 'success' => true, + 'message' => 'Candidate status updated to Waiting for Review.', + 'data' => [ + 'worker_id' => $worker->id, + 'worker_status' => 'Waiting for Review', + 'application' => $application, + 'offer' => $offer, + ] + ], 200); + } else { + // Step 2: Confirm hire + if ($offer) { + $offer->update(['status' => 'pending']); + $updatedOffer = $offer; + } elseif ($application) { + $application->update(['status' => 'hire_requested']); + $history = $application->status_history ?: []; + $history[] = [ + 'status' => 'hire_requested', + 'timestamp' => now()->toIso8601String(), + 'notes' => 'Hire request initiated by employer.', + ]; + $application->update(['status_history' => $history]); + $updatedApplication = $application; + } + } + // Dispatch push & database notification to worker for confirmation request if ($worker) { if ($updatedApplication) { @@ -991,6 +1032,28 @@ public function getWorkerDetail(Request $request, $id) ->where('worker_id', $w->id) ->first(); + $pendingOfferOrApp = JobOffer::where('employer_id', $employer->id) + ->where('worker_id', $w->id) + ->where('status', 'pending') + ->exists() || + \App\Models\JobApplication::where('worker_id', $w->id) + ->whereHas('jobPost', function($q) use ($employer) { + $q->where('employer_id', $employer->id); + })->where('status', 'hire_requested') + ->exists(); + + $waitingForReviewOfferOrApp = JobOffer::where('employer_id', $employer->id) + ->where('worker_id', $w->id) + ->where('status', 'waiting_for_review') + ->exists() || + \App\Models\JobApplication::where('worker_id', $w->id) + ->whereHas('jobPost', function($q) use ($employer) { + $q->where('employer_id', $employer->id); + })->where('status', 'waiting_for_review') + ->exists(); + + $availabilityStatus = $pendingOfferOrApp ? 'Pending Confirmation' : ($waitingForReviewOfferOrApp ? 'Waiting for Review' : ($w->status === 'Hired' ? 'Hired' : $w->status)); + $workerProfile = [ 'id' => $w->id, 'name' => $w->name, @@ -1002,6 +1065,7 @@ public function getWorkerDetail(Request $request, $id) 'experience' => $w->experience, 'verified' => (bool)$w->verified, 'status' => $w->status, + 'availability_status' => $availabilityStatus, 'created_at' => $w->created_at?->toISOString(), 'updated_at' => $w->updated_at?->toISOString(), 'deleted_at' => $w->deleted_at?->toISOString(), diff --git a/app/Http/Controllers/Api/WorkerJobController.php b/app/Http/Controllers/Api/WorkerJobController.php index 284e54c..5fca20c 100644 --- a/app/Http/Controllers/Api/WorkerJobController.php +++ b/app/Http/Controllers/Api/WorkerJobController.php @@ -1233,6 +1233,68 @@ public function declineHire(Request $request, $id) ]); } + /** + * API for workers to check for any pending hiring requests. + * GET /api/workers/pending-hiring-request + */ + public function pendingHiringRequest(Request $request) + { + /** @var Worker $worker */ + $worker = $request->attributes->get('worker'); + + if (!$worker) { + return response()->json([ + 'success' => false, + 'message' => 'Unauthenticated.' + ], 401); + } + + // Find any pending hire requests (JobApplication in 'hire_requested' status) + $pendingApp = JobApplication::where('worker_id', $worker->id) + ->where('status', 'hire_requested') + ->with(['jobPost.employer.employerProfile']) + ->first(); + + $pendingHiringRequest = null; + + if ($pendingApp) { + $pendingHiringRequest = [ + 'id' => $pendingApp->id, + 'type' => 'hire_request', + 'employer_name' => $pendingApp->jobPost->employer->name ?? 'Employer', + 'job_title' => $pendingApp->jobPost->title ?? 'Job Post', + 'salary' => $pendingApp->jobPost->salary ?? 0, + 'location' => $pendingApp->jobPost->location ?? '', + 'message' => "Employer " . ($pendingApp->jobPost->employer->name ?? "Employer") . " wants to hire you for '" . ($pendingApp->jobPost->title ?? 'Job Post') . "'. Please confirm.", + ]; + } else { + // Find any pending direct job offers (JobOffer in 'pending' status) + $pendingOffer = \App\Models\JobOffer::where('worker_id', $worker->id) + ->where('status', 'pending') + ->with(['employer.employerProfile']) + ->first(); + + if ($pendingOffer) { + $pendingHiringRequest = [ + 'id' => $pendingOffer->id, + 'type' => 'direct_hire_request', + 'employer_name' => $pendingOffer->employer->name ?? 'Employer', + 'job_title' => 'Direct Sponsoring Offer', + 'salary' => $pendingOffer->salary ?? 0, + 'location' => $pendingOffer->location ?? '', + 'message' => ($pendingOffer->employer->name ?? "Employer") . " wants to hire you. Do you want to accept this job offer?", + ]; + } + } + + return response()->json([ + 'success' => true, + 'data' => [ + 'pending_hiring_request' => $pendingHiringRequest + ] + ]); + } + /** * Dispatch FCM Push Notification & In-app database notifications. */ diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index b514c59..3fdd238 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -695,6 +695,44 @@ public function getDashboard(Request $request) ]; }); + // Find any pending hire requests (JobApplication in 'hire_requested' status) + $pendingApp = \App\Models\JobApplication::where('worker_id', $worker->id) + ->where('status', 'hire_requested') + ->with(['jobPost.employer.employerProfile']) + ->first(); + + $pendingHiringRequest = null; + + if ($pendingApp) { + $pendingHiringRequest = [ + 'id' => $pendingApp->id, + 'type' => 'hire_request', + 'employer_name' => $pendingApp->jobPost->employer->name ?? 'Employer', + 'job_title' => $pendingApp->jobPost->title ?? 'Job Post', + 'salary' => $pendingApp->jobPost->salary ?? 0, + 'location' => $pendingApp->jobPost->location ?? '', + 'message' => "Employer " . ($pendingApp->jobPost->employer->name ?? "Employer") . " wants to hire you for '" . ($pendingApp->jobPost->title ?? 'Job Post') . "'. Please confirm.", + ]; + } else { + // Find any pending direct job offers (JobOffer in 'pending' status) + $pendingOffer = JobOffer::where('worker_id', $worker->id) + ->where('status', 'pending') + ->with(['employer.employerProfile']) + ->first(); + + if ($pendingOffer) { + $pendingHiringRequest = [ + 'id' => $pendingOffer->id, + 'type' => 'direct_hire_request', + 'employer_name' => $pendingOffer->employer->name ?? 'Employer', + 'job_title' => 'Direct Sponsoring Offer', + 'salary' => $pendingOffer->salary ?? 0, + 'location' => $pendingOffer->location ?? '', + 'message' => ($pendingOffer->employer->name ?? "Employer") . " wants to hire you. Do you want to accept this job offer?", + ]; + } + } + return response()->json([ 'success' => true, 'data' => [ @@ -704,7 +742,8 @@ public function getDashboard(Request $request) 'profile_viewed' => $profileViewed, 'currently_working_sponsor' => $currentSponsor, 'current_employer' => $currentSponsor, - 'latest_charity_events_list' => $charityEvents + 'latest_charity_events_list' => $charityEvents, + 'pending_hiring_request' => $pendingHiringRequest, ] ], 200); diff --git a/app/Http/Controllers/Employer/MessageController.php b/app/Http/Controllers/Employer/MessageController.php index c788f38..8aab787 100644 --- a/app/Http/Controllers/Employer/MessageController.php +++ b/app/Http/Controllers/Employer/MessageController.php @@ -245,14 +245,33 @@ public function show($id) ]; } + $hasJobAccess = $this->checkJobAccess($user); + return Inertia::render('Employer/Messages/Show', [ 'conversations' => $conversations, 'conversation' => $conversationData, 'initialMessages' => $initialMessages, 'application' => $applicationData, + 'hasJobAccess' => $hasJobAccess, ]); } + 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 send(Request $request, $id) { $user = $this->resolveCurrentUser(); diff --git a/app/Http/Controllers/Employer/WorkerController.php b/app/Http/Controllers/Employer/WorkerController.php index 13a6b7d..9b99d85 100644 --- a/app/Http/Controllers/Employer/WorkerController.php +++ b/app/Http/Controllers/Employer/WorkerController.php @@ -365,6 +365,16 @@ public function show($id) $visaStatus = $w->visa_status; + $waitingForReviewOfferOrApp = JobOffer::where('employer_id', $user ? $user->id : 0) + ->where('worker_id', $w->id) + ->where('status', 'waiting_for_review') + ->exists() || + \App\Models\JobApplication::where('worker_id', $w->id) + ->whereHas('jobPost', function($q) use ($user) { + $q->where('employer_id', $user ? $user->id : 0); + })->where('status', 'waiting_for_review') + ->exists(); + $pendingOfferOrApp = JobOffer::where('employer_id', $user ? $user->id : 0) ->where('worker_id', $w->id) ->where('status', 'pending') @@ -402,7 +412,7 @@ public function show($id) 'reviews' => $reviews, 'similar_workers' => $similarWorkers, 'status' => $w->status, - 'availability_status' => $pendingOfferOrApp ? 'Pending Confirmation' : ($w->status === 'Hired' ? 'Hired' : $w->status), + 'availability_status' => $pendingOfferOrApp ? 'Pending Confirmation' : ($waitingForReviewOfferOrApp ? 'Waiting for Review' : ($w->status === 'Hired' ? 'Hired' : $w->status)), 'document_expiry_status' => $w->document_expiry_status, 'document_expiry_days' => $w->document_expiry_days, 'visa_expiry_date' => $w->visa_expiry_date, @@ -482,36 +492,41 @@ public function markHired(Request $request, $id) $worker = Worker::findOrFail($id); - // Do NOT update worker status to 'Hired' directly. - // Check if there is an existing job offer for this employer and worker $offer = JobOffer::where('employer_id', $employerId) ->where('worker_id', $worker->id) ->first(); - $application = null; + // Also check if there's an existing job application + $jobIds = \App\Models\JobPost::where('employer_id', $employerId)->pluck('id'); + $application = \App\Models\JobApplication::whereIn('job_id', $jobIds) + ->where('worker_id', $worker->id) + ->first(); - if ($offer) { - $offer->update(['status' => 'pending']); - } else { - // Also check if there's an existing job application - $jobIds = \App\Models\JobPost::where('employer_id', $employerId)->pluck('id'); - $application = \App\Models\JobApplication::whereIn('job_id', $jobIds) - ->where('worker_id', $worker->id) - ->first(); + // Determine if this is first click ("Hire") or second click ("Confirm Hire") + // If they have an existing offer or application with status 'waiting_for_review', then it's the second click. + $isWaitingForReview = false; + if ($offer && $offer->status === 'waiting_for_review') { + $isWaitingForReview = true; + } elseif ($application && $application->status === 'waiting_for_review') { + $isWaitingForReview = true; + } - if ($application) { - $application->update(['status' => 'hire_requested']); - + if (!$isWaitingForReview) { + // Step 1: Transition to 'waiting_for_review' + if ($offer) { + $offer->update(['status' => 'waiting_for_review']); + } elseif ($application) { + $application->update(['status' => 'waiting_for_review']); $history = $application->status_history ?: []; $history[] = [ - 'status' => 'hire_requested', + 'status' => 'waiting_for_review', 'timestamp' => now()->toIso8601String(), - 'notes' => 'Hire request initiated by employer.', + 'notes' => 'Candidate status moved to waiting for review.', ]; $application->update(['status_history' => $history]); } else { - // Create a new direct job offer with status pending + // Create a new direct offer in 'waiting_for_review' status $offer = JobOffer::create([ 'employer_id' => $employerId, 'worker_id' => $worker->id, @@ -519,35 +534,51 @@ public function markHired(Request $request, $id) 'location' => 'Dubai', 'salary' => $worker->salary, 'notes' => 'Direct sponsoring matching initiated.', - 'status' => 'pending', + 'status' => 'waiting_for_review', ]); } - } - // Notify the worker - if ($offer) { - $worker->notify(new \App\Notifications\GenericNotification( - "Direct Hiring Offer", - ($user->name ?? "Employer") . " wants to hire you. Do you want to accept this job offer?", - [ - 'type' => 'direct_hire_request', - 'offer_id' => $offer->id, - 'status' => 'pending', - ] - )); + return back()->with('success', 'Candidate status updated to Waiting for Review.'); } else { - $worker->notify(new \App\Notifications\GenericNotification( - "Action Required: Confirm Hiring", - "Employer " . ($user->name ?? "Employer") . " wants to hire you for '" . ($application->jobPost->title ?? 'Job Post') . "'. Please confirm.", - [ - 'type' => 'hire_request', - 'job_id' => $application->jobPost->id ?? '', - 'application_id' => $application->id, + // Step 2: Confirm hire (Transition to pending/hire_requested and send notifications) + if ($offer) { + $offer->update(['status' => 'pending']); + } elseif ($application) { + $application->update(['status' => 'hire_requested']); + $history = $application->status_history ?: []; + $history[] = [ 'status' => 'hire_requested', - ] - )); - } + 'timestamp' => now()->toIso8601String(), + 'notes' => 'Hire request initiated by employer.', + ]; + $application->update(['status_history' => $history]); + } - return back()->with('success', 'Hire request initiated successfully! Awaiting candidate confirmation.'); + // Notify the worker + if ($offer) { + $worker->notify(new \App\Notifications\GenericNotification( + "Direct Hiring Offer", + ($user->name ?? "Employer") . " wants to hire you. Do you want to accept this job offer?", + [ + 'type' => 'direct_hire_request', + 'offer_id' => $offer->id, + 'status' => 'pending', + ] + )); + } else { + $worker->notify(new \App\Notifications\GenericNotification( + "Action Required: Confirm Hiring", + "Employer " . ($user->name ?? "Employer") . " wants to hire you for '" . ($application->jobPost->title ?? 'Job Post') . "'. Please confirm.", + [ + 'type' => 'hire_request', + 'job_id' => $application->jobPost->id ?? '', + 'application_id' => $application->id, + 'status' => 'hire_requested', + ] + )); + } + + return back()->with('success', 'Hire request initiated successfully! Awaiting candidate confirmation.'); + } } } diff --git a/resources/js/Pages/Employer/Auth/Register.jsx b/resources/js/Pages/Employer/Auth/Register.jsx index 6608cf3..505cde2 100644 --- a/resources/js/Pages/Employer/Auth/Register.jsx +++ b/resources/js/Pages/Employer/Auth/Register.jsx @@ -160,7 +160,7 @@ export default function Register({ prefillData }) { const newErrors = {}; if (!data.name.trim()) { - newErrors.name = 'Sponsor name is required.'; + newErrors.name = 'Employer name is required.'; } if (!data.email.trim()) { @@ -266,7 +266,7 @@ export default function Register({ prefillData }) { return (