diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php
new file mode 100644
index 0000000..d9cc1e5
--- /dev/null
+++ b/app/Http/Controllers/Api/EmployerWorkerController.php
@@ -0,0 +1,362 @@
+id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
+
+ // Preferred job types: full-time / part-time / live-in / live-out
+ $jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
+ $preferredJobType = $jobTypes[$w->id % 4];
+
+ // Availability status: Active / Hidden / Hired
+ $availabilityStatus = ucfirst($w->status === 'active' ? 'Active' : ($w->status === 'hidden' ? 'Hidden' : ($w->status === 'Hired' ? 'Hired' : 'Active')));
+
+ // Emirates ID verification status
+ $emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending';
+
+ // Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening
+ $skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
+ $mappedSkills = [
+ $skillsList[$w->id % 6],
+ $skillsList[($w->id + 2) % 6]
+ ];
+
+ // Visa status
+ $visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa'];
+ $visaStatus = $visaStatusesList[$w->id % 5];
+
+ // Optional profile photos
+ $photos = [
+ 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200',
+ 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=200',
+ 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=200',
+ null
+ ];
+ $photo = $photos[$w->id % 4];
+
+ $rating = 4.0 + (($w->id * 3) % 10) / 10.0;
+ $reviewsCount = ($w->id * 4) % 20 + 2;
+
+ return [
+ 'id' => $w->id,
+ 'name' => $w->name,
+ 'nationality' => $w->nationality,
+ 'photo' => $photo,
+ 'emirates_id_status' => $emiratesIdStatus,
+ 'category' => $w->category ? $w->category->name : 'Domestic Worker',
+ 'skills' => $mappedSkills,
+ 'availability_status' => $availabilityStatus,
+ 'visa_status' => $visaStatus,
+ 'experience' => $w->experience,
+ 'salary' => (int)$w->salary,
+ 'religion' => $w->religion,
+ 'languages' => $langs,
+ 'age' => $w->age,
+ 'verified' => (bool)$w->verified,
+ 'preferred_job_type' => $preferredJobType,
+ 'bio' => $w->bio,
+ 'rating' => $rating,
+ 'reviews_count' => $reviewsCount,
+ ];
+ }
+
+ /**
+ * 1. GET /api/employers/workers
+ * List of workers available for hiring.
+ */
+ public function getWorkers(Request $request)
+ {
+ try {
+ $query = Worker::with(['category', 'skills'])
+ ->where('status', '!=', 'Hired')
+ ->where('status', '!=', 'hidden');
+
+ // Apply search filter if provided
+ if ($request->filled('search')) {
+ $search = $request->search;
+ $query->where(function($q) use ($search) {
+ $q->where('name', 'like', "%{$search}%")
+ ->orWhere('nationality', 'like', "%{$search}%")
+ ->orWhere('religion', 'like', "%{$search}%");
+ });
+ }
+
+ // Apply category filter if provided
+ if ($request->filled('category_id')) {
+ $query->where('category_id', $request->category_id);
+ }
+
+ // Apply nationality filter if provided
+ if ($request->filled('nationality')) {
+ $query->where('nationality', $request->nationality);
+ }
+
+ // Apply salary filters
+ if ($request->filled('min_salary')) {
+ $query->where('salary', '>=', $request->min_salary);
+ }
+ if ($request->filled('max_salary')) {
+ $query->where('salary', '<=', $request->max_salary);
+ }
+
+ $dbWorkers = $query->latest()->get();
+
+ $formattedWorkers = $dbWorkers->map(function ($w) {
+ return $this->formatWorker($w);
+ });
+
+ return response()->json([
+ 'success' => true,
+ 'data' => [
+ 'workers' => $formattedWorkers
+ ]
+ ], 200);
+
+ } catch (\Exception $e) {
+ logger()->error('Mobile API Employer Get Workers Failure: ' . $e->getMessage());
+
+ return response()->json([
+ 'success' => false,
+ 'message' => 'An error occurred while fetching the worker list.',
+ 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
+ ], 500);
+ }
+ }
+
+ /**
+ * 2. GET /api/employers/candidates
+ * List of candidates representing standard job applications and direct hiring offers.
+ */
+ public function getCandidates(Request $request)
+ {
+ /** @var User $employer */
+ $employer = $request->attributes->get('employer');
+
+ try {
+ $employerId = $employer->id;
+
+ // Fetch Job Applications
+ $jobIds = JobPost::where('employer_id', $employerId)->pluck('id');
+ $applications = JobApplication::whereIn('job_id', $jobIds)
+ ->with(['worker.category', 'jobPost'])
+ ->get();
+
+ $selectedWorkers = $applications->map(function ($app) {
+ $w = $app->worker;
+ if (!$w) return null;
+
+ $status = 'Reviewing';
+ if ($app->status === 'hired') $status = 'Hired';
+ elseif ($app->status === 'rejected') $status = 'Rejected';
+ elseif ($app->status === 'shortlisted' || $app->status === 'offer_sent') $status = 'Offer Sent';
+ elseif ($app->status === 'applied') $status = 'Reviewing';
+ else $status = ucfirst($app->status);
+
+ return [
+ 'id' => $app->id,
+ 'worker_id' => $w->id,
+ 'name' => $w->name,
+ 'nationality' => $w->nationality,
+ 'category' => $w->category ? $w->category->name : 'General Helper',
+ 'salary' => (int)$w->salary,
+ 'status' => $status,
+ 'applied_at' => $app->created_at->format('Y-m-d H:i:s'),
+ 'type' => 'application',
+ ];
+ })->filter()->values()->toArray();
+
+ // Fetch Direct Hiring Offers
+ $directOffers = JobOffer::where('employer_id', $employerId)
+ ->with('worker.category')
+ ->get();
+
+ $directWorkers = $directOffers->map(function ($offer) {
+ $w = $offer->worker;
+ if (!$w) return null;
+
+ $status = 'Offer Sent';
+ if ($offer->status === 'accepted') $status = 'Hired';
+ elseif ($offer->status === 'rejected') $status = 'Rejected';
+ elseif ($offer->status === 'pending') $status = 'Offer Sent';
+
+ return [
+ 'id' => 'offer_' . $offer->id,
+ 'worker_id' => $w->id,
+ 'name' => $w->name,
+ 'nationality' => $w->nationality,
+ 'category' => $w->category ? $w->category->name : 'General Helper',
+ 'salary' => (int)$offer->salary,
+ 'status' => $status,
+ 'applied_at' => $offer->created_at->format('Y-m-d H:i:s'),
+ 'type' => 'direct_offer',
+ ];
+ })->filter()->values()->toArray();
+
+ // Merge and sort
+ $mergedCandidates = array_merge($selectedWorkers, $directWorkers);
+
+ // Optional status filtering
+ if ($request->filled('status')) {
+ $filterStatus = $request->status;
+ $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($filterStatus) {
+ return strcasecmp($c['status'], $filterStatus) === 0;
+ }));
+ }
+
+ return response()->json([
+ 'success' => true,
+ 'data' => [
+ 'candidates' => $mergedCandidates
+ ]
+ ], 200);
+
+ } catch (\Exception $e) {
+ logger()->error('Mobile API Employer Get Candidates Failure: ' . $e->getMessage());
+
+ return response()->json([
+ 'success' => false,
+ 'message' => 'An error occurred while fetching the candidate list.',
+ 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
+ ], 500);
+ }
+ }
+
+ /**
+ * 3. POST /api/employers/candidates/hire or POST /api/employers/candidates/{id}/hire
+ * Update status from active (or reviewing/pending) to hired.
+ */
+ public function hireCandidate(Request $request, $id = null)
+ {
+ /** @var User $employer */
+ $employer = $request->attributes->get('employer');
+
+ // Let the client pass either route parameter id OR body parameter identifier
+ $targetId = $id ?: $request->input('id') ?: $request->input('worker_id') ?: $request->input('application_id') ?: $request->input('offer_id');
+
+ if (!$targetId) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Candidate or Worker identifier is required.'
+ ], 422);
+ }
+
+ try {
+ $employerId = $employer->id;
+ $worker = null;
+ $updatedApplication = null;
+ $updatedOffer = null;
+
+ // Direct Offer check
+ if (is_string($targetId) && str_starts_with($targetId, 'offer_')) {
+ $offerId = substr($targetId, 6);
+ $offer = JobOffer::where('id', $offerId)->where('employer_id', $employerId)->firstOrFail();
+
+ $offer->update(['status' => 'accepted']);
+ if ($offer->worker) {
+ $offer->worker->update(['status' => 'Hired']);
+ $worker = $offer->worker;
+ }
+ $updatedOffer = $offer;
+ } else {
+ // Check if targetId is an application
+ $application = JobApplication::where('id', $targetId)
+ ->whereHas('jobPost', function($q) use ($employerId) {
+ $q->where('employer_id', $employerId);
+ })->first();
+
+ if ($application) {
+ $application->update(['status' => 'hired']);
+ if ($application->worker) {
+ $application->worker->update(['status' => 'Hired']);
+ $worker = $application->worker;
+ }
+ $updatedApplication = $application;
+ } else {
+ // Try targeting by Worker ID directly
+ $worker = Worker::find($targetId);
+
+ if (!$worker) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Worker, application, or job offer not found with the provided identifier.'
+ ], 404);
+ }
+
+ $worker->update(['status' => 'Hired']);
+
+ // Find existing direct offer
+ $offer = JobOffer::where('employer_id', $employerId)
+ ->where('worker_id', $worker->id)
+ ->first();
+
+ if ($offer) {
+ $offer->update(['status' => 'accepted']);
+ $updatedOffer = $offer;
+ } else {
+ // Find existing application
+ $jobIds = JobPost::where('employer_id', $employerId)->pluck('id');
+ $application = JobApplication::whereIn('job_id', $jobIds)
+ ->where('worker_id', $worker->id)
+ ->first();
+
+ if ($application) {
+ $application->update(['status' => 'hired']);
+ $updatedApplication = $application;
+ } else {
+ // Create a new direct job offer and accept it
+ $updatedOffer = JobOffer::create([
+ 'employer_id' => $employerId,
+ 'worker_id' => $worker->id,
+ 'work_date' => now()->format('Y-m-d'),
+ 'location' => 'Dubai',
+ 'salary' => $worker->salary,
+ 'notes' => 'Direct sponsoring matching finalized via mobile API.',
+ 'status' => 'accepted',
+ ]);
+ }
+ }
+ }
+ }
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Candidate successfully marked as Hired!',
+ 'data' => [
+ 'worker_id' => $worker ? $worker->id : null,
+ 'worker_status' => $worker ? $worker->status : 'Hired',
+ 'application' => $updatedApplication,
+ 'offer' => $updatedOffer,
+ ]
+ ], 200);
+
+ } catch (\Exception $e) {
+ logger()->error('Mobile API Employer Hire Candidate Failure: ' . $e->getMessage());
+
+ return response()->json([
+ 'success' => false,
+ 'message' => 'An error occurred while marking the candidate as hired.',
+ 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
+ ], 500);
+ }
+ }
+}
diff --git a/app/Http/Controllers/Employer/CandidateController.php b/app/Http/Controllers/Employer/CandidateController.php
index 2813633..c69c562 100644
--- a/app/Http/Controllers/Employer/CandidateController.php
+++ b/app/Http/Controllers/Employer/CandidateController.php
@@ -103,6 +103,11 @@ public function index(Request $request)
// Merge standard job applications with direct hiring offers for unified tracking
$mergedCandidates = array_merge($selectedWorkers, $directWorkers);
+ // Only display Hired candidates in the candidates pipeline
+ $mergedCandidates = array_values(array_filter($mergedCandidates, function ($w) {
+ return $w && $w['status'] === 'Hired';
+ }));
+
return Inertia::render('Employer/SelectedCandidates', [
'selectedWorkers' => $mergedCandidates,
]);
diff --git a/app/Http/Controllers/Employer/WorkerController.php b/app/Http/Controllers/Employer/WorkerController.php
index c8b9516..a070c52 100644
--- a/app/Http/Controllers/Employer/WorkerController.php
+++ b/app/Http/Controllers/Employer/WorkerController.php
@@ -42,7 +42,10 @@ public function index(Request $request)
$user = $this->resolveCurrentUser();
$employerId = $user ? $user->id : 2;
- $dbWorkers = Worker::with(['category', 'skills'])->get();
+ $dbWorkers = Worker::with(['category', 'skills'])
+ ->where('status', '!=', 'Hired')
+ ->where('status', '!=', 'hidden')
+ ->get();
$workers = $dbWorkers->map(function ($w) {
// Map languages with country names
@@ -177,6 +180,8 @@ public function show($id)
$simDb = Worker::with('category')
->where('id', '!=', $w->id)
+ ->where('status', '!=', 'Hired')
+ ->where('status', '!=', 'hidden')
->limit(3)
->get();
$similarWorkers = $simDb->map(function($sw) {
@@ -254,4 +259,47 @@ public function sendOffer(Request $request, $id)
return redirect()->route('employer.hiring.success', ['id' => $id])
->with('success', 'Hiring offer has been successfully dispatched to the worker!');
}
+
+ public function markHired(Request $request, $id)
+ {
+ $user = $this->resolveCurrentUser();
+ $employerId = $user ? $user->id : 2;
+
+ $worker = Worker::findOrFail($id);
+ $worker->update([
+ 'status' => 'Hired',
+ ]);
+
+ // Check if there is an existing job offer for this employer and worker
+ $offer = JobOffer::where('employer_id', $employerId)
+ ->where('worker_id', $worker->id)
+ ->first();
+
+ if ($offer) {
+ $offer->update(['status' => 'accepted']);
+ } else {
+ // Also check if there's an existing job application
+ $jobIds = \App\Models\JobPost::where('employer_id', $employerId)->pluck('id');
+ $application = \App\Models\JobApplication::whereIn('job_id', $jobIds)
+ ->where('worker_id', $worker->id)
+ ->first();
+
+ if ($application) {
+ $application->update(['status' => 'hired']);
+ } else {
+ // Create a new direct job offer with status accepted
+ JobOffer::create([
+ 'employer_id' => $employerId,
+ 'worker_id' => $worker->id,
+ 'work_date' => now()->format('Y-m-d'),
+ 'location' => 'Dubai',
+ 'salary' => $worker->salary,
+ 'notes' => 'Direct sponsoring matching finalized.',
+ 'status' => 'accepted',
+ ]);
+ }
+ }
+
+ return back()->with('success', 'Worker successfully marked as Hired!');
+ }
}
diff --git a/resources/js/Pages/Employer/SelectedCandidates.jsx b/resources/js/Pages/Employer/SelectedCandidates.jsx
index 8395f74..26b6f86 100644
--- a/resources/js/Pages/Employer/SelectedCandidates.jsx
+++ b/resources/js/Pages/Employer/SelectedCandidates.jsx
@@ -50,44 +50,6 @@ export default function SelectedCandidates({ selectedWorkers }) {
});
};
- const stats = [
- {
- label: t('total_candidates', 'Total Candidates'),
- count: workers.length,
- icon: Users,
- color: 'text-slate-600',
- bg: 'bg-slate-50'
- },
- {
- label: t('reviewing', 'Reviewing'),
- count: workers.filter(w => w.status === 'Reviewing' || !w.status).length,
- icon: Search,
- color: 'text-amber-600',
- bg: 'bg-amber-50'
- },
- {
- label: t('offer_sent', 'Offer Sent'),
- count: workers.filter(w => w.status === 'Offer Sent').length,
- icon: Send,
- color: 'text-blue-600',
- bg: 'bg-blue-50'
- },
- {
- label: t('hired', 'Hired'),
- count: workers.filter(w => w.status === 'Hired').length,
- icon: CheckCircle2,
- color: 'text-emerald-600',
- bg: 'bg-emerald-50'
- },
- {
- label: t('rejected', 'Rejected'),
- count: workers.filter(w => w.status === 'Rejected').length,
- icon: UserCheck,
- color: 'text-rose-600',
- bg: 'bg-rose-50'
- },
- ];
-
return (