job sent for worker job list
This commit is contained in:
parent
e475a7558b
commit
88e4f3fdb4
@ -132,7 +132,7 @@ public function updateProfile(Request $request, $id)
|
|||||||
'experience' => 'required|string',
|
'experience' => 'required|string',
|
||||||
'salary' => 'nullable|numeric',
|
'salary' => 'nullable|numeric',
|
||||||
'nationality' => 'nullable|string',
|
'nationality' => 'nullable|string',
|
||||||
'visa_status' => 'nullable|string|in:Tourist Visa,Employment Visa,Residence Visa',
|
'visa_status' => 'nullable|string',
|
||||||
'age' => 'nullable|integer',
|
'age' => 'nullable|integer',
|
||||||
'in_country' => 'nullable',
|
'in_country' => 'nullable',
|
||||||
'preferred_job_type' => 'nullable|string',
|
'preferred_job_type' => 'nullable|string',
|
||||||
|
|||||||
@ -20,7 +20,7 @@ class EmployerWorkerController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Helper to map worker DB models to API presentation format.
|
* Helper to map worker DB models to API presentation format.
|
||||||
*/
|
*/
|
||||||
private function formatWorker(Worker $w)
|
public function formatWorker(Worker $w)
|
||||||
{
|
{
|
||||||
// Map languages from database comma-separated string
|
// Map languages from database comma-separated string
|
||||||
$langs = $w->language ? array_map('trim', explode(',', $w->language)) : ['English'];
|
$langs = $w->language ? array_map('trim', explode(',', $w->language)) : ['English'];
|
||||||
@ -777,11 +777,8 @@ public function hireCandidate(Request $request, $id = null)
|
|||||||
$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' => 'accepted']);
|
$offer->update(['status' => 'pending']);
|
||||||
if ($offer->worker) {
|
|
||||||
$offer->worker->update(['status' => 'Hired']);
|
|
||||||
$worker = $offer->worker;
|
$worker = $offer->worker;
|
||||||
}
|
|
||||||
$updatedOffer = $offer;
|
$updatedOffer = $offer;
|
||||||
} else {
|
} else {
|
||||||
// Check if targetId is an application
|
// Check if targetId is an application
|
||||||
@ -791,26 +788,9 @@ public function hireCandidate(Request $request, $id = null)
|
|||||||
})->first();
|
})->first();
|
||||||
|
|
||||||
if ($application) {
|
if ($application) {
|
||||||
$application->update(['status' => 'hired']);
|
$application->update(['status' => 'hire_requested']);
|
||||||
if ($application->worker) {
|
|
||||||
$application->worker->update(['status' => 'Hired']);
|
|
||||||
$worker = $application->worker;
|
$worker = $application->worker;
|
||||||
}
|
|
||||||
$updatedApplication = $application;
|
$updatedApplication = $application;
|
||||||
// Create/update the JobOffer record to satisfy the Worker Employer List API
|
|
||||||
JobOffer::updateOrCreate(
|
|
||||||
[
|
|
||||||
'employer_id' => $employerId,
|
|
||||||
'worker_id' => $application->worker_id,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'work_date' => $application->jobPost->start_date ?? now(),
|
|
||||||
'location' => $application->jobPost->location ?? 'Dubai',
|
|
||||||
'salary' => $application->jobPost->salary ?? 0,
|
|
||||||
'status' => 'accepted',
|
|
||||||
'notes' => $application->notes ?? 'Hired through Job Application',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Try targeting by Worker ID directly
|
// Try targeting by Worker ID directly
|
||||||
$worker = Worker::find($targetId);
|
$worker = Worker::find($targetId);
|
||||||
@ -822,15 +802,13 @@ public function hireCandidate(Request $request, $id = null)
|
|||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$worker->update(['status' => 'Hired']);
|
// Find existing direct offer or create a new one with status 'pending'
|
||||||
|
|
||||||
// Find existing direct offer
|
|
||||||
$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' => 'accepted']);
|
$offer->update(['status' => 'pending']);
|
||||||
$updatedOffer = $offer;
|
$updatedOffer = $offer;
|
||||||
} else {
|
} else {
|
||||||
// Find existing application
|
// Find existing application
|
||||||
@ -840,57 +818,57 @@ public function hireCandidate(Request $request, $id = null)
|
|||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($application) {
|
if ($application) {
|
||||||
$application->update(['status' => 'hired']);
|
$application->update(['status' => 'hire_requested']);
|
||||||
$updatedApplication = $application;
|
$updatedApplication = $application;
|
||||||
// Create/update the JobOffer record to satisfy the Worker Employer List API
|
|
||||||
JobOffer::updateOrCreate(
|
|
||||||
[
|
|
||||||
'employer_id' => $employerId,
|
|
||||||
'worker_id' => $application->worker_id,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'work_date' => $application->jobPost->start_date ?? now(),
|
|
||||||
'location' => $application->jobPost->location ?? 'Dubai',
|
|
||||||
'salary' => $application->jobPost->salary ?? 0,
|
|
||||||
'status' => 'accepted',
|
|
||||||
'notes' => $application->notes ?? 'Hired through Job Application',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Create a new direct job offer and accept it
|
// Create a new direct job offer in 'pending' status
|
||||||
$updatedOffer = 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 finalized via mobile API.',
|
'notes' => 'Direct sponsoring matching initiated via API.',
|
||||||
'status' => 'accepted',
|
'status' => 'pending',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch push notification to worker
|
// Dispatch push & database notification to worker for confirmation request
|
||||||
if ($worker && $worker->fcm_token) {
|
if ($worker) {
|
||||||
\App\Services\FCMService::sendPushNotification(
|
if ($updatedApplication) {
|
||||||
$worker->fcm_token,
|
$job = $updatedApplication->jobPost;
|
||||||
"Congratulations! You've been Hired",
|
$worker->notify(new \App\Notifications\GenericNotification(
|
||||||
"Employer " . ($employer->name ?? "Employer") . " has hired you.",
|
"Action Required: Confirm Hiring",
|
||||||
|
"Employer " . ($employer->name ?? "Employer") . " wants to hire you for '" . ($job->title ?? 'Job Post') . "'. Please confirm.",
|
||||||
[
|
[
|
||||||
'type' => 'hired',
|
'type' => 'hire_request',
|
||||||
'employer_id' => $employer->id,
|
'job_id' => $job->id ?? '',
|
||||||
|
'application_id' => $updatedApplication->id,
|
||||||
|
'status' => 'hire_requested',
|
||||||
]
|
]
|
||||||
);
|
));
|
||||||
|
} else if ($updatedOffer) {
|
||||||
|
$worker->notify(new \App\Notifications\GenericNotification(
|
||||||
|
"New Job Offer",
|
||||||
|
"Employer " . ($employer->name ?? "Employer") . " has sent you a direct hire offer.",
|
||||||
|
[
|
||||||
|
'type' => 'hire_request',
|
||||||
|
'offer_id' => $updatedOffer->id,
|
||||||
|
'status' => 'pending',
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Candidate successfully marked as Hired!',
|
'message' => 'Hire request initiated successfully. Awaiting candidate confirmation.',
|
||||||
'data' => [
|
'data' => [
|
||||||
'worker_id' => $worker ? $worker->id : null,
|
'worker_id' => $worker ? $worker->id : null,
|
||||||
'worker_status' => $worker ? $worker->status : 'Hired',
|
'worker_status' => $worker ? $worker->status : 'Active',
|
||||||
'application' => $updatedApplication,
|
'application' => $updatedApplication,
|
||||||
'offer' => $updatedOffer,
|
'offer' => $updatedOffer,
|
||||||
]
|
]
|
||||||
|
|||||||
@ -318,7 +318,7 @@ public function register(Request $request)
|
|||||||
'age' => 'nullable|integer',
|
'age' => 'nullable|integer',
|
||||||
'experience' => 'nullable|string|max:100',
|
'experience' => 'nullable|string|max:100',
|
||||||
'in_country' => 'nullable',
|
'in_country' => 'nullable',
|
||||||
'visa_status' => 'nullable|string|in:Tourist Visa,Employment Visa,Residence Visa',
|
'visa_status' => 'nullable|string',
|
||||||
'preferred_job_type' => 'nullable|string|max:100',
|
'preferred_job_type' => 'nullable|string|max:100',
|
||||||
'main_profession' => 'nullable|string|max:100',
|
'main_profession' => 'nullable|string|max:100',
|
||||||
'gender' => 'nullable|string|in:male,female,other',
|
'gender' => 'nullable|string|in:male,female,other',
|
||||||
|
|||||||
@ -18,14 +18,38 @@ class WorkerJobController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function listJobs(Request $request)
|
public function listJobs(Request $request)
|
||||||
{
|
{
|
||||||
$jobs = JobPost::where('status', 'active')
|
/** @var Worker $worker */
|
||||||
->with(['employer.employerProfile'])
|
$worker = $request->attributes->get('worker');
|
||||||
->latest()
|
|
||||||
|
$query = JobPost::where('status', 'active')
|
||||||
|
->with(['employer.employerProfile', 'skills']);
|
||||||
|
|
||||||
|
if ($worker) {
|
||||||
|
$workerProfession = trim($worker->main_profession);
|
||||||
|
$workerSkillIds = $worker->skills->pluck('id')->toArray();
|
||||||
|
|
||||||
|
if ($workerProfession !== '' || !empty($workerSkillIds)) {
|
||||||
|
$query->where(function($q) use ($workerProfession, $workerSkillIds) {
|
||||||
|
if ($workerProfession !== '') {
|
||||||
|
$q->where(\Illuminate\Support\Facades\DB::raw('LOWER(main_profession)'), strtolower($workerProfession));
|
||||||
|
}
|
||||||
|
if (!empty($workerSkillIds)) {
|
||||||
|
$q->orWhereHas('skills', function ($q2) use ($workerSkillIds) {
|
||||||
|
$q2->whereIn('skills.id', $workerSkillIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$jobs = $query->latest()
|
||||||
->get()
|
->get()
|
||||||
->map(function ($job) {
|
->map(function ($job) {
|
||||||
return [
|
return [
|
||||||
'id' => $job->id,
|
'id' => $job->id,
|
||||||
'title' => $job->title,
|
'title' => $job->title,
|
||||||
|
'main_profession' => $job->main_profession,
|
||||||
|
'skills' => $job->skills->pluck('name')->toArray(),
|
||||||
'location' => $job->location,
|
'location' => $job->location,
|
||||||
'salary' => (int) $job->salary,
|
'salary' => (int) $job->salary,
|
||||||
'workers_needed' => $job->workers_needed,
|
'workers_needed' => $job->workers_needed,
|
||||||
@ -54,7 +78,7 @@ public function listJobs(Request $request)
|
|||||||
public function jobDetails($id)
|
public function jobDetails($id)
|
||||||
{
|
{
|
||||||
$job = JobPost::where('status', 'active')
|
$job = JobPost::where('status', 'active')
|
||||||
->with(['employer.employerProfile'])
|
->with(['employer.employerProfile', 'skills'])
|
||||||
->find($id);
|
->find($id);
|
||||||
|
|
||||||
if (!$job) {
|
if (!$job) {
|
||||||
@ -70,6 +94,8 @@ public function jobDetails($id)
|
|||||||
'job' => [
|
'job' => [
|
||||||
'id' => $job->id,
|
'id' => $job->id,
|
||||||
'title' => $job->title,
|
'title' => $job->title,
|
||||||
|
'main_profession' => $job->main_profession,
|
||||||
|
'skills' => $job->skills->pluck('name')->toArray(),
|
||||||
'location' => $job->location,
|
'location' => $job->location,
|
||||||
'salary' => (int) $job->salary,
|
'salary' => (int) $job->salary,
|
||||||
'workers_needed' => $job->workers_needed,
|
'workers_needed' => $job->workers_needed,
|
||||||
@ -121,6 +147,19 @@ public function applyJob(Request $request, $id)
|
|||||||
'status' => 'applied'
|
'status' => 'applied'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$employer = $job->employer;
|
||||||
|
if ($employer) {
|
||||||
|
$employer->notify(new \App\Notifications\GenericNotification(
|
||||||
|
"New Job Application",
|
||||||
|
"A worker (" . ($worker->name ?? 'Candidate') . ") has applied for your job: " . ($job->title ?? 'Job Post'),
|
||||||
|
[
|
||||||
|
'type' => 'job_application_submitted',
|
||||||
|
'job_id' => $job->id,
|
||||||
|
'application_id' => $application->id,
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Successfully applied for the job.',
|
'message' => 'Successfully applied for the job.',
|
||||||
@ -244,6 +283,8 @@ public function employerCreateJob(Request $request)
|
|||||||
|
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'title' => 'required|string|max:255',
|
'title' => 'required|string|max:255',
|
||||||
|
'main_profession' => 'required|string|max:255',
|
||||||
|
'skills' => 'required',
|
||||||
'workers_needed' => 'required|integer|min:1',
|
'workers_needed' => 'required|integer|min:1',
|
||||||
'location' => 'required|string|max:255',
|
'location' => 'required|string|max:255',
|
||||||
'salary' => 'required|numeric|min:0',
|
'salary' => 'required|numeric|min:0',
|
||||||
@ -264,6 +305,7 @@ public function employerCreateJob(Request $request)
|
|||||||
$job = JobPost::create([
|
$job = JobPost::create([
|
||||||
'employer_id' => $employer->id,
|
'employer_id' => $employer->id,
|
||||||
'title' => $request->title,
|
'title' => $request->title,
|
||||||
|
'main_profession' => $request->main_profession,
|
||||||
'workers_needed' => $request->workers_needed,
|
'workers_needed' => $request->workers_needed,
|
||||||
'job_type' => $request->job_type,
|
'job_type' => $request->job_type,
|
||||||
'location' => $request->location,
|
'location' => $request->location,
|
||||||
@ -274,6 +316,27 @@ public function employerCreateJob(Request $request)
|
|||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Sync skills
|
||||||
|
$skillsInput = $request->skills;
|
||||||
|
$skillIds = [];
|
||||||
|
if (is_array($skillsInput)) {
|
||||||
|
foreach ($skillsInput as $skillNameOrId) {
|
||||||
|
if (is_numeric($skillNameOrId)) {
|
||||||
|
$skillIds[] = (int)$skillNameOrId;
|
||||||
|
} else {
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => trim($skillNameOrId)]);
|
||||||
|
$skillIds[] = $skill->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (is_string($skillsInput)) {
|
||||||
|
$skillsArray = array_filter(array_map('trim', explode(',', $skillsInput)));
|
||||||
|
foreach ($skillsArray as $name) {
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => $name]);
|
||||||
|
$skillIds[] = $skill->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$job->skills()->sync($skillIds);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Job posted successfully.',
|
'message' => 'Job posted successfully.',
|
||||||
@ -281,6 +344,8 @@ public function employerCreateJob(Request $request)
|
|||||||
'job' => [
|
'job' => [
|
||||||
'id' => $job->id,
|
'id' => $job->id,
|
||||||
'title' => $job->title,
|
'title' => $job->title,
|
||||||
|
'main_profession' => $job->main_profession,
|
||||||
|
'skills' => $job->skills()->pluck('name')->toArray(),
|
||||||
'location' => $job->location,
|
'location' => $job->location,
|
||||||
'salary' => (int) $job->salary,
|
'salary' => (int) $job->salary,
|
||||||
'workers_needed' => $job->workers_needed,
|
'workers_needed' => $job->workers_needed,
|
||||||
@ -368,8 +433,17 @@ public function employerUpdateJob(Request $request, $id)
|
|||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($job->applications()->exists()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'This job cannot be edited because workers have already applied for it.'
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'title' => 'required|string|max:255',
|
'title' => 'required|string|max:255',
|
||||||
|
'main_profession' => 'required|string|max:255',
|
||||||
|
'skills' => 'required',
|
||||||
'workers_needed' => 'required|integer|min:1',
|
'workers_needed' => 'required|integer|min:1',
|
||||||
'location' => 'required|string|max:255',
|
'location' => 'required|string|max:255',
|
||||||
'salary' => 'required|numeric|min:0',
|
'salary' => 'required|numeric|min:0',
|
||||||
@ -390,6 +464,7 @@ public function employerUpdateJob(Request $request, $id)
|
|||||||
|
|
||||||
$job->update([
|
$job->update([
|
||||||
'title' => $request->title,
|
'title' => $request->title,
|
||||||
|
'main_profession' => $request->main_profession,
|
||||||
'workers_needed' => $request->workers_needed,
|
'workers_needed' => $request->workers_needed,
|
||||||
'job_type' => $request->job_type,
|
'job_type' => $request->job_type,
|
||||||
'location' => $request->location,
|
'location' => $request->location,
|
||||||
@ -400,6 +475,27 @@ public function employerUpdateJob(Request $request, $id)
|
|||||||
'status' => strtolower($request->status),
|
'status' => strtolower($request->status),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Sync skills
|
||||||
|
$skillsInput = $request->skills;
|
||||||
|
$skillIds = [];
|
||||||
|
if (is_array($skillsInput)) {
|
||||||
|
foreach ($skillsInput as $skillNameOrId) {
|
||||||
|
if (is_numeric($skillNameOrId)) {
|
||||||
|
$skillIds[] = (int)$skillNameOrId;
|
||||||
|
} else {
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => trim($skillNameOrId)]);
|
||||||
|
$skillIds[] = $skill->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (is_string($skillsInput)) {
|
||||||
|
$skillsArray = array_filter(array_map('trim', explode(',', $skillsInput)));
|
||||||
|
foreach ($skillsArray as $name) {
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => $name]);
|
||||||
|
$skillIds[] = $skill->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$job->skills()->sync($skillIds);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Job updated successfully.',
|
'message' => 'Job updated successfully.',
|
||||||
@ -407,6 +503,8 @@ public function employerUpdateJob(Request $request, $id)
|
|||||||
'job' => [
|
'job' => [
|
||||||
'id' => $job->id,
|
'id' => $job->id,
|
||||||
'title' => $job->title,
|
'title' => $job->title,
|
||||||
|
'main_profession' => $job->main_profession,
|
||||||
|
'skills' => $job->skills()->pluck('name')->toArray(),
|
||||||
'location' => $job->location,
|
'location' => $job->location,
|
||||||
'salary' => (int) $job->salary,
|
'salary' => (int) $job->salary,
|
||||||
'workers_needed' => $job->workers_needed,
|
'workers_needed' => $job->workers_needed,
|
||||||
@ -562,7 +660,7 @@ public function employerUpdateApplicationStatus(Request $request, $id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'status' => 'required|string|in:applied,shortlisted,contacted,interview scheduled,interview_scheduled,selected,rejected,hired',
|
'status' => 'required|string|in:applied,shortlisted,contacted,interview scheduled,interview_scheduled,selected,rejected,hired,hire_requested',
|
||||||
'notes' => 'nullable|string',
|
'notes' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -587,9 +685,12 @@ public function employerUpdateApplicationStatus(Request $request, $id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
$dbStatus = strtolower($request->status);
|
$dbStatus = strtolower($request->status);
|
||||||
|
if ($dbStatus === 'hired') {
|
||||||
|
$dbStatus = 'hire_requested';
|
||||||
|
}
|
||||||
|
|
||||||
// Contact Limit check: if moving to 'shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired' for the first time
|
// Contact Limit check: if moving to 'shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired', 'hire_requested'
|
||||||
$contactStatuses = ['shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired'];
|
$contactStatuses = ['shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired', 'hire_requested'];
|
||||||
if (in_array($dbStatus, $contactStatuses)) {
|
if (in_array($dbStatus, $contactStatuses)) {
|
||||||
if (!User::canContactWorker($employer->id, $application->worker_id)) {
|
if (!User::canContactWorker($employer->id, $application->worker_id)) {
|
||||||
$activeSub = \App\Models\Subscription::where('user_id', $employer->id)->where('status', 'active')->first();
|
$activeSub = \App\Models\Subscription::where('user_id', $employer->id)->where('status', 'active')->first();
|
||||||
@ -638,26 +739,7 @@ public function employerUpdateApplicationStatus(Request $request, $id)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If hired, also update worker status
|
if ($dbStatus === 'rejected') {
|
||||||
if ($dbStatus === 'hired') {
|
|
||||||
if ($application->worker) {
|
|
||||||
$application->worker->update(['status' => 'Hired']);
|
|
||||||
}
|
|
||||||
// Create/update the JobOffer record to satisfy the Worker Employer List API
|
|
||||||
\App\Models\JobOffer::updateOrCreate(
|
|
||||||
[
|
|
||||||
'employer_id' => $employer->id,
|
|
||||||
'worker_id' => $application->worker_id,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'work_date' => $application->jobPost->start_date ?? now(),
|
|
||||||
'location' => $application->jobPost->location ?? 'Dubai',
|
|
||||||
'salary' => $application->jobPost->salary ?? 0,
|
|
||||||
'status' => 'accepted',
|
|
||||||
'notes' => $application->notes ?? 'Hired through Job Application',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} elseif ($dbStatus === 'rejected') {
|
|
||||||
if ($application->worker) {
|
if ($application->worker) {
|
||||||
$application->worker->update(['status' => 'active']);
|
$application->worker->update(['status' => 'active']);
|
||||||
}
|
}
|
||||||
@ -756,7 +838,164 @@ public function withdrawApplication(Request $request, $id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch FCM Push Notification.
|
* API for workers to confirm a hire request.
|
||||||
|
* POST /api/workers/applied-jobs/{id}/confirm-hire
|
||||||
|
*/
|
||||||
|
public function confirmHire(Request $request, $id)
|
||||||
|
{
|
||||||
|
/** @var Worker $worker */
|
||||||
|
$worker = $request->attributes->get('worker');
|
||||||
|
|
||||||
|
$application = JobApplication::where('worker_id', $worker->id)->find($id);
|
||||||
|
|
||||||
|
if (!$application) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Application not found.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strtolower($application->status) !== 'hire_requested') {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'This application is not pending confirmation.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update application status
|
||||||
|
$history = $application->status_history ?: [];
|
||||||
|
$history[] = [
|
||||||
|
'status' => 'hired',
|
||||||
|
'timestamp' => now()->toIso8601String(),
|
||||||
|
'notes' => 'Hiring confirmed by worker.',
|
||||||
|
];
|
||||||
|
|
||||||
|
$application->update([
|
||||||
|
'status' => 'hired',
|
||||||
|
'status_history' => $history,
|
||||||
|
'joining_confirmed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update worker status
|
||||||
|
$worker->update([
|
||||||
|
'status' => 'Hired',
|
||||||
|
'availability' => 'Hired'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create/update the JobOffer record to satisfy the Worker Employer List API
|
||||||
|
$job = $application->jobPost;
|
||||||
|
$employer = $job ? $job->employer : null;
|
||||||
|
if ($employer) {
|
||||||
|
\App\Models\JobOffer::updateOrCreate(
|
||||||
|
[
|
||||||
|
'employer_id' => $employer->id,
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'work_date' => $job->start_date ?? now(),
|
||||||
|
'location' => $job->location ?? 'Dubai',
|
||||||
|
'salary' => $job->salary ?? 0,
|
||||||
|
'status' => 'accepted',
|
||||||
|
'notes' => $application->notes ?? 'Hired through Job Application',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Notify Employer: Hire confirmation
|
||||||
|
$employer->notify(new \App\Notifications\GenericNotification(
|
||||||
|
"Hire Confirmation",
|
||||||
|
"Worker " . ($worker->name ?? "Candidate") . " has confirmed the hiring for '" . ($job->title ?? 'Job Post') . "'.",
|
||||||
|
[
|
||||||
|
'type' => 'hire_confirmation',
|
||||||
|
'job_id' => $job->id,
|
||||||
|
'application_id' => $application->id,
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify Worker: Hired
|
||||||
|
$worker->notify(new \App\Notifications\GenericNotification(
|
||||||
|
"Congratulations! You've been Hired",
|
||||||
|
"You are now officially hired for: '" . ($job->title ?? 'Job Post') . "'.",
|
||||||
|
[
|
||||||
|
'type' => 'hired',
|
||||||
|
'job_id' => $job->id,
|
||||||
|
'application_id' => $application->id,
|
||||||
|
]
|
||||||
|
));
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Hiring confirmed successfully.',
|
||||||
|
'data' => [
|
||||||
|
'application' => $application->fresh()
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API for workers to decline a hire request.
|
||||||
|
* POST /api/workers/applied-jobs/{id}/decline-hire
|
||||||
|
*/
|
||||||
|
public function declineHire(Request $request, $id)
|
||||||
|
{
|
||||||
|
/** @var Worker $worker */
|
||||||
|
$worker = $request->attributes->get('worker');
|
||||||
|
|
||||||
|
$application = JobApplication::where('worker_id', $worker->id)->find($id);
|
||||||
|
|
||||||
|
if (!$application) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Application not found.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strtolower($application->status) !== 'hire_requested') {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'This application is not pending confirmation.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update application status back to declined
|
||||||
|
$history = $application->status_history ?: [];
|
||||||
|
$history[] = [
|
||||||
|
'status' => 'declined',
|
||||||
|
'timestamp' => now()->toIso8601String(),
|
||||||
|
'notes' => 'Hiring declined by worker.',
|
||||||
|
];
|
||||||
|
|
||||||
|
$application->update([
|
||||||
|
'status' => 'declined',
|
||||||
|
'status_history' => $history,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$job = $application->jobPost;
|
||||||
|
$employer = $job ? $job->employer : null;
|
||||||
|
if ($employer) {
|
||||||
|
// Notify Employer: Hire declined
|
||||||
|
$employer->notify(new \App\Notifications\GenericNotification(
|
||||||
|
"Hire Request Declined",
|
||||||
|
"Worker " . ($worker->name ?? "Candidate") . " has declined the hiring request for '" . ($job->title ?? 'Job Post') . "'.",
|
||||||
|
[
|
||||||
|
'type' => 'hire_declined',
|
||||||
|
'job_id' => $job->id,
|
||||||
|
'application_id' => $application->id,
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Hiring request declined successfully.',
|
||||||
|
'data' => [
|
||||||
|
'application' => $application->fresh()
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch FCM Push Notification & In-app database notifications.
|
||||||
*/
|
*/
|
||||||
private function triggerStatusNotification($application, $status)
|
private function triggerStatusNotification($application, $status)
|
||||||
{
|
{
|
||||||
@ -766,61 +1005,76 @@ private function triggerStatusNotification($application, $status)
|
|||||||
|
|
||||||
// When a new application is submitted: notify employer
|
// When a new application is submitted: notify employer
|
||||||
if ($status === 'applied') {
|
if ($status === 'applied') {
|
||||||
if ($employer && $employer->fcm_token) {
|
if ($employer) {
|
||||||
\App\Services\FCMService::sendPushNotification(
|
$employer->notify(new \App\Notifications\GenericNotification(
|
||||||
$employer->fcm_token,
|
|
||||||
"New Job Application",
|
"New Job Application",
|
||||||
"A candidate has applied for your job: " . ($job->title ?? 'Job Post'),
|
"A candidate has applied for your job: " . ($job->title ?? 'Job Post'),
|
||||||
[
|
[
|
||||||
'type' => 'new_job_application',
|
'type' => 'job_application_submitted',
|
||||||
'job_id' => $job->id,
|
'job_id' => $job->id,
|
||||||
'application_id' => $application->id,
|
'application_id' => $application->id,
|
||||||
]
|
]
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When status is updated: notify worker
|
// When status is updated: notify worker
|
||||||
if ($worker && $worker->fcm_token) {
|
if ($worker) {
|
||||||
$title = "Job Application Update";
|
$title = "Job Application Update";
|
||||||
$body = "";
|
$body = "";
|
||||||
|
$type = 'job_application_status_update';
|
||||||
switch (strtolower($status)) {
|
switch (strtolower($status)) {
|
||||||
case 'shortlisted':
|
case 'shortlisted':
|
||||||
$body = "You have been shortlisted for the job: " . ($job->title ?? 'Job Post');
|
$body = "You have been shortlisted for the job: " . ($job->title ?? 'Job Post');
|
||||||
|
$type = 'shortlisted';
|
||||||
|
break;
|
||||||
|
case 'review':
|
||||||
|
case 'reviewing':
|
||||||
|
$body = "Your application is under review for the job: " . ($job->title ?? 'Job Post');
|
||||||
|
$type = 'review';
|
||||||
break;
|
break;
|
||||||
case 'contacted':
|
case 'contacted':
|
||||||
$body = "An employer wants to contact you regarding the job: " . ($job->title ?? 'Job Post');
|
$body = "An employer wants to contact you regarding the job: " . ($job->title ?? 'Job Post');
|
||||||
|
$type = 'contacted';
|
||||||
break;
|
break;
|
||||||
case 'interview scheduled':
|
case 'interview scheduled':
|
||||||
case 'interview_scheduled':
|
case 'interview_scheduled':
|
||||||
$body = "An interview has been scheduled for the job: " . ($job->title ?? 'Job Post');
|
$body = "An interview has been scheduled for the job: " . ($job->title ?? 'Job Post');
|
||||||
|
$type = 'interview_scheduled';
|
||||||
break;
|
break;
|
||||||
case 'selected':
|
case 'selected':
|
||||||
$body = "Congratulations! You have been selected for the job: " . ($job->title ?? 'Job Post');
|
$body = "Congratulations! You have been selected for the job: " . ($job->title ?? 'Job Post');
|
||||||
|
$type = 'selected';
|
||||||
break;
|
break;
|
||||||
case 'rejected':
|
case 'rejected':
|
||||||
$body = "Your application for the job: " . ($job->title ?? 'Job Post') . " has been reviewed and rejected.";
|
$body = "Your application for the job: " . ($job->title ?? 'Job Post') . " has been reviewed and rejected.";
|
||||||
|
$type = 'rejected';
|
||||||
|
break;
|
||||||
|
case 'hire_requested':
|
||||||
|
$title = "Action Required: Confirm Hiring";
|
||||||
|
$body = "An employer wants to hire you for the job: " . ($job->title ?? 'Job Post') . ". Please confirm.";
|
||||||
|
$type = 'hire_request';
|
||||||
break;
|
break;
|
||||||
case 'hired':
|
case 'hired':
|
||||||
$body = "Congratulations! You have been hired for the job: " . ($job->title ?? 'Job Post');
|
$body = "Congratulations! You have been hired for the job: " . ($job->title ?? 'Job Post');
|
||||||
|
$type = 'hired';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$body = "Your application status for the job: " . ($job->title ?? 'Job Post') . " has changed to " . ucfirst($status);
|
$body = "Your application status for the job: " . ($job->title ?? 'Job Post') . " has changed to " . ucfirst($status);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
\App\Services\FCMService::sendPushNotification(
|
$worker->notify(new \App\Notifications\GenericNotification(
|
||||||
$worker->fcm_token,
|
|
||||||
$title,
|
$title,
|
||||||
$body,
|
$body,
|
||||||
[
|
[
|
||||||
'type' => 'job_application_status_update',
|
'type' => $type,
|
||||||
'job_id' => $job->id ?? '',
|
'job_id' => $job->id ?? '',
|
||||||
'application_id' => $application->id,
|
'application_id' => $application->id,
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
]
|
]
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,7 +78,7 @@ public function updateProfile(Request $request)
|
|||||||
'skills' => 'nullable|array',
|
'skills' => 'nullable|array',
|
||||||
'skills.*' => 'exists:skills,id',
|
'skills.*' => 'exists:skills,id',
|
||||||
'in_country' => 'nullable',
|
'in_country' => 'nullable',
|
||||||
'visa_status' => 'nullable|string|in:Tourist Visa,Employment Visa,Residence Visa',
|
'visa_status' => 'nullable|string',
|
||||||
'preferred_job_type' => 'nullable|string|max:100',
|
'preferred_job_type' => 'nullable|string|max:100',
|
||||||
'gender' => 'nullable|string|in:male,female,other',
|
'gender' => 'nullable|string|in:male,female,other',
|
||||||
'live_in_out' => 'nullable|string|in:live_in,live_out',
|
'live_in_out' => 'nullable|string|in:live_in,live_out',
|
||||||
@ -472,10 +472,9 @@ public function respondToOffer(Request $request, $id)
|
|||||||
|
|
||||||
// Dispatch push notification to employer
|
// Dispatch push notification to employer
|
||||||
$employer = $offer->employer;
|
$employer = $offer->employer;
|
||||||
if ($employer && $employer->fcm_token) {
|
if ($employer) {
|
||||||
$statusMessage = $responseStatus === 'accepted' ? 'accepted' : 'rejected';
|
$statusMessage = $responseStatus === 'accepted' ? 'accepted' : 'rejected';
|
||||||
\App\Services\FCMService::sendPushNotification(
|
$employer->notify(new \App\Notifications\GenericNotification(
|
||||||
$employer->fcm_token,
|
|
||||||
"Job Offer " . ucfirst($statusMessage),
|
"Job Offer " . ucfirst($statusMessage),
|
||||||
"Worker " . ($worker->name ?? "Candidate") . " has " . $statusMessage . " your job offer.",
|
"Worker " . ($worker->name ?? "Candidate") . " has " . $statusMessage . " your job offer.",
|
||||||
[
|
[
|
||||||
@ -483,7 +482,7 @@ public function respondToOffer(Request $request, $id)
|
|||||||
'offer_id' => $offer->id,
|
'offer_id' => $offer->id,
|
||||||
'status' => $responseStatus,
|
'status' => $responseStatus,
|
||||||
]
|
]
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|||||||
@ -316,7 +316,7 @@ private function checkJobAccess($user)
|
|||||||
public function updateStatus(Request $request, $id)
|
public function updateStatus(Request $request, $id)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'status' => 'required|string|in:Applied,Reviewing,Shortlisted,Contacted,Interview Scheduled,Selected,Rejected,Hired,Offer Sent,applied,shortlisted,contacted,interview scheduled,interview_scheduled,selected,rejected,hired,offer sent,offer_sent',
|
'status' => 'required|string|in:Applied,Reviewing,Shortlisted,Contacted,Interview Scheduled,Selected,Rejected,Hired,Offer Sent,applied,shortlisted,contacted,interview scheduled,interview_scheduled,selected,rejected,hired,offer sent,offer_sent,hire_requested,hire_requested',
|
||||||
'notes' => 'nullable|string',
|
'notes' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -327,18 +327,31 @@ public function updateStatus(Request $request, $id)
|
|||||||
|
|
||||||
$dbStatus = 'pending';
|
$dbStatus = 'pending';
|
||||||
if (in_array(strtolower($request->status), ['hired', 'accepted'])) {
|
if (in_array(strtolower($request->status), ['hired', 'accepted'])) {
|
||||||
$dbStatus = 'accepted';
|
// Under two-way confirmation, it must NOT immediately set status to accepted/Hired.
|
||||||
$offer->worker->update(['status' => 'Hired']);
|
// It should set offer status to 'pending' (waiting for worker confirmation).
|
||||||
|
$dbStatus = 'pending';
|
||||||
} elseif (in_array(strtolower($request->status), ['rejected'])) {
|
} elseif (in_array(strtolower($request->status), ['rejected'])) {
|
||||||
$dbStatus = 'rejected';
|
$dbStatus = 'rejected';
|
||||||
$offer->worker->update(['status' => 'active']);
|
|
||||||
} elseif (in_array(strtolower($request->status), ['offer sent', 'offer_sent', 'pending'])) {
|
} elseif (in_array(strtolower($request->status), ['offer sent', 'offer_sent', 'pending'])) {
|
||||||
$dbStatus = 'pending';
|
$dbStatus = 'pending';
|
||||||
}
|
}
|
||||||
|
|
||||||
$offer->update(['status' => $dbStatus]);
|
$offer->update(['status' => $dbStatus]);
|
||||||
|
|
||||||
return back()->with('success', 'Direct candidate hiring offer status updated successfully to ' . $request->status);
|
// Notify worker about the hire offer if it is set to pending
|
||||||
|
if ($dbStatus === 'pending') {
|
||||||
|
$offer->worker->notify(new \App\Notifications\GenericNotification(
|
||||||
|
"Action Required: Confirm Hiring",
|
||||||
|
"Employer " . ($offer->employer->name ?? "Employer") . " has sent you a direct hire offer. Please confirm.",
|
||||||
|
[
|
||||||
|
'type' => 'hire_request',
|
||||||
|
'offer_id' => $offer->id,
|
||||||
|
'status' => 'pending',
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', 'Hire request initiated successfully. Awaiting candidate confirmation.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standard job application status update
|
// Standard job application status update
|
||||||
@ -356,8 +369,8 @@ public function updateStatus(Request $request, $id)
|
|||||||
|
|
||||||
$dbStatus = 'applied';
|
$dbStatus = 'applied';
|
||||||
$statusLower = strtolower($request->status);
|
$statusLower = strtolower($request->status);
|
||||||
if ($statusLower === 'hired') {
|
if ($statusLower === 'hired' || $statusLower === 'hire_requested') {
|
||||||
$dbStatus = 'hired';
|
$dbStatus = 'hire_requested';
|
||||||
} elseif ($statusLower === 'rejected') {
|
} elseif ($statusLower === 'rejected') {
|
||||||
$dbStatus = 'rejected';
|
$dbStatus = 'rejected';
|
||||||
} elseif ($statusLower === 'shortlisted' || $statusLower === 'offer sent' || $statusLower === 'offer_sent') {
|
} elseif ($statusLower === 'shortlisted' || $statusLower === 'offer sent' || $statusLower === 'offer_sent') {
|
||||||
@ -372,8 +385,8 @@ public function updateStatus(Request $request, $id)
|
|||||||
$dbStatus = 'applied';
|
$dbStatus = 'applied';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contact Limit check: if moving to 'shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired'
|
// Contact Limit check: if moving to 'shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired', 'hire_requested'
|
||||||
$contactStatuses = ['shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired'];
|
$contactStatuses = ['shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired', 'hire_requested'];
|
||||||
if (in_array($dbStatus, $contactStatuses)) {
|
if (in_array($dbStatus, $contactStatuses)) {
|
||||||
if (!User::canContactWorker($user->id, $app->worker_id)) {
|
if (!User::canContactWorker($user->id, $app->worker_id)) {
|
||||||
$activeSub = \App\Models\Subscription::where('user_id', $user->id)->where('status', 'active')->first();
|
$activeSub = \App\Models\Subscription::where('user_id', $user->id)->where('status', 'active')->first();
|
||||||
@ -419,26 +432,7 @@ public function updateStatus(Request $request, $id)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If hired, also update worker status
|
if ($dbStatus === 'rejected') {
|
||||||
if ($dbStatus === 'hired') {
|
|
||||||
if ($app->worker) {
|
|
||||||
$app->worker->update(['status' => 'Hired']);
|
|
||||||
}
|
|
||||||
// Create/update the JobOffer record to satisfy the Worker Employer List API
|
|
||||||
JobOffer::updateOrCreate(
|
|
||||||
[
|
|
||||||
'employer_id' => $user->id,
|
|
||||||
'worker_id' => $app->worker_id,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'work_date' => $app->jobPost->start_date ?? now(),
|
|
||||||
'location' => $app->jobPost->location ?? 'Dubai',
|
|
||||||
'salary' => $app->jobPost->salary ?? 0,
|
|
||||||
'status' => 'accepted',
|
|
||||||
'notes' => $app->notes ?? 'Hired through Job Application',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} elseif ($dbStatus === 'rejected') {
|
|
||||||
if ($app->worker) {
|
if ($app->worker) {
|
||||||
$app->worker->update(['status' => 'active']);
|
$app->worker->update(['status' => 'active']);
|
||||||
}
|
}
|
||||||
@ -458,61 +452,76 @@ private function triggerStatusNotification($application, $status)
|
|||||||
|
|
||||||
// When a new application is submitted: notify employer
|
// When a new application is submitted: notify employer
|
||||||
if ($status === 'applied') {
|
if ($status === 'applied') {
|
||||||
if ($employer && $employer->fcm_token) {
|
if ($employer) {
|
||||||
\App\Services\FCMService::sendPushNotification(
|
$employer->notify(new \App\Notifications\GenericNotification(
|
||||||
$employer->fcm_token,
|
|
||||||
"New Job Application",
|
"New Job Application",
|
||||||
"A candidate has applied for your job: " . ($job->title ?? 'Job Post'),
|
"A candidate has applied for your job: " . ($job->title ?? 'Job Post'),
|
||||||
[
|
[
|
||||||
'type' => 'new_job_application',
|
'type' => 'job_application_submitted',
|
||||||
'job_id' => $job->id,
|
'job_id' => $job->id,
|
||||||
'application_id' => $application->id,
|
'application_id' => $application->id,
|
||||||
]
|
]
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When status is updated: notify worker
|
// When status is updated: notify worker
|
||||||
if ($worker && $worker->fcm_token) {
|
if ($worker) {
|
||||||
$title = "Job Application Update";
|
$title = "Job Application Update";
|
||||||
$body = "";
|
$body = "";
|
||||||
|
$type = 'job_application_status_update';
|
||||||
switch (strtolower($status)) {
|
switch (strtolower($status)) {
|
||||||
case 'shortlisted':
|
case 'shortlisted':
|
||||||
$body = "You have been shortlisted for the job: " . ($job->title ?? 'Job Post');
|
$body = "You have been shortlisted for the job: " . ($job->title ?? 'Job Post');
|
||||||
|
$type = 'shortlisted';
|
||||||
|
break;
|
||||||
|
case 'review':
|
||||||
|
case 'reviewing':
|
||||||
|
$body = "Your application is under review for the job: " . ($job->title ?? 'Job Post');
|
||||||
|
$type = 'review';
|
||||||
break;
|
break;
|
||||||
case 'contacted':
|
case 'contacted':
|
||||||
$body = "An employer wants to contact you regarding the job: " . ($job->title ?? 'Job Post');
|
$body = "An employer wants to contact you regarding the job: " . ($job->title ?? 'Job Post');
|
||||||
|
$type = 'contacted';
|
||||||
break;
|
break;
|
||||||
case 'interview scheduled':
|
case 'interview scheduled':
|
||||||
case 'interview_scheduled':
|
case 'interview_scheduled':
|
||||||
$body = "An interview has been scheduled for the job: " . ($job->title ?? 'Job Post');
|
$body = "An interview has been scheduled for the job: " . ($job->title ?? 'Job Post');
|
||||||
|
$type = 'interview_scheduled';
|
||||||
break;
|
break;
|
||||||
case 'selected':
|
case 'selected':
|
||||||
$body = "Congratulations! You have been selected for the job: " . ($job->title ?? 'Job Post');
|
$body = "Congratulations! You have been selected for the job: " . ($job->title ?? 'Job Post');
|
||||||
|
$type = 'selected';
|
||||||
break;
|
break;
|
||||||
case 'rejected':
|
case 'rejected':
|
||||||
$body = "Your application for the job: " . ($job->title ?? 'Job Post') . " has been reviewed and rejected.";
|
$body = "Your application for the job: " . ($job->title ?? 'Job Post') . " has been reviewed and rejected.";
|
||||||
|
$type = 'rejected';
|
||||||
|
break;
|
||||||
|
case 'hire_requested':
|
||||||
|
$title = "Action Required: Confirm Hiring";
|
||||||
|
$body = "An employer wants to hire you for the job: " . ($job->title ?? 'Job Post') . ". Please confirm.";
|
||||||
|
$type = 'hire_request';
|
||||||
break;
|
break;
|
||||||
case 'hired':
|
case 'hired':
|
||||||
$body = "Congratulations! You have been hired for the job: " . ($job->title ?? 'Job Post');
|
$body = "Congratulations! You have been hired for the job: " . ($job->title ?? 'Job Post');
|
||||||
|
$type = 'hired';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$body = "Your application status for the job: " . ($job->title ?? 'Job Post') . " has changed to " . ucfirst($status);
|
$body = "Your application status for the job: " . ($job->title ?? 'Job Post') . " has changed to " . ucfirst($status);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
\App\Services\FCMService::sendPushNotification(
|
$worker->notify(new \App\Notifications\GenericNotification(
|
||||||
$worker->fcm_token,
|
|
||||||
$title,
|
$title,
|
||||||
$body,
|
$body,
|
||||||
[
|
[
|
||||||
'type' => 'job_application_status_update',
|
'type' => $type,
|
||||||
'job_id' => $job->id ?? '',
|
'job_id' => $job->id ?? '',
|
||||||
'application_id' => $application->id,
|
'application_id' => $application->id,
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
]
|
]
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,7 +139,32 @@ public function create()
|
|||||||
->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.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Inertia::render('Employer/Jobs/Create');
|
$professions = \App\Models\Skill::orderBy('name')
|
||||||
|
->pluck('name')
|
||||||
|
->map(function ($name) {
|
||||||
|
return ucwords($name);
|
||||||
|
})
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
if (empty($professions)) {
|
||||||
|
$professions = [
|
||||||
|
'Housekeeper', 'Nanny', 'Maid', 'Electrician', 'Mason', 'Plumber', 'Cleaner', 'Driver', 'Caregiver', 'Cook'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$skills = \App\Models\Skill::all()->map(function ($skill) {
|
||||||
|
return [
|
||||||
|
'id' => $skill->id,
|
||||||
|
'name' => $skill->name,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return Inertia::render('Employer/Jobs/Create', [
|
||||||
|
'professions' => $professions,
|
||||||
|
'availableSkills' => $skills,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
@ -156,6 +181,8 @@ public function store(Request $request)
|
|||||||
|
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'title' => 'required|string|max:255',
|
'title' => 'required|string|max:255',
|
||||||
|
'main_profession' => 'required|string|max:255',
|
||||||
|
'skills' => 'required',
|
||||||
'workers_needed' => 'required|integer|min:1',
|
'workers_needed' => 'required|integer|min:1',
|
||||||
'location' => 'required|string|max:255',
|
'location' => 'required|string|max:255',
|
||||||
'salary' => 'required|numeric|min:0',
|
'salary' => 'required|numeric|min:0',
|
||||||
@ -165,9 +192,10 @@ public function store(Request $request)
|
|||||||
'requirements' => 'nullable|string',
|
'requirements' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
JobPost::create([
|
$job = JobPost::create([
|
||||||
'employer_id' => $user->id,
|
'employer_id' => $user->id,
|
||||||
'title' => $request->title,
|
'title' => $request->title,
|
||||||
|
'main_profession' => $request->main_profession,
|
||||||
'workers_needed' => $request->workers_needed,
|
'workers_needed' => $request->workers_needed,
|
||||||
'job_type' => $request->job_type,
|
'job_type' => $request->job_type,
|
||||||
'location' => $request->location,
|
'location' => $request->location,
|
||||||
@ -178,6 +206,27 @@ public function store(Request $request)
|
|||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Sync skills
|
||||||
|
$skillsInput = $request->skills;
|
||||||
|
$skillIds = [];
|
||||||
|
if (is_array($skillsInput)) {
|
||||||
|
foreach ($skillsInput as $skillNameOrId) {
|
||||||
|
if (is_numeric($skillNameOrId)) {
|
||||||
|
$skillIds[] = (int)$skillNameOrId;
|
||||||
|
} else {
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => trim($skillNameOrId)]);
|
||||||
|
$skillIds[] = $skill->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (is_string($skillsInput)) {
|
||||||
|
$skillsArray = array_filter(array_map('trim', explode(',', $skillsInput)));
|
||||||
|
foreach ($skillsArray as $name) {
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => $name]);
|
||||||
|
$skillIds[] = $skill->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$job->skills()->sync($skillIds);
|
||||||
|
|
||||||
return redirect()->route('employer.jobs')->with('success', 'Job posted successfully.');
|
return redirect()->route('employer.jobs')->with('success', 'Job posted successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,7 +291,7 @@ public function edit($id)
|
|||||||
->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.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$job = JobPost::where('employer_id', $user->id)->where('id', $id)->firstOrFail();
|
$job = JobPost::where('employer_id', $user->id)->where('id', $id)->with('skills')->firstOrFail();
|
||||||
|
|
||||||
// Convert start_date format to Y-m-d for date input
|
// Convert start_date format to Y-m-d for date input
|
||||||
$jobData = $job->toArray();
|
$jobData = $job->toArray();
|
||||||
@ -250,8 +299,40 @@ public function edit($id)
|
|||||||
$jobData['start_date'] = $job->start_date->format('Y-m-d');
|
$jobData['start_date'] = $job->start_date->format('Y-m-d');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add skills list
|
||||||
|
$jobData['skills'] = $job->skills->map(function ($s) {
|
||||||
|
return [
|
||||||
|
'id' => $s->id,
|
||||||
|
'name' => $s->name,
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
|
$professions = \App\Models\Skill::orderBy('name')
|
||||||
|
->pluck('name')
|
||||||
|
->map(function ($name) {
|
||||||
|
return ucwords($name);
|
||||||
|
})
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
if (empty($professions)) {
|
||||||
|
$professions = [
|
||||||
|
'Housekeeper', 'Nanny', 'Maid', 'Electrician', 'Mason', 'Plumber', 'Cleaner', 'Driver', 'Caregiver', 'Cook'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$skills = \App\Models\Skill::all()->map(function ($skill) {
|
||||||
|
return [
|
||||||
|
'id' => $skill->id,
|
||||||
|
'name' => $skill->name,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
return Inertia::render('Employer/Jobs/Edit', [
|
return Inertia::render('Employer/Jobs/Edit', [
|
||||||
'job' => $jobData,
|
'job' => $jobData,
|
||||||
|
'professions' => $professions,
|
||||||
|
'availableSkills' => $skills,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,8 +350,14 @@ public function update(Request $request, $id)
|
|||||||
|
|
||||||
$job = JobPost::where('employer_id', $user->id)->where('id', $id)->firstOrFail();
|
$job = JobPost::where('employer_id', $user->id)->where('id', $id)->firstOrFail();
|
||||||
|
|
||||||
|
if ($job->applications()->exists()) {
|
||||||
|
return back()->withErrors(['general' => 'This job cannot be edited because workers have already applied for it.']);
|
||||||
|
}
|
||||||
|
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'title' => 'required|string|max:255',
|
'title' => 'required|string|max:255',
|
||||||
|
'main_profession' => 'required|string|max:255',
|
||||||
|
'skills' => 'required',
|
||||||
'workers_needed' => 'required|integer|min:1',
|
'workers_needed' => 'required|integer|min:1',
|
||||||
'location' => 'required|string|max:255',
|
'location' => 'required|string|max:255',
|
||||||
'salary' => 'required|numeric|min:0',
|
'salary' => 'required|numeric|min:0',
|
||||||
@ -283,6 +370,7 @@ public function update(Request $request, $id)
|
|||||||
|
|
||||||
$job->update([
|
$job->update([
|
||||||
'title' => $request->title,
|
'title' => $request->title,
|
||||||
|
'main_profession' => $request->main_profession,
|
||||||
'workers_needed' => $request->workers_needed,
|
'workers_needed' => $request->workers_needed,
|
||||||
'job_type' => $request->job_type,
|
'job_type' => $request->job_type,
|
||||||
'location' => $request->location,
|
'location' => $request->location,
|
||||||
@ -293,6 +381,27 @@ public function update(Request $request, $id)
|
|||||||
'status' => strtolower($request->status),
|
'status' => strtolower($request->status),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Sync skills
|
||||||
|
$skillsInput = $request->skills;
|
||||||
|
$skillIds = [];
|
||||||
|
if (is_array($skillsInput)) {
|
||||||
|
foreach ($skillsInput as $skillNameOrId) {
|
||||||
|
if (is_numeric($skillNameOrId)) {
|
||||||
|
$skillIds[] = (int)$skillNameOrId;
|
||||||
|
} else {
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => trim($skillNameOrId)]);
|
||||||
|
$skillIds[] = $skill->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (is_string($skillsInput)) {
|
||||||
|
$skillsArray = array_filter(array_map('trim', explode(',', $skillsInput)));
|
||||||
|
foreach ($skillsArray as $name) {
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => $name]);
|
||||||
|
$skillIds[] = $skill->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$job->skills()->sync($skillIds);
|
||||||
|
|
||||||
return redirect()->route('employer.jobs')->with('success', 'Job updated successfully.');
|
return redirect()->route('employer.jobs')->with('success', 'Job updated successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,21 @@ private function resolveCurrentUser()
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getWorkerStatus($employerId, $workerId, $defaultStatus)
|
||||||
|
{
|
||||||
|
$pendingOfferOrApp = JobOffer::where('employer_id', $employerId)
|
||||||
|
->where('worker_id', $workerId)
|
||||||
|
->where('status', 'pending')
|
||||||
|
->exists() ||
|
||||||
|
\App\Models\JobApplication::where('worker_id', $workerId)
|
||||||
|
->whereHas('jobPost', function($q) use ($employerId) {
|
||||||
|
$q->where('employer_id', $employerId);
|
||||||
|
})->where('status', 'hire_requested')
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
return $pendingOfferOrApp ? 'pending_confirmation' : strtolower($defaultStatus ?? 'active');
|
||||||
|
}
|
||||||
|
|
||||||
public static function processWorkerResponse($conv, $worker, $text)
|
public static function processWorkerResponse($conv, $worker, $text)
|
||||||
{
|
{
|
||||||
$replyText = strtolower(trim($text));
|
$replyText = strtolower(trim($text));
|
||||||
@ -105,13 +120,13 @@ public function index(Request $request)
|
|||||||
->latest('updated_at')
|
->latest('updated_at')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$conversations = $dbConversations->map(function ($conv) {
|
$conversations = $dbConversations->map(function ($conv) use ($user) {
|
||||||
$lastMsg = $conv->messages->last();
|
$lastMsg = $conv->messages->last();
|
||||||
return [
|
return [
|
||||||
'id' => $conv->id,
|
'id' => $conv->id,
|
||||||
'worker_id' => $conv->worker_id,
|
'worker_id' => $conv->worker_id,
|
||||||
'worker_name' => $conv->worker->name ?? 'Candidate',
|
'worker_name' => $conv->worker->name ?? 'Candidate',
|
||||||
'worker_status' => strtolower($conv->worker->status ?? 'active'),
|
'worker_status' => $this->getWorkerStatus($user->id, $conv->worker_id, $conv->worker->status),
|
||||||
'last_message' => $lastMsg->text ?? 'No messages yet.',
|
'last_message' => $lastMsg->text ?? 'No messages yet.',
|
||||||
'unread' => $lastMsg ? ($lastMsg->sender_type === 'worker' && is_null($lastMsg->read_at)) : false,
|
'unread' => $lastMsg ? ($lastMsg->sender_type === 'worker' && is_null($lastMsg->read_at)) : false,
|
||||||
'online' => true,
|
'online' => true,
|
||||||
@ -136,13 +151,13 @@ public function show($id)
|
|||||||
->latest('updated_at')
|
->latest('updated_at')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$conversations = $dbConversations->map(function ($conv) {
|
$conversations = $dbConversations->map(function ($conv) use ($user) {
|
||||||
$lastMsg = $conv->messages->last();
|
$lastMsg = $conv->messages->last();
|
||||||
return [
|
return [
|
||||||
'id' => $conv->id,
|
'id' => $conv->id,
|
||||||
'worker_id' => $conv->worker_id,
|
'worker_id' => $conv->worker_id,
|
||||||
'worker_name' => $conv->worker->name ?? 'Candidate',
|
'worker_name' => $conv->worker->name ?? 'Candidate',
|
||||||
'worker_status' => strtolower($conv->worker->status ?? 'active'),
|
'worker_status' => $this->getWorkerStatus($user->id, $conv->worker_id, $conv->worker->status),
|
||||||
'last_message' => $lastMsg->text ?? 'No messages yet.',
|
'last_message' => $lastMsg->text ?? 'No messages yet.',
|
||||||
'unread' => $lastMsg ? ($lastMsg->sender_type === 'worker' && is_null($lastMsg->read_at)) : false,
|
'unread' => $lastMsg ? ($lastMsg->sender_type === 'worker' && is_null($lastMsg->read_at)) : false,
|
||||||
'online' => true,
|
'online' => true,
|
||||||
@ -165,7 +180,7 @@ public function show($id)
|
|||||||
'id' => $activeConv->id,
|
'id' => $activeConv->id,
|
||||||
'worker_id' => $activeConv->worker_id,
|
'worker_id' => $activeConv->worker_id,
|
||||||
'worker_name' => $activeConv->worker->name ?? 'Candidate',
|
'worker_name' => $activeConv->worker->name ?? 'Candidate',
|
||||||
'worker_status' => strtolower($activeConv->worker->status ?? 'active'),
|
'worker_status' => $this->getWorkerStatus($user->id, $activeConv->worker_id, $activeConv->worker->status),
|
||||||
'online' => true,
|
'online' => true,
|
||||||
'salary' => ($activeConv->worker->salary ?? 2000) . ' AED',
|
'salary' => ($activeConv->worker->salary ?? 2000) . ' AED',
|
||||||
'nationality' => $activeConv->worker->nationality ?? 'Unknown',
|
'nationality' => $activeConv->worker->nationality ?? 'Unknown',
|
||||||
|
|||||||
@ -365,6 +365,16 @@ public function show($id)
|
|||||||
|
|
||||||
$visaStatus = $w->visa_status;
|
$visaStatus = $w->visa_status;
|
||||||
|
|
||||||
|
$pendingOfferOrApp = JobOffer::where('employer_id', $user ? $user->id : 0)
|
||||||
|
->where('worker_id', $w->id)
|
||||||
|
->where('status', 'pending')
|
||||||
|
->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', 'hire_requested')
|
||||||
|
->exists();
|
||||||
|
|
||||||
$worker = [
|
$worker = [
|
||||||
'id' => $w->id,
|
'id' => $w->id,
|
||||||
'name' => $w->name,
|
'name' => $w->name,
|
||||||
@ -392,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' => $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,
|
||||||
@ -479,9 +489,8 @@ public function markHired(Request $request, $id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
$worker = Worker::findOrFail($id);
|
$worker = Worker::findOrFail($id);
|
||||||
$worker->update([
|
|
||||||
'status' => 'Hired',
|
// 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)
|
||||||
@ -489,7 +498,7 @@ public function markHired(Request $request, $id)
|
|||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($offer) {
|
if ($offer) {
|
||||||
$offer->update(['status' => 'accepted']);
|
$offer->update(['status' => 'pending']);
|
||||||
} else {
|
} 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');
|
||||||
@ -498,21 +507,41 @@ public function markHired(Request $request, $id)
|
|||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($application) {
|
if ($application) {
|
||||||
$application->update(['status' => 'hired']);
|
$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]);
|
||||||
} else {
|
} else {
|
||||||
// Create a new direct job offer with status accepted
|
// Create a new direct job offer with status pending
|
||||||
JobOffer::create([
|
$offer = 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 finalized.',
|
'notes' => 'Direct sponsoring matching initiated.',
|
||||||
'status' => 'accepted',
|
'status' => 'pending',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return back()->with('success', 'Worker successfully marked as Hired!');
|
// Notify the worker
|
||||||
|
$worker->notify(new \App\Notifications\GenericNotification(
|
||||||
|
"Action Required: Confirm Hiring",
|
||||||
|
"Employer " . ($user->name ?? "Employer") . " has sent you a hire request. Please confirm.",
|
||||||
|
[
|
||||||
|
'type' => 'hire_request',
|
||||||
|
'offer_id' => $offer->id ?? '',
|
||||||
|
'application_id' => $application->id ?? '',
|
||||||
|
'status' => $offer ? 'pending' : 'hire_requested',
|
||||||
|
]
|
||||||
|
));
|
||||||
|
|
||||||
|
return back()->with('success', 'Hire request initiated successfully! Awaiting candidate confirmation.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ class JobPost extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'employer_id',
|
'employer_id',
|
||||||
'title',
|
'title',
|
||||||
|
'main_profession',
|
||||||
'workers_needed',
|
'workers_needed',
|
||||||
'job_type',
|
'job_type',
|
||||||
'location',
|
'location',
|
||||||
@ -34,6 +35,10 @@ public function employer()
|
|||||||
return $this->belongsTo(User::class, 'employer_id');
|
return $this->belongsTo(User::class, 'employer_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function skills()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Skill::class, 'job_post_skills', 'job_post_id', 'skill_id');
|
||||||
|
}
|
||||||
|
|
||||||
public function applications()
|
public function applications()
|
||||||
{
|
{
|
||||||
|
|||||||
65
app/Notifications/GenericNotification.php
Normal file
65
app/Notifications/GenericNotification.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use App\Notifications\Channels\FcmChannel;
|
||||||
|
|
||||||
|
class GenericNotification extends Notification
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public $title;
|
||||||
|
public $body;
|
||||||
|
public $data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new notification instance.
|
||||||
|
*/
|
||||||
|
public function __construct($title, $body, array $data = [])
|
||||||
|
{
|
||||||
|
$this->title = $title;
|
||||||
|
$this->body = $body;
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's delivery channels.
|
||||||
|
*/
|
||||||
|
public function via($notifiable): array
|
||||||
|
{
|
||||||
|
$channels = ['database'];
|
||||||
|
if (!empty($notifiable->fcm_token)) {
|
||||||
|
$channels[] = FcmChannel::class;
|
||||||
|
}
|
||||||
|
return $channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the array representation of the notification for in-app database storage.
|
||||||
|
*/
|
||||||
|
public function toArray($notifiable): array
|
||||||
|
{
|
||||||
|
return array_merge([
|
||||||
|
'title' => $this->title,
|
||||||
|
'body' => $this->body,
|
||||||
|
], $this->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the push notification representation.
|
||||||
|
*/
|
||||||
|
public function toFcm($notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'token' => $notifiable->fcm_token,
|
||||||
|
'title' => $this->title,
|
||||||
|
'body' => $this->body,
|
||||||
|
'data' => array_merge([
|
||||||
|
'title' => $this->title,
|
||||||
|
'body' => $this->body,
|
||||||
|
], $this->data),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('job_posts', function (Blueprint $table) {
|
||||||
|
$table->string('main_profession')->nullable()->after('title');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('job_post_skills', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('job_post_id')->constrained('job_posts')->onDelete('cascade');
|
||||||
|
$table->foreignId('skill_id')->constrained('skills')->onDelete('cascade');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('job_post_skills');
|
||||||
|
|
||||||
|
Schema::table('job_posts', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('main_profession');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -193,10 +193,11 @@ export default function Applicants({ job, applicants: initialApplicants, isShort
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest cursor-pointer hover:opacity-85 transition-opacity ${
|
<span className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest cursor-pointer hover:opacity-85 transition-opacity ${
|
||||||
applicant.status?.toLowerCase() === 'hired' ? 'bg-emerald-100 text-emerald-700' :
|
applicant.status?.toLowerCase() === 'hired' ? 'bg-emerald-100 text-emerald-700' :
|
||||||
|
applicant.status?.toLowerCase() === 'hire_requested' ? 'bg-amber-50 text-amber-600 border border-amber-200' :
|
||||||
applicant.status?.toLowerCase() === 'rejected' ? 'bg-rose-100 text-rose-700' :
|
applicant.status?.toLowerCase() === 'rejected' ? 'bg-rose-100 text-rose-700' :
|
||||||
'bg-amber-100 text-amber-700'
|
'bg-blue-50 text-[#185FA5]'
|
||||||
}`} onClick={() => openStatusModal(applicant)}>
|
}`} onClick={() => openStatusModal(applicant)}>
|
||||||
{applicant.status}
|
{applicant.status?.toLowerCase() === 'hire_requested' ? 'Pending Confirmation' : applicant.status}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center space-x-1 text-[10px] font-bold text-slate-400">
|
<div className="flex items-center space-x-1 text-[10px] font-bold text-slate-400">
|
||||||
<Link href={`/employer/workers/${applicant.id}`} className="hover:text-[#185FA5] flex items-center underline underline-offset-2">
|
<Link href={`/employer/workers/${applicant.id}`} className="hover:text-[#185FA5] flex items-center underline underline-offset-2">
|
||||||
@ -261,6 +262,11 @@ export default function Applicants({ job, applicants: initialApplicants, isShort
|
|||||||
|
|
||||||
{/* Modal Body */}
|
{/* Modal Body */}
|
||||||
<div className="p-6 space-y-5">
|
<div className="p-6 space-y-5">
|
||||||
|
{selectedApp?.status?.toLowerCase() === 'hire_requested' && (
|
||||||
|
<div className="p-3 bg-amber-50 rounded-xl border border-amber-100 text-[10px] text-amber-700 font-bold">
|
||||||
|
A hiring offer is pending. Waiting for worker confirmation.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Status Selector */}
|
{/* Status Selector */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Select Stage</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Select Stage</label>
|
||||||
@ -275,7 +281,7 @@ export default function Applicants({ job, applicants: initialApplicants, isShort
|
|||||||
<option value="Interview Scheduled">Interview Scheduled</option>
|
<option value="Interview Scheduled">Interview Scheduled</option>
|
||||||
<option value="Selected">Selected</option>
|
<option value="Selected">Selected</option>
|
||||||
<option value="Rejected">Rejected</option>
|
<option value="Rejected">Rejected</option>
|
||||||
<option value="Hired">Hired</option>
|
<option value="Hired">Hired (Request Confirmation)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -7,29 +7,31 @@ import {
|
|||||||
Briefcase,
|
Briefcase,
|
||||||
MapPin,
|
MapPin,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Calendar,
|
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
Users,
|
Users,
|
||||||
FileText,
|
FileText,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
ShieldCheck
|
ShieldCheck,
|
||||||
|
X
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export default function Create({ categories: initialCategories }) {
|
export default function Create({ professions, availableSkills }) {
|
||||||
const categories = initialCategories || [
|
const categoryList = professions || [
|
||||||
'Electrician', 'Mason', 'Plumber', 'Cleaner', 'Site Supervisor', 'Driver', 'General Helper'
|
'Housekeeper', 'Nanny', 'Maid', 'Electrician', 'Mason', 'Plumber', 'Cleaner', 'Driver', 'Caregiver', 'Cook'
|
||||||
];
|
];
|
||||||
|
const skillList = availableSkills || [];
|
||||||
|
|
||||||
const { data, setData, post, processing, errors } = useForm({
|
const { data, setData, post, processing, errors } = useForm({
|
||||||
title: '',
|
title: '',
|
||||||
|
main_profession: '',
|
||||||
|
skills: [],
|
||||||
workers_needed: 1,
|
workers_needed: 1,
|
||||||
location: '',
|
location: '',
|
||||||
salary: '',
|
salary: '',
|
||||||
job_type: 'Full Time',
|
job_type: 'Full Time',
|
||||||
start_date: '',
|
start_date: new Date().toISOString().split('T')[0],
|
||||||
description: '',
|
description: '',
|
||||||
requirements: ''
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [clientErrors, setClientErrors] = useState({});
|
const [clientErrors, setClientErrors] = useState({});
|
||||||
@ -42,6 +44,12 @@ export default function Create({ categories: initialCategories }) {
|
|||||||
if (!data.title.trim()) {
|
if (!data.title.trim()) {
|
||||||
newErrors.title = 'Job title is required.';
|
newErrors.title = 'Job title is required.';
|
||||||
}
|
}
|
||||||
|
if (!data.main_profession) {
|
||||||
|
newErrors.main_profession = 'Main profession is required.';
|
||||||
|
}
|
||||||
|
if (!data.skills || data.skills.length === 0) {
|
||||||
|
newErrors.skills = 'At least one skill is required.';
|
||||||
|
}
|
||||||
if (!data.workers_needed || data.workers_needed < 1) {
|
if (!data.workers_needed || data.workers_needed < 1) {
|
||||||
newErrors.workers_needed = 'Workers needed must be at least 1.';
|
newErrors.workers_needed = 'Workers needed must be at least 1.';
|
||||||
}
|
}
|
||||||
@ -133,7 +141,29 @@ export default function Create({ categories: initialCategories }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 col-span-2 md:col-span-1">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Main Profession</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Briefcase className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<select
|
||||||
|
className={`w-full pl-11 pr-10 py-3 bg-slate-50 border rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all appearance-none cursor-pointer ${
|
||||||
|
(clientErrors.main_profession || errors.main_profession) ? 'border-red-500 bg-red-50/20' : 'border-slate-100'
|
||||||
|
}`}
|
||||||
|
value={data.main_profession}
|
||||||
|
onChange={e => setData('main_profession', e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Select main profession...</option>
|
||||||
|
{categoryList.map(prof => (
|
||||||
|
<option key={prof} value={prof}>{prof}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{(clientErrors.main_profession || errors.main_profession) && (
|
||||||
|
<p className="text-xs font-bold text-red-500 mt-1 ml-1">{clientErrors.main_profession || errors.main_profession}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 col-span-2 md:col-span-1">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Workers Needed</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Workers Needed</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Users className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
<Users className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
@ -152,37 +182,7 @@ export default function Create({ categories: initialCategories }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 col-span-2 md:col-span-1">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Type</label>
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
{['Full Time', 'Part Time', 'Contract'].map(type => (
|
|
||||||
<button
|
|
||||||
key={type}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setData('job_type', type)}
|
|
||||||
className={`py-3 text-[10px] font-black rounded-xl border transition-all ${
|
|
||||||
data.job_type === type
|
|
||||||
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-md shadow-blue-500/20'
|
|
||||||
: 'bg-white text-slate-400 border-slate-100 hover:border-slate-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{type.toUpperCase()}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Section 2: Logistics & Pay */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<h3 className="text-xs font-black text-[#185FA5] uppercase tracking-[0.2em] flex items-center">
|
|
||||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full mr-2" />
|
|
||||||
Logistics & Compensation
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Location</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Location</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<MapPin className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
<MapPin className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
@ -201,7 +201,7 @@ export default function Create({ categories: initialCategories }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 col-span-2 md:col-span-1">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Salary (AED / mo)</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Salary (AED / mo)</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<DollarSign className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
<DollarSign className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
@ -220,27 +220,29 @@ export default function Create({ categories: initialCategories }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 col-span-2 md:col-span-1">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Start Date</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Type</label>
|
||||||
<div className="relative">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<Calendar className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
{['Full Time', 'Part Time'].map(type => (
|
||||||
<input
|
<button
|
||||||
type="date"
|
key={type}
|
||||||
className={`w-full pl-11 pr-4 py-3 bg-slate-50 border rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all ${
|
type="button"
|
||||||
(clientErrors.start_date || errors.start_date) ? 'border-red-500 bg-red-50/20' : 'border-slate-100'
|
onClick={() => setData('job_type', type)}
|
||||||
|
className={`py-3 text-[10px] font-black rounded-xl border transition-all ${
|
||||||
|
data.job_type === type
|
||||||
|
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-md shadow-blue-500/20'
|
||||||
|
: 'bg-white text-slate-400 border-slate-100 hover:border-slate-300'
|
||||||
}`}
|
}`}
|
||||||
value={data.start_date}
|
>
|
||||||
onChange={e => setData('start_date', e.target.value)}
|
{type.toUpperCase()}
|
||||||
/>
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{(clientErrors.start_date || errors.start_date) && (
|
|
||||||
<p className="text-xs font-bold text-red-500 mt-1 ml-1">{clientErrors.start_date || errors.start_date}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Section 3: Details */}
|
{/* Section 2: Detailed Requirements */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h3 className="text-xs font-black text-[#185FA5] uppercase tracking-[0.2em] flex items-center">
|
<h3 className="text-xs font-black text-[#185FA5] uppercase tracking-[0.2em] flex items-center">
|
||||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full mr-2" />
|
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full mr-2" />
|
||||||
@ -248,6 +250,56 @@ export default function Create({ categories: initialCategories }) {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Skills (Select from list)</label>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Briefcase className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<select
|
||||||
|
className={`w-full pl-11 pr-10 py-3 bg-slate-50 border rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all appearance-none cursor-pointer ${
|
||||||
|
(clientErrors.skills || errors.skills) ? 'border-red-500 bg-red-50/20' : 'border-slate-100'
|
||||||
|
}`}
|
||||||
|
value=""
|
||||||
|
onChange={e => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val && !data.skills.includes(val)) {
|
||||||
|
setData('skills', [...data.skills, val]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">Choose skills to add...</option>
|
||||||
|
{skillList.filter(s => !data.skills.includes(s.name)).map(skill => (
|
||||||
|
<option key={skill.id} value={skill.name}>{skill.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selected skills pills */}
|
||||||
|
{data.skills.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 p-3 bg-slate-50 rounded-2xl border border-slate-100">
|
||||||
|
{data.skills.map(skill => (
|
||||||
|
<span
|
||||||
|
key={skill}
|
||||||
|
className="inline-flex items-center space-x-1.5 px-3 py-1.5 bg-[#185FA5] text-white rounded-xl text-xs font-black uppercase tracking-wider shadow-xs"
|
||||||
|
>
|
||||||
|
<span>{skill}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setData('skills', data.skills.filter(s => s !== skill))}
|
||||||
|
className="hover:bg-blue-700/50 p-0.5 rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-3.5 h-3.5 text-white" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{(clientErrors.skills || errors.skills) && (
|
||||||
|
<p className="text-xs font-bold text-red-500 mt-1 ml-1">{clientErrors.skills || errors.skills}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Description</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
@ -268,19 +320,6 @@ export default function Create({ categories: initialCategories }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Special Requirements (Optional)</label>
|
|
||||||
<div className="relative">
|
|
||||||
<FileText className="absolute left-4 top-5 w-4 h-4 text-slate-400" />
|
|
||||||
<textarea
|
|
||||||
placeholder="e.g. Must speak Arabic, 5+ years experience, transferable visa..."
|
|
||||||
className="w-full pl-11 pr-4 py-4 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all min-h-[100px] resize-none"
|
|
||||||
value={data.requirements}
|
|
||||||
onChange={e => setData('requirements', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -7,26 +7,32 @@ import {
|
|||||||
Briefcase,
|
Briefcase,
|
||||||
MapPin,
|
MapPin,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Calendar,
|
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
Users,
|
Users,
|
||||||
FileText,
|
FileText,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Save
|
Save,
|
||||||
|
X
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export default function Edit({ job }) {
|
export default function Edit({ job, professions, availableSkills }) {
|
||||||
|
const categoryList = professions || [
|
||||||
|
'Housekeeper', 'Nanny', 'Maid', 'Electrician', 'Mason', 'Plumber', 'Cleaner', 'Driver', 'Caregiver', 'Cook'
|
||||||
|
];
|
||||||
|
const skillList = availableSkills || [];
|
||||||
|
|
||||||
const { data, setData, post, processing, errors } = useForm({
|
const { data, setData, post, processing, errors } = useForm({
|
||||||
title: job.title || '',
|
title: job.title || '',
|
||||||
|
main_profession: job.main_profession || '',
|
||||||
|
skills: job.skills ? job.skills.map(s => s.name) : [],
|
||||||
workers_needed: job.workers_needed || 1,
|
workers_needed: job.workers_needed || 1,
|
||||||
location: job.location || '',
|
location: job.location || '',
|
||||||
salary: job.salary ? Math.round(job.salary) : '',
|
salary: job.salary ? Math.round(job.salary) : '',
|
||||||
job_type: job.job_type || 'Full Time',
|
job_type: job.job_type || 'Full Time',
|
||||||
start_date: job.start_date || '',
|
start_date: job.start_date || new Date().toISOString().split('T')[0],
|
||||||
description: job.description || '',
|
description: job.description || '',
|
||||||
requirements: job.requirements || '',
|
|
||||||
status: job.status || 'active'
|
status: job.status || 'active'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -40,6 +46,12 @@ export default function Edit({ job }) {
|
|||||||
if (!data.title.trim()) {
|
if (!data.title.trim()) {
|
||||||
newErrors.title = 'Job title is required.';
|
newErrors.title = 'Job title is required.';
|
||||||
}
|
}
|
||||||
|
if (!data.main_profession) {
|
||||||
|
newErrors.main_profession = 'Main profession is required.';
|
||||||
|
}
|
||||||
|
if (!data.skills || data.skills.length === 0) {
|
||||||
|
newErrors.skills = 'At least one skill is required.';
|
||||||
|
}
|
||||||
if (!data.workers_needed || data.workers_needed < 1) {
|
if (!data.workers_needed || data.workers_needed < 1) {
|
||||||
newErrors.workers_needed = 'Workers needed must be at least 1.';
|
newErrors.workers_needed = 'Workers needed must be at least 1.';
|
||||||
}
|
}
|
||||||
@ -130,7 +142,29 @@ export default function Edit({ job }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 col-span-2 md:col-span-1">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Main Profession</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Briefcase className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<select
|
||||||
|
className={`w-full pl-11 pr-10 py-3 bg-slate-50 border rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all appearance-none cursor-pointer ${
|
||||||
|
(clientErrors.main_profession || errors.main_profession) ? 'border-red-500 bg-red-50/20' : 'border-slate-100'
|
||||||
|
}`}
|
||||||
|
value={data.main_profession}
|
||||||
|
onChange={e => setData('main_profession', e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Select main profession...</option>
|
||||||
|
{categoryList.map(prof => (
|
||||||
|
<option key={prof} value={prof}>{prof}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{(clientErrors.main_profession || errors.main_profession) && (
|
||||||
|
<p className="text-xs font-bold text-red-500 mt-1 ml-1">{clientErrors.main_profession || errors.main_profession}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 col-span-2 md:col-span-1">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Workers Needed</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Workers Needed</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Users className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
<Users className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
@ -149,10 +183,48 @@ export default function Edit({ job }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 col-span-2 md:col-span-1">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Location</label>
|
||||||
|
<div className="relative">
|
||||||
|
<MapPin className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. Dubai Marina"
|
||||||
|
className={`w-full pl-11 pr-4 py-3 bg-slate-50 border rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all ${
|
||||||
|
(clientErrors.location || errors.location) ? 'border-red-500 bg-red-50/20' : 'border-slate-100'
|
||||||
|
}`}
|
||||||
|
value={data.location}
|
||||||
|
onChange={e => setData('location', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{(clientErrors.location || errors.location) && (
|
||||||
|
<p className="text-xs font-bold text-red-500 mt-1 ml-1">{clientErrors.location || errors.location}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 col-span-2 md:col-span-1">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Salary (AED / mo)</label>
|
||||||
|
<div className="relative">
|
||||||
|
<DollarSign className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="e.g. 2500"
|
||||||
|
className={`w-full pl-11 pr-4 py-3 bg-slate-50 border rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all ${
|
||||||
|
(clientErrors.salary || errors.salary) ? 'border-red-500 bg-red-50/20' : 'border-slate-100'
|
||||||
|
}`}
|
||||||
|
value={data.salary}
|
||||||
|
onChange={e => setData('salary', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{(clientErrors.salary || errors.salary) && (
|
||||||
|
<p className="text-xs font-bold text-red-500 mt-1 ml-1">{clientErrors.salary || errors.salary}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 col-span-2 md:col-span-1">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Type</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Type</label>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{['Full Time', 'Part Time', 'Contract'].map(type => (
|
{['Full Time', 'Part Time'].map(type => (
|
||||||
<button
|
<button
|
||||||
key={type}
|
key={type}
|
||||||
type="button"
|
type="button"
|
||||||
@ -169,7 +241,7 @@ export default function Edit({ job }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 col-span-2">
|
<div className="space-y-2 col-span-2 md:col-span-1">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Status</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Status</label>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
{[
|
{[
|
||||||
@ -195,73 +267,7 @@ export default function Edit({ job }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Section 2: Logistics & Pay */}
|
{/* Section 2: Detailed Requirements */}
|
||||||
<div className="space-y-6">
|
|
||||||
<h3 className="text-xs font-black text-[#185FA5] uppercase tracking-[0.2em] flex items-center">
|
|
||||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full mr-2" />
|
|
||||||
Logistics & Compensation
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Location</label>
|
|
||||||
<div className="relative">
|
|
||||||
<MapPin className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="e.g. Dubai Marina"
|
|
||||||
className={`w-full pl-11 pr-4 py-3 bg-slate-50 border rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all ${
|
|
||||||
(clientErrors.location || errors.location) ? 'border-red-500 bg-red-50/20' : 'border-slate-100'
|
|
||||||
}`}
|
|
||||||
value={data.location}
|
|
||||||
onChange={e => setData('location', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{(clientErrors.location || errors.location) && (
|
|
||||||
<p className="text-xs font-bold text-red-500 mt-1 ml-1">{clientErrors.location || errors.location}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Salary (AED / mo)</label>
|
|
||||||
<div className="relative">
|
|
||||||
<DollarSign className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
placeholder="e.g. 2500"
|
|
||||||
className={`w-full pl-11 pr-4 py-3 bg-slate-50 border rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all ${
|
|
||||||
(clientErrors.salary || errors.salary) ? 'border-red-500 bg-red-50/20' : 'border-slate-100'
|
|
||||||
}`}
|
|
||||||
value={data.salary}
|
|
||||||
onChange={e => setData('salary', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{(clientErrors.salary || errors.salary) && (
|
|
||||||
<p className="text-xs font-bold text-red-500 mt-1 ml-1">{clientErrors.salary || errors.salary}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Start Date</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Calendar className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
className={`w-full pl-11 pr-4 py-3 bg-slate-50 border rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all ${
|
|
||||||
(clientErrors.start_date || errors.start_date) ? 'border-red-500 bg-red-50/20' : 'border-slate-100'
|
|
||||||
}`}
|
|
||||||
value={data.start_date}
|
|
||||||
onChange={e => setData('start_date', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{(clientErrors.start_date || errors.start_date) && (
|
|
||||||
<p className="text-xs font-bold text-red-500 mt-1 ml-1">{clientErrors.start_date || errors.start_date}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Section 3: Details */}
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h3 className="text-xs font-black text-[#185FA5] uppercase tracking-[0.2em] flex items-center">
|
<h3 className="text-xs font-black text-[#185FA5] uppercase tracking-[0.2em] flex items-center">
|
||||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full mr-2" />
|
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full mr-2" />
|
||||||
@ -269,6 +275,56 @@ export default function Edit({ job }) {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Skills (Select from list)</label>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Briefcase className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<select
|
||||||
|
className={`w-full pl-11 pr-10 py-3 bg-slate-50 border rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all appearance-none cursor-pointer ${
|
||||||
|
(clientErrors.skills || errors.skills) ? 'border-red-500 bg-red-50/20' : 'border-slate-100'
|
||||||
|
}`}
|
||||||
|
value=""
|
||||||
|
onChange={e => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val && !data.skills.includes(val)) {
|
||||||
|
setData('skills', [...data.skills, val]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">Choose skills to add...</option>
|
||||||
|
{skillList.filter(s => !data.skills.includes(s.name)).map(skill => (
|
||||||
|
<option key={skill.id} value={skill.name}>{skill.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selected skills pills */}
|
||||||
|
{data.skills.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 p-3 bg-slate-50 rounded-2xl border border-slate-100">
|
||||||
|
{data.skills.map(skill => (
|
||||||
|
<span
|
||||||
|
key={skill}
|
||||||
|
className="inline-flex items-center space-x-1.5 px-3 py-1.5 bg-[#185FA5] text-white rounded-xl text-xs font-black uppercase tracking-wider shadow-xs"
|
||||||
|
>
|
||||||
|
<span>{skill}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setData('skills', data.skills.filter(s => s !== skill))}
|
||||||
|
className="hover:bg-blue-700/50 p-0.5 rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-3.5 h-3.5 text-white" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{(clientErrors.skills || errors.skills) && (
|
||||||
|
<p className="text-xs font-bold text-red-500 mt-1 ml-1">{clientErrors.skills || errors.skills}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Description</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
@ -289,19 +345,6 @@ export default function Edit({ job }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Special Requirements (Optional)</label>
|
|
||||||
<div className="relative">
|
|
||||||
<FileText className="absolute left-4 top-5 w-4 h-4 text-slate-400" />
|
|
||||||
<textarea
|
|
||||||
placeholder="e.g. Must speak Arabic, 5+ years experience, transferable visa..."
|
|
||||||
className="w-full pl-11 pr-4 py-4 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all min-h-[100px] resize-none"
|
|
||||||
value={data.requirements}
|
|
||||||
onChange={e => setData('requirements', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,8 @@ import {
|
|||||||
UploadCloud,
|
UploadCloud,
|
||||||
FileText,
|
FileText,
|
||||||
Mic,
|
Mic,
|
||||||
Square
|
Square,
|
||||||
|
Clock
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@ -192,7 +193,9 @@ export default function Show({ conversation, initialMessages, conversations = []
|
|||||||
router.post(`/employer/workers/${conversation.worker_id}/mark-hired`, {}, {
|
router.post(`/employer/workers/${conversation.worker_id}/mark-hired`, {}, {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t('marked_hired', 'Worker successfully marked as Hired!'));
|
toast.success(t('hire_request_sent', 'Hiring request sent successfully!'), {
|
||||||
|
description: t('hire_request_sent_desc', 'Awaiting worker confirmation of the hiring offer.')
|
||||||
|
});
|
||||||
router.reload();
|
router.reload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -312,7 +315,12 @@ 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">
|
||||||
{conversation.worker_status !== 'hired' && conversation.worker_status !== 'hidden' ? (
|
{conversation.worker_status === 'pending_confirmation' ? (
|
||||||
|
<span className="px-3.5 py-2 bg-amber-50 text-amber-700 rounded-xl text-xs font-black uppercase tracking-wider border border-amber-200 flex items-center space-x-1.5">
|
||||||
|
<Clock className="w-4 h-4 text-amber-600 animate-pulse" />
|
||||||
|
<span>{t('pending_confirmation', 'Pending Confirmation')}</span>
|
||||||
|
</span>
|
||||||
|
) : conversation.worker_status !== 'hired' && conversation.worker_status !== 'hidden' ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowHireConfirmModal(true)}
|
onClick={() => setShowHireConfirmModal(true)}
|
||||||
|
|||||||
@ -25,7 +25,8 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
User,
|
User,
|
||||||
Phone,
|
Phone,
|
||||||
MapPin
|
MapPin,
|
||||||
|
Clock
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@ -161,10 +162,10 @@ 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: () => {
|
||||||
setAvailabilityStatus('Hired');
|
setAvailabilityStatus('Pending Confirmation');
|
||||||
setShowHiredModal(true);
|
setShowHiredModal(true);
|
||||||
toast.success(t('marked_hired', 'Worker successfully marked as Hired!'), {
|
toast.success(t('hire_request_sent', 'Hiring request sent successfully!'), {
|
||||||
description: t('marked_hired_desc', 'Sponsorship direct match finalized. Please leave an anonymous trust review!')
|
description: t('hire_request_sent_desc', 'Awaiting worker confirmation of the hiring offer.')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -290,7 +291,12 @@ export default function Show({ worker }) {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Stage 4 Hire: Mark Hired (Finalize Hiring Direct Flow) */}
|
{/* Stage 4 Hire: Mark Hired (Finalize Hiring Direct Flow) */}
|
||||||
{availabilityStatus !== 'Hired' && (
|
{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">
|
||||||
|
<Clock className="w-4 h-4 text-amber-600 animate-pulse" />
|
||||||
|
<span>{t('pending_confirmation', 'Pending Confirmation')}</span>
|
||||||
|
</div>
|
||||||
|
) : availabilityStatus !== 'Hired' && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowHireConfirmModal(true)}
|
onClick={() => setShowHireConfirmModal(true)}
|
||||||
@ -885,6 +891,28 @@ export default function Show({ worker }) {
|
|||||||
<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">
|
||||||
<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">
|
<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">
|
||||||
|
|
||||||
|
{availabilityStatus === 'Pending Confirmation' ? (
|
||||||
|
<>
|
||||||
|
<div className="text-center space-y-3">
|
||||||
|
<div className="w-16 h-16 bg-amber-50 text-amber-600 rounded-full flex items-center justify-center mx-auto shadow-inner">
|
||||||
|
<Clock className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-extrabold text-lg text-slate-900">{t('hire_request_sent_title', 'Hiring Request Sent')}</h4>
|
||||||
|
<p className="text-xs text-slate-500 leading-relaxed">
|
||||||
|
{t('hired_under_sponsorship_pending_desc', 'Your hiring request has been sent to {name}. The worker must accept the offer to finalize the placement.').replace('{name}', worker.name)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-2 text-xs font-bold">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowHiredModal(false)}
|
||||||
|
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white py-3 rounded-xl flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span>{t('ok', 'OK')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className="text-center space-y-3">
|
<div className="text-center space-y-3">
|
||||||
<div className="w-16 h-16 bg-emerald-50 text-emerald-600 rounded-full flex items-center justify-center mx-auto shadow-inner">
|
<div className="w-16 h-16 bg-emerald-50 text-emerald-600 rounded-full flex items-center justify-center mx-auto shadow-inner">
|
||||||
<CheckCircle2 className="w-8 h-8" />
|
<CheckCircle2 className="w-8 h-8" />
|
||||||
@ -920,6 +948,8 @@ export default function Show({ worker }) {
|
|||||||
{t('back_to_worker_directory', 'Back to Worker Directory')}
|
{t('back_to_worker_directory', 'Back to Worker Directory')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -115,6 +115,8 @@
|
|||||||
Route::get('/workers/applied-jobs', [\App\Http\Controllers\Api\WorkerJobController::class, 'appliedJobs']);
|
Route::get('/workers/applied-jobs', [\App\Http\Controllers\Api\WorkerJobController::class, 'appliedJobs']);
|
||||||
Route::get('/workers/applied-jobs/{id}', [\App\Http\Controllers\Api\WorkerJobController::class, 'appliedJobDetail']);
|
Route::get('/workers/applied-jobs/{id}', [\App\Http\Controllers\Api\WorkerJobController::class, 'appliedJobDetail']);
|
||||||
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}/decline-hire', [\App\Http\Controllers\Api\WorkerJobController::class, 'declineHire']);
|
||||||
|
|
||||||
// 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']);
|
||||||
|
|||||||
@ -107,11 +107,19 @@ public function test_employer_can_view_jobs_list()
|
|||||||
*/
|
*/
|
||||||
public function test_employer_can_view_create_job_form()
|
public function test_employer_can_view_create_job_form()
|
||||||
{
|
{
|
||||||
|
// Seed a custom skill in the database
|
||||||
|
\App\Models\Skill::create(['name' => 'painting']);
|
||||||
|
|
||||||
$response = $this->actingAs($this->employer)
|
$response = $this->actingAs($this->employer)
|
||||||
->withSession(['user' => $this->employer])
|
->withSession(['user' => $this->employer])
|
||||||
->get('/employer/jobs/create');
|
->get('/employer/jobs/create');
|
||||||
|
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
$inertiaData = $response->original->getData();
|
||||||
|
$professions = $inertiaData['page']['props']['professions'] ?? [];
|
||||||
|
|
||||||
|
$this->assertContains('Painting', $professions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -123,6 +131,8 @@ public function test_employer_can_post_job()
|
|||||||
->withSession(['user' => $this->employer])
|
->withSession(['user' => $this->employer])
|
||||||
->post('/employer/jobs/create', [
|
->post('/employer/jobs/create', [
|
||||||
'title' => 'Villa Cleaner',
|
'title' => 'Villa Cleaner',
|
||||||
|
'main_profession' => 'Cleaner',
|
||||||
|
'skills' => 'Housekeeping',
|
||||||
'workers_needed' => 2,
|
'workers_needed' => 2,
|
||||||
'job_type' => 'Part Time',
|
'job_type' => 'Part Time',
|
||||||
'location' => 'Al Barsha',
|
'location' => 'Al Barsha',
|
||||||
@ -138,6 +148,7 @@ public function test_employer_can_post_job()
|
|||||||
$this->assertDatabaseHas('job_posts', [
|
$this->assertDatabaseHas('job_posts', [
|
||||||
'employer_id' => $this->employer->id,
|
'employer_id' => $this->employer->id,
|
||||||
'title' => 'Villa Cleaner',
|
'title' => 'Villa Cleaner',
|
||||||
|
'main_profession' => 'Cleaner',
|
||||||
'workers_needed' => 2,
|
'workers_needed' => 2,
|
||||||
'job_type' => 'Part Time',
|
'job_type' => 'Part Time',
|
||||||
'location' => 'Al Barsha',
|
'location' => 'Al Barsha',
|
||||||
@ -158,7 +169,7 @@ public function test_job_posting_requires_fields()
|
|||||||
'salary' => -100,
|
'salary' => -100,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response->assertSessionHasErrors(['title', 'workers_needed', 'location', 'salary', 'job_type', 'start_date', 'description']);
|
$response->assertSessionHasErrors(['title', 'main_profession', 'skills', 'workers_needed', 'location', 'salary', 'job_type', 'start_date', 'description']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -292,4 +303,63 @@ public function test_employer_can_view_shortlisted_applicants()
|
|||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
$response->assertSee('Shortlisted John');
|
$response->assertSee('Shortlisted John');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test employer cannot edit a job posting that has active applications.
|
||||||
|
*/
|
||||||
|
public function test_employer_cannot_edit_job_with_applications()
|
||||||
|
{
|
||||||
|
$job = JobPost::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'title' => 'Job to Edit',
|
||||||
|
'main_profession' => 'Cleaner',
|
||||||
|
'workers_needed' => 1,
|
||||||
|
'job_type' => 'Full Time',
|
||||||
|
'location' => 'Dubai',
|
||||||
|
'salary' => 2000,
|
||||||
|
'start_date' => '2026-07-20',
|
||||||
|
'description' => 'Will have applications.',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$worker = Worker::create([
|
||||||
|
'name' => 'Applicant Worker',
|
||||||
|
'email' => 'worker2@example.com',
|
||||||
|
'phone' => '+971500000003',
|
||||||
|
'nationality' => 'Indian',
|
||||||
|
'age' => 28,
|
||||||
|
'salary' => 2200,
|
||||||
|
'availability' => 'Available',
|
||||||
|
'experience' => '3 Years',
|
||||||
|
'religion' => 'Christian',
|
||||||
|
'bio' => 'Ready to clean.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
JobApplication::create([
|
||||||
|
'job_id' => $job->id,
|
||||||
|
'worker_id' => $worker->id,
|
||||||
|
'status' => 'applied',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->employer)
|
||||||
|
->withSession(['user' => $this->employer])
|
||||||
|
->post("/employer/jobs/{$job->id}/edit", [
|
||||||
|
'title' => 'Trying to Edit Title',
|
||||||
|
'main_profession' => 'Cleaner',
|
||||||
|
'skills' => 'Housekeeping',
|
||||||
|
'workers_needed' => 1,
|
||||||
|
'location' => 'Dubai',
|
||||||
|
'salary' => 2500,
|
||||||
|
'job_type' => 'Full Time',
|
||||||
|
'start_date' => '2026-07-20',
|
||||||
|
'description' => 'Will have applications.',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSessionHasErrors(['general']);
|
||||||
|
$this->assertDatabaseHas('job_posts', [
|
||||||
|
'id' => $job->id,
|
||||||
|
'title' => 'Job to Edit', // should NOT change
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -499,6 +499,12 @@ public function test_worker_hired_via_job_application_appears_in_employers_list(
|
|||||||
|
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
// Worker confirms the hire
|
||||||
|
$response = $this->postJson("/api/workers/applied-jobs/{$application->id}/confirm-hire", [], [
|
||||||
|
'Authorization' => 'Bearer worker-hired-token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
|
||||||
// Verify that the JobOffer with accepted status has been created
|
// Verify that the JobOffer with accepted status has been created
|
||||||
$this->assertDatabaseHas('job_offers', [
|
$this->assertDatabaseHas('job_offers', [
|
||||||
'employer_id' => $employer->id,
|
'employer_id' => $employer->id,
|
||||||
|
|||||||
@ -62,7 +62,11 @@ protected function setUp(): void
|
|||||||
'religion' => 'Christian',
|
'religion' => 'Christian',
|
||||||
'bio' => 'Helper info',
|
'bio' => 'Helper info',
|
||||||
'passport_status' => 'valid',
|
'passport_status' => 'valid',
|
||||||
|
'main_profession' => 'Plumber',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Piping']);
|
||||||
|
$this->worker->skills()->sync([$skill->id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_employer_job_crud_web()
|
public function test_employer_job_crud_web()
|
||||||
@ -72,6 +76,8 @@ public function test_employer_job_crud_web()
|
|||||||
->withSession(['user' => $this->employer])
|
->withSession(['user' => $this->employer])
|
||||||
->post('/employer/jobs/create', [
|
->post('/employer/jobs/create', [
|
||||||
'title' => 'Test Mason Job',
|
'title' => 'Test Mason Job',
|
||||||
|
'main_profession' => 'Plumber',
|
||||||
|
'skills' => 'Piping',
|
||||||
'workers_needed' => 3,
|
'workers_needed' => 3,
|
||||||
'location' => 'Dubai',
|
'location' => 'Dubai',
|
||||||
'salary' => 2000,
|
'salary' => 2000,
|
||||||
@ -103,6 +109,8 @@ public function test_employer_job_crud_web()
|
|||||||
->withSession(['user' => $this->employer])
|
->withSession(['user' => $this->employer])
|
||||||
->post("/employer/jobs/{$job->id}/edit", [
|
->post("/employer/jobs/{$job->id}/edit", [
|
||||||
'title' => 'Updated Mason Job',
|
'title' => 'Updated Mason Job',
|
||||||
|
'main_profession' => 'Plumber',
|
||||||
|
'skills' => 'Piping',
|
||||||
'workers_needed' => 2,
|
'workers_needed' => 2,
|
||||||
'location' => 'Abu Dhabi',
|
'location' => 'Abu Dhabi',
|
||||||
'salary' => 2500,
|
'salary' => 2500,
|
||||||
@ -137,6 +145,7 @@ public function test_worker_job_apis()
|
|||||||
$job = JobPost::create([
|
$job = JobPost::create([
|
||||||
'employer_id' => $this->employer->id,
|
'employer_id' => $this->employer->id,
|
||||||
'title' => 'Available Plumber Job',
|
'title' => 'Available Plumber Job',
|
||||||
|
'main_profession' => 'Plumber',
|
||||||
'location' => 'Dubai Marina',
|
'location' => 'Dubai Marina',
|
||||||
'salary' => 3000,
|
'salary' => 3000,
|
||||||
'workers_needed' => 1,
|
'workers_needed' => 1,
|
||||||
@ -145,6 +154,8 @@ public function test_worker_job_apis()
|
|||||||
'description' => 'Plumber description',
|
'description' => 'Plumber description',
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
]);
|
]);
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Piping']);
|
||||||
|
$job->skills()->sync([$skill->id]);
|
||||||
|
|
||||||
// 1. List available jobs
|
// 1. List available jobs
|
||||||
$response = $this->getJson('/api/workers/jobs', [
|
$response = $this->getJson('/api/workers/jobs', [
|
||||||
@ -233,6 +244,8 @@ public function test_employer_job_apis_and_access_restrictions()
|
|||||||
// 2. Create job via API
|
// 2. Create job via API
|
||||||
$response = $this->postJson('/api/employers/jobs', [
|
$response = $this->postJson('/api/employers/jobs', [
|
||||||
'title' => 'API Clean Job',
|
'title' => 'API Clean Job',
|
||||||
|
'main_profession' => 'Plumber',
|
||||||
|
'skills' => 'Piping',
|
||||||
'workers_needed' => 3,
|
'workers_needed' => 3,
|
||||||
'location' => 'Dubai JLT',
|
'location' => 'Dubai JLT',
|
||||||
'salary' => 2200,
|
'salary' => 2200,
|
||||||
@ -263,6 +276,8 @@ public function test_employer_job_apis_and_access_restrictions()
|
|||||||
// 4. Update job via API
|
// 4. Update job via API
|
||||||
$response = $this->postJson("/api/employers/jobs/{$jobId}/update", [
|
$response = $this->postJson("/api/employers/jobs/{$jobId}/update", [
|
||||||
'title' => 'Updated API Clean Job',
|
'title' => 'Updated API Clean Job',
|
||||||
|
'main_profession' => 'Plumber',
|
||||||
|
'skills' => 'Piping',
|
||||||
'workers_needed' => 4,
|
'workers_needed' => 4,
|
||||||
'location' => 'Dubai Marina',
|
'location' => 'Dubai Marina',
|
||||||
'salary' => 2500,
|
'salary' => 2500,
|
||||||
@ -523,6 +538,7 @@ public function test_hired_worker_visibility_and_profile_access()
|
|||||||
$job = JobPost::create([
|
$job = JobPost::create([
|
||||||
'employer_id' => $this->employer->id,
|
'employer_id' => $this->employer->id,
|
||||||
'title' => 'Special Hired Job',
|
'title' => 'Special Hired Job',
|
||||||
|
'main_profession' => 'Plumber',
|
||||||
'location' => 'Dubai Jumeirah',
|
'location' => 'Dubai Jumeirah',
|
||||||
'salary' => 4500,
|
'salary' => 4500,
|
||||||
'workers_needed' => 1,
|
'workers_needed' => 1,
|
||||||
@ -531,13 +547,15 @@ public function test_hired_worker_visibility_and_profile_access()
|
|||||||
'description' => 'A special job details description',
|
'description' => 'A special job details description',
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
]);
|
]);
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Piping']);
|
||||||
|
$job->skills()->sync([$skill->id]);
|
||||||
|
|
||||||
// 2. Worker applies for the job
|
// 2. Worker applies for the job
|
||||||
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
|
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
|
||||||
'Authorization' => 'Bearer worker_api_token'
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
]);
|
]);
|
||||||
$response->assertStatus(201);
|
$response->assertStatus(201);
|
||||||
$app = JobApplication::first();
|
$app = JobApplication::latest()->first();
|
||||||
|
|
||||||
// 3. Employer hires the worker by setting status to 'hired' (lowercase, as in the database)
|
// 3. Employer hires the worker by setting status to 'hired' (lowercase, as in the database)
|
||||||
$response = $this->postJson("/api/employers/applications/{$app->id}/status", [
|
$response = $this->postJson("/api/employers/applications/{$app->id}/status", [
|
||||||
@ -548,6 +566,18 @@ public function test_hired_worker_visibility_and_profile_access()
|
|||||||
]);
|
]);
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
// Under two-way confirmation, it should be in hire_requested status
|
||||||
|
$this->assertDatabaseHas('job_applications', [
|
||||||
|
'id' => $app->id,
|
||||||
|
'status' => 'hire_requested',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Worker confirms the hiring
|
||||||
|
$response = $this->postJson("/api/workers/applied-jobs/{$app->id}/confirm-hire", [], [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
|
||||||
// Verify status updates
|
// Verify status updates
|
||||||
$this->assertDatabaseHas('job_applications', [
|
$this->assertDatabaseHas('job_applications', [
|
||||||
'id' => $app->id,
|
'id' => $app->id,
|
||||||
@ -604,4 +634,100 @@ public function test_hired_worker_visibility_and_profile_access()
|
|||||||
$response->assertJsonPath('data.current_employer.id', $this->employer->id);
|
$response->assertJsonPath('data.current_employer.id', $this->employer->id);
|
||||||
$response->assertJsonPath('data.currently_working_sponsor.id', $this->employer->id);
|
$response->assertJsonPath('data.currently_working_sponsor.id', $this->employer->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_worker_can_decline_hire()
|
||||||
|
{
|
||||||
|
// 1. Create a job posting matching the worker's profession/skills
|
||||||
|
$job = JobPost::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'title' => 'Housekeeper/Nanny',
|
||||||
|
'main_profession' => 'Housekeeper',
|
||||||
|
'location' => 'Dubai Marina',
|
||||||
|
'salary' => 2500,
|
||||||
|
'workers_needed' => 1,
|
||||||
|
'job_type' => 'Full Time',
|
||||||
|
'start_date' => now()->addDays(5)->format('Y-m-d'),
|
||||||
|
'description' => 'Need clean house and nanny care.',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Piping']);
|
||||||
|
$job->skills()->sync([$skill->id]);
|
||||||
|
|
||||||
|
// 2. Worker applies for the job
|
||||||
|
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(201);
|
||||||
|
$app = JobApplication::latest()->first();
|
||||||
|
|
||||||
|
// 3. Employer hires the worker by setting status to 'hired'
|
||||||
|
$response = $this->postJson("/api/employers/applications/{$app->id}/status", [
|
||||||
|
'status' => 'hired',
|
||||||
|
'notes' => 'Declining test offer'
|
||||||
|
], [
|
||||||
|
'Authorization' => 'Bearer employer_api_token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
// Under two-way confirmation, it should be in hire_requested status
|
||||||
|
$this->assertDatabaseHas('job_applications', [
|
||||||
|
'id' => $app->id,
|
||||||
|
'status' => 'hire_requested',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Worker declines the hiring
|
||||||
|
$response = $this->postJson("/api/workers/applied-jobs/{$app->id}/decline-hire", [], [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
|
||||||
|
// Verify status updates
|
||||||
|
$this->assertDatabaseHas('job_applications', [
|
||||||
|
'id' => $app->id,
|
||||||
|
'status' => 'declined',
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('workers', [
|
||||||
|
'id' => $this->worker->id,
|
||||||
|
'status' => 'active', // Should still be active
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_worker_can_view_job_matching_only_skills()
|
||||||
|
{
|
||||||
|
// 1. Create a job where main_profession is different from worker's (Plumber)
|
||||||
|
// Let's set it to 'Electrician'
|
||||||
|
$job = JobPost::create([
|
||||||
|
'employer_id' => $this->employer->id,
|
||||||
|
'title' => 'Electrician Needed',
|
||||||
|
'main_profession' => 'Electrician',
|
||||||
|
'location' => 'Dubai Marina',
|
||||||
|
'salary' => 3000,
|
||||||
|
'workers_needed' => 1,
|
||||||
|
'job_type' => 'Contract',
|
||||||
|
'start_date' => now()->addDays(2),
|
||||||
|
'description' => 'Electrician description',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sync the same skill 'Piping' (which the worker has)
|
||||||
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Piping']);
|
||||||
|
$job->skills()->sync([$skill->id]);
|
||||||
|
|
||||||
|
// 2. Request list jobs
|
||||||
|
$response = $this->getJson('/api/workers/jobs', [
|
||||||
|
'Authorization' => 'Bearer worker_api_token'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
// The worker's main profession is 'Plumber', but the job's main profession is 'Electrician'.
|
||||||
|
// However, the worker has the 'Piping' skill, which matches the job.
|
||||||
|
// Therefore, the job should show up.
|
||||||
|
$response->assertJsonFragment([
|
||||||
|
'title' => 'Electrician Needed'
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user