worker hire flow
This commit is contained in:
parent
2483e2c933
commit
34870a2ac6
@ -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(),
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-slate-50 font-sans">
|
||||
<Head title="Sponsor Registration - Migrant Portal" />
|
||||
<Head title="Employer Registration - Migrant Portal" />
|
||||
|
||||
{/* Left Brand Panel (Desktop Only) */}
|
||||
<div className="hidden lg:flex lg:col-span-5 bg-[#185FA5] p-12 flex-col justify-between text-white relative overflow-hidden select-none">
|
||||
@ -282,7 +282,7 @@ export default function Register({ prefillData }) {
|
||||
|
||||
<div className="space-y-3 pt-6">
|
||||
<span className="inline-block px-3 py-1 bg-white/10 border border-white/20 rounded-full text-xs font-semibold uppercase tracking-wider">
|
||||
Sponsor Registration
|
||||
Employer Registration
|
||||
</span>
|
||||
<h1 className="text-4xl font-extrabold tracking-tight leading-tight">
|
||||
Find verified domestic workers directly.
|
||||
@ -326,14 +326,14 @@ export default function Register({ prefillData }) {
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">Create Account</h2>
|
||||
<p className="text-xs text-gray-500 mt-1">Register your sponsor portal to start searching</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Register your employer portal to start searching</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} noValidate className="space-y-4">
|
||||
{/* Company Name */}
|
||||
{/* Sponsor Name */}
|
||||
{/* Employer Name */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Sponsor Name</label>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Employer Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.name}
|
||||
@ -359,7 +359,7 @@ export default function Register({ prefillData }) {
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
placeholder="sponsor@domain.com"
|
||||
placeholder="employer@domain.com"
|
||||
className={`w-full px-3.5 py-3 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all ${
|
||||
errors.email
|
||||
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'
|
||||
|
||||
@ -32,7 +32,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function Show({ conversation, initialMessages, conversations = [], application = null }) {
|
||||
export default function Show({ conversation, initialMessages, conversations = [], application = null, hasJobAccess = false }) {
|
||||
const { t } = useTranslation();
|
||||
const [messages, setMessages] = useState(initialMessages || []);
|
||||
const [input, setInput] = useState('');
|
||||
@ -343,7 +343,7 @@ export default function Show({ conversation, initialMessages, conversations = []
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{application ? (
|
||||
{application && hasJobAccess ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openStatusModal(application)}
|
||||
|
||||
@ -208,11 +208,20 @@ export default function Show({ worker }) {
|
||||
router.post(`/employer/workers/${worker.id}/mark-hired`, {}, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
setAvailabilityStatus('Pending Confirmation');
|
||||
setShowHiredModal(true);
|
||||
toast.success(t('hire_request_sent', 'Hiring request sent successfully!'), {
|
||||
description: t('hire_request_sent_desc', 'Awaiting worker confirmation of the hiring offer.')
|
||||
});
|
||||
if (availabilityStatus !== 'Waiting for Review') {
|
||||
setAvailabilityStatus('Waiting for Review');
|
||||
setShowHireConfirmModal(false);
|
||||
toast.success(t('moved_to_review', 'Candidate moved to review!'), {
|
||||
description: t('moved_to_review_desc', 'Candidate status updated to Waiting for Review. Click Confirm Hire to proceed.')
|
||||
});
|
||||
} else {
|
||||
setAvailabilityStatus('Pending Confirmation');
|
||||
setShowHiredModal(true);
|
||||
toast.success(t('hire_request_sent', 'Hiring request sent successfully!'), {
|
||||
description: t('hire_request_sent_desc', 'Awaiting worker confirmation of the hiring offer.')
|
||||
});
|
||||
setShowHireConfirmModal(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -340,11 +349,11 @@ export default function Show({ worker }) {
|
||||
{availabilityStatus !== 'Pending Confirmation' && availabilityStatus !== 'Hired' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleMarkHired}
|
||||
onClick={() => setShowHireConfirmModal(true)}
|
||||
className="bg-emerald-600 hover:bg-emerald-700 border border-emerald-700 text-white px-6 py-3 rounded-xl font-bold text-sm shadow-xs transition-all flex items-center justify-center space-x-2 flex-1 sm:flex-initial cursor-pointer"
|
||||
>
|
||||
<UserCheck className="w-4 h-4 text-white" />
|
||||
<span>{t('hire_worker', 'Hire Worker')}</span>
|
||||
<span>{availabilityStatus === 'Waiting for Review' ? t('confirm_hire', 'Confirm Hire') : t('hire', 'Hire')}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@ -954,6 +963,52 @@ export default function Show({ worker }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirm Hire Modal Overlay */}
|
||||
{showHireConfirmModal && (
|
||||
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">
|
||||
<div className="bg-white rounded-3xl w-full max-w-md p-6 relative animate-zoom-in space-y-6 border border-slate-200 shadow-2xl">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowHireConfirmModal(false)}
|
||||
className="absolute top-4 right-4 text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div className="text-center space-y-3">
|
||||
<div className="w-16 h-16 bg-blue-50 text-blue-600 rounded-full flex items-center justify-center mx-auto shadow-inner">
|
||||
<UserCheck className="w-8 h-8" />
|
||||
</div>
|
||||
<h4 className="font-extrabold text-lg text-slate-900">
|
||||
{availabilityStatus === 'Waiting for Review'
|
||||
? t('confirm_hire_title', 'Confirm Sponsoring / Hiring Offer')
|
||||
: t('initiate_hire_title', 'Initiate Sponsoring / Hiring')}
|
||||
</h4>
|
||||
<p className="text-xs text-slate-500 leading-relaxed">
|
||||
{availabilityStatus === 'Waiting for Review'
|
||||
? t('confirm_hire_desc', 'Are you sure you want to hire this worker? This action will send a hiring request to the worker.')
|
||||
: t('initiate_hire_desc', 'Are you sure you want to initiate the hiring process for this worker? This will add them to your review list.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3 text-xs font-bold">
|
||||
<button
|
||||
onClick={() => setShowHireConfirmModal(false)}
|
||||
className="flex-1 bg-slate-50 border border-slate-250 hover:bg-slate-100 text-slate-700 py-3 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<span>{t('cancel', 'Cancel')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMarkHired}
|
||||
className="flex-1 bg-emerald-600 hover:bg-emerald-700 text-white py-3 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<span>{t('confirm', 'Confirm')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Report Abuse Modal Overlay */}
|
||||
{showReportModal && (
|
||||
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">
|
||||
|
||||
@ -117,6 +117,7 @@
|
||||
Route::post('/workers/applied-jobs/{id}/withdraw', [\App\Http\Controllers\Api\WorkerJobController::class, 'withdrawApplication']);
|
||||
Route::post('/workers/applied-jobs/{id}/confirm-hire', [\App\Http\Controllers\Api\WorkerJobController::class, 'confirmHire']);
|
||||
Route::post('/workers/applied-jobs/{id}/decline-hire', [\App\Http\Controllers\Api\WorkerJobController::class, 'declineHire']);
|
||||
Route::get('/workers/pending-hiring-request', [\App\Http\Controllers\Api\WorkerJobController::class, 'pendingHiringRequest']);
|
||||
|
||||
// Review Management (Worker reviewing Employer)
|
||||
Route::get('/workers/reviews', [\App\Http\Controllers\Api\WorkerReviewController::class, 'getReviews']);
|
||||
|
||||
@ -1133,6 +1133,64 @@ public function test_basic_plan_employer_can_directly_hire_without_subscription_
|
||||
'status' => 'hire_requested',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_two_step_hiring_flow()
|
||||
{
|
||||
// 1. Direct hiring flow via Web Controller
|
||||
// First click: transition from Available to 'Waiting for Review'
|
||||
$response = $this->actingAs($this->employer)
|
||||
->withSession(['user' => $this->employer])
|
||||
->post("/employer/workers/{$this->worker->id}/mark-hired");
|
||||
|
||||
$response->assertRedirect();
|
||||
$this->assertDatabaseHas('job_offers', [
|
||||
'employer_id' => $this->employer->id,
|
||||
'worker_id' => $this->worker->id,
|
||||
'status' => 'waiting_for_review',
|
||||
]);
|
||||
|
||||
// Fetch the created job offer
|
||||
$offer = \App\Models\JobOffer::where('employer_id', $this->employer->id)
|
||||
->where('worker_id', $this->worker->id)
|
||||
->first();
|
||||
$this->assertNotNull($offer);
|
||||
|
||||
// Second click: transition to 'Pending Confirmation' (status 'pending') and notify worker
|
||||
$response2 = $this->actingAs($this->employer)
|
||||
->withSession(['user' => $this->employer])
|
||||
->post("/employer/workers/{$this->worker->id}/mark-hired");
|
||||
|
||||
$response2->assertRedirect();
|
||||
$offer->refresh();
|
||||
$this->assertEquals('pending', $offer->status);
|
||||
|
||||
// 2. Check pending hiring request via worker API
|
||||
$responseWorkerDashboard = $this->getJson('/api/workers/dashboard', [
|
||||
'Authorization' => 'Bearer worker_api_token'
|
||||
]);
|
||||
$responseWorkerDashboard->assertStatus(200);
|
||||
$responseWorkerDashboard->assertJsonPath('data.pending_hiring_request.id', $offer->id);
|
||||
$responseWorkerDashboard->assertJsonPath('data.pending_hiring_request.type', 'direct_hire_request');
|
||||
|
||||
$responsePendingApi = $this->getJson('/api/workers/pending-hiring-request', [
|
||||
'Authorization' => 'Bearer worker_api_token'
|
||||
]);
|
||||
$responsePendingApi->assertStatus(200);
|
||||
$responsePendingApi->assertJsonPath('data.pending_hiring_request.id', $offer->id);
|
||||
|
||||
// 3. Worker confirms direct job offer
|
||||
$responseConfirm = $this->postJson("/api/workers/offers/{$offer->id}/respond", [
|
||||
'status' => 'accepted'
|
||||
], [
|
||||
'Authorization' => 'Bearer worker_api_token'
|
||||
]);
|
||||
$responseConfirm->assertStatus(200);
|
||||
|
||||
$offer->refresh();
|
||||
$this->assertEquals('accepted', $offer->status);
|
||||
$this->worker->refresh();
|
||||
$this->assertEquals('Hired', $this->worker->status);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ export default defineConfig({
|
||||
port: 5173,
|
||||
cors: true,
|
||||
hmr: {
|
||||
host: '192.168.0.217',
|
||||
host: '192.168.0.241',
|
||||
},
|
||||
watch: {
|
||||
ignored: ['**/storage/framework/views/**'],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user