diff --git a/app/Http/Controllers/Api/WorkerJobController.php b/app/Http/Controllers/Api/WorkerJobController.php index 4db8e37..85ef88f 100644 --- a/app/Http/Controllers/Api/WorkerJobController.php +++ b/app/Http/Controllers/Api/WorkerJobController.php @@ -21,7 +21,7 @@ public function listJobs(Request $request) /** @var Worker $worker */ $worker = $request->attributes->get('worker'); - $query = JobPost::where('status', 'active') + $query = JobPost::whereIn('status', ['active', 'closed']) ->with(['employer.employerProfile', 'skills']); if ($worker) { @@ -42,9 +42,85 @@ public function listJobs(Request $request) } } - $jobs = $query->latest() - ->get() - ->map(function ($job) { + $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, @@ -60,6 +136,7 @@ public function listJobs(Request $request) 'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor', 'posted_by' => $job->employer->name ?? 'Private Sponsor', 'posted_at' => $job->created_at->toIso8601String(), + 'status' => $status, ]; }); @@ -77,7 +154,7 @@ public function listJobs(Request $request) */ public function jobDetails($id) { - $job = JobPost::where('status', 'active') + $job = JobPost::whereIn('status', ['active', 'closed']) ->with(['employer.employerProfile', 'skills']) ->find($id); @@ -88,6 +165,21 @@ public function jobDetails($id) ], 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' => [ @@ -106,6 +198,7 @@ public function jobDetails($id) 'company_name' => $job->employer->employerProfile->company_name ?? 'Private Sponsor', 'posted_by' => $job->employer->name ?? 'Private Sponsor', 'posted_at' => $job->created_at->toIso8601String(), + 'status' => $status, ] ] ]); @@ -187,6 +280,7 @@ public function appliedJobs(Request $request) return [ 'application_id' => $app->id, 'status' => $app->status, + 'employer_status' => $app->employer_status, 'applied_at' => $app->created_at->toIso8601String(), 'job' => $job ? [ 'id' => $job->id, @@ -660,7 +754,7 @@ public function employerUpdateApplicationStatus(Request $request, $id) } $validator = Validator::make($request->all(), [ - 'status' => 'required|string|in:applied,shortlisted,contacted,interview scheduled,interview_scheduled,selected,rejected,hired,hire_requested', + 'status' => 'required|string|in:applied,review,reviewing,shortlisted,contacted,interview scheduled,interview_scheduled,selected,rejected,hired,hire_requested', 'notes' => 'nullable|string', ]); @@ -687,6 +781,8 @@ public function employerUpdateApplicationStatus(Request $request, $id) $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' @@ -715,8 +811,16 @@ public function employerUpdateApplicationStatus(Request $request, $id) '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, ]; @@ -785,6 +889,7 @@ public function appliedJobDetail(Request $request, $id) '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 ? [ diff --git a/app/Http/Controllers/Employer/CandidateController.php b/app/Http/Controllers/Employer/CandidateController.php index 37a0ede..f53d200 100644 --- a/app/Http/Controllers/Employer/CandidateController.php +++ b/app/Http/Controllers/Employer/CandidateController.php @@ -54,12 +54,12 @@ public function index(Request $request) if (!$w) return null; // Map DB status to UI friendly status - $status = 'Reviewing'; + $status = 'Review'; $dbStatusLower = strtolower($app->status ?? ''); if ($dbStatusLower === 'hired') $status = 'Hired'; elseif ($dbStatusLower === 'rejected') $status = 'Rejected'; - elseif ($dbStatusLower === 'shortlisted' || $dbStatusLower === 'offer_sent' || $dbStatusLower === 'offer sent') $status = 'Offer Sent'; - elseif ($dbStatusLower === 'applied') $status = 'Reviewing'; + elseif ($dbStatusLower === 'shortlisted' || $dbStatusLower === 'offer_sent' || $dbStatusLower === 'offer sent') $status = 'Shortlisted'; + elseif ($dbStatusLower === 'applied' || $dbStatusLower === 'review' || $dbStatusLower === 'reviewing') $status = 'Review'; else $status = ucfirst($app->status); return [ @@ -316,7 +316,7 @@ private function checkJobAccess($user) public function updateStatus(Request $request, $id) { $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,hire_requested,hire_requested', + 'status' => 'required|string|in:Applied,Review,review,Reviewing,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', 'notes' => 'nullable|string', ]); @@ -381,7 +381,7 @@ public function updateStatus(Request $request, $id) $dbStatus = 'interview_scheduled'; } elseif ($statusLower === 'selected') { $dbStatus = 'selected'; - } elseif ($statusLower === 'reviewing' || $statusLower === 'applied') { + } elseif ($statusLower === 'reviewing' || $statusLower === 'applied' || $statusLower === 'review') { $dbStatus = 'applied'; } @@ -408,8 +408,16 @@ public function updateStatus(Request $request, $id) 'notes' => $request->input('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, ]; diff --git a/app/Http/Controllers/Employer/MessageController.php b/app/Http/Controllers/Employer/MessageController.php index 2c37799..c788f38 100644 --- a/app/Http/Controllers/Employer/MessageController.php +++ b/app/Http/Controllers/Employer/MessageController.php @@ -197,10 +197,59 @@ public function show($id) ]; })->toArray(); + $application = \App\Models\JobApplication::where('worker_id', $activeConv->worker_id) + ->whereHas('jobPost', function($q) use ($user) { + $q->where('employer_id', $user->id); + }) + ->latest('id') + ->first(); + + if (!$application) { + $jobPost = \App\Models\JobPost::where('employer_id', $user->id)->latest('id')->first(); + if (!$jobPost) { + $jobPost = \App\Models\JobPost::create([ + 'employer_id' => $user->id, + 'title' => 'General Helper', + 'location' => 'Dubai', + 'salary' => 2000, + 'job_type' => 'full-time', + 'status' => 'open', + 'start_date' => now()->format('Y-m-d'), + 'description' => 'General domestic helper position.', + 'requirements' => 'General helper requirements.', + ]); + } + $application = \App\Models\JobApplication::create([ + 'job_id' => $jobPost->id, + 'worker_id' => $activeConv->worker_id, + 'status' => 'applied', + 'employer_status' => 'review', + 'status_history' => [ + [ + 'status' => 'applied', + 'timestamp' => now()->toIso8601String(), + 'notes' => 'Application created via chat conversation.', + ] + ], + ]); + } + + $applicationData = null; + if ($application) { + $applicationData = [ + 'application_id' => $application->id, + 'status' => $application->status, + 'notes' => $application->notes, + 'status_history' => $application->status_history ?: [], + 'name' => $activeConv->worker->name ?? 'Candidate', + ]; + } + return Inertia::render('Employer/Messages/Show', [ 'conversations' => $conversations, 'conversation' => $conversationData, 'initialMessages' => $initialMessages, + 'application' => $applicationData, ]); } diff --git a/app/Models/JobApplication.php b/app/Models/JobApplication.php index dd67e81..7bff8f7 100644 --- a/app/Models/JobApplication.php +++ b/app/Models/JobApplication.php @@ -13,6 +13,7 @@ class JobApplication extends Model 'job_id', 'worker_id', 'status', + 'employer_status', 'notes', 'status_history', 'joining_confirmed_at', diff --git a/database/migrations/2026_07_06_144202_add_employer_status_to_job_applications_table.php b/database/migrations/2026_07_06_144202_add_employer_status_to_job_applications_table.php new file mode 100644 index 0000000..0fb158a --- /dev/null +++ b/database/migrations/2026_07_06_144202_add_employer_status_to_job_applications_table.php @@ -0,0 +1,28 @@ +string('employer_status')->nullable()->after('status'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('job_applications', function (Blueprint $table) { + $table->dropColumn('employer_status'); + }); + } +}; diff --git a/public/swagger.json b/public/swagger.json index bb228af..7279854 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -4138,6 +4138,56 @@ ], "summary": "List Available Jobs", "description": "Returns a list of all active job postings available for workers to discover and apply.", + "parameters": [ + { + "name": "job_type", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Direct filter by exact job type (e.g., 'Full Time', 'Part Time', 'Contract')." + }, + { + "name": "skills", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Comma-separated list of skill names or IDs to filter by (e.g. 'Cooking,Cleaning')." + }, + { + "name": "salary_min", + "in": "query", + "schema": { + "type": "number" + }, + "description": "Minimum salary filter." + }, + { + "name": "salary_max", + "in": "query", + "schema": { + "type": "number" + }, + "description": "Maximum salary filter." + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + }, + "description": "General search query for title, description, or location." + }, + { + "name": "sortBy", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Sorting order (e.g. 'LATEST FIRST', 'OLDEST FIRST', 'PRICE: LOW TO HIGH', 'PRICE: HIGH TO LOW')." + } + ], "security": [ { "bearerAuth": [] diff --git a/resources/js/Pages/Employer/Jobs/Applicants.jsx b/resources/js/Pages/Employer/Jobs/Applicants.jsx index f2e69d0..0cfe4bc 100644 --- a/resources/js/Pages/Employer/Jobs/Applicants.jsx +++ b/resources/js/Pages/Employer/Jobs/Applicants.jsx @@ -37,7 +37,18 @@ export default function Applicants({ job, applicants: initialApplicants, isShort const openStatusModal = (app, statusPreset = '') => { setSelectedApp(app); - setNewStatus(statusPreset || app.status || 'Applied'); + let resolvedStatus = statusPreset || app.status || 'Applied'; + const lowerStatus = resolvedStatus.toLowerCase(); + if (lowerStatus === 'hire_requested' || lowerStatus === 'hired') { + resolvedStatus = 'Hired'; + } else if (lowerStatus === 'rejected') { + resolvedStatus = 'Rejected'; + } else if (lowerStatus === 'shortlisted') { + resolvedStatus = 'Shortlisted'; + } else { + resolvedStatus = 'Applied'; + } + setNewStatus(resolvedStatus); setNotes(app.notes || ''); setShowModal(true); }; @@ -197,7 +208,8 @@ export default function Applicants({ job, applicants: initialApplicants, isShort applicant.status?.toLowerCase() === 'rejected' ? 'bg-rose-100 text-rose-700' : 'bg-blue-50 text-[#185FA5]' }`} onClick={() => openStatusModal(applicant)}> - {applicant.status?.toLowerCase() === 'hire_requested' ? 'Pending Confirmation' : applicant.status} + {applicant.status?.toLowerCase() === 'hire_requested' ? 'Pending Confirmation' : + applicant.status?.toLowerCase() === 'applied' ? 'Review' : applicant.status}
- {t('confirm_hire_desc', 'Verify this worker with your needs and make sure to hire this worker. If hired, you can view this worker in the Hired Workers page.')} -
-{selectedApp?.name}
+