migrant-web/app/Http/Controllers/Api/WorkerJobController.php

809 lines
28 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)
{
$jobs = JobPost::where('status', 'active')
->with(['employer.employerProfile'])
->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,
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
'posted_at' => $job->created_at->toIso8601String(),
];
});
return response()->json([
'success' => true,
'data' => [
'jobs' => $jobs
]
]);
}
/**
* API to retrieve job details.
* GET /api/workers/jobs/{id}
*/
public function jobDetails($id)
{
$job = JobPost::where('status', 'active')
->with(['employer.employerProfile'])
->find($id);
if (!$job) {
return response()->json([
'success' => false,
'message' => 'Job not found.'
], 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,
'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor',
'posted_at' => $job->created_at->toIso8601String(),
]
]
]);
}
/**
* 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'
]);
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,
'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',
] : 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',
'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,
'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',
]);
return response()->json([
'success' => true,
'message' => 'Job posted successfully.',
'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,
'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);
}
$validator = Validator::make($request->all(), [
'title' => 'required|string|max:255',
'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,
'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),
]);
return response()->json([
'success' => true,
'message' => 'Job updated successfully.',
'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,
'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));
}
$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,shortlisted,contacted,interview scheduled,interview_scheduled,selected,rejected,hired',
'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);
// Contact Limit check: if moving to 'shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired' for the first time
$contactStatuses = ['shortlisted', 'contacted', 'interview_scheduled', 'selected', 'hired'];
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,
];
$updateData = [
'status' => $dbStatus,
'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 hired, also update worker status
if ($dbStatus === 'hired') {
if ($application->worker) {
$application->worker->update(['status' => 'Hired']);
}
} elseif ($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,
'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_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.'
]);
}
/**
* Dispatch FCM Push Notification.
*/
private function triggerStatusNotification($application, $status)
{
$worker = $application->worker;
$job = $application->jobPost;
$employer = $job ? $job->employer : null;
// When a new application is submitted: notify employer
if ($status === 'applied') {
if ($employer && $employer->fcm_token) {
\App\Services\FCMService::sendPushNotification(
$employer->fcm_token,
"New Job Application",
"A candidate has applied for your job: " . ($job->title ?? 'Job Post'),
[
'type' => 'new_job_application',
'job_id' => $job->id,
'application_id' => $application->id,
]
);
}
return;
}
// When status is updated: notify worker
if ($worker && $worker->fcm_token) {
$title = "Job Application Update";
$body = "";
switch (strtolower($status)) {
case 'shortlisted':
$body = "You have been shortlisted for the job: " . ($job->title ?? 'Job Post');
break;
case 'contacted':
$body = "An employer wants to contact you regarding the job: " . ($job->title ?? 'Job Post');
break;
case 'interview scheduled':
case 'interview_scheduled':
$body = "An interview has been scheduled for the job: " . ($job->title ?? 'Job Post');
break;
case 'selected':
$body = "Congratulations! You have been selected for the job: " . ($job->title ?? 'Job Post');
break;
case 'rejected':
$body = "Your application for the job: " . ($job->title ?? 'Job Post') . " has been reviewed and rejected.";
break;
case 'hired':
$body = "Congratulations! You have been hired for the job: " . ($job->title ?? 'Job Post');
break;
default:
$body = "Your application status for the job: " . ($job->title ?? 'Job Post') . " has changed to " . ucfirst($status);
break;
}
\App\Services\FCMService::sendPushNotification(
$worker->fcm_token,
$title,
$body,
[
'type' => 'job_application_status_update',
'job_id' => $job->id ?? '',
'application_id' => $application->id,
'status' => $status,
]
);
}
}
}