From 34870a2ac652ad8071f9c02bafe5ba65f5e603d4 Mon Sep 17 00:00:00 2001
From: mohanmd
Date: Fri, 10 Jul 2026 14:57:13 +0530
Subject: [PATCH] worker hire flow
---
.../Api/EmployerWorkerController.php | 118 ++++++++++++++----
.../Controllers/Api/WorkerJobController.php | 62 +++++++++
.../Api/WorkerProfileController.php | 41 +++++-
.../Employer/MessageController.php | 19 +++
.../Controllers/Employer/WorkerController.php | 115 ++++++++++-------
resources/js/Pages/Employer/Auth/Register.jsx | 14 +--
resources/js/Pages/Employer/Messages/Show.jsx | 4 +-
resources/js/Pages/Employer/Workers/Show.jsx | 69 ++++++++--
routes/api.php | 1 +
tests/Feature/WorkerJobApiTest.php | 58 +++++++++
vite.config.js | 2 +-
11 files changed, 416 insertions(+), 87 deletions(-)
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 (
-
+
{/* Left Brand Panel (Desktop Only) */}
@@ -282,7 +282,7 @@ export default function Register({ prefillData }) {
- Sponsor Registration
+ Employer Registration
Find verified domestic workers directly.
@@ -326,14 +326,14 @@ export default function Register({ prefillData }) {
Create Account
-
Register your sponsor portal to start searching
+
Register your employer portal to start searching