1305 lines
48 KiB
PHP
1305 lines
48 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\Request;
|
|
use App\Models\JobPost;
|
|
use App\Models\JobApplication;
|
|
use App\Models\Worker;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Validator;
|
|
|
|
class WorkerJobController extends Controller
|
|
{
|
|
/**
|
|
* API to list available jobs.
|
|
* GET /api/workers/jobs
|
|
*/
|
|
public function listJobs(Request $request)
|
|
{
|
|
/** @var Worker $worker */
|
|
$worker = $request->attributes->get('worker');
|
|
|
|
$query = JobPost::whereIn('status', ['active', 'closed'])
|
|
->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);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
$workerApplications = [];
|
|
if ($worker) {
|
|
$workerApplications = JobApplication::where('worker_id', $worker->id)
|
|
->pluck('status', 'job_id')
|
|
->toArray();
|
|
}
|
|
|
|
// Apply filters
|
|
// Filter by job_type / jobType
|
|
if ($request->has('job_type') && $request->input('job_type') !== '') {
|
|
$query->where('job_type', $request->input('job_type'));
|
|
}
|
|
if ($request->has('jobType') && $request->input('jobType') !== '') {
|
|
$query->where('job_type', $request->input('jobType'));
|
|
}
|
|
|
|
// Filter by skills
|
|
if ($request->has('skills') && $request->input('skills') !== '') {
|
|
$skills = $request->input('skills');
|
|
if (is_string($skills)) {
|
|
$skills = array_filter(array_map('trim', explode(',', $skills)));
|
|
}
|
|
if (!empty($skills)) {
|
|
$query->whereHas('skills', function ($q) use ($skills) {
|
|
$q->where(function($q2) use ($skills) {
|
|
$q2->whereIn('skills.id', $skills)
|
|
->orWhereIn('skills.name', $skills);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// Filter by salary
|
|
if ($request->has('salary_min') && $request->input('salary_min') !== '') {
|
|
$query->where('salary', '>=', (float) $request->input('salary_min'));
|
|
}
|
|
if ($request->has('salary_max') && $request->input('salary_max') !== '') {
|
|
$query->where('salary', '<=', (float) $request->input('salary_max'));
|
|
}
|
|
if ($request->has('salary') && $request->input('salary') !== '') {
|
|
$query->where('salary', '>=', (float) $request->input('salary'));
|
|
}
|
|
|
|
if ($request->has('search') && $request->input('search') !== '') {
|
|
$search = $request->input('search');
|
|
$query->where(function($q) use ($search) {
|
|
$q->where('title', 'like', "%{$search}%")
|
|
->orWhere('description', 'like', "%{$search}%")
|
|
->orWhere('location', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// Apply Sorting
|
|
if ($request->has('sortBy') && $request->input('sortBy') !== '') {
|
|
$sortBy = strtoupper($request->input('sortBy'));
|
|
if ($sortBy === 'LATEST FIRST') {
|
|
$query->latest();
|
|
} elseif ($sortBy === 'OLDEST FIRST') {
|
|
$query->oldest();
|
|
} elseif ($sortBy === 'PRICE: LOW TO HIGH') {
|
|
$query->orderBy('salary', 'asc');
|
|
} elseif ($sortBy === 'PRICE: HIGH TO LOW') {
|
|
$query->orderBy('salary', 'desc');
|
|
} else {
|
|
$query->latest();
|
|
}
|
|
} else {
|
|
$query->latest();
|
|
}
|
|
|
|
$jobs = $query->get()
|
|
->map(function ($job) use ($worker, $workerApplications) {
|
|
$status = 'new';
|
|
if ($worker && isset($workerApplications[$job->id])) {
|
|
$status = $workerApplications[$job->id];
|
|
} elseif ($job->status === 'closed') {
|
|
$status = 'closed';
|
|
}
|
|
|
|
return [
|
|
'id' => $job->id,
|
|
'title' => $job->title,
|
|
'main_profession' => $job->main_profession,
|
|
'skills' => $job->skills->pluck('name')->toArray(),
|
|
'location' => $job->location,
|
|
'salary' => (int) $job->salary,
|
|
'workers_needed' => $job->workers_needed,
|
|
'job_type' => $job->job_type,
|
|
'start_date' => $job->start_date ? $job->start_date->format('Y-m-d') : null,
|
|
'description' => $job->description,
|
|
'requirements' => $job->requirements,
|
|
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
|
|
'posted_by' => $job->employer->name ?? 'Private Sponsor',
|
|
'posted_at' => $job->created_at->toIso8601String(),
|
|
'status' => $status,
|
|
];
|
|
});
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'jobs' => $jobs
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* API to retrieve job details.
|
|
* GET /api/workers/jobs/{id}
|
|
*/
|
|
public function jobDetails($id)
|
|
{
|
|
$job = JobPost::whereIn('status', ['active', 'closed'])
|
|
->with(['employer.employerProfile', 'skills'])
|
|
->find($id);
|
|
|
|
if (!$job) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Job not found.'
|
|
], 404);
|
|
}
|
|
|
|
$worker = request()->attributes->get('worker');
|
|
$status = 'new';
|
|
if ($worker) {
|
|
$application = JobApplication::where('worker_id', $worker->id)
|
|
->where('job_id', $job->id)
|
|
->first();
|
|
if ($application) {
|
|
$status = $application->status;
|
|
} elseif ($job->status === 'closed') {
|
|
$status = 'closed';
|
|
}
|
|
} elseif ($job->status === 'closed') {
|
|
$status = 'closed';
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'job' => [
|
|
'id' => $job->id,
|
|
'title' => $job->title,
|
|
'main_profession' => $job->main_profession,
|
|
'skills' => $job->skills->pluck('name')->toArray(),
|
|
'location' => $job->location,
|
|
'salary' => (int) $job->salary,
|
|
'workers_needed' => $job->workers_needed,
|
|
'job_type' => $job->job_type,
|
|
'start_date' => $job->start_date ? $job->start_date->format('Y-m-d') : null,
|
|
'description' => $job->description,
|
|
'requirements' => $job->requirements,
|
|
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
|
|
'posted_by' => $job->employer->name ?? 'Private Sponsor',
|
|
'posted_at' => $job->created_at->toIso8601String(),
|
|
'status' => $status,
|
|
]
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* API for workers to apply for a job.
|
|
* POST /api/workers/jobs/{id}/apply
|
|
*/
|
|
public function applyJob(Request $request, $id)
|
|
{
|
|
/** @var Worker $worker */
|
|
$worker = $request->attributes->get('worker');
|
|
|
|
$job = JobPost::where('status', 'active')->find($id);
|
|
|
|
if (!$job) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Job not found.'
|
|
], 404);
|
|
}
|
|
|
|
// Prevent duplicate applications
|
|
$exists = JobApplication::where('worker_id', $worker->id)
|
|
->where('job_id', $job->id)
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'You have already applied for this job.'
|
|
], 409);
|
|
}
|
|
|
|
$application = JobApplication::create([
|
|
'worker_id' => $worker->id,
|
|
'job_id' => $job->id,
|
|
'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([
|
|
'success' => true,
|
|
'message' => 'Successfully applied for the job.',
|
|
'data' => [
|
|
'application' => $application
|
|
]
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* API for workers to view their applied jobs.
|
|
* GET /api/workers/applied-jobs
|
|
*/
|
|
public function appliedJobs(Request $request)
|
|
{
|
|
/** @var Worker $worker */
|
|
$worker = $request->attributes->get('worker');
|
|
|
|
$applications = JobApplication::where('worker_id', $worker->id)
|
|
->with(['jobPost.employer.employerProfile'])
|
|
->latest()
|
|
->get()
|
|
->map(function ($app) {
|
|
$job = $app->jobPost;
|
|
return [
|
|
'application_id' => $app->id,
|
|
'status' => $app->status,
|
|
'employer_status' => $app->employer_status,
|
|
'applied_at' => $app->created_at->toIso8601String(),
|
|
'job' => $job ? [
|
|
'id' => $job->id,
|
|
'title' => $job->title,
|
|
'location' => $job->location,
|
|
'salary' => (int) $job->salary,
|
|
'job_type' => $job->job_type,
|
|
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
|
|
'posted_by' => $job->employer->name ?? 'Private Sponsor',
|
|
] : null
|
|
];
|
|
});
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'applications' => $applications
|
|
]
|
|
]);
|
|
}
|
|
|
|
private function checkJobAccess($employerId)
|
|
{
|
|
$sub = \Illuminate\Support\Facades\DB::table('subscriptions')
|
|
->where('user_id', $employerId)
|
|
->where('status', 'active')
|
|
->latest('id')
|
|
->first();
|
|
|
|
$planId = $sub ? $sub->plan_id : 'basic';
|
|
return $planId !== 'basic';
|
|
}
|
|
|
|
/**
|
|
* API for employers to list their jobs.
|
|
* GET /api/employers/jobs
|
|
*/
|
|
public function employerListJobs(Request $request)
|
|
{
|
|
/** @var User $employer */
|
|
$employer = $request->attributes->get('employer');
|
|
|
|
if (!$this->checkJobAccess($employer->id)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Your current plan does not support the Job Apply module. Please upgrade to a Premium plan to access this feature.'
|
|
], 403);
|
|
}
|
|
|
|
$jobs = JobPost::where('employer_id', $employer->id)
|
|
->with(['applications'])
|
|
->latest()
|
|
->get()
|
|
->map(function ($job) {
|
|
return [
|
|
'id' => $job->id,
|
|
'title' => $job->title,
|
|
'location' => $job->location,
|
|
'salary' => (int) $job->salary,
|
|
'workers_needed' => $job->workers_needed,
|
|
'job_type' => $job->job_type,
|
|
'start_date' => $job->start_date ? $job->start_date->format('Y-m-d') : null,
|
|
'description' => $job->description,
|
|
'requirements' => $job->requirements,
|
|
'applied_count' => $job->applications->count(),
|
|
'posted_at' => $job->created_at->toIso8601String(),
|
|
'status' => $job->status,
|
|
];
|
|
});
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'jobs' => $jobs
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* API for employers to create a job.
|
|
* POST /api/employers/jobs
|
|
*/
|
|
public function employerCreateJob(Request $request)
|
|
{
|
|
/** @var User $employer */
|
|
$employer = $request->attributes->get('employer');
|
|
|
|
if (!$this->checkJobAccess($employer->id)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Your current plan does not support the Job Apply module. Please upgrade to a Premium plan to access this feature.'
|
|
], 403);
|
|
}
|
|
|
|
$validator = Validator::make($request->all(), [
|
|
'title' => 'required|string|max:255',
|
|
'main_profession' => 'required|string|max:255',
|
|
'skills' => 'required',
|
|
'workers_needed' => 'required|integer|min:1',
|
|
'location' => 'required|string|max:255',
|
|
'salary' => 'required|numeric|min:0',
|
|
'job_type' => 'required|string|in:Full Time,Part Time,Contract',
|
|
'start_date' => 'required|date',
|
|
'description' => 'required|string|max:1000',
|
|
'requirements' => 'nullable|string',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Validation error.',
|
|
'errors' => $validator->errors()
|
|
], 422);
|
|
}
|
|
|
|
$job = JobPost::create([
|
|
'employer_id' => $employer->id,
|
|
'title' => $request->title,
|
|
'main_profession' => $request->main_profession,
|
|
'workers_needed' => $request->workers_needed,
|
|
'job_type' => $request->job_type,
|
|
'location' => $request->location,
|
|
'salary' => $request->salary,
|
|
'start_date' => $request->start_date,
|
|
'description' => $request->description,
|
|
'requirements' => $request->requirements,
|
|
'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([
|
|
'success' => true,
|
|
'message' => 'Job posted successfully.',
|
|
'data' => [
|
|
'job' => [
|
|
'id' => $job->id,
|
|
'title' => $job->title,
|
|
'main_profession' => $job->main_profession,
|
|
'skills' => $job->skills()->pluck('name')->toArray(),
|
|
'location' => $job->location,
|
|
'salary' => (int) $job->salary,
|
|
'workers_needed' => $job->workers_needed,
|
|
'job_type' => $job->job_type,
|
|
'start_date' => $job->start_date ? $job->start_date->format('Y-m-d') : null,
|
|
'description' => $job->description,
|
|
'requirements' => $job->requirements,
|
|
'posted_at' => $job->created_at->toIso8601String(),
|
|
'status' => $job->status,
|
|
]
|
|
]
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* API for employers to retrieve details of a specific job.
|
|
* GET /api/employers/jobs/{id}
|
|
*/
|
|
public function employerJobDetail(Request $request, $id)
|
|
{
|
|
/** @var User $employer */
|
|
$employer = $request->attributes->get('employer');
|
|
|
|
if (!$this->checkJobAccess($employer->id)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Your current plan does not support the Job Apply module. Please upgrade to a Premium plan to access this feature.'
|
|
], 403);
|
|
}
|
|
|
|
$job = JobPost::where('employer_id', $employer->id)
|
|
->with(['applications'])
|
|
->find($id);
|
|
|
|
if (!$job) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Job not found or unauthorized.'
|
|
], 404);
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'job' => [
|
|
'id' => $job->id,
|
|
'title' => $job->title,
|
|
'location' => $job->location,
|
|
'salary' => (int) $job->salary,
|
|
'workers_needed' => $job->workers_needed,
|
|
'job_type' => $job->job_type,
|
|
'start_date' => $job->start_date ? $job->start_date->format('Y-m-d') : null,
|
|
'description' => $job->description,
|
|
'requirements' => $job->requirements,
|
|
'applied_count' => $job->applications->count(),
|
|
'posted_at' => $job->created_at->toIso8601String(),
|
|
'status' => $job->status,
|
|
]
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* API for employers to update a specific job.
|
|
* POST /api/employers/jobs/{id}/update
|
|
*/
|
|
public function employerUpdateJob(Request $request, $id)
|
|
{
|
|
/** @var User $employer */
|
|
$employer = $request->attributes->get('employer');
|
|
|
|
if (!$this->checkJobAccess($employer->id)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Your current plan does not support the Job Apply module. Please upgrade to a Premium plan to access this feature.'
|
|
], 403);
|
|
}
|
|
|
|
$job = JobPost::where('employer_id', $employer->id)->find($id);
|
|
|
|
if (!$job) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Job not found or unauthorized.'
|
|
], 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(), [
|
|
'title' => 'required|string|max:255',
|
|
'main_profession' => 'required|string|max:255',
|
|
'skills' => 'required',
|
|
'workers_needed' => 'required|integer|min:1',
|
|
'location' => 'required|string|max:255',
|
|
'salary' => 'required|numeric|min:0',
|
|
'job_type' => 'required|string|in:Full Time,Part Time,Contract',
|
|
'start_date' => 'required|date',
|
|
'description' => 'required|string|max:1000',
|
|
'requirements' => 'nullable|string',
|
|
'status' => 'required|string|in:active,closed,draft',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Validation error.',
|
|
'errors' => $validator->errors()
|
|
], 422);
|
|
}
|
|
|
|
$job->update([
|
|
'title' => $request->title,
|
|
'main_profession' => $request->main_profession,
|
|
'workers_needed' => $request->workers_needed,
|
|
'job_type' => $request->job_type,
|
|
'location' => $request->location,
|
|
'salary' => $request->salary,
|
|
'start_date' => $request->start_date,
|
|
'description' => $request->description,
|
|
'requirements' => $request->requirements,
|
|
'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([
|
|
'success' => true,
|
|
'message' => 'Job updated successfully.',
|
|
'data' => [
|
|
'job' => [
|
|
'id' => $job->id,
|
|
'title' => $job->title,
|
|
'main_profession' => $job->main_profession,
|
|
'skills' => $job->skills()->pluck('name')->toArray(),
|
|
'location' => $job->location,
|
|
'salary' => (int) $job->salary,
|
|
'workers_needed' => $job->workers_needed,
|
|
'job_type' => $job->job_type,
|
|
'start_date' => $job->start_date ? $job->start_date->format('Y-m-d') : null,
|
|
'description' => $job->description,
|
|
'requirements' => $job->requirements,
|
|
'posted_at' => $job->created_at->toIso8601String(),
|
|
'status' => $job->status,
|
|
]
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* API for employers to delete a job.
|
|
* DELETE /api/employers/jobs/{id}
|
|
*/
|
|
public function employerDeleteJob(Request $request, $id)
|
|
{
|
|
/** @var User $employer */
|
|
$employer = $request->attributes->get('employer');
|
|
|
|
if (!$this->checkJobAccess($employer->id)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Your current plan does not support the Job Apply module. Please upgrade to a Premium plan to access this feature.'
|
|
], 403);
|
|
}
|
|
|
|
$job = JobPost::where('employer_id', $employer->id)->find($id);
|
|
|
|
if (!$job) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Job not found or unauthorized.'
|
|
], 404);
|
|
}
|
|
|
|
$job->delete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Job deleted successfully.'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* API for employers to view applicants for each job.
|
|
* GET /api/employers/jobs/{id}/applicants
|
|
*/
|
|
public function employerJobApplicants(Request $request, $id)
|
|
{
|
|
/** @var User $employer */
|
|
$employer = $request->attributes->get('employer');
|
|
|
|
if (!$this->checkJobAccess($employer->id)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Your current plan does not support the Job Apply module. Please upgrade to a Premium plan to access this feature.'
|
|
], 403);
|
|
}
|
|
|
|
$job = JobPost::where('employer_id', $employer->id)->find($id);
|
|
|
|
if (!$job) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Job not found or unauthorized.'
|
|
], 404);
|
|
}
|
|
|
|
$query = JobApplication::where('job_id', $job->id)
|
|
->with(['worker.skills']);
|
|
|
|
// Search filter: name, nationality
|
|
if ($request->filled('search')) {
|
|
$search = $request->search;
|
|
$query->whereHas('worker', function($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('nationality', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// Status filter
|
|
if ($request->filled('status')) {
|
|
$query->where('status', strtolower($request->status));
|
|
}
|
|
|
|
// Apply filters based on worker attributes
|
|
$query->whereHas('worker', function($q) use ($request) {
|
|
// Gender filter
|
|
if ($request->filled('gender')) {
|
|
$q->where('gender', strtolower($request->gender));
|
|
}
|
|
|
|
// Nationality filter
|
|
if ($request->filled('nationality')) {
|
|
$natInput = $request->nationality;
|
|
$natsArray = is_array($natInput) ? $natInput : array_filter(array_map('trim', explode(',', $natInput)));
|
|
if (!empty($natsArray)) {
|
|
$q->where(function($subQ) use ($natsArray) {
|
|
foreach ($natsArray as $n) {
|
|
$subQ->orWhere('nationality', 'like', "%{$n}%");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Main Profession filter
|
|
if ($request->filled('main_profession')) {
|
|
$q->where('main_profession', strtolower($request->main_profession));
|
|
}
|
|
|
|
// Age filter (integer or range e.g. "18-25")
|
|
if ($request->filled('age')) {
|
|
$ageVal = $request->age;
|
|
if (str_contains($ageVal, '-')) {
|
|
[$minAge, $maxAge] = array_map('intval', explode('-', $ageVal));
|
|
$q->whereBetween('age', [$minAge, $maxAge]);
|
|
} else {
|
|
$q->where('age', $ageVal);
|
|
}
|
|
}
|
|
|
|
// Experience filter
|
|
if ($request->filled('experience')) {
|
|
$q->where('experience', 'like', "%{$request->experience}%");
|
|
}
|
|
|
|
// Salary range filter
|
|
if ($request->filled('salary_min')) {
|
|
$q->where('salary', '>=', (float)$request->salary_min);
|
|
}
|
|
if ($request->filled('salary_max')) {
|
|
$q->where('salary', '<=', (float)$request->salary_max);
|
|
}
|
|
|
|
// Preferred location filter
|
|
if ($request->filled('preferred_location')) {
|
|
$prefLoc = $request->preferred_location;
|
|
$locsArray = is_array($prefLoc) ? $prefLoc : array_filter(array_map('trim', explode(',', $prefLoc)));
|
|
if (!empty($locsArray)) {
|
|
$q->where(function($subQ) use ($locsArray) {
|
|
foreach ($locsArray as $l) {
|
|
$subQ->orWhere('preferred_location', 'like', "%{$l}%");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Job type filter (preferred_job_type)
|
|
$jobTypeParam = $request->input('job_type') ?? $request->input('preferred_job_type');
|
|
if ($jobTypeParam) {
|
|
$jobTypesArray = is_array($jobTypeParam) ? $jobTypeParam : array_filter(array_map('trim', explode(',', $jobTypeParam)));
|
|
if (!empty($jobTypesArray)) {
|
|
$q->where(function($subQ) use ($jobTypesArray) {
|
|
foreach ($jobTypesArray as $jt) {
|
|
$subQ->orWhere('preferred_job_type', 'like', "%{$jt}%");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Accommodation filter (live_in_out)
|
|
$accParam = $request->input('live_in_out') ?? $request->input('accommodation_type') ?? $request->input('accomadation_type');
|
|
if ($accParam) {
|
|
$accsArray = is_array($accParam) ? $accParam : array_filter(array_map('trim', explode(',', $accParam)));
|
|
if (!empty($accsArray)) {
|
|
$q->where(function($subQ) use ($accsArray) {
|
|
foreach ($accsArray as $a) {
|
|
$normA = str_replace(['_', '-'], ' ', $a);
|
|
$subQ->orWhere('live_in_out', 'like', "%{$normA}%");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// In country filter
|
|
if ($request->has('in_country')) {
|
|
$inCountryVal = $request->input('in_country');
|
|
if ($inCountryVal !== null && $inCountryVal !== '') {
|
|
$isInCountry = filter_var($inCountryVal, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
if ($isInCountry === null) {
|
|
$strVal = strtolower(trim($inCountryVal));
|
|
if ($strVal === 'in' || $strVal === 'in_country' || $strVal === 'yes') {
|
|
$isInCountry = true;
|
|
} elseif ($strVal === 'out' || $strVal === 'out_country' || $strVal === 'no') {
|
|
$isInCountry = false;
|
|
}
|
|
}
|
|
if ($isInCountry !== null) {
|
|
$q->where('in_country', $isInCountry);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Visa status filter
|
|
$visaParam = $request->input('visa_status') ?? $request->input('next_visa_type') ?? $request->input('visa_type');
|
|
if ($visaParam) {
|
|
$visasArray = is_array($visaParam) ? $visaParam : array_filter(array_map('trim', explode(',', $visaParam)));
|
|
if (!empty($visasArray)) {
|
|
$q->where(function($subQ) use ($visasArray) {
|
|
foreach ($visasArray as $v) {
|
|
$subQ->orWhere('visa_status', 'like', "%{$v}%");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Skills filter
|
|
if ($request->filled('skills')) {
|
|
$skills = $request->skills;
|
|
$skillsArray = is_array($skills) ? $skills : array_filter(array_map('trim', explode(',', $skills)));
|
|
if (!empty($skillsArray)) {
|
|
$q->whereHas('skills', function($skillQ) use ($skillsArray) {
|
|
$skillQ->whereIn('name', $skillsArray)
|
|
->orWhereIn('id', array_filter($skillsArray, 'is_numeric'));
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
$sortBy = $request->input('sort_by', 'created_at');
|
|
$sortOrder = $request->input('sort_order', 'desc');
|
|
|
|
if ($sortBy === 'created_at' || $sortBy === 'applied_at') {
|
|
$query->orderBy('created_at', $sortOrder === 'asc' ? 'asc' : 'desc');
|
|
}
|
|
|
|
$applicants = $query->get()->map(function ($app) use ($employer) {
|
|
$w = $app->worker;
|
|
if (!$w) return null;
|
|
|
|
// Check if worker is saved in Saved Workers (Shortlist)
|
|
$isSaved = \App\Models\Shortlist::where('employer_id', $employer->id)
|
|
->where('worker_id', $w->id)
|
|
->exists();
|
|
|
|
return [
|
|
'application_id' => $app->id,
|
|
'status' => $app->status,
|
|
'notes' => $app->notes,
|
|
'status_history' => $app->status_history ?: [],
|
|
'applied_at' => $app->created_at->toIso8601String(),
|
|
'is_saved' => $isSaved,
|
|
'worker' => [
|
|
'id' => $w->id,
|
|
'name' => $w->name,
|
|
'nationality' => $w->nationality,
|
|
'salary' => (int) $w->salary,
|
|
'preferred_location' => $w->preferred_location,
|
|
'preferred_job_type' => $w->preferred_job_type,
|
|
'visa_status' => $w->visa_status,
|
|
'experience' => $w->experience,
|
|
'skills' => $w->skills->pluck('name')->toArray(),
|
|
]
|
|
];
|
|
})->filter()->values();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'job' => [
|
|
'id' => $job->id,
|
|
'title' => $job->title,
|
|
],
|
|
'applicants' => $applicants
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* API for employers to update application status.
|
|
* POST /api/employers/applications/{id}/status
|
|
*/
|
|
public function employerUpdateApplicationStatus(Request $request, $id)
|
|
{
|
|
/** @var User $employer */
|
|
$employer = $request->attributes->get('employer');
|
|
|
|
if (!$this->checkJobAccess($employer->id)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Your current plan does not support the Job Apply module. Please upgrade to a Premium plan to access this feature.'
|
|
], 403);
|
|
}
|
|
|
|
$validator = Validator::make($request->all(), [
|
|
'status' => 'required|string|in:applied,review,reviewing,shortlisted,contacted,interview scheduled,interview_scheduled,selected,rejected,hired,hire_requested',
|
|
'notes' => 'nullable|string',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Validation error.',
|
|
'errors' => $validator->errors()
|
|
], 422);
|
|
}
|
|
|
|
$application = JobApplication::where('id', $id)
|
|
->whereHas('jobPost', function($q) use ($employer) {
|
|
$q->where('employer_id', $employer->id);
|
|
})->first();
|
|
|
|
if (!$application) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Job application not found or unauthorized.'
|
|
], 404);
|
|
}
|
|
|
|
$dbStatus = strtolower($request->status);
|
|
if ($dbStatus === 'hired') {
|
|
$dbStatus = 'hire_requested';
|
|
} elseif ($dbStatus === 'review' || $dbStatus === 'reviewing') {
|
|
$dbStatus = 'applied';
|
|
}
|
|
|
|
// Contact Limit check: if moving to 'shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired', 'hire_requested'
|
|
$contactStatuses = ['shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired', 'hire_requested'];
|
|
if (in_array($dbStatus, $contactStatuses)) {
|
|
if (!User::canContactWorker($employer->id, $application->worker_id)) {
|
|
$activeSub = \App\Models\Subscription::where('user_id', $employer->id)->where('status', 'active')->first();
|
|
$planId = $activeSub ? $activeSub->plan_id : 'basic';
|
|
$plan = \App\Models\Plan::find($planId);
|
|
$maxContacts = $plan ? $plan->max_contact_people : 10;
|
|
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'You have reached your contacted workers limit of ' . $maxContacts . ' under your active plan. Please upgrade or renew your subscription to contact more workers.'
|
|
], 403);
|
|
}
|
|
}
|
|
|
|
$notes = $request->input('notes');
|
|
|
|
// Update application status & history
|
|
$history = $application->status_history ?: [];
|
|
$history[] = [
|
|
'status' => $dbStatus,
|
|
'timestamp' => now()->toIso8601String(),
|
|
'notes' => $notes,
|
|
];
|
|
|
|
$empStatus = strtolower($request->status);
|
|
if ($empStatus === 'applied' || $empStatus === 'reviewing' || $empStatus === 'review') {
|
|
$empStatus = 'review';
|
|
} elseif ($empStatus === 'hire_requested') {
|
|
$empStatus = 'hired';
|
|
}
|
|
|
|
$updateData = [
|
|
'status' => $dbStatus,
|
|
'employer_status' => $empStatus,
|
|
'status_history' => $history,
|
|
];
|
|
|
|
if ($notes !== null) {
|
|
$updateData['notes'] = $notes;
|
|
}
|
|
|
|
$application->update($updateData);
|
|
|
|
// If shortlisted, automatically add to Saved Workers (Shortlist table) if not already saved
|
|
if ($dbStatus === 'shortlisted') {
|
|
$exists = \App\Models\Shortlist::where('employer_id', $employer->id)
|
|
->where('worker_id', $application->worker_id)
|
|
->exists();
|
|
if (!$exists) {
|
|
\App\Models\Shortlist::create([
|
|
'employer_id' => $employer->id,
|
|
'worker_id' => $application->worker_id,
|
|
]);
|
|
}
|
|
}
|
|
|
|
if ($dbStatus === 'rejected') {
|
|
if ($application->worker) {
|
|
$application->worker->update(['status' => 'active']);
|
|
}
|
|
}
|
|
|
|
// Trigger notifications
|
|
$this->triggerStatusNotification($application, $dbStatus);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Application status updated successfully.',
|
|
'data' => [
|
|
'application' => $application->fresh()
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* API for workers to view details of a specific job application.
|
|
* GET /api/workers/applied-jobs/{id}
|
|
*/
|
|
public function appliedJobDetail(Request $request, $id)
|
|
{
|
|
/** @var Worker $worker */
|
|
$worker = $request->attributes->get('worker');
|
|
|
|
$application = JobApplication::where('worker_id', $worker->id)
|
|
->with(['jobPost.employer.employerProfile'])
|
|
->find($id);
|
|
|
|
if (!$application) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Application not found.'
|
|
], 404);
|
|
}
|
|
|
|
$job = $application->jobPost;
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'application' => [
|
|
'id' => $application->id,
|
|
'status' => $application->status,
|
|
'employer_status' => $application->employer_status,
|
|
'status_history' => $application->status_history ?: [],
|
|
'applied_at' => $application->created_at->toIso8601String(),
|
|
'job' => $job ? [
|
|
'id' => $job->id,
|
|
'title' => $job->title,
|
|
'location' => $job->location,
|
|
'salary' => (int) $job->salary,
|
|
'job_type' => $job->job_type,
|
|
'description' => $job->description,
|
|
'requirements' => $job->requirements,
|
|
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
|
|
'posted_by' => $job->employer->name ?? 'Private Sponsor',
|
|
'posted_at' => $job->created_at->toIso8601String(),
|
|
] : null
|
|
]
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* API for workers to withdraw their job application.
|
|
* POST /api/workers/applied-jobs/{id}/withdraw
|
|
*/
|
|
public function withdrawApplication(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) !== 'applied') {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'You can only withdraw applications that are still in "Applied" status.'
|
|
], 400);
|
|
}
|
|
|
|
$application->delete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Application successfully withdrawn.'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
$worker = $application->worker;
|
|
$job = $application->jobPost;
|
|
$employer = $job ? $job->employer : null;
|
|
|
|
// When status is updated: notify worker
|
|
if ($worker) {
|
|
$title = "Job Application Update";
|
|
$body = "";
|
|
$type = 'job_application_status_update';
|
|
switch (strtolower($status)) {
|
|
case 'shortlisted':
|
|
$body = "You have been shortlisted for the job: " . ($job->title ?? 'Job Post');
|
|
$type = 'shortlisted';
|
|
break;
|
|
case 'applied':
|
|
case 'review':
|
|
case 'reviewing':
|
|
$body = "Your application is under review for the job: " . ($job->title ?? 'Job Post');
|
|
$type = 'review';
|
|
break;
|
|
case 'contacted':
|
|
$body = "An employer wants to contact you regarding the job: " . ($job->title ?? 'Job Post');
|
|
$type = 'contacted';
|
|
break;
|
|
case 'interview scheduled':
|
|
case 'interview_scheduled':
|
|
$body = "An interview has been scheduled for the job: " . ($job->title ?? 'Job Post');
|
|
$type = 'interview_scheduled';
|
|
break;
|
|
case 'selected':
|
|
$body = "Congratulations! You have been selected for the job: " . ($job->title ?? 'Job Post');
|
|
$type = 'selected';
|
|
break;
|
|
case '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;
|
|
case 'hired':
|
|
$body = "Congratulations! You have been hired for the job: " . ($job->title ?? 'Job Post');
|
|
$type = 'hired';
|
|
break;
|
|
default:
|
|
$body = "Your application status for the job: " . ($job->title ?? 'Job Post') . " has changed to " . ucfirst($status);
|
|
break;
|
|
}
|
|
|
|
$worker->notify(new \App\Notifications\GenericNotification(
|
|
$title,
|
|
$body,
|
|
[
|
|
'type' => $type,
|
|
'job_id' => $job->id ?? '',
|
|
'application_id' => $application->id,
|
|
'status' => $status,
|
|
]
|
|
));
|
|
}
|
|
}
|
|
}
|