Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38aae3e6a3 | |||
| 9077d40064 |
@ -772,14 +772,14 @@ public function hireCandidate(Request $request, $id = null)
|
|||||||
], 403);
|
], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find existing offer or application if targeting by Worker ID directly
|
// Direct Offer check
|
||||||
$offer = null;
|
|
||||||
$application = null;
|
|
||||||
|
|
||||||
if (is_string($targetId) && str_starts_with($targetId, 'offer_')) {
|
if (is_string($targetId) && str_starts_with($targetId, 'offer_')) {
|
||||||
$offerId = substr($targetId, 6);
|
$offerId = substr($targetId, 6);
|
||||||
$offer = JobOffer::where('id', $offerId)->where('employer_id', $employerId)->firstOrFail();
|
$offer = JobOffer::where('id', $offerId)->where('employer_id', $employerId)->firstOrFail();
|
||||||
|
|
||||||
|
$offer->update(['status' => 'pending']);
|
||||||
$worker = $offer->worker;
|
$worker = $offer->worker;
|
||||||
|
$updatedOffer = $offer;
|
||||||
} else {
|
} else {
|
||||||
// Check if targetId is an application
|
// Check if targetId is an application
|
||||||
$application = JobApplication::where('id', $targetId)
|
$application = JobApplication::where('id', $targetId)
|
||||||
@ -788,7 +788,9 @@ public function hireCandidate(Request $request, $id = null)
|
|||||||
})->first();
|
})->first();
|
||||||
|
|
||||||
if ($application) {
|
if ($application) {
|
||||||
|
$application->update(['status' => 'hire_requested']);
|
||||||
$worker = $application->worker;
|
$worker = $application->worker;
|
||||||
|
$updatedApplication = $application;
|
||||||
} else {
|
} else {
|
||||||
// Try targeting by Worker ID directly
|
// Try targeting by Worker ID directly
|
||||||
$worker = Worker::find($targetId);
|
$worker = Worker::find($targetId);
|
||||||
@ -800,80 +802,37 @@ public function hireCandidate(Request $request, $id = null)
|
|||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find existing direct offer
|
// Find existing direct offer or create a new one with status 'pending'
|
||||||
$offer = JobOffer::where('employer_id', $employerId)
|
$offer = JobOffer::where('employer_id', $employerId)
|
||||||
->where('worker_id', $worker->id)
|
->where('worker_id', $worker->id)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if (!$offer) {
|
if ($offer) {
|
||||||
|
$offer->update(['status' => 'pending']);
|
||||||
|
$updatedOffer = $offer;
|
||||||
|
} else {
|
||||||
// Find existing application
|
// Find existing application
|
||||||
$jobIds = JobPost::where('employer_id', $employerId)->pluck('id');
|
$jobIds = JobPost::where('employer_id', $employerId)->pluck('id');
|
||||||
$application = JobApplication::whereIn('job_id', $jobIds)
|
$application = JobApplication::whereIn('job_id', $jobIds)
|
||||||
->where('worker_id', $worker->id)
|
->where('worker_id', $worker->id)
|
||||||
->first();
|
->first();
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if this is first click ("Hire") or second click ("Confirm Hire")
|
if ($application) {
|
||||||
$isWaitingForReview = false;
|
$application->update(['status' => 'hire_requested']);
|
||||||
if ($offer && $offer->status === 'waiting_for_review') {
|
$updatedApplication = $application;
|
||||||
$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 {
|
} else {
|
||||||
// Create new offer in 'waiting_for_review' status
|
// Create a new direct job offer in 'pending' status
|
||||||
$offer = JobOffer::create([
|
$updatedOffer = JobOffer::create([
|
||||||
'employer_id' => $employerId,
|
'employer_id' => $employerId,
|
||||||
'worker_id' => $worker->id,
|
'worker_id' => $worker->id,
|
||||||
'work_date' => now()->format('Y-m-d'),
|
'work_date' => now()->format('Y-m-d'),
|
||||||
'location' => 'Dubai',
|
'location' => 'Dubai',
|
||||||
'salary' => $worker->salary,
|
'salary' => $worker->salary,
|
||||||
'notes' => 'Direct sponsoring matching initiated via API.',
|
'notes' => 'Direct sponsoring matching initiated via API.',
|
||||||
'status' => 'waiting_for_review',
|
'status' => 'pending',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1032,28 +991,6 @@ public function getWorkerDetail(Request $request, $id)
|
|||||||
->where('worker_id', $w->id)
|
->where('worker_id', $w->id)
|
||||||
->first();
|
->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 = [
|
$workerProfile = [
|
||||||
'id' => $w->id,
|
'id' => $w->id,
|
||||||
'name' => $w->name,
|
'name' => $w->name,
|
||||||
@ -1065,7 +1002,6 @@ public function getWorkerDetail(Request $request, $id)
|
|||||||
'experience' => $w->experience,
|
'experience' => $w->experience,
|
||||||
'verified' => (bool)$w->verified,
|
'verified' => (bool)$w->verified,
|
||||||
'status' => $w->status,
|
'status' => $w->status,
|
||||||
'availability_status' => $availabilityStatus,
|
|
||||||
'created_at' => $w->created_at?->toISOString(),
|
'created_at' => $w->created_at?->toISOString(),
|
||||||
'updated_at' => $w->updated_at?->toISOString(),
|
'updated_at' => $w->updated_at?->toISOString(),
|
||||||
'deleted_at' => $w->deleted_at?->toISOString(),
|
'deleted_at' => $w->deleted_at?->toISOString(),
|
||||||
|
|||||||
@ -1233,68 +1233,6 @@ 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.
|
* Dispatch FCM Push Notification & In-app database notifications.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -695,44 +695,6 @@ 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([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
@ -742,8 +704,7 @@ public function getDashboard(Request $request)
|
|||||||
'profile_viewed' => $profileViewed,
|
'profile_viewed' => $profileViewed,
|
||||||
'currently_working_sponsor' => $currentSponsor,
|
'currently_working_sponsor' => $currentSponsor,
|
||||||
'current_employer' => $currentSponsor,
|
'current_employer' => $currentSponsor,
|
||||||
'latest_charity_events_list' => $charityEvents,
|
'latest_charity_events_list' => $charityEvents
|
||||||
'pending_hiring_request' => $pendingHiringRequest,
|
|
||||||
]
|
]
|
||||||
], 200);
|
], 200);
|
||||||
|
|
||||||
|
|||||||
@ -362,10 +362,7 @@ public function updateStatus(Request $request, $id)
|
|||||||
return redirect()->route('employer.login');
|
return redirect()->route('employer.login');
|
||||||
}
|
}
|
||||||
|
|
||||||
$statusLower = strtolower($request->status);
|
if (!$this->checkJobAccess($user)) {
|
||||||
$isDirectHiringAction = in_array($statusLower, ['hired', 'hire_requested']);
|
|
||||||
|
|
||||||
if (!$isDirectHiringAction && !$this->checkJobAccess($user)) {
|
|
||||||
return redirect()->route('employer.subscription')
|
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.');
|
->with('error', 'Your current plan does not support the Job Apply module. Please upgrade to a Premium plan to access this feature.');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -245,33 +245,14 @@ public function show($id)
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasJobAccess = $this->checkJobAccess($user);
|
|
||||||
|
|
||||||
return Inertia::render('Employer/Messages/Show', [
|
return Inertia::render('Employer/Messages/Show', [
|
||||||
'conversations' => $conversations,
|
'conversations' => $conversations,
|
||||||
'conversation' => $conversationData,
|
'conversation' => $conversationData,
|
||||||
'initialMessages' => $initialMessages,
|
'initialMessages' => $initialMessages,
|
||||||
'application' => $applicationData,
|
'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)
|
public function send(Request $request, $id)
|
||||||
{
|
{
|
||||||
$user = $this->resolveCurrentUser();
|
$user = $this->resolveCurrentUser();
|
||||||
|
|||||||
@ -365,16 +365,6 @@ public function show($id)
|
|||||||
|
|
||||||
$visaStatus = $w->visa_status;
|
$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)
|
$pendingOfferOrApp = JobOffer::where('employer_id', $user ? $user->id : 0)
|
||||||
->where('worker_id', $w->id)
|
->where('worker_id', $w->id)
|
||||||
->where('status', 'pending')
|
->where('status', 'pending')
|
||||||
@ -412,7 +402,7 @@ public function show($id)
|
|||||||
'reviews' => $reviews,
|
'reviews' => $reviews,
|
||||||
'similar_workers' => $similarWorkers,
|
'similar_workers' => $similarWorkers,
|
||||||
'status' => $w->status,
|
'status' => $w->status,
|
||||||
'availability_status' => $pendingOfferOrApp ? 'Pending Confirmation' : ($waitingForReviewOfferOrApp ? 'Waiting for Review' : ($w->status === 'Hired' ? 'Hired' : $w->status)),
|
'availability_status' => $pendingOfferOrApp ? 'Pending Confirmation' : ($w->status === 'Hired' ? 'Hired' : $w->status),
|
||||||
'document_expiry_status' => $w->document_expiry_status,
|
'document_expiry_status' => $w->document_expiry_status,
|
||||||
'document_expiry_days' => $w->document_expiry_days,
|
'document_expiry_days' => $w->document_expiry_days,
|
||||||
'visa_expiry_date' => $w->visa_expiry_date,
|
'visa_expiry_date' => $w->visa_expiry_date,
|
||||||
@ -492,59 +482,27 @@ public function markHired(Request $request, $id)
|
|||||||
|
|
||||||
$worker = Worker::findOrFail($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
|
// Check if there is an existing job offer for this employer and worker
|
||||||
$offer = JobOffer::where('employer_id', $employerId)
|
$offer = JobOffer::where('employer_id', $employerId)
|
||||||
->where('worker_id', $worker->id)
|
->where('worker_id', $worker->id)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
|
$application = null;
|
||||||
|
|
||||||
|
if ($offer) {
|
||||||
|
$offer->update(['status' => 'pending']);
|
||||||
|
} else {
|
||||||
// Also check if there's an existing job application
|
// Also check if there's an existing job application
|
||||||
$jobIds = \App\Models\JobPost::where('employer_id', $employerId)->pluck('id');
|
$jobIds = \App\Models\JobPost::where('employer_id', $employerId)->pluck('id');
|
||||||
$application = \App\Models\JobApplication::whereIn('job_id', $jobIds)
|
$application = \App\Models\JobApplication::whereIn('job_id', $jobIds)
|
||||||
->where('worker_id', $worker->id)
|
->where('worker_id', $worker->id)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
// Determine if this is first click ("Hire") or second click ("Confirm Hire")
|
if ($application) {
|
||||||
// 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 (!$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.',
|
|
||||||
];
|
|
||||||
$application->update(['status_history' => $history]);
|
|
||||||
} else {
|
|
||||||
// Create a new direct 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.',
|
|
||||||
'status' => 'waiting_for_review',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return back()->with('success', 'Candidate status updated to Waiting for Review.');
|
|
||||||
} else {
|
|
||||||
// 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']);
|
$application->update(['status' => 'hire_requested']);
|
||||||
|
|
||||||
$history = $application->status_history ?: [];
|
$history = $application->status_history ?: [];
|
||||||
$history[] = [
|
$history[] = [
|
||||||
'status' => 'hire_requested',
|
'status' => 'hire_requested',
|
||||||
@ -552,6 +510,18 @@ public function markHired(Request $request, $id)
|
|||||||
'notes' => 'Hire request initiated by employer.',
|
'notes' => 'Hire request initiated by employer.',
|
||||||
];
|
];
|
||||||
$application->update(['status_history' => $history]);
|
$application->update(['status_history' => $history]);
|
||||||
|
} else {
|
||||||
|
// Create a new direct job offer with status pending
|
||||||
|
$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.',
|
||||||
|
'status' => 'pending',
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the worker
|
// Notify the worker
|
||||||
@ -581,4 +551,3 @@ public function markHired(Request $request, $id)
|
|||||||
return back()->with('success', 'Hire request initiated successfully! Awaiting candidate confirmation.');
|
return back()->with('success', 'Hire request initiated successfully! Awaiting candidate confirmation.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@ -32,6 +32,10 @@ public function run(): void
|
|||||||
SkillSeeder::class,
|
SkillSeeder::class,
|
||||||
AdminSeeder::class,
|
AdminSeeder::class,
|
||||||
NationalitySeeder::class,
|
NationalitySeeder::class,
|
||||||
|
WorkerSeeder::class,
|
||||||
|
EmployerProfileSeeder::class,
|
||||||
|
JobPostSeeder::class,
|
||||||
|
ConversationSeeder::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
database/seeders/EmployerProfileSeeder.php
Normal file
39
database/seeders/EmployerProfileSeeder.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class EmployerProfileSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Create Employer User
|
||||||
|
$userId = \Illuminate\Support\Facades\DB::table('users')->insertGetId([
|
||||||
|
'name' => 'Employer One',
|
||||||
|
'email' => 'employer1@example.com',
|
||||||
|
'password' => \Illuminate\Support\Facades\Hash::make('password'),
|
||||||
|
'role' => 'employer',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create Employer Profile
|
||||||
|
\Illuminate\Support\Facades\DB::table('employer_profiles')->insert([
|
||||||
|
'user_id' => $userId,
|
||||||
|
'company_name' => 'Al Mansoor Household',
|
||||||
|
'phone' => '+971 50 123 4567',
|
||||||
|
'verification_status' => 'approved',
|
||||||
|
'emirates_id_front' => '[DELETED_FOR_PDPL_COMPLIANCE]',
|
||||||
|
'emirates_id_back' => '[DELETED_FOR_PDPL_COMPLIANCE]',
|
||||||
|
'emirates_id_number' => '784-1990-1234567-8',
|
||||||
|
'emirates_id_expiry' => '2028-12-31',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
253
database/seeders/WorkerSeeder.php
Normal file
253
database/seeders/WorkerSeeder.php
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class WorkerSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$workers = [
|
||||||
|
[
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john.doe@example.com',
|
||||||
|
'phone' => '+971 50 111 2222',
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
'area' => 'Dubai Marina',
|
||||||
|
'preferred_location' => 'Dubai Marina',
|
||||||
|
'live_in_out' => 'Live-in (Stay with family)',
|
||||||
|
'language' => 'English',
|
||||||
|
'age' => 30,
|
||||||
|
'gender' => 'Male',
|
||||||
|
'salary' => 2500.00,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => '5+ Years',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => '',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Ali Hassan',
|
||||||
|
'email' => 'ali.hassan@example.com',
|
||||||
|
'phone' => '+971 50 333 4444',
|
||||||
|
'nationality' => 'Pakistani',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'city' => 'Dubai',
|
||||||
|
'area' => 'Al Barsha',
|
||||||
|
'preferred_location' => 'Al Barsha',
|
||||||
|
'live_in_out' => 'Live-out (External housing)',
|
||||||
|
'language' => 'English',
|
||||||
|
'age' => 28,
|
||||||
|
'gender' => 'Male',
|
||||||
|
'salary' => 2000.00,
|
||||||
|
'availability' => '1 Month Notice',
|
||||||
|
'experience' => '3 Years',
|
||||||
|
'religion' => 'Muslim',
|
||||||
|
'bio' => 'Skilled mason with experience in block work and plastering.',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Maria Santos',
|
||||||
|
'email' => 'maria.santos@example.com',
|
||||||
|
'phone' => '+971 50 555 6666',
|
||||||
|
'nationality' => 'Filipino',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'city' => 'Yas Island',
|
||||||
|
'area' => 'Yas Island',
|
||||||
|
'preferred_location' => 'Yas Island',
|
||||||
|
'live_in_out' => 'Live-in (Stay with family)',
|
||||||
|
'language' => 'Tagalog',
|
||||||
|
'age' => 32,
|
||||||
|
'gender' => 'Female',
|
||||||
|
'salary' => 1800.00,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => '5+ Years',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => 'Experienced nanny with 6 years working with expatriate families in Dubai. Certified in pediatric first aid.',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Lakshmi Sharma',
|
||||||
|
'email' => 'lakshmi.sharma@example.com',
|
||||||
|
'phone' => '+971 50 777 8888',
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'city' => 'Abu Dhabi',
|
||||||
|
'area' => 'Khalifa City',
|
||||||
|
'preferred_location' => 'Khalifa City',
|
||||||
|
'live_in_out' => 'Live-out (External housing)',
|
||||||
|
'language' => 'Hindi',
|
||||||
|
'age' => 38,
|
||||||
|
'gender' => 'Female',
|
||||||
|
'salary' => 1600.00,
|
||||||
|
'availability' => '2 Weeks',
|
||||||
|
'experience' => '3-5 Years',
|
||||||
|
'religion' => 'Hindu',
|
||||||
|
'bio' => 'Patient caregiver specializing in elderly assistance, mobility support, and vegetarian cooking.',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Siti Aminah',
|
||||||
|
'email' => 'siti.aminah@example.com',
|
||||||
|
'phone' => '+971 50 999 0000',
|
||||||
|
'nationality' => 'Indonesian',
|
||||||
|
'country' => 'United Arab Emirates',
|
||||||
|
'city' => 'Sharjah',
|
||||||
|
'area' => 'Al Nahda',
|
||||||
|
'preferred_location' => 'Al Nahda',
|
||||||
|
'live_in_out' => 'Live-in (Stay with family)',
|
||||||
|
'language' => 'Arabic',
|
||||||
|
'age' => 29,
|
||||||
|
'gender' => 'Female',
|
||||||
|
'salary' => 1400.00,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => '3-5 Years',
|
||||||
|
'religion' => 'Muslim',
|
||||||
|
'bio' => 'Hardworking and meticulous housekeeper with excellent references from Abu Dhabi households.',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Grace Osei',
|
||||||
|
'email' => 'grace.osei@example.com',
|
||||||
|
'phone' => '+971 50 222 3333',
|
||||||
|
'nationality' => 'Ghanaian',
|
||||||
|
'country' => 'Saudi Arabia',
|
||||||
|
'city' => 'Riyadh',
|
||||||
|
'area' => 'Al Olaya',
|
||||||
|
'preferred_location' => 'Al Olaya',
|
||||||
|
'live_in_out' => 'Live-out (External housing)',
|
||||||
|
'language' => 'English',
|
||||||
|
'age' => 26,
|
||||||
|
'gender' => 'Female',
|
||||||
|
'salary' => 1500.00,
|
||||||
|
'availability' => '1 Month',
|
||||||
|
'experience' => '1-2 Years',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => 'Energetic and educated nanny. Fluent in English, great with toddlers and assisting with homework.',
|
||||||
|
'verified' => false,
|
||||||
|
'status' => 'active',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Anoma Perera',
|
||||||
|
'email' => 'anoma.perera@example.com',
|
||||||
|
'phone' => '+971 50 444 5555',
|
||||||
|
'nationality' => 'Sri Lankan',
|
||||||
|
'country' => 'Saudi Arabia',
|
||||||
|
'city' => 'Jeddah',
|
||||||
|
'area' => 'Al Hamra',
|
||||||
|
'preferred_location' => 'Al Hamra',
|
||||||
|
'live_in_out' => 'Live-in (Stay with family)',
|
||||||
|
'language' => 'English',
|
||||||
|
'age' => 41,
|
||||||
|
'gender' => 'Female',
|
||||||
|
'salary' => 2200.00,
|
||||||
|
'availability' => 'Immediate',
|
||||||
|
'experience' => '5+ Years',
|
||||||
|
'religion' => 'Buddhist',
|
||||||
|
'bio' => 'Professional domestic cook skilled in Arabic, Continental, and Asian cuisine. Highly organized.',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Ramesh Thapa',
|
||||||
|
'email' => 'ramesh.thapa@example.com',
|
||||||
|
'phone' => '+971 50 666 7777',
|
||||||
|
'nationality' => 'Nepalese',
|
||||||
|
'country' => 'Saudi Arabia',
|
||||||
|
'city' => 'Riyadh',
|
||||||
|
'area' => 'Al Yasmin',
|
||||||
|
'preferred_location' => 'Al Yasmin',
|
||||||
|
'live_in_out' => 'Live-out (External housing)',
|
||||||
|
'language' => 'English',
|
||||||
|
'age' => 36,
|
||||||
|
'gender' => 'Male',
|
||||||
|
'salary' => 2500.00,
|
||||||
|
'availability' => '2 Weeks',
|
||||||
|
'experience' => '5+ Years',
|
||||||
|
'religion' => 'Hindu',
|
||||||
|
'bio' => 'Valid UAE driving license with clean record. Familiar with all Dubai and Sharjah school routes.',
|
||||||
|
'verified' => true,
|
||||||
|
'status' => 'active',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Dynamic Skill Resolution
|
||||||
|
$skillIds = \Illuminate\Support\Facades\DB::table('skills')->pluck('id')->toArray();
|
||||||
|
if (empty($skillIds)) {
|
||||||
|
foreach (['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'] as $skillName) {
|
||||||
|
$skillIds[] = \Illuminate\Support\Facades\DB::table('skills')->insertGetId([
|
||||||
|
'name' => $skillName,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sId1 = $skillIds[0] ?? 1;
|
||||||
|
$sId2 = $skillIds[1] ?? 2;
|
||||||
|
|
||||||
|
foreach ($workers as $workerData) {
|
||||||
|
// Exclude email duplicates to avoid unique constraint violations
|
||||||
|
if (\Illuminate\Support\Facades\DB::table('workers')->where('email', $workerData['email'])->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$workerId = \Illuminate\Support\Facades\DB::table('workers')->insertGetId(array_merge($workerData, [
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Seed Documents
|
||||||
|
\Illuminate\Support\Facades\DB::table('worker_documents')->insert([
|
||||||
|
[
|
||||||
|
'worker_id' => $workerId,
|
||||||
|
'type' => 'passport',
|
||||||
|
'number' => 'P' . rand(100000, 999999),
|
||||||
|
'issue_date' => '2020-01-01',
|
||||||
|
'expiry_date' => '2030-01-01',
|
||||||
|
'ocr_accuracy' => 95.5,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'worker_id' => $workerId,
|
||||||
|
'type' => 'visa',
|
||||||
|
'number' => 'V' . rand(100000, 999999),
|
||||||
|
'issue_date' => '2022-01-01',
|
||||||
|
'expiry_date' => '2025-01-01',
|
||||||
|
'ocr_accuracy' => 98.0,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Seed Skills using resolved IDs
|
||||||
|
\Illuminate\Support\Facades\DB::table('worker_skills')->insert([
|
||||||
|
[
|
||||||
|
'worker_id' => $workerId,
|
||||||
|
'skill_id' => $sId1,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'worker_id' => $workerId,
|
||||||
|
'skill_id' => $sId2,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -160,7 +160,7 @@ export default function Register({ prefillData }) {
|
|||||||
const newErrors = {};
|
const newErrors = {};
|
||||||
|
|
||||||
if (!data.name.trim()) {
|
if (!data.name.trim()) {
|
||||||
newErrors.name = 'Employer name is required.';
|
newErrors.name = 'Sponsor name is required.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.email.trim()) {
|
if (!data.email.trim()) {
|
||||||
@ -266,7 +266,7 @@ export default function Register({ prefillData }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-slate-50 font-sans">
|
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-slate-50 font-sans">
|
||||||
<Head title="Employer Registration - Migrant Portal" />
|
<Head title="Sponsor Registration - Migrant Portal" />
|
||||||
|
|
||||||
{/* Left Brand Panel (Desktop Only) */}
|
{/* 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">
|
<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">
|
<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">
|
<span className="inline-block px-3 py-1 bg-white/10 border border-white/20 rounded-full text-xs font-semibold uppercase tracking-wider">
|
||||||
Employer Registration
|
Sponsor Registration
|
||||||
</span>
|
</span>
|
||||||
<h1 className="text-4xl font-extrabold tracking-tight leading-tight">
|
<h1 className="text-4xl font-extrabold tracking-tight leading-tight">
|
||||||
Find verified domestic workers directly.
|
Find verified domestic workers directly.
|
||||||
@ -326,14 +326,14 @@ export default function Register({ prefillData }) {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">Create Account</h2>
|
<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 employer portal to start searching</p>
|
<p className="text-xs text-gray-500 mt-1">Register your sponsor portal to start searching</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} noValidate className="space-y-4">
|
<form onSubmit={handleSubmit} noValidate className="space-y-4">
|
||||||
{/* Company Name */}
|
{/* Company Name */}
|
||||||
{/* Employer Name */}
|
{/* Sponsor Name */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Employer Name</label>
|
<label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Sponsor Name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={data.name}
|
value={data.name}
|
||||||
@ -359,7 +359,7 @@ export default function Register({ prefillData }) {
|
|||||||
type="email"
|
type="email"
|
||||||
value={data.email}
|
value={data.email}
|
||||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||||
placeholder="employer@domain.com"
|
placeholder="sponsor@domain.com"
|
||||||
className={`w-full px-3.5 py-3 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all ${
|
className={`w-full px-3.5 py-3 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all ${
|
||||||
errors.email
|
errors.email
|
||||||
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'
|
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export default function Show({ conversation, initialMessages, conversations = [], application = null, hasJobAccess = false }) {
|
export default function Show({ conversation, initialMessages, conversations = [], application = null }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [messages, setMessages] = useState(initialMessages || []);
|
const [messages, setMessages] = useState(initialMessages || []);
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
@ -343,7 +343,7 @@ export default function Show({ conversation, initialMessages, conversations = []
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{application && hasJobAccess ? (
|
{application ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openStatusModal(application)}
|
onClick={() => openStatusModal(application)}
|
||||||
|
|||||||
@ -208,20 +208,11 @@ export default function Show({ worker }) {
|
|||||||
router.post(`/employer/workers/${worker.id}/mark-hired`, {}, {
|
router.post(`/employer/workers/${worker.id}/mark-hired`, {}, {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
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');
|
setAvailabilityStatus('Pending Confirmation');
|
||||||
setShowHiredModal(true);
|
setShowHiredModal(true);
|
||||||
toast.success(t('hire_request_sent', 'Hiring request sent successfully!'), {
|
toast.success(t('hire_request_sent', 'Hiring request sent successfully!'), {
|
||||||
description: t('hire_request_sent_desc', 'Awaiting worker confirmation of the hiring offer.')
|
description: t('hire_request_sent_desc', 'Awaiting worker confirmation of the hiring offer.')
|
||||||
});
|
});
|
||||||
setShowHireConfirmModal(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -345,18 +336,6 @@ export default function Show({ worker }) {
|
|||||||
<span>{t('start_direct_chat', 'Start Direct Chat')}</span>
|
<span>{t('start_direct_chat', 'Start Direct Chat')}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Stage 4 Hire: Hire Worker Button */}
|
|
||||||
{availabilityStatus !== 'Pending Confirmation' && availabilityStatus !== 'Hired' && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
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>{availabilityStatus === 'Waiting for Review' ? t('confirm_hire', 'Confirm Hire') : t('hire', 'Hire')}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Stage 4 Hire: Pending Confirmation Indicator */}
|
{/* Stage 4 Hire: Pending Confirmation Indicator */}
|
||||||
{availabilityStatus === 'Pending Confirmation' && (
|
{availabilityStatus === 'Pending Confirmation' && (
|
||||||
<div className="bg-amber-50 text-amber-700 border border-amber-200 px-6 py-3 rounded-xl font-bold text-sm flex items-center justify-center space-x-2">
|
<div className="bg-amber-50 text-amber-700 border border-amber-200 px-6 py-3 rounded-xl font-bold text-sm flex items-center justify-center space-x-2">
|
||||||
@ -963,52 +942,6 @@ export default function Show({ worker }) {
|
|||||||
</div>
|
</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 */}
|
{/* Report Abuse Modal Overlay */}
|
||||||
{showReportModal && (
|
{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">
|
<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,7 +117,6 @@
|
|||||||
Route::post('/workers/applied-jobs/{id}/withdraw', [\App\Http\Controllers\Api\WorkerJobController::class, 'withdrawApplication']);
|
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}/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::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)
|
// Review Management (Worker reviewing Employer)
|
||||||
Route::get('/workers/reviews', [\App\Http\Controllers\Api\WorkerReviewController::class, 'getReviews']);
|
Route::get('/workers/reviews', [\App\Http\Controllers\Api\WorkerReviewController::class, 'getReviews']);
|
||||||
|
|||||||
@ -1077,120 +1077,6 @@ public function test_direct_hiring_sends_correct_notifications_to_worker()
|
|||||||
unlink($credentialsPath);
|
unlink($credentialsPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_basic_plan_employer_can_directly_hire_without_subscription_redirect()
|
|
||||||
{
|
|
||||||
// 1. Change employer subscription to 'basic'
|
|
||||||
$this->employer->subscription()->delete();
|
|
||||||
Subscription::create([
|
|
||||||
'user_id' => $this->employer->id,
|
|
||||||
'plan_id' => 'basic',
|
|
||||||
'amount_aed' => 0.00,
|
|
||||||
'starts_at' => now(),
|
|
||||||
'expires_at' => now()->addDays(30),
|
|
||||||
'status' => 'active',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create a job post (representing a placeholder or existing one)
|
|
||||||
$job = JobPost::create([
|
|
||||||
'employer_id' => $this->employer->id,
|
|
||||||
'title' => 'General Helper Placeholder',
|
|
||||||
'location' => 'Dubai',
|
|
||||||
'salary' => 2000,
|
|
||||||
'job_type' => 'Full Time',
|
|
||||||
'start_date' => now()->addDays(5)->format('Y-m-d'),
|
|
||||||
'description' => 'Placeholder',
|
|
||||||
'status' => 'active',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create a job application
|
|
||||||
$application = JobApplication::create([
|
|
||||||
'worker_id' => $this->worker->id,
|
|
||||||
'job_id' => $job->id,
|
|
||||||
'status' => 'applied'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 2. Try to update status to 'Shortlisted' - should redirect to subscription
|
|
||||||
$responseShortlist = $this->actingAs($this->employer)
|
|
||||||
->withSession(['user' => $this->employer])
|
|
||||||
->post("/employer/candidates/{$application->id}/status", [
|
|
||||||
'status' => 'Shortlisted',
|
|
||||||
'notes' => 'Try to shortlist'
|
|
||||||
]);
|
|
||||||
$responseShortlist->assertRedirect(route('employer.subscription'));
|
|
||||||
|
|
||||||
// 3. Try to update status to 'Hired' - should bypass redirect and succeed
|
|
||||||
$responseHire = $this->actingAs($this->employer)
|
|
||||||
->withSession(['user' => $this->employer])
|
|
||||||
->post("/employer/candidates/{$application->id}/status", [
|
|
||||||
'status' => 'Hired',
|
|
||||||
'notes' => 'Direct hiring worker'
|
|
||||||
]);
|
|
||||||
$responseHire->assertSessionHasNoErrors();
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('job_applications', [
|
|
||||||
'id' => $application->id,
|
|
||||||
'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,
|
port: 5173,
|
||||||
cors: true,
|
cors: true,
|
||||||
hmr: {
|
hmr: {
|
||||||
host: '192.168.0.241',
|
host: '192.168.0.217',
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
ignored: ['**/storage/framework/views/**'],
|
ignored: ['**/storage/framework/views/**'],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user