diff --git a/app/Http/Controllers/Admin/AdminExtraController.php b/app/Http/Controllers/Admin/AdminExtraController.php index 595a151..7e5ef60 100644 --- a/app/Http/Controllers/Admin/AdminExtraController.php +++ b/app/Http/Controllers/Admin/AdminExtraController.php @@ -440,7 +440,7 @@ public function auditLogs(Request $request) } /** - * Announcements management + * Charity Events & Drives management */ public function announcements() { @@ -456,15 +456,38 @@ public function announcements() $organization = optional($ann->employer->employerProfile)->company_name ?? 'Employer'; } + // Decode charity details if they exist in json + $charityDetails = null; + $content = $ann->body; + if (strpos($ann->body, '{"type":"Charity"') === 0) { + $decoded = json_decode($ann->body, true); + if ($decoded) { + $charityDetails = $decoded; + $content = $decoded['content'] ?? $ann->body; + } + } else { + // Fallback structured details if plain text existed previously + $charityDetails = [ + 'type' => 'Charity', + 'provided_items' => 'Free Medical Checks & Food Supplies', + 'event_date' => $ann->created_at->addDays(2)->format('Y-m-d'), + 'event_time' => '9:00 AM - 3:00 PM', + 'location_details' => 'Al Quoz Community Center, Dubai', + 'location_pin' => 'https://maps.google.com', + 'content' => $ann->body, + ]; + } + return [ 'id' => $ann->id, 'title' => $ann->title, - 'content' => $ann->body, - 'type' => $ann->type, + 'content' => $content, + 'type' => $ann->type ?? 'Charity', 'posted_by' => $postedBy, 'organization' => $organization, 'status' => $ann->status ?? 'pending', 'created_at' => $ann->created_at ? $ann->created_at->format('M d, Y h:i A') : 'Just now', + 'charityDetails' => $charityDetails, ]; }); @@ -474,56 +497,70 @@ public function announcements() } /** - * Store admin announcement + * Store admin charity event */ public function storeAnnouncement(Request $request) { $request->validate([ 'title' => 'required|string|max:255', 'content' => 'required|string', - 'type' => 'nullable|string', + 'provided_items' => 'required|string', + 'event_date' => 'required|string', + 'event_time' => 'required|string', + 'location_details' => 'required|string', + 'location_pin' => 'required|string|url', + ]); + + $body = json_encode([ + 'type' => 'Charity', + 'provided_items' => $request->provided_items, + 'event_date' => $request->event_date, + 'event_time' => $request->event_time, + 'location_details' => $request->location_details, + 'location_pin' => $request->location_pin, + 'content' => $request->content, ]); \App\Models\Announcement::create([ 'title' => $request->title, - 'body' => $request->content, - 'type' => $request->type ?? 'info', + 'body' => $body, + 'type' => 'Charity', 'status' => 'approved', ]); - return back()->with('success', 'Announcement broadcasted successfully.'); + return back()->with('success', 'Charity Event created successfully.'); } /** - * Approve announcement + * Approve charity event */ public function approveAnnouncement(Request $request, $id) { $ann = \App\Models\Announcement::findOrFail($id); $ann->update(['status' => 'approved']); - return back()->with('success', 'Announcement approved successfully.'); + return back()->with('success', 'Charity Event approved successfully.'); } /** - * Reject announcement + * Reject charity event */ public function rejectAnnouncement(Request $request, $id) { $ann = \App\Models\Announcement::findOrFail($id); $ann->update(['status' => 'rejected']); - return back()->with('success', 'Announcement rejected successfully.'); + return back()->with('success', 'Charity Event rejected successfully.'); } /** - * Delete announcement + * Delete charity event */ public function deleteAnnouncement(Request $request, $id) { $ann = \App\Models\Announcement::findOrFail($id); $ann->delete(); - return back()->with('success', 'Announcement deleted successfully.'); + return back()->with('success', 'Charity Event deleted successfully.'); } } diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index 8b1becd..d7da971 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -19,6 +19,9 @@ public function index() $inactiveWorkers = $totalWorkers - $activeWorkers; $totalSponsors = \App\Models\Sponsor::count(); + $activeSponsors = \App\Models\Sponsor::where('status', 'active')->count(); + $inactiveSponsors = $totalSponsors - $activeSponsors; + $activeSubs = \App\Models\Sponsor::where('subscription_status', 'active')->count(); $expiredSubs = \App\Models\Sponsor::where('subscription_status', 'expired')->count(); $newSponsors = \App\Models\Sponsor::where('created_at', '>=', now()->subDays(7))->count(); @@ -83,6 +86,8 @@ public function index() 'active_workers' => $activeWorkers, 'inactive_workers' => $inactiveWorkers, 'total_employers' => $totalSponsors, + 'active_employers' => $activeSponsors, + 'inactive_employers' => $inactiveSponsors, 'active_subscriptions' => $activeSubs, 'revenue_this_month_aed' => $revenueSum, 'new_employers_this_week' => $newSponsors, diff --git a/app/Http/Controllers/Admin/SupportTicketController.php b/app/Http/Controllers/Admin/SupportTicketController.php index f803e49..2e3f8cf 100644 --- a/app/Http/Controllers/Admin/SupportTicketController.php +++ b/app/Http/Controllers/Admin/SupportTicketController.php @@ -112,6 +112,7 @@ public function reply(Request $request, $id) $request->validate([ 'message' => 'required|string', 'is_developer_response' => 'nullable|boolean', + 'close_ticket' => 'nullable|boolean', ]); SupportTicketReply::create([ @@ -121,9 +122,11 @@ public function reply(Request $request, $id) 'is_developer_response' => $request->is_developer_response ?? false, ]); - // Auto transition status to resolved if developer replies, or keep as in_progress + // Auto transition status to resolved/closed or keep as in_progress $newStatus = $ticket->status; - if ($ticket->status === 'open') { + if ($request->close_ticket) { + $newStatus = 'closed'; + } else if ($ticket->status === 'open') { $newStatus = 'in_progress'; } $ticket->update(['status' => $newStatus]); diff --git a/app/Http/Controllers/Api/EmployerAnnouncementController.php b/app/Http/Controllers/Api/EmployerAnnouncementController.php index 79920ef..3b70148 100644 --- a/app/Http/Controllers/Api/EmployerAnnouncementController.php +++ b/app/Http/Controllers/Api/EmployerAnnouncementController.php @@ -111,6 +111,7 @@ public function createAnnouncement(Request $request) 'body' => $bodyText, 'type' => $request->type ?? 'info', 'employer_id' => $employer->id, + 'status' => 'pending', ]); return response()->json([ diff --git a/app/Http/Controllers/Api/EmployerMessageController.php b/app/Http/Controllers/Api/EmployerMessageController.php index affbe04..854326a 100644 --- a/app/Http/Controllers/Api/EmployerMessageController.php +++ b/app/Http/Controllers/Api/EmployerMessageController.php @@ -267,32 +267,6 @@ public function sendMessage(Request $request, $id) // Touch conversation updated_at for sorting $conv->touch(); - - // Check if employer sent predefined message "are you looking job?" - $textLower = strtolower($request->text); - if ( - str_contains($textLower, 'looking job') || - str_contains($textLower, 'looking for a job') || - str_contains($textLower, 'looking for job') || - str_contains($textLower, 'are you looking') - ) { - // Automatically simulate worker response 'Yes' (triggers the S6 hired flow) - $workerResponseText = 'Yes'; - - // Create the message in database - Message::create([ - 'conversation_id' => $conv->id, - 'sender_type' => 'worker', - 'sender_id' => $conv->worker_id, - 'text' => $workerResponseText, - ]); - - // Process the worker response to update status to Hired! - $worker = Worker::find($conv->worker_id); - if ($worker) { - self::processWorkerResponse($conv, $worker, $workerResponseText); - } - } }); return response()->json([ diff --git a/app/Http/Controllers/Api/EmployerProfileController.php b/app/Http/Controllers/Api/EmployerProfileController.php index 5e58630..b3f31da 100644 --- a/app/Http/Controllers/Api/EmployerProfileController.php +++ b/app/Http/Controllers/Api/EmployerProfileController.php @@ -219,7 +219,7 @@ public function getDashboard(Request $request) ]; // Recent Announcements / Events - $dbAnnouncements = \App\Models\Announcement::latest()->limit(5)->get(); + $dbAnnouncements = \App\Models\Announcement::where('status', 'approved')->latest()->limit(5)->get(); $recentAnnouncements = $dbAnnouncements->map(function ($ann) { $body = $ann->body; $eventDate = null; diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php index fbaa6a0..b6e1ac9 100644 --- a/app/Http/Controllers/Api/EmployerWorkerController.php +++ b/app/Http/Controllers/Api/EmployerWorkerController.php @@ -52,8 +52,9 @@ private function formatWorker(Worker $w) ]; $photo = $photos[$w->id % 4]; - $rating = 4.0 + (($w->id * 3) % 10) / 10.0; - $reviewsCount = ($w->id * 4) % 20 + 2; + $dbReviews = \App\Models\Review::where('worker_id', $w->id)->get(); + $reviewsCount = $dbReviews->count(); + $rating = $reviewsCount > 0 ? round($dbReviews->avg('rating'), 1) : 0.0; return [ 'id' => $w->id, @@ -829,24 +830,9 @@ public function getWorkerDetail(Request $request, $id) if ($reviewsCount > 0) { $rating = round($dbReviews->avg('rating'), 1); } else { - $rating = 4.0 + (($w->id * 3) % 10) / 10.0; - $reviewsCount = ($w->id * 4) % 20 + 2; - $reviews = [ - [ - 'id' => 1, - 'employer_name' => 'Fatima Al Mansoori', - 'rating' => 5, - 'date' => 'May 10, 2026', - 'comment' => 'Extremely reliable and respectful. Professional work and great communication.', - ], - [ - 'id' => 2, - 'employer_name' => 'Michael Harrison', - 'rating' => 4, - 'date' => 'Mar 24, 2026', - 'comment' => 'Very punctual, did exactly what was expected. Highly recommend.', - ] - ]; + $rating = 0.0; + $reviewsCount = 0; + $reviews = []; } // Similar workers matching web view diff --git a/app/Http/Controllers/Api/SponsorController.php b/app/Http/Controllers/Api/SponsorController.php index d45671e..e0c80b3 100644 --- a/app/Http/Controllers/Api/SponsorController.php +++ b/app/Http/Controllers/Api/SponsorController.php @@ -183,6 +183,7 @@ public function postCharityEvent(Request $request) 'body' => $request->body, 'type' => $request->type ?? 'charity', 'sponsor_id' => $sponsor->id, + 'status' => 'pending', ]); return response()->json([ diff --git a/app/Http/Controllers/Api/WorkerAnnouncementController.php b/app/Http/Controllers/Api/WorkerAnnouncementController.php index 3711439..ec1d1a7 100644 --- a/app/Http/Controllers/Api/WorkerAnnouncementController.php +++ b/app/Http/Controllers/Api/WorkerAnnouncementController.php @@ -20,21 +20,44 @@ public function getAnnouncements(Request $request) $page = (int)$request->input('page', 1); $perPage = (int)$request->input('per_page', 15); - $query = Announcement::with('employer.employerProfile')->where('status', 'approved')->latest(); + $query = Announcement::with(['employer.employerProfile', 'sponsor'])->where('status', 'approved')->latest(); $total = $query->count(); $offset = ($page - 1) * $perPage; $announcements = $query->skip($offset)->take($perPage)->get() ->map(function ($announcement) { + $postedBy = 'System'; + $organization = 'Migrant Support'; + + if ($announcement->sponsor_id) { + $postedBy = $announcement->sponsor->full_name; + $organization = $announcement->sponsor->organization_name; + } elseif ($announcement->employer_id) { + $postedBy = $announcement->employer->name; + $organization = $announcement->employer->employerProfile->company_name ?? 'Employer'; + } + + // Decode charity details if they exist in json + $charityDetails = null; + $content = $announcement->body; + if (strpos($announcement->body, '{"type":"Charity"') === 0) { + $decoded = json_decode($announcement->body, true); + if ($decoded) { + $charityDetails = $decoded; + $content = $decoded['content'] ?? $announcement->body; + } + } + return [ 'id' => $announcement->id, 'title' => $announcement->title, - 'body' => $announcement->body, + 'body' => $content, 'type' => $announcement->type, - 'employer_name' => $announcement->employer->name ?? 'System', - 'company_name' => $announcement->employer->employerProfile->company_name ?? 'Migrant Support', + 'employer_name' => $postedBy, + 'company_name' => $organization, 'created_at' => $announcement->created_at->toISOString(), 'time_ago' => $announcement->created_at->diffForHumans(), + 'charity_details' => $charityDetails, ]; }); diff --git a/app/Http/Controllers/Api/WorkerAuthController.php b/app/Http/Controllers/Api/WorkerAuthController.php index 847093c..b85be59 100644 --- a/app/Http/Controllers/Api/WorkerAuthController.php +++ b/app/Http/Controllers/Api/WorkerAuthController.php @@ -41,6 +41,7 @@ public function skills() return [ 'id' => $skill->id, 'name' => $skill->name, + 'image_url' => $skill->image_path ? asset('storage/' . $skill->image_path) : null, ]; }); @@ -304,9 +305,6 @@ public function register(Request $request) 'gender' => 'nullable|string|in:male,female,other', 'live_in_out' => 'nullable|string|in:live_in,live_out', 'preferred_location' => 'nullable|string|max:255', - 'country' => 'nullable|string|max:100', - 'city' => 'nullable|string|max:100', - 'area' => 'nullable|string|max:100', ]); if ($validator->fails()) { @@ -386,9 +384,6 @@ public function register(Request $request) 'in_country' => $inCountry, 'visa_status' => $inCountry ? $request->visa_status : null, 'preferred_job_type' => $request->preferred_job_type, - 'country' => $request->country, - 'city' => $request->city, - 'area' => $request->area, ]); if (!empty($skillsArray)) { diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index 08c38f3..aedba66 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -562,20 +562,44 @@ public function getDashboard(Request $request) } // 4. Latest charity events / announcements list - $charityEvents = Announcement::with('employer.employerProfile') + $charityEvents = Announcement::with(['employer.employerProfile', 'sponsor']) + ->where('status', 'approved') ->latest() ->limit(5) ->get() ->map(function ($event) { + $postedBy = 'System'; + $organization = 'Migrant Support'; + + if ($event->sponsor_id) { + $postedBy = $event->sponsor->full_name; + $organization = $event->sponsor->organization_name; + } elseif ($event->employer_id) { + $postedBy = $event->employer->name; + $organization = $event->employer->employerProfile->company_name ?? 'Employer'; + } + + // Decode charity details if they exist in json + $charityDetails = null; + $content = $event->body; + if (strpos($event->body, '{"type":"Charity"') === 0) { + $decoded = json_decode($event->body, true); + if ($decoded) { + $charityDetails = $decoded; + $content = $decoded['content'] ?? $event->body; + } + } + return [ 'id' => $event->id, 'title' => $event->title, - 'body' => $event->body, + 'body' => $content, 'type' => $event->type, - 'employer_name' => $event->employer->name ?? 'System', - 'company_name' => $event->employer->employerProfile->company_name ?? 'Migrant Support', + 'employer_name' => $postedBy, + 'company_name' => $organization, 'created_at' => $event->created_at->toIso8601String(), 'time_ago' => $event->created_at->diffForHumans(), + 'charity_details' => $charityDetails, ]; }); diff --git a/app/Http/Controllers/Employer/AnnouncementController.php b/app/Http/Controllers/Employer/AnnouncementController.php index aa44f31..cdc3fb0 100644 --- a/app/Http/Controllers/Employer/AnnouncementController.php +++ b/app/Http/Controllers/Employer/AnnouncementController.php @@ -11,7 +11,13 @@ class AnnouncementController extends Controller { public function index(Request $request) { - $dbAnnouncements = Announcement::where('status', 'approved')->latest()->get(); + $sess = session('user'); + $sessId = is_array($sess) ? ($sess['id'] ?? null) : ($sess->id ?? null); + + $dbAnnouncements = Announcement::where('status', 'approved') + ->orWhere('employer_id', $sessId) + ->latest() + ->get(); $announcements = $dbAnnouncements->map(function ($ann) { $isCharity = true; @@ -45,6 +51,7 @@ public function index(Request $request) 'audience' => 'Charity', 'isCharity' => $isCharity, 'charityDetails' => $charityDetails, + 'status' => $ann->status ?? 'pending', 'created_at' => $ann->created_at->diffForHumans(), ]; })->toArray(); @@ -84,6 +91,7 @@ public function store(Request $request) 'body' => $body, 'type' => 'Charity', 'employer_id' => $sessId, + 'status' => 'pending', ]); return back()->with('success', 'Charity Event posted successfully.'); diff --git a/app/Http/Controllers/Employer/CandidateController.php b/app/Http/Controllers/Employer/CandidateController.php index c69c562..f7e5577 100644 --- a/app/Http/Controllers/Employer/CandidateController.php +++ b/app/Http/Controllers/Employer/CandidateController.php @@ -70,6 +70,11 @@ public function index(Request $request) 'salary' => (int)$w->salary, 'status' => $status, 'applied_at' => $app->created_at->format('M d, Y'), + 'preferred_location' => $w->preferred_location, + 'preferred_job_type' => $w->preferred_job_type, + 'live_in_out' => $w->live_in_out, + 'in_country' => (bool)$w->in_country, + 'visa_status' => $w->visa_status, ]; })->filter()->values()->toArray(); @@ -97,6 +102,11 @@ public function index(Request $request) 'salary' => (int)$offer->salary, 'status' => $status, 'applied_at' => $offer->created_at->format('M d, Y'), + 'preferred_location' => $w->preferred_location, + 'preferred_job_type' => $w->preferred_job_type, + 'live_in_out' => $w->live_in_out, + 'in_country' => (bool)$w->in_country, + 'visa_status' => $w->visa_status, ]; })->filter()->values()->toArray(); @@ -108,6 +118,138 @@ public function index(Request $request) return $w && $w['status'] === 'Hired'; })); + // Apply filters: preferred_location, job_type, live_in_out, nationality, in_country, visa_status + if ($request->filled('preferred_location')) { + $prefLoc = $request->preferred_location; + $locsArray = is_array($prefLoc) ? $prefLoc : array_filter(array_map('trim', explode(',', $prefLoc))); + $locsArray = array_map('strtolower', $locsArray); + + $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($locsArray) { + if (!isset($c['preferred_location'])) return false; + $wLoc = strtolower($c['preferred_location']); + foreach ($locsArray as $l) { + if (str_contains($wLoc, $l)) { + return true; + } + } + return false; + })); + } + + $jobTypeParam = $request->input('job_type') ?? $request->input('preferred_job_type'); + if ($jobTypeParam) { + $jobTypesArray = is_array($jobTypeParam) ? $jobTypeParam : array_filter(array_map('trim', explode(',', $jobTypeParam))); + $jobTypesArray = array_map('strtolower', $jobTypesArray); + + $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($jobTypesArray) { + if (!isset($c['preferred_job_type'])) return false; + $wJobType = strtolower($c['preferred_job_type']); + foreach ($jobTypesArray as $jt) { + if (str_contains($wJobType, $jt)) { + return true; + } + } + return false; + })); + } + + $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))); + $accsArray = array_map(function($val) { + return str_replace(['_', '-'], ' ', strtolower($val)); + }, $accsArray); + + $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($accsArray) { + if (!isset($c['live_in_out'])) return false; + $wAcc = str_replace(['_', '-'], ' ', strtolower($c['live_in_out'])); + foreach ($accsArray as $a) { + if (str_contains($wAcc, $a)) { + return true; + } + } + return false; + })); + } + + if ($request->filled('nationality')) { + $natInput = $request->nationality; + $natsArray = is_array($natInput) ? $natInput : array_filter(array_map('trim', explode(',', $natInput))); + $natsArray = array_map('strtolower', $natsArray); + + $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($natsArray) { + if (!isset($c['nationality'])) return false; + $wNat = strtolower($c['nationality']); + foreach ($natsArray as $n) { + if (str_contains($wNat, $n) || $wNat === $n) { + return true; + } + } + return false; + })); + } + + 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) { + $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($isInCountry) { + return isset($c['in_country']) && (bool)$c['in_country'] === $isInCountry; + })); + } + } + } + + if ($request->has('out_country')) { + $outCountryVal = $request->input('out_country'); + if ($outCountryVal !== null && $outCountryVal !== '') { + $isOutCountry = filter_var($outCountryVal, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + if ($isOutCountry === null) { + $strVal = strtolower(trim($outCountryVal)); + if ($strVal === 'out' || $strVal === 'out_country' || $strVal === 'yes') { + $isOutCountry = true; + } elseif ($strVal === 'in' || $strVal === 'in_country' || $strVal === 'no') { + $isOutCountry = false; + } + } + if ($isOutCountry !== null) { + $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($isOutCountry) { + return isset($c['in_country']) && (bool)$c['in_country'] !== $isOutCountry; + })); + } + } + } + + $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))); + $visasArray = array_map('strtolower', $visasArray); + + $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($visasArray) { + if (!isset($c['visa_status'])) return false; + $isInCountry = isset($c['in_country']) && (bool)$c['in_country']; + if (!$isInCountry) { + return false; + } + $wVisa = strtolower($c['visa_status']); + foreach ($visasArray as $v) { + if (str_contains($wVisa, $v)) { + return true; + } + } + return false; + })); + } + return Inertia::render('Employer/SelectedCandidates', [ 'selectedWorkers' => $mergedCandidates, ]); diff --git a/app/Http/Controllers/Employer/MessageController.php b/app/Http/Controllers/Employer/MessageController.php index ff691ad..12a0f35 100644 --- a/app/Http/Controllers/Employer/MessageController.php +++ b/app/Http/Controllers/Employer/MessageController.php @@ -235,34 +235,6 @@ public function send(Request $request, $id) // Touch conversation updated_at for sorting $conv->touch(); - // Check if employer sent predefined message "are you looking job?" - if ($request->text) { - $textLower = strtolower($request->text); - if ( - str_contains($textLower, 'looking job') || - str_contains($textLower, 'looking for a job') || - str_contains($textLower, 'looking for job') || - str_contains($textLower, 'are you looking') - ) { - // Automatically simulate worker response 'Yes' (triggers the S6 hired flow) - $workerResponseText = 'Yes'; - - // Create the message in database - Message::create([ - 'conversation_id' => $conv->id, - 'sender_type' => 'worker', - 'sender_id' => $conv->worker_id, - 'text' => $workerResponseText, - ]); - - // Process the worker response to update status to Hired! - $worker = Worker::find($conv->worker_id); - if ($worker) { - self::processWorkerResponse($conv, $worker, $workerResponseText); - } - } - } - return back(); } diff --git a/app/Http/Controllers/Employer/ShortlistController.php b/app/Http/Controllers/Employer/ShortlistController.php index 62bae6b..79d0ded 100644 --- a/app/Http/Controllers/Employer/ShortlistController.php +++ b/app/Http/Controllers/Employer/ShortlistController.php @@ -85,8 +85,9 @@ public function index(Request $request) ]; $photo = $photos[$w->id % 4]; - $rating = 4.0 + (($w->id * 3) % 10) / 10.0; - $reviewsCount = ($w->id * 4) % 20 + 2; + $dbReviews = \App\Models\Review::where('worker_id', $w->id)->get(); + $reviewsCount = $dbReviews->count(); + $rating = $reviewsCount > 0 ? round($dbReviews->avg('rating'), 1) : 0.0; return [ 'id' => $w->id, diff --git a/app/Http/Controllers/Employer/SupportTicketController.php b/app/Http/Controllers/Employer/SupportTicketController.php index e19c925..f9af728 100644 --- a/app/Http/Controllers/Employer/SupportTicketController.php +++ b/app/Http/Controllers/Employer/SupportTicketController.php @@ -6,6 +6,7 @@ use App\Models\SupportTicket; use App\Models\SupportTicketReply; use App\Models\User; +use App\Models\ReportReason; use Illuminate\Http\Request; use Inertia\Inertia; @@ -41,6 +42,7 @@ public function index(Request $request) } $tickets = SupportTicket::where('user_id', $user->id) + ->with('reason') ->orderBy('created_at', 'desc') ->get() ->map(function ($ticket) { @@ -48,6 +50,7 @@ public function index(Request $request) 'id' => $ticket->id, 'ticket_number' => $ticket->ticket_number, 'subject' => $ticket->subject, + 'reason' => $ticket->reason ? $ticket->reason->reason : null, 'status' => $ticket->status, 'priority' => $ticket->priority, 'created_at' => $ticket->created_at->format('Y-m-d H:i'), @@ -67,7 +70,14 @@ public function create(Request $request) return redirect()->route('employer.login'); } - return Inertia::render('Employer/Support/Create'); + $reasons = ReportReason::where('status', 'Active') + ->where('type', 'Support') + ->orderBy('reason', 'asc') + ->get(['id', 'reason', 'type']); + + return Inertia::render('Employer/Support/Create', [ + 'reasons' => $reasons, + ]); } public function store(Request $request) @@ -78,6 +88,7 @@ public function store(Request $request) } $request->validate([ + 'reason_id' => 'nullable|exists:report_reasons,id', 'subject' => 'required|string|max:255', 'description' => 'required|string', 'priority' => 'required|string|in:low,medium,high', @@ -86,6 +97,7 @@ public function store(Request $request) $ticket = SupportTicket::create([ 'ticket_number' => 'TKT-' . rand(100000, 999999), 'user_id' => $user->id, + 'reason_id' => $request->reason_id, 'subject' => $request->subject, 'description' => $request->description, 'priority' => $request->priority, @@ -103,7 +115,7 @@ public function show(Request $request, $id) return redirect()->route('employer.login'); } - $ticket = SupportTicket::where('user_id', $user->id)->findOrFail($id); + $ticket = SupportTicket::where('user_id', $user->id)->with('reason')->findOrFail($id); $replies = SupportTicketReply::where('support_ticket_id', $ticket->id) ->with(['user', 'worker']) @@ -125,6 +137,7 @@ public function show(Request $request, $id) 'id' => $ticket->id, 'ticket_number' => $ticket->ticket_number, 'subject' => $ticket->subject, + 'reason' => $ticket->reason ? $ticket->reason->reason : null, 'description' => $ticket->description, 'status' => $ticket->status, 'priority' => $ticket->priority, diff --git a/app/Http/Controllers/Employer/WorkerController.php b/app/Http/Controllers/Employer/WorkerController.php index fe1d9b9..5d58afe 100644 --- a/app/Http/Controllers/Employer/WorkerController.php +++ b/app/Http/Controllers/Employer/WorkerController.php @@ -62,7 +62,7 @@ public function index(Request $request) // 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]; + $preferredJobType = $w->preferred_job_type ?? $jobTypes[$w->id % 4]; // Emirates ID verification status (now dynamic passport status) $emiratesIdStatus = $w->emirates_id_status; @@ -85,8 +85,9 @@ public function index(Request $request) ]; $photo = $photos[$w->id % 4]; - $rating = 4.0 + (($w->id * 3) % 10) / 10.0; - $reviewsCount = ($w->id * 4) % 20 + 2; + $dbReviews = \App\Models\Review::where('worker_id', $w->id)->get(); + $reviewsCount = $dbReviews->count(); + $rating = $reviewsCount > 0 ? round($dbReviews->avg('rating'), 1) : 0.0; return [ 'id' => $w->id, @@ -111,9 +112,144 @@ public function index(Request $request) 'bio' => $w->bio, 'rating' => $rating, 'reviews_count' => $reviewsCount, + 'preferred_location' => $w->preferred_location, + 'in_country' => (bool) $w->in_country, ]; })->filter()->values()->toArray(); + // Apply request filters: preferred_location, job_type, live_in_out, nationality, in_country, visa_status + if ($request->filled('preferred_location')) { + $prefLoc = $request->preferred_location; + $locsArray = is_array($prefLoc) ? $prefLoc : array_filter(array_map('trim', explode(',', $prefLoc))); + $locsArray = array_map('strtolower', $locsArray); + + $workers = array_values(array_filter($workers, function ($c) use ($locsArray) { + if (!isset($c['preferred_location'])) return false; + $wLoc = strtolower($c['preferred_location']); + foreach ($locsArray as $l) { + if (str_contains($wLoc, $l)) { + return true; + } + } + return false; + })); + } + + $jobTypeParam = $request->input('job_type') ?? $request->input('preferred_job_type'); + if ($jobTypeParam) { + $jobTypesArray = is_array($jobTypeParam) ? $jobTypeParam : array_filter(array_map('trim', explode(',', $jobTypeParam))); + $jobTypesArray = array_map('strtolower', $jobTypesArray); + + $workers = array_values(array_filter($workers, function ($c) use ($jobTypesArray) { + if (!isset($c['preferred_job_type'])) return false; + $wJobType = strtolower($c['preferred_job_type']); + foreach ($jobTypesArray as $jt) { + if (str_contains($wJobType, $jt)) { + return true; + } + } + return false; + })); + } + + $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))); + $accsArray = array_map(function($val) { + return str_replace(['_', '-'], ' ', strtolower($val)); + }, $accsArray); + + $workers = array_values(array_filter($workers, function ($c) use ($accsArray) { + if (!isset($c['live_in_out'])) return false; + $wAcc = str_replace(['_', '-'], ' ', strtolower($c['live_in_out'])); + foreach ($accsArray as $a) { + if (str_contains($wAcc, $a)) { + return true; + } + } + return false; + })); + } + + if ($request->filled('nationality')) { + $natInput = $request->nationality; + $natsArray = is_array($natInput) ? $natInput : array_filter(array_map('trim', explode(',', $natInput))); + $natsArray = array_map('strtolower', $natsArray); + + $workers = array_values(array_filter($workers, function ($c) use ($natsArray) { + if (!isset($c['nationality'])) return false; + $wNat = strtolower($c['nationality']); + foreach ($natsArray as $n) { + if (str_contains($wNat, $n) || $wNat === $n) { + return true; + } + } + return false; + })); + } + + 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) { + $workers = array_values(array_filter($workers, function ($c) use ($isInCountry) { + return isset($c['in_country']) && (bool)$c['in_country'] === $isInCountry; + })); + } + } + } + + if ($request->has('out_country')) { + $outCountryVal = $request->input('out_country'); + if ($outCountryVal !== null && $outCountryVal !== '') { + $isOutCountry = filter_var($outCountryVal, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + if ($isOutCountry === null) { + $strVal = strtolower(trim($outCountryVal)); + if ($strVal === 'out' || $strVal === 'out_country' || $strVal === 'yes') { + $isOutCountry = true; + } elseif ($strVal === 'in' || $strVal === 'in_country' || $strVal === 'no') { + $isOutCountry = false; + } + } + if ($isOutCountry !== null) { + $workers = array_values(array_filter($workers, function ($c) use ($isOutCountry) { + return isset($c['in_country']) && (bool)$c['in_country'] !== $isOutCountry; + })); + } + } + } + + $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))); + $visasArray = array_map('strtolower', $visasArray); + + $workers = array_values(array_filter($workers, function ($c) use ($visasArray) { + if (!isset($c['visa_status'])) return false; + $isInCountry = isset($c['in_country']) && (bool)$c['in_country']; + if (!$isInCountry) { + return false; + } + $wVisa = strtolower($c['visa_status']); + foreach ($visasArray as $v) { + if (str_contains($wVisa, $v)) { + return true; + } + } + return false; + })); + } + + $shortlistedIds = Shortlist::where('employer_id', $employerId)->pluck('worker_id')->toArray(); $dbCategories = WorkerCategory::pluck('name')->toArray(); @@ -190,28 +326,7 @@ public function show($id) })->toArray(); $reviewsCount = count($reviews); - if ($reviewsCount > 0) { - $rating = round($dbReviews->avg('rating'), 1); - } else { - $rating = 4.0 + (($w->id * 3) % 10) / 10.0; - $reviewsCount = ($w->id * 4) % 20 + 2; - $reviews = [ - [ - 'id' => 1, - 'employer_name' => 'Fatima Al Mansoori', - 'rating' => 5, - 'date' => 'May 10, 2026', - 'comment' => 'Extremely reliable and respectful. Professional work and great communication.', - ], - [ - 'id' => 2, - 'employer_name' => 'Michael Harrison', - 'rating' => 4, - 'date' => 'Mar 24, 2026', - 'comment' => 'Very punctual, did exactly what was expected. Highly recommend.', - ] - ]; - } + $rating = $reviewsCount > 0 ? round($dbReviews->avg('rating'), 1) : 0; $simDb = Worker::with('category') ->where('id', '!=', $w->id) diff --git a/app/Models/Skill.php b/app/Models/Skill.php index 2928332..e43c2a2 100644 --- a/app/Models/Skill.php +++ b/app/Models/Skill.php @@ -9,7 +9,7 @@ class Skill extends Model { use HasFactory; - protected $fillable = ['name']; + protected $fillable = ['name', 'image_path']; public function workers() { diff --git a/app/Models/SupportTicket.php b/app/Models/SupportTicket.php index 9cf60f0..691a866 100644 --- a/app/Models/SupportTicket.php +++ b/app/Models/SupportTicket.php @@ -12,6 +12,7 @@ class SupportTicket extends Model protected $fillable = [ 'ticket_number', 'user_id', + 'reason_id', 'worker_id', 'subject', 'description', @@ -24,6 +25,11 @@ public function user() return $this->belongsTo(User::class, 'user_id'); } + public function reason() + { + return $this->belongsTo(ReportReason::class, 'reason_id'); + } + public function worker() { return $this->belongsTo(Worker::class, 'worker_id'); diff --git a/database/migrations/2026_06_09_123604_add_reason_id_to_support_tickets_table.php b/database/migrations/2026_06_09_123604_add_reason_id_to_support_tickets_table.php new file mode 100644 index 0000000..436528e --- /dev/null +++ b/database/migrations/2026_06_09_123604_add_reason_id_to_support_tickets_table.php @@ -0,0 +1,24 @@ +unsignedBigInteger('reason_id')->nullable()->after('user_id'); + $table->foreign('reason_id')->references('id')->on('report_reasons')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('support_tickets', function (Blueprint $table) { + $table->dropForeign(['reason_id']); + $table->dropColumn('reason_id'); + }); + } +}; diff --git a/database/migrations/2026_06_09_152000_seed_support_reasons_to_report_reasons_table.php b/database/migrations/2026_06_09_152000_seed_support_reasons_to_report_reasons_table.php new file mode 100644 index 0000000..2812814 --- /dev/null +++ b/database/migrations/2026_06_09_152000_seed_support_reasons_to_report_reasons_table.php @@ -0,0 +1,29 @@ + 'Billing & Payment Issues', 'type' => 'Support', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Subscription Plan Changes', 'type' => 'Support', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Technical Bugs & Site Errors', 'type' => 'Support', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Worker Profile Verification Help', 'type' => 'Support', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Employer Account Settings', 'type' => 'Support', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'General Feedback & Suggestions', 'type' => 'Support', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ['reason' => 'Other Queries', 'type' => 'Support', 'status' => 'Active', 'created_at' => now(), 'updated_at' => now()], + ]; + + DB::table('report_reasons')->insert($reasons); + } + + public function down(): void + { + DB::table('report_reasons')->where('type', 'Support')->delete(); + } +}; diff --git a/database/migrations/2026_06_09_154000_seed_new_skills_to_skills_table.php b/database/migrations/2026_06_09_154000_seed_new_skills_to_skills_table.php new file mode 100644 index 0000000..fc71f63 --- /dev/null +++ b/database/migrations/2026_06_09_154000_seed_new_skills_to_skills_table.php @@ -0,0 +1,25 @@ + 'Ironing', 'created_at' => now(), 'updated_at' => now()], + ['name' => 'Car Washing', 'created_at' => now(), 'updated_at' => now()], + ['name' => 'Pet Sitting', 'created_at' => now(), 'updated_at' => now()], + ]; + + foreach ($skills as $skill) { + DB::table('skills')->updateOrInsert(['name' => $skill['name']], $skill); + } + } + + public function down(): void + { + DB::table('skills')->whereIn('name', ['Ironing', 'Car Washing', 'Pet Sitting'])->delete(); + } +}; diff --git a/database/migrations/2026_06_09_155000_add_image_path_to_skills_table.php b/database/migrations/2026_06_09_155000_add_image_path_to_skills_table.php new file mode 100644 index 0000000..0275cc9 --- /dev/null +++ b/database/migrations/2026_06_09_155000_add_image_path_to_skills_table.php @@ -0,0 +1,22 @@ +string('image_path')->nullable()->after('name'); + }); + } + + public function down(): void + { + Schema::table('skills', function (Blueprint $table) { + $table->dropColumn('image_path'); + }); + } +}; diff --git a/public/swagger.json b/public/swagger.json index 490284d..ea39e7a 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -438,23 +438,20 @@ }, "live_in_out": { "type": "string", - "example": "Live-out", - "description": "Accommodation preference (e.g. 'Live-in', 'Live-out')." + "enum": ["live_in", "live_out"], + "example": "live_out", + "description": "Accommodation preference (live_in, live_out)." }, - "country": { + "gender": { "type": "string", - "example": "UAE", - "description": "Preferred location country." + "enum": ["male", "female", "other"], + "example": "male", + "description": "Gender of the worker (male, female, other)." }, - "city": { + "preferred_location": { "type": "string", - "example": "Dubai", - "description": "Preferred location city." - }, - "area": { - "type": "string", - "example": "Marina", - "description": "Preferred location area." + "example": "Dubai Marina", + "description": "Preferred work location/area." } } } @@ -998,6 +995,23 @@ 8 ], "description": "Array of skill IDs" + }, + "gender": { + "type": "string", + "enum": ["male", "female", "other"], + "example": "male", + "description": "Gender of the worker (male, female, other)." + }, + "live_in_out": { + "type": "string", + "enum": ["live_in", "live_out"], + "example": "live_out", + "description": "Accommodation preference (live_in, live_out)." + }, + "preferred_location": { + "type": "string", + "example": "Dubai Marina", + "description": "Preferred work location/area." } } } @@ -2618,6 +2632,60 @@ "type": "string" } }, + { + "name": "preferred_location", + "in": "query", + "required": false, + "description": "Filter by preferred location (comma-separated list, e.g. Dubai,Abu Dhabi).", + "schema": { + "type": "string" + } + }, + { + "name": "job_type", + "in": "query", + "required": false, + "description": "Filter by job type (comma-separated list, e.g. full-time,part-time).", + "schema": { + "type": "string" + } + }, + { + "name": "accommodation_type", + "in": "query", + "required": false, + "description": "Filter by accommodation type (comma-separated list, e.g. live_in,live_out).", + "schema": { + "type": "string" + } + }, + { + "name": "in_country", + "in": "query", + "required": false, + "description": "Filter by in country status (true/false or yes/no or in/out).", + "schema": { + "type": "string" + } + }, + { + "name": "out_country", + "in": "query", + "required": false, + "description": "Filter by out country status (true/false or yes/no or out/in).", + "schema": { + "type": "string" + } + }, + { + "name": "visa_status", + "in": "query", + "required": false, + "description": "Filter by next visa status (comma-separated list, e.g. Residence Visa,Tourist Visa). Only applicable if in country.", + "schema": { + "type": "string" + } + }, { "name": "availability", "in": "query", @@ -2864,6 +2932,60 @@ "type": "string" } }, + { + "name": "preferred_location", + "in": "query", + "required": false, + "description": "Filter candidates by preferred location (comma-separated list, e.g. Dubai,Abu Dhabi).", + "schema": { + "type": "string" + } + }, + { + "name": "job_type", + "in": "query", + "required": false, + "description": "Filter candidates by job type (comma-separated list, e.g. full-time,part-time).", + "schema": { + "type": "string" + } + }, + { + "name": "accommodation_type", + "in": "query", + "required": false, + "description": "Filter candidates by accommodation type (comma-separated list, e.g. live_in,live_out).", + "schema": { + "type": "string" + } + }, + { + "name": "in_country", + "in": "query", + "required": false, + "description": "Filter candidates by in country status (true/false or yes/no or in/out).", + "schema": { + "type": "string" + } + }, + { + "name": "out_country", + "in": "query", + "required": false, + "description": "Filter candidates by out country status (true/false or yes/no or out/in).", + "schema": { + "type": "string" + } + }, + { + "name": "visa_status", + "in": "query", + "required": false, + "description": "Filter candidates by next visa status (comma-separated list, e.g. Residence Visa,Tourist Visa). Only applicable if in country.", + "schema": { + "type": "string" + } + }, { "name": "availability", "in": "query", @@ -3759,6 +3881,15 @@ "type": "string", "example": "Live-out" }, + "gender": { + "type": "string", + "enum": ["male", "female", "other"], + "example": "male" + }, + "preferred_location": { + "type": "string", + "example": "Dubai Marina" + }, "country": { "type": "string", "example": "UAE" diff --git a/public/uploads/documents/1780988095_passport_passport.pdf b/public/uploads/documents/1780988095_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780988183_passport_passport.pdf b/public/uploads/documents/1780988183_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780988392_passport_passport.pdf b/public/uploads/documents/1780988392_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780988597_passport_passport.pdf b/public/uploads/documents/1780988597_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780988676_passport_passport.pdf b/public/uploads/documents/1780988676_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780988798_passport_passport.pdf b/public/uploads/documents/1780988798_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780988812_passport_passport.pdf b/public/uploads/documents/1780988812_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780988845_passport_passport.pdf b/public/uploads/documents/1780988845_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780989037_passport_passport.pdf b/public/uploads/documents/1780989037_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780989071_passport_passport.pdf b/public/uploads/documents/1780989071_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780989259_passport_passport.pdf b/public/uploads/documents/1780989259_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780990037_passport_passport.pdf b/public/uploads/documents/1780990037_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780990124_passport_passport.pdf b/public/uploads/documents/1780990124_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780990245_passport_passport.pdf b/public/uploads/documents/1780990245_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780990329_passport_passport.pdf b/public/uploads/documents/1780990329_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780990399_passport_passport.pdf b/public/uploads/documents/1780990399_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780990412_passport_mds1.png b/public/uploads/documents/1780990412_passport_mds1.png new file mode 100644 index 0000000..ae18a13 Binary files /dev/null and b/public/uploads/documents/1780990412_passport_mds1.png differ diff --git a/public/uploads/documents/1780990465_passport_passport.pdf b/public/uploads/documents/1780990465_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780990749_passport_passport.pdf b/public/uploads/documents/1780990749_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780991005_passport_passport.pdf b/public/uploads/documents/1780991005_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780991060_passport_passport.pdf b/public/uploads/documents/1780991060_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/documents/1780996836_passport_1000100671.jpg b/public/uploads/documents/1780996836_passport_1000100671.jpg new file mode 100644 index 0000000..66666c9 Binary files /dev/null and b/public/uploads/documents/1780996836_passport_1000100671.jpg differ diff --git a/public/uploads/documents/1780996836_visa_1000100671.jpg b/public/uploads/documents/1780996836_visa_1000100671.jpg new file mode 100644 index 0000000..66666c9 Binary files /dev/null and b/public/uploads/documents/1780996836_visa_1000100671.jpg differ diff --git a/public/uploads/documents/1780999934_passport_1000418145.jpg b/public/uploads/documents/1780999934_passport_1000418145.jpg new file mode 100644 index 0000000..7d2ecb2 Binary files /dev/null and b/public/uploads/documents/1780999934_passport_1000418145.jpg differ diff --git a/public/uploads/documents/1780999934_visa_1000418145.jpg b/public/uploads/documents/1780999934_visa_1000418145.jpg new file mode 100644 index 0000000..7d2ecb2 Binary files /dev/null and b/public/uploads/documents/1780999934_visa_1000418145.jpg differ diff --git a/public/uploads/documents/1781002405_passport_1000100687.jpg b/public/uploads/documents/1781002405_passport_1000100687.jpg new file mode 100644 index 0000000..d9a219b Binary files /dev/null and b/public/uploads/documents/1781002405_passport_1000100687.jpg differ diff --git a/public/uploads/documents/1781002405_visa_1000100687.jpg b/public/uploads/documents/1781002405_visa_1000100687.jpg new file mode 100644 index 0000000..d9a219b Binary files /dev/null and b/public/uploads/documents/1781002405_visa_1000100687.jpg differ diff --git a/public/uploads/documents/1781082894_passport_passport.pdf b/public/uploads/documents/1781082894_passport_passport.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780988093_license_license.pdf b/public/uploads/licenses/1780988093_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780988183_license_license.pdf b/public/uploads/licenses/1780988183_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780988391_license_license.pdf b/public/uploads/licenses/1780988391_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780988597_license_license.pdf b/public/uploads/licenses/1780988597_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780988676_license_license.pdf b/public/uploads/licenses/1780988676_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780988798_license_license.pdf b/public/uploads/licenses/1780988798_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780988812_license_license.pdf b/public/uploads/licenses/1780988812_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780988844_license_license.pdf b/public/uploads/licenses/1780988844_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780989036_license_license.pdf b/public/uploads/licenses/1780989036_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780989071_license_license.pdf b/public/uploads/licenses/1780989071_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780989258_license_license.pdf b/public/uploads/licenses/1780989258_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780990035_license_license.pdf b/public/uploads/licenses/1780990035_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780990120_license_license.pdf b/public/uploads/licenses/1780990120_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780990245_license_license.pdf b/public/uploads/licenses/1780990245_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780990328_license_license.pdf b/public/uploads/licenses/1780990328_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780990399_license_license.pdf b/public/uploads/licenses/1780990399_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780990462_license_license.pdf b/public/uploads/licenses/1780990462_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780990748_license_license.pdf b/public/uploads/licenses/1780990748_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780991005_license_license.pdf b/public/uploads/licenses/1780991005_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780991060_license_license.pdf b/public/uploads/licenses/1780991060_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1780997733_license_fDi5IHoHP14P_1024_500.png b/public/uploads/licenses/1780997733_license_fDi5IHoHP14P_1024_500.png new file mode 100644 index 0000000..24d963e Binary files /dev/null and b/public/uploads/licenses/1780997733_license_fDi5IHoHP14P_1024_500.png differ diff --git a/public/uploads/licenses/1781082884_license_license.pdf b/public/uploads/licenses/1781082884_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/public/uploads/licenses/1781082893_license_license.pdf b/public/uploads/licenses/1781082893_license_license.pdf new file mode 100644 index 0000000..e69de29 diff --git a/resources/js/Layouts/AdminLayout.jsx b/resources/js/Layouts/AdminLayout.jsx index c47166d..0326a11 100644 --- a/resources/js/Layouts/AdminLayout.jsx +++ b/resources/js/Layouts/AdminLayout.jsx @@ -18,7 +18,8 @@ import { BarChart3, History, List, - LifeBuoy + LifeBuoy, + Heart } from 'lucide-react'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; @@ -39,7 +40,7 @@ export default function AdminLayout({ children, title = 'Dashboard' }) { { name: 'Campaigns & Alerts', href: '/admin/notifications', icon: BellRing }, { name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 }, { name: 'System Audit Logs', href: '/admin/audit-logs', icon: History }, - { name: 'Announcements', href: '/admin/announcements', icon: Megaphone }, + { name: 'Charity Events', href: '/admin/announcements', icon: Heart }, { name: 'Skills', href: '/admin/master-data/skills', icon: Settings }, { name: 'Reason Master', href: '/admin/master-data/reasons', icon: List }, ]; diff --git a/resources/js/Pages/Admin/Announcements/Index.jsx b/resources/js/Pages/Admin/Announcements/Index.jsx index 99e6c2e..83ff9b0 100644 --- a/resources/js/Pages/Admin/Announcements/Index.jsx +++ b/resources/js/Pages/Admin/Announcements/Index.jsx @@ -1,12 +1,43 @@ import React, { useState } from 'react'; import { Head, router } from '@inertiajs/react'; import AdminLayout from '../../../Layouts/AdminLayout'; -import { Megaphone, Plus, Trash2, Send, CheckCircle2, Check, X } from 'lucide-react'; +import { + Heart, Plus, Trash2, Send, CheckCircle2, Check, X, + Calendar, MapPin, Clock, Gift, Sparkles, Search, + ChevronDown, ChevronUp, Building, User, XCircle, Eye, BellRing +} from 'lucide-react'; + +const hoursList = [ + '12:00', '12:30', '01:00', '01:30', '02:00', '02:30', + '03:00', '03:30', '04:00', '04:30', '05:00', '05:30', + '06:00', '06:30', '07:00', '07:30', '08:00', '08:30', + '09:00', '09:30', '10:00', '10:30', '11:00', '11:30' +]; export default function Announcements({ initialAnnouncements }) { const [announcements, setAnnouncements] = useState(initialAnnouncements || []); const [isFormOpen, setIsFormOpen] = useState(false); - const [newAnnouncement, setNewAnnouncement] = useState({ title: '', content: '', audience: 'Both' }); + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('All'); + const [expandedCards, setExpandedCards] = useState({}); + + // Time picker states + const [startTimeHour, setStartTimeHour] = useState('09:00'); + const [startTimeAmpm, setStartTimeAmpm] = useState('AM'); + const [endTimeHour, setEndTimeHour] = useState('04:00'); + const [endTimeAmpm, setEndTimeAmpm] = useState('PM'); + const [isStartPickerOpen, setIsStartPickerOpen] = useState(false); + const [isEndPickerOpen, setIsEndPickerOpen] = useState(false); + + const [newAnnouncement, setNewAnnouncement] = useState({ + title: '', + content: '', + provided_items: '', + event_date: '', + event_time: '09:00 AM - 04:00 PM', + location_details: '', + location_pin: '' + }); const [errors, setErrors] = useState({}); const [toastMessage, setToastMessage] = useState(null); @@ -19,7 +50,16 @@ export default function Announcements({ initialAnnouncements }) { e.preventDefault(); let newErrors = {}; if (!newAnnouncement.title.trim()) newErrors.title = 'Title is required'; - if (!newAnnouncement.content.trim()) newErrors.content = 'Content is required'; + if (!newAnnouncement.content.trim()) newErrors.content = 'Description is required'; + if (!newAnnouncement.provided_items.trim()) newErrors.provided_items = 'Provided items is required'; + if (!newAnnouncement.event_date.trim()) newErrors.event_date = 'Event date is required'; + if (!newAnnouncement.event_time.trim()) newErrors.event_time = 'Event time is required'; + if (!newAnnouncement.location_details.trim()) newErrors.location_details = 'Location details is required'; + if (!newAnnouncement.location_pin.trim()) { + newErrors.location_pin = 'Location pin URL is required'; + } else if (!newAnnouncement.location_pin.startsWith('http://') && !newAnnouncement.location_pin.startsWith('https://')) { + newErrors.location_pin = 'Must be a valid URL'; + } if (Object.keys(newErrors).length > 0) { setErrors(newErrors); @@ -29,22 +69,45 @@ export default function Announcements({ initialAnnouncements }) { router.post(route('admin.announcements.store'), { title: newAnnouncement.title, content: newAnnouncement.content, - type: newAnnouncement.audience === 'Both' ? 'info' : (newAnnouncement.audience === 'Employers' ? 'success' : 'warning') + provided_items: newAnnouncement.provided_items, + event_date: newAnnouncement.event_date, + event_time: newAnnouncement.event_time, + location_details: newAnnouncement.location_details, + location_pin: newAnnouncement.location_pin }, { onSuccess: () => { - setNewAnnouncement({ title: '', content: '', audience: 'Both' }); + setNewAnnouncement({ + title: '', + content: '', + provided_items: '', + event_date: '', + event_time: '09:00 AM - 04:00 PM', + location_details: '', + location_pin: '' + }); + setStartTimeHour('09:00'); + setStartTimeAmpm('AM'); + setEndTimeHour('04:00'); + setEndTimeAmpm('PM'); setErrors({}); setIsFormOpen(false); - showToast('Announcement broadcasted successfully'); + showToast('Charity Event created successfully'); setTimeout(() => window.location.reload(), 500); } }); }; + const updateEventTime = (sh, sa, eh, ea) => { + setNewAnnouncement(prev => ({ + ...prev, + event_time: `${sh} ${sa} - ${eh} ${ea}` + })); + }; + const handleApprove = (id) => { router.post(route('admin.announcements.approve', id), {}, { onSuccess: () => { - showToast('Announcement approved successfully'); + showToast('Charity Event approved successfully'); setTimeout(() => window.location.reload(), 500); } }); @@ -53,26 +116,53 @@ export default function Announcements({ initialAnnouncements }) { const handleReject = (id) => { router.post(route('admin.announcements.reject', id), {}, { onSuccess: () => { - showToast('Announcement rejected successfully'); + showToast('Charity Event rejected successfully'); setTimeout(() => window.location.reload(), 500); } }); }; const handleDelete = (id) => { - if (confirm('Are you sure you want to delete this announcement?')) { + if (confirm('Are you sure you want to delete this charity event?')) { router.delete(route('admin.announcements.delete', id), { onSuccess: () => { - showToast('Announcement deleted'); + showToast('Charity Event deleted'); setTimeout(() => window.location.reload(), 500); } }); } }; + const toggleExpand = (id) => { + setExpandedCards(prev => ({ + ...prev, + [id]: !prev[id] + })); + }; + + // Calculate dynamic statistics + const totalDrives = announcements.length; + const pendingCount = announcements.filter(a => a.status === 'pending').length; + const approvedCount = announcements.filter(a => a.status === 'approved').length; + const rejectedCount = announcements.filter(a => a.status === 'rejected').length; + + // Filter announcements + const filteredAnnouncements = announcements.filter(ann => { + const matchesSearch = + ann.title.toLowerCase().includes(searchQuery.toLowerCase()) || + ann.content.toLowerCase().includes(searchQuery.toLowerCase()) || + (ann.posted_by && ann.posted_by.toLowerCase().includes(searchQuery.toLowerCase())) || + (ann.organization && ann.organization.toLowerCase().includes(searchQuery.toLowerCase())) || + (ann.charityDetails?.provided_items && ann.charityDetails.provided_items.toLowerCase().includes(searchQuery.toLowerCase())); + + const matchesStatus = statusFilter === 'All' || ann.status === statusFilter.toLowerCase(); + + return matchesSearch && matchesStatus; + }); + return ( - - + + {/* Toast Notification */} {toastMessage && ( @@ -82,64 +172,342 @@ export default function Announcements({ initialAnnouncements }) { )} -
-
+
+ + {/* Upper Header section */} +
-

Platform Announcements & Events

-

Approve user-submitted events or broadcast messages to all users.

+

Charity Events & Drives

+

Manage, approve, or launch community charity initiatives.

+ {/* Dashboard Stats Overview (Compact) */} +
+
+
+ +
+
+
Total Drives
+
{totalDrives}
+
+
+
+
+ +
+
+
Pending
+
{pendingCount}
+
+
+
+
+ +
+
+
Approved
+
{approvedCount}
+
+
+
+
+ +
+
+
Rejected
+
{rejectedCount}
+
+
+
+ + {/* Search & Filtering Control Bar */} +
+ {/* Search Field */} +
+ + setSearchQuery(e.target.value)} + placeholder="Search events by title, description, or sponsor..." + className="w-full pl-10 pr-4 py-2 text-sm bg-slate-50 border border-slate-200 rounded-lg outline-none focus:bg-white focus:border-[#0F6E56] focus:ring-2 focus:ring-[#0F6E56]/10 transition-all" + /> + {searchQuery && ( + + )} +
+ + {/* Filter Tabs */} +
+ {['All', 'Pending', 'Approved', 'Rejected'].map((tab) => ( + + ))} +
+
+ + {/* Event Creation Modal */} {isFormOpen && ( -
-
-
- -

Compose Message

+
+
+
+
+ +

Compose Charity Event

+
+
- + setNewAnnouncement({ ...newAnnouncement, title: e.target.value })} - className={`w-full px-4 py-2.5 rounded-xl border ${errors.title ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5] outline-none transition-all`} - placeholder="Enter announcement title..." + className={`w-full px-4 py-2.5 rounded-xl border ${errors.title ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none transition-all`} + placeholder="e.g. Free Dental checkup by Emirates Charity" /> - {errors.title &&

{errors.title}

} + {errors.title &&

{errors.title}

}
+ +
+
+ + setNewAnnouncement({ ...newAnnouncement, event_date: e.target.value })} + className={`w-full h-11 px-3.5 rounded-xl border ${errors.event_date ? 'border-red-500' : 'border-slate-300'} text-xs focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none transition-all`} + /> + {errors.event_date &&

{errors.event_date}

} +
+ +
+ {isStartPickerOpen && ( +
setIsStartPickerOpen(false)} /> + )} +
+ + + + {isStartPickerOpen && ( +
+
+ {hoursList.map(h => ( + + ))} +
+
+ {['AM', 'PM'].map(ampm => ( + + ))} +
+
+ )} +
+
+ +
+ {isEndPickerOpen && ( +
setIsEndPickerOpen(false)} /> + )} +
+ + + + {isEndPickerOpen && ( +
+
+ {hoursList.map(h => ( + + ))} +
+
+ {['AM', 'PM'].map(ampm => ( + + ))} +
+
+ )} +
+
+
+
- + + setNewAnnouncement({ ...newAnnouncement, provided_items: e.target.value })} + className={`w-full px-4 py-2.5 rounded-xl border ${errors.provided_items ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none transition-all`} + placeholder="e.g. Free Medical Checks & Food Supplies" + /> + {errors.provided_items &&

{errors.provided_items}

} +
+ +
+
+ + setNewAnnouncement({ ...newAnnouncement, location_details: e.target.value })} + className={`w-full px-4 py-2.5 rounded-xl border ${errors.location_details ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none transition-all`} + placeholder="e.g. Al Quoz Community Center, Dubai" + /> + {errors.location_details &&

{errors.location_details}

} +
+
+ + setNewAnnouncement({ ...newAnnouncement, location_pin: e.target.value })} + className={`w-full px-4 py-2.5 rounded-xl border ${errors.location_pin ? 'border-red-500' : 'border-slate-300'} text-sm focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] outline-none transition-all`} + placeholder="e.g. https://maps.app.goo.gl/xyz" + /> + {errors.location_pin &&

{errors.location_pin}

} +
+
+ +
+ - {errors.content &&

{errors.content}

} + {errors.content &&

{errors.content}

}
-
- - -
-
+ +
@@ -160,73 +528,157 @@ export default function Announcements({ initialAnnouncements }) {
)} -
- {announcements.length === 0 ? ( -
- -

No announcements published yet.

-
- ) : ( - announcements.map((ann) => ( -
-
-
-

{ann.title}

- - Type: {ann.type} - - - Status: {ann.status} - + {/* Compact Grid of Events */} + {filteredAnnouncements.length === 0 ? ( +
+ +

No charity events match your criteria.

+
+ ) : ( +
+ {filteredAnnouncements.map((ann) => { + const details = ann.charityDetails; + const isExpanded = !!expandedCards[ann.id]; + + // Color accents based on status + const statusColorClass = + ann.status === 'approved' ? 'border-l-emerald-500' : + ann.status === 'rejected' ? 'border-l-rose-500' : + 'border-l-amber-500'; + + return ( +
+ {/* Top Card Area */} +
+
+
+
+

+ + {ann.title} +

+ + {ann.status} + + {ann.status === 'approved' && ( + + + Push & Reminder Scheduled + + )} +
+
+ + {ann.organization} + + + {ann.posted_by} +
+
+ + {/* Action Control Buttons */} +
+ {ann.status === 'pending' && ( +
+ + +
+ )} + +
+
+ + {/* Collapsible Content */} +
+

+ {ann.content} +

+ + {ann.content.length > 90 && ( + + )} +
+ + {/* Details metadata row (compact icons) */} + {details && ( +
+
+ + + {details.provided_items} + +
+
+ + + {details.event_date} + +
+
+ + + {details.location_details} + +
+
+ )}
-

{ann.content}

-
- Posted by: {ann.posted_by} ({ann.organization}) - + + {/* Bottom Card Footer */} +
Published: {ann.created_at} + {details?.location_pin && ( + + View on Map + + + )}
-
- {ann.status === 'pending' && ( - <> - - - - )} - -
-
- )) - )} -
+ ); + })} +
+ )}
); diff --git a/resources/js/Pages/Admin/Dashboard.jsx b/resources/js/Pages/Admin/Dashboard.jsx index c707945..8825f0b 100644 --- a/resources/js/Pages/Admin/Dashboard.jsx +++ b/resources/js/Pages/Admin/Dashboard.jsx @@ -114,22 +114,23 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
- {/* Verification Stats */} + {/* Employers Registered & active/inactive */}
- Verification & OCR Rate -
- + Employers Registered +
+

- {stats?.verification_rate || 0}% + {stats?.total_employers?.toLocaleString() || 0}

-

- - {stats?.verified_workers_count?.toLocaleString() || 0} Profiles Auto-OCR Verified -

+
+ {stats?.active_employers || 0} Active + + {stats?.inactive_employers || 0} Inactive +
diff --git a/resources/js/Pages/Admin/Disputes/Index.jsx b/resources/js/Pages/Admin/Disputes/Index.jsx index 65210f0..a216ff6 100644 --- a/resources/js/Pages/Admin/Disputes/Index.jsx +++ b/resources/js/Pages/Admin/Disputes/Index.jsx @@ -289,12 +289,6 @@ export default function DisputesHub({ tickets }) {
-
diff --git a/resources/js/Pages/Admin/MasterData/WorkerSkills.jsx b/resources/js/Pages/Admin/MasterData/WorkerSkills.jsx index 6aaf6d5..3bf3555 100644 --- a/resources/js/Pages/Admin/MasterData/WorkerSkills.jsx +++ b/resources/js/Pages/Admin/MasterData/WorkerSkills.jsx @@ -31,11 +31,13 @@ export default function WorkerSkills({ skills }) { const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingSkill, setEditingSkill] = useState(null); const [skillName, setSkillName] = useState(''); + const [imageFile, setImageFile] = useState(null); const [error, setError] = useState(''); const handleOpenAdd = () => { setEditingSkill(null); setSkillName(''); + setImageFile(null); setError(''); setIsDialogOpen(true); }; @@ -43,6 +45,7 @@ export default function WorkerSkills({ skills }) { const handleOpenEdit = (skill) => { setEditingSkill(skill); setSkillName(skill.name); + setImageFile(null); setError(''); setIsDialogOpen(true); }; @@ -54,23 +57,28 @@ export default function WorkerSkills({ skills }) { return; } + const formData = { + name: skillName.trim() + }; + if (imageFile) { + formData.image = imageFile; + } + if (editingSkill) { - router.post(`/admin/master-data/skills/${editingSkill.id}/update`, { - name: skillName.trim() - }, { + router.post(`/admin/master-data/skills/${editingSkill.id}/update`, formData, { onSuccess: () => { setIsDialogOpen(false); + setImageFile(null); }, onError: (err) => { setError(err.name || 'Failed to update skill.'); } }); } else { - router.post('/admin/master-data/skills', { - name: skillName.trim() - }, { + router.post('/admin/master-data/skills', formData, { onSuccess: () => { setIsDialogOpen(false); + setImageFile(null); }, onError: (err) => { setError(err.name || 'Failed to add skill.'); @@ -147,7 +155,16 @@ export default function WorkerSkills({ skills }) { - {skill.name} +
+ {skill.image_url ? ( + {skill.name} + ) : ( +
+ {skill.name.charAt(0)} +
+ )} + {skill.name} +
@@ -222,6 +239,22 @@ export default function WorkerSkills({ skills }) {

{error}

)}
+ +
+ + {editingSkill && editingSkill.image_url && !imageFile && ( +
+ Current + Current Image +
+ )} + setImageFile(e.target.files[0])} + className="w-full text-xs text-slate-500 file:mr-4 file:py-2.5 file:px-4 file:rounded-xl file:border-0 file:text-xs file:font-black file:uppercase file:bg-teal-50 file:text-[#0F6E56] hover:file:bg-teal-100 cursor-pointer" + /> +
diff --git a/resources/js/Pages/Admin/Safety/Index.jsx b/resources/js/Pages/Admin/Safety/Index.jsx index dcd4e53..c7954cd 100644 --- a/resources/js/Pages/Admin/Safety/Index.jsx +++ b/resources/js/Pages/Admin/Safety/Index.jsx @@ -303,12 +303,6 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
-
@@ -247,22 +205,24 @@ export default function Show({ ticket, replies }) {
- {/* Dev response toggle */} - + {/* Toggles Group */} +
+ {/* Close ticket toggle */} + +
-
{worker.phone} • {worker.email}
+
{worker.phone}
@@ -314,9 +314,6 @@ export default function WorkerManagement({ workers }) {
- {worker.category} -
-
{worker.nationality} • {worker.experience}
@@ -374,21 +371,6 @@ export default function WorkerManagement({ workers }) { )} - - - handleAvailabilityOverride(worker.id, 'Available Now')} - className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-slate-700 hover:bg-slate-50" - > - Mark Available - - - handleAvailabilityOverride(worker.id, 'Engaged')} - className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-slate-700 hover:bg-slate-50" - > - Mark Engaged -
@@ -423,8 +405,15 @@ export default function WorkerManagement({ workers }) { {selectedWorker?.name ? selectedWorker.name.charAt(0) : ''}
-

{selectedWorker?.name}

-

{selectedWorker?.nationality} • ID #{selectedWorker?.id}

+
+

{selectedWorker?.name}

+ + {selectedWorker?.verified ? 'Verified' : 'Pending'} + +
+

{selectedWorker?.nationality} • ID #{selectedWorker?.id}

@@ -483,16 +472,6 @@ export default function WorkerManagement({ workers }) {

Classification & Experience

-
- - setEditForm({ ...editForm, category: e.target.value })} - className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20" - required - /> -
-
- -
@@ -236,7 +400,7 @@ export default function Announcements({ initialAnnouncements }) {
-
+
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-[#185FA5]/20 transition-all outline-none" + /> +
+ + {/* Status Filters */} +
+ {['All', 'Pending', 'Approved', 'Rejected'].map((status) => ( + + ))} +
+
+ +
{filteredAnnouncements.length === 0 ? ( -
+
@@ -259,94 +457,100 @@ export default function Announcements({ initialAnnouncements }) { ) : ( filteredAnnouncements.map((ann) => { const details = ann.charityDetails; + const isExpanded = !!expandedCards[ann.id]; + const statusColorClass = + ann.status === 'approved' ? 'border-l-emerald-500' : + ann.status === 'rejected' ? 'border-l-rose-500' : + 'border-l-amber-500'; return (
-
- -
-
- - - {t('community_charity_drive', 'COMMUNITY CHARITY DRIVE')} - - -
- - {ann.created_at} -
- -
- - {t('push_reminder_scheduled', 'Push & Reminder Scheduled')} + {/* Top Card Area */} +
+
+
+
+

+ + {ann.title} +

+ + {ann.status === 'approved' ? t('status_approved', 'APPROVED') : (ann.status === 'rejected' ? t('status_rejected', 'REJECTED') : t('status_pending', 'PENDING'))} + + {ann.status === 'approved' && ( + + + {t('push_reminder_scheduled', 'Push & Reminder Scheduled')} + + )} +
-
-

- - {ann.title} -

-

{ann.content}

+ {/* Collapsible Content */} +
+

+ {ann.content} +

+ + {ann.content.length > 90 && ( + + )}
+ {/* Details metadata row (compact icons) */} {details && ( -
-
-
- -
-
-
{t('provided_packages', 'Provided Packages')}
-
{details.provided_items}
-
+
+
+ + + {details.provided_items} +
- -
-
- -
-
-
{t('event_timing', 'Event Timing')}
-
- {details.event_date} • {details.event_time} -
-
+
+ + + {details.event_date} +
- -
-
- -
-
-
-
{t('event_location_address', 'Event Location Address')}
-
{details.location_details}
-
- {details.location_pin && ( - - - {t('view_map_location_pin', 'View Map Location Pin')} - - )} -
+
+ + + {details.location_details} +
)}
-
- + {/* Bottom Card Footer */} +
+ {t('posted_label', 'Posted')}: {ann.created_at} + {details?.location_pin && ( + + {t('view_on_map', 'View on Map')} + + + )}
); diff --git a/resources/js/Pages/Employer/Messages/Show.jsx b/resources/js/Pages/Employer/Messages/Show.jsx index 3391ca6..e433afe 100644 --- a/resources/js/Pages/Employer/Messages/Show.jsx +++ b/resources/js/Pages/Employer/Messages/Show.jsx @@ -123,37 +123,7 @@ export default function Show({ conversation, initialMessages, conversations = [] }, { preserveScroll: true, onSuccess: () => { - // Show immediate confirmation of the push alert popping up on worker mobile app - toast.success(t('direct_push_sent', 'Direct Mobile Push Notification sent!'), { - description: t('popped_up_instantly_desc', "Popped up instantly on {name}'s mobile device.").replace('{name}', conversation.worker_name), - duration: 4500, - }); - - // Simulate worker typing response shortly after - setTimeout(() => { - setIsTyping(true); - setTimeout(() => { - setIsTyping(false); - - // Reload conversation to pull actual database messages (including automated worker 'Yes') - router.reload({ - preserveScroll: true, - onSuccess: () => { - if (isLookingJobQuery) { - toast.success(t('candidate_hired_success', "Candidate response: YES! Automatically marked as HIRED!"), { - description: t('candidate_hired_desc', "{name} has been successfully added to your Hired candidate pipeline.").replace('{name}', conversation.worker_name), - duration: 6000, - }); - } else { - toast.info(t('new_message_from', 'New message from {name}!').replace('{name}', conversation.worker_name), { - description: t('auto_reply_text', "Thank you for your response! I have reviewed your offer proposal and am looking forward to our video interview."), - duration: 5000 - }); - } - } - }); - }, 2000); - }, 1500); + router.reload({ preserveScroll: true }); } }); }; diff --git a/resources/js/Pages/Employer/SelectedCandidates.jsx b/resources/js/Pages/Employer/SelectedCandidates.jsx index 26b6f86..c37574b 100644 --- a/resources/js/Pages/Employer/SelectedCandidates.jsx +++ b/resources/js/Pages/Employer/SelectedCandidates.jsx @@ -1,26 +1,18 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { Head, Link, router } from '@inertiajs/react'; import EmployerLayout from '../../Layouts/EmployerLayout'; import { useTranslation } from '../../lib/LanguageContext'; import { - CheckCircle2, - Globe2, - Briefcase, - DollarSign, MessageSquare, UserCheck, Search, - FileText, - Download, - Printer, - Copy, - FileSpreadsheet, - FileJson, Filter, - ChevronRight, MoreHorizontal, - Users, - Send + MapPin, + Briefcase, + Home, + Globe2, + Plane } from 'lucide-react'; import { DropdownMenu, @@ -30,31 +22,212 @@ import { DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import FilterDrawer, { FilterSection, FilterSelect, FilterInput, FilterChips } from '../../Components/Employer/FilterDrawer'; export default function SelectedCandidates({ selectedWorkers }) { const { t } = useTranslation(); const [workers, setWorkers] = useState((selectedWorkers || []).filter(w => w.status !== 'Searching')); const [searchTerm, setSearchTerm] = useState(''); + // Drawer state + const [drawerOpen, setDrawerOpen] = useState(false); + + // Filter States + const [filterLocation, setFilterLocation] = useState(''); + const [filterJobType, setFilterJobType] = useState('All'); + const [filterAccommodation, setFilterAccommodation] = useState('All'); + const [filterNationality, setFilterNationality] = useState('All'); + const [filterInCountry, setFilterInCountry] = useState('All'); + const [filterVisaType, setFilterVisaType] = useState('All'); + useEffect(() => { setWorkers((selectedWorkers || []).filter(w => w.status !== 'Searching')); }, [selectedWorkers]); const changeStatus = (id, newStatus) => { - // Optimistic UI state update setWorkers(workers.map(w => w.id === id ? { ...w, status: newStatus } : w)); - - // Persist change to database router.post(`/employer/candidates/${id}/status`, { status: newStatus }, { preserveScroll: true }); }; + // Extract unique nationalities for filter dropdown + const nationalities = useMemo(() => { + const nats = (selectedWorkers || []).map(w => w.nationality).filter(Boolean); + return [...new Set(nats)]; + }, [selectedWorkers]); + + // Apply Client-Side Filters + const filteredWorkers = useMemo(() => { + let list = (selectedWorkers || []).filter(w => w.status !== 'Searching'); + + if (searchTerm.trim() !== '') { + const query = searchTerm.toLowerCase(); + list = list.filter(w => w.name.toLowerCase().includes(query)); + } + if (filterLocation.trim() !== '') { + const query = filterLocation.toLowerCase(); + list = list.filter(w => w.preferred_location && w.preferred_location.toLowerCase().includes(query)); + } + if (filterJobType !== 'All') { + list = list.filter(w => w.preferred_job_type && w.preferred_job_type.toLowerCase() === filterJobType.toLowerCase()); + } + if (filterAccommodation !== 'All') { + list = list.filter(w => w.live_in_out && w.live_in_out.toLowerCase() === filterAccommodation.toLowerCase()); + } + if (filterNationality !== 'All') { + list = list.filter(w => w.nationality && w.nationality.toLowerCase() === filterNationality.toLowerCase()); + } + if (filterInCountry !== 'All') { + const wantInCountry = filterInCountry === 'In Country'; + list = list.filter(w => { + const wVal = w.in_country; + return (wVal === true || wVal === '1' || wVal === 1) === wantInCountry; + }); + } + if (filterInCountry === 'In Country' && filterVisaType !== 'All') { + list = list.filter(w => w.visa_status && w.visa_status.toLowerCase().includes(filterVisaType.toLowerCase())); + } + + return list; + }, [selectedWorkers, searchTerm, filterLocation, filterJobType, filterAccommodation, filterNationality, filterInCountry, filterVisaType]); + + const resetFilters = () => { + setFilterLocation(''); + setFilterJobType('All'); + setFilterAccommodation('All'); + setFilterNationality('All'); + setFilterInCountry('All'); + setFilterVisaType('All'); + }; + + const activeFilterCount = useMemo(() => { + let count = 0; + if (filterLocation.trim() !== '') count++; + if (filterJobType !== 'All') count++; + if (filterAccommodation !== 'All') count++; + if (filterNationality !== 'All') count++; + if (filterInCountry !== 'All') count++; + if (filterInCountry === 'In Country' && filterVisaType !== 'All') count++; + return count; + }, [filterLocation, filterJobType, filterAccommodation, filterNationality, filterInCountry, filterVisaType]); + + // Active filter tags for display + const activeFilterTags = useMemo(() => { + const tags = []; + if (filterLocation.trim()) tags.push({ key: 'location', label: `📍 ${filterLocation}`, clear: () => setFilterLocation('') }); + if (filterJobType !== 'All') tags.push({ key: 'job', label: `💼 ${filterJobType}`, clear: () => setFilterJobType('All') }); + if (filterAccommodation !== 'All') tags.push({ key: 'acc', label: `🏠 ${filterAccommodation.replace('_', '-')}`, clear: () => setFilterAccommodation('All') }); + if (filterNationality !== 'All') tags.push({ key: 'nat', label: `🌍 ${filterNationality}`, clear: () => setFilterNationality('All') }); + if (filterInCountry !== 'All') tags.push({ key: 'country', label: `✈️ ${filterInCountry}`, clear: () => { setFilterInCountry('All'); setFilterVisaType('All'); } }); + if (filterInCountry === 'In Country' && filterVisaType !== 'All') tags.push({ key: 'visa', label: `🪪 ${filterVisaType}`, clear: () => setFilterVisaType('All') }); + return tags; + }, [filterLocation, filterJobType, filterAccommodation, filterNationality, filterInCountry, filterVisaType]); + return ( -
+ {/* ── Filter Drawer ── */} + setDrawerOpen(false)} + onReset={resetFilters} + activeCount={activeFilterCount} + title="Filter Candidates" + > + {/* Preferred Location */} + + setFilterLocation(e.target.value)} + placeholder="e.g. Dubai, Abu Dhabi…" + /> + + + {/* Job Type */} + + + + + {/* Accommodation */} + + + + + {/* Nationality */} + + setFilterNationality(e.target.value)} + > + + {nationalities.map(nat => ( + + ))} + + + + {/* Country Status */} + + { + setFilterInCountry(val); + if (val !== 'In Country') setFilterVisaType('All'); + }} + /> + + + {/* Visa Type */} + +
+ setFilterVisaType(e.target.value)} + disabled={filterInCountry !== 'In Country'} + > + + + + + + + + {filterInCountry !== 'In Country' && ( +

+ Select "In Country" to enable this filter +

+ )} +
+
+
+ +
{/* Header Section */}
@@ -65,32 +238,76 @@ export default function SelectedCandidates({ selectedWorkers }) { {/* Table Section */}
-
-
+ {/* Toolbar */} +
+ {/* Search */} +
setSearchTerm(e.target.value)} + onChange={e => setSearchTerm(e.target.value)} />
-
-
+
+ {/* Results count */} + + {filteredWorkers.length} result{filteredWorkers.length !== 1 ? 's' : ''} + + + {/* Export actions */} +
{['COPY', 'CSV', 'PDF', 'PRINT'].map(action => ( ))}
-
+ {/* Active filter tags */} + {activeFilterTags.length > 0 && ( +
+ Active: + {activeFilterTags.map(tag => ( + + ))} + +
+ )} + + {/* Table */}
@@ -104,57 +321,67 @@ export default function SelectedCandidates({ selectedWorkers }) { - {workers.length > 0 ? ( - workers.filter(w => w.name.toLowerCase().includes(searchTerm.toLowerCase())).map((worker) => ( - - - - - - - - - )) + {filteredWorkers.length > 0 ? ( + filteredWorkers.map((worker) => ( + + + + + + + + + )) ) : ( - @@ -163,21 +390,15 @@ export default function SelectedCandidates({ selectedWorkers }) {
-
-
- {worker.name.charAt(0)} -
-
-
{worker.name}
-
{worker.nationality}
-
-
-
Nov 14, 2026 - {worker.category} - - {worker.salary} AED - - - - {t('hired', 'Hired').toUpperCase()} - - -
- - - - - - -
-
+
+
+ {worker.name.charAt(0)} +
+
+
{worker.name}
+
+ + {worker.nationality || '—'} +
+
+
+
{worker.applied_at || '—'} + {worker.category} + + {worker.salary} AED + + + + {t('hired', 'Hired').toUpperCase()} + + +
+ + + + + + +
+
+
- +
+ +
{t('no_candidates_found', 'No candidates found')}
+ {activeFilterCount > 0 && ( + + )}
- {/* Pagination Footer */} + {/* Footer */}
- {t('showing_candidates', 'Showing {start} to {end} of {total} candidates') - .replace('{start}', 1) - .replace('{end}', workers.length) - .replace('{total}', workers.length)} + Showing {filteredWorkers.length} of {(selectedWorkers || []).filter(w => w.status !== 'Searching').length} candidates
-
- - -
- + +
diff --git a/resources/js/Pages/Employer/Support/Create.jsx b/resources/js/Pages/Employer/Support/Create.jsx index 0c2f463..9ffe383 100644 --- a/resources/js/Pages/Employer/Support/Create.jsx +++ b/resources/js/Pages/Employer/Support/Create.jsx @@ -9,10 +9,11 @@ import { CheckCircle } from 'lucide-react'; -export default function Create() { +export default function Create({ reasons = [] }) { const { t } = useTranslation(); const { data, setData, post, processing, errors } = useForm({ + reason_id: '', subject: '', description: '', priority: 'medium', @@ -51,6 +52,28 @@ export default function Create() {
+ {/* Support Reason */} +
+ + + {errors.reason_id && ( +

{errors.reason_id}

+ )} +
+ {/* Subject */}
diff --git a/resources/js/Pages/Employer/Support/Index.jsx b/resources/js/Pages/Employer/Support/Index.jsx index 8f67c1b..209f910 100644 --- a/resources/js/Pages/Employer/Support/Index.jsx +++ b/resources/js/Pages/Employer/Support/Index.jsx @@ -162,6 +162,11 @@ export default function Index({ tickets }) { {ticket.ticket_number} + {ticket.reason && ( + + {ticket.reason} + + )} {getPriorityBadge(ticket.priority)}

diff --git a/resources/js/Pages/Employer/Support/Show.jsx b/resources/js/Pages/Employer/Support/Show.jsx index 6912369..62f7975 100644 --- a/resources/js/Pages/Employer/Support/Show.jsx +++ b/resources/js/Pages/Employer/Support/Show.jsx @@ -122,6 +122,12 @@ export default function Show({ ticket, replies }) { {t('raised_on', 'Raised on:')} {ticket.created_at}

+ {ticket.reason && ( +
+ Support Category: + {ticket.reason} +
+ )}

{ticket.subject}

diff --git a/resources/js/Pages/Employer/Workers/Index.jsx b/resources/js/Pages/Employer/Workers/Index.jsx index 39a7593..fe25fad 100644 --- a/resources/js/Pages/Employer/Workers/Index.jsx +++ b/resources/js/Pages/Employer/Workers/Index.jsx @@ -28,6 +28,7 @@ import { ShieldCheck, User } from 'lucide-react'; +import FilterDrawer, { FilterSection, FilterSelect, FilterInput, FilterChips, FilterCheckboxList } from '../../../Components/Employer/FilterDrawer'; const getLanguageFlag = (lang) => { const flags = { @@ -50,7 +51,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [], const [selectedNationality, setSelectedNationality] = useState('All Nationalities'); const [selectedExperience, setSelectedExperience] = useState('All Experience'); const [selectedReligion, setSelectedReligion] = useState('All Religions'); - const [maxSalary, setMaxSalary] = useState(3000); + const [maxSalary, setMaxSalary] = useState(5000); // Advanced Multi-Select Filters & Sorting const [selectedLanguages, setSelectedLanguages] = useState([]); @@ -58,6 +59,14 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [], const [selectedSkills, setSelectedSkills] = useState([]); const [selectedWorkTypes, setSelectedWorkTypes] = useState([]); const [sortBy, setSortBy] = useState('default'); + + // Advanced Filters Toggle & Details + const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); + const [filterLocation, setFilterLocation] = useState(''); + const [filterJobType, setFilterJobType] = useState('All'); + const [filterAccommodation, setFilterAccommodation] = useState('All'); + const [filterInCountry, setFilterInCountry] = useState('All'); + const [filterVisaType, setFilterVisaType] = useState('All'); // UI Helpers const [isLangOpen, setIsLangOpen] = useState(false); @@ -79,6 +88,9 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [], const [presetNameInput, setPresetNameInput] = useState(''); const [showPresetModal, setShowPresetModal] = useState(false); + // Drawer + const [drawerOpen, setDrawerOpen] = useState(false); + // Worker Comparison & Quick Preview const [comparisonIds, setComparisonIds] = useState([]); const [previewWorker, setPreviewWorker] = useState(null); @@ -89,10 +101,47 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [], const cat = params.get('category'); const nat = params.get('nationality'); const sal = params.get('max_salary'); + const loc = params.get('preferred_location'); + const job = params.get('job_type') || params.get('preferred_job_type'); + const acc = params.get('live_in_out') || params.get('accommodation_type') || params.get('accomadation_type'); + const inCountryVal = params.get('in_country'); + const outCountryVal = params.get('out_country'); + const visa = params.get('visa_status') || params.get('next_visa_type') || params.get('visa_type'); + + let hasAdvanced = false; if (cat) setSelectedCategory(cat); if (nat) setSelectedNationality(nat); if (sal) setMaxSalary(Number(sal)); + if (loc) { + setFilterLocation(loc); + hasAdvanced = true; + } + if (job) { + setFilterJobType(job); + hasAdvanced = true; + } + if (acc) { + setFilterAccommodation(acc); + hasAdvanced = true; + } + if (inCountryVal) { + const isTrue = inCountryVal === 'true' || inCountryVal === '1' || inCountryVal === 'yes' || inCountryVal === 'in' || inCountryVal === 'In Country'; + setFilterInCountry(isTrue ? 'In Country' : 'Out of Country'); + hasAdvanced = true; + } else if (outCountryVal) { + const isTrue = outCountryVal === 'true' || outCountryVal === '1' || outCountryVal === 'yes' || outCountryVal === 'out' || outCountryVal === 'Out of Country'; + setFilterInCountry(isTrue ? 'Out of Country' : 'In Country'); + hasAdvanced = true; + } + if (visa) { + setFilterVisaType(visa); + hasAdvanced = true; + } + + if (hasAdvanced) { + setShowAdvancedFilters(true); + } }, []); const toggleShortlist = (id) => { @@ -150,15 +199,48 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [], setSelectedNationality('All Nationalities'); setSelectedExperience('All Experience'); setSelectedReligion('All Religions'); - setMaxSalary(3000); + setMaxSalary(5000); setSelectedLanguages([]); setSelectedVisaStatuses([]); setSelectedSkills([]); setSelectedWorkTypes([]); setSortBy('default'); setVisibleCount(6); + setFilterLocation(''); + setFilterJobType('All'); + setFilterAccommodation('All'); + setFilterInCountry('All'); + setFilterVisaType('All'); }; + const activeFilterCount = useMemo(() => { + let count = 0; + if (filterLocation.trim() !== '') count++; + if (filterJobType !== 'All') count++; + if (filterAccommodation !== 'All') count++; + if (selectedNationality !== 'All Nationalities') count++; + if (filterInCountry !== 'All') count++; + if (filterInCountry === 'In Country' && filterVisaType !== 'All') count++; + if (selectedSkills.length > 0) count++; + if (selectedLanguages.length > 0) count++; + if (maxSalary < 5000) count++; + return count; + }, [filterLocation, filterJobType, filterAccommodation, selectedNationality, filterInCountry, filterVisaType, selectedSkills, selectedLanguages, maxSalary]); + + const activeFilterTags = useMemo(() => { + const tags = []; + if (filterLocation.trim()) tags.push({ key: 'loc', label: `📍 ${filterLocation}`, clear: () => setFilterLocation('') }); + if (filterJobType !== 'All') tags.push({ key: 'job', label: `💼 ${filterJobType}`, clear: () => setFilterJobType('All') }); + if (filterAccommodation !== 'All') tags.push({ key: 'acc', label: `🏠 ${filterAccommodation.replace('_', '-')}`, clear: () => setFilterAccommodation('All') }); + if (selectedNationality !== 'All Nationalities') tags.push({ key: 'nat', label: `🌍 ${selectedNationality}`, clear: () => setSelectedNationality('All Nationalities') }); + if (filterInCountry !== 'All') tags.push({ key: 'country', label: `✈️ ${filterInCountry}`, clear: () => { setFilterInCountry('All'); setFilterVisaType('All'); } }); + if (filterInCountry === 'In Country' && filterVisaType !== 'All') tags.push({ key: 'visa', label: `🪪 ${filterVisaType}`, clear: () => setFilterVisaType('All') }); + if (selectedSkills.length > 0) tags.push({ key: 'skills', label: `🛠 ${selectedSkills.length} skill${selectedSkills.length > 1 ? 's' : ''}`, clear: () => setSelectedSkills([]) }); + if (selectedLanguages.length > 0) tags.push({ key: 'lang', label: `🗣 ${selectedLanguages.length} lang`, clear: () => setSelectedLanguages([]) }); + if (maxSalary < 5000) tags.push({ key: 'salary', label: `💰 ≤${maxSalary} AED`, clear: () => setMaxSalary(5000) }); + return tags; + }, [filterLocation, filterJobType, filterAccommodation, selectedNationality, filterInCountry, filterVisaType, selectedSkills, selectedLanguages, maxSalary]); + // Filter and Sort Worker List const filteredWorkers = useMemo(() => { let workers = initialWorkers.filter(worker => { @@ -176,7 +258,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [], if (selectedNationality !== 'All Nationalities' && worker.nationality !== selectedNationality) return false; if (selectedExperience !== 'All Experience' && worker.experience !== selectedExperience) return false; if (selectedReligion !== 'All Religions' && worker.religion !== selectedReligion) return false; - if (worker.salary > maxSalary) return false; + if (maxSalary < 5000 && worker.salary > maxSalary) return false; // Multi-select filters if (selectedLanguages.length > 0) { @@ -192,6 +274,34 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [], } if (selectedWorkTypes.length > 0 && !selectedWorkTypes.includes(worker.preferred_job_type)) return false; + // Preferred Location + if (filterLocation.trim() !== '') { + const query = filterLocation.toLowerCase(); + if (!worker.preferred_location || !worker.preferred_location.toLowerCase().includes(query)) return false; + } + + // Job Type + if (filterJobType !== 'All') { + if (!worker.preferred_job_type || worker.preferred_job_type.toLowerCase() !== filterJobType.toLowerCase()) return false; + } + + // Accommodation + if (filterAccommodation !== 'All') { + if (!worker.live_in_out || worker.live_in_out.toLowerCase() !== filterAccommodation.toLowerCase()) return false; + } + + // In/Out Country + if (filterInCountry !== 'All') { + const wantInCountry = filterInCountry === 'In Country'; + const wVal = worker.in_country; + if ((wVal === true || wVal === '1' || wVal === 1) !== wantInCountry) return false; + } + + // Visa Type (if in country) + if (filterInCountry === 'In Country' && filterVisaType !== 'All') { + if (!worker.visa_status || !worker.visa_status.toLowerCase().includes(filterVisaType.toLowerCase())) return false; + } + return true; }); @@ -219,7 +329,12 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [], selectedVisaStatuses, selectedSkills, selectedWorkTypes, - sortBy + sortBy, + filterLocation, + filterJobType, + filterAccommodation, + filterInCountry, + filterVisaType ]); const paginatedWorkers = useMemo(() => { @@ -265,10 +380,145 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
+ {/* ── Filter Drawer ── */} + setDrawerOpen(false)} + onReset={resetFilters} + activeCount={activeFilterCount} + title="Filter Workers" + > + {/* Preferred Location */} + + setFilterLocation(e.target.value)} + placeholder="e.g. Dubai, Abu Dhabi…" + /> + + + {/* Job Type */} + + + + + {/* Accommodation */} + + + + + {/* Nationality */} + + setSelectedNationality(e.target.value)} + > + {filtersMetadata.nationalities?.map(nat => ( + + ))} + + + + {/* Country Status */} + + { + setFilterInCountry(val); + if (val !== 'In Country') setFilterVisaType('All'); + }} + /> + + + {/* Visa Type */} + +
+ setFilterVisaType(e.target.value)} + disabled={filterInCountry !== 'In Country'} + > + + + + + + + + {filterInCountry !== 'In Country' && ( +

Select "In Country" to enable this filter

+ )} +
+
+ + {/* Skills */} + {filtersMetadata.skills?.length > 1 && ( + + s.toLowerCase())} + selected={selectedSkills} + onToggle={skill => toggleMultiSelect(skill, selectedSkills, setSelectedSkills)} + /> + + )} + + {/* Languages */} + {filtersMetadata.languages?.length > 1 && ( + + toggleMultiSelect(lang, selectedLanguages, setSelectedLanguages)} + /> + + )} + + {/* Salary Range */} + + setMaxSalary(Number(e.target.value))} + className="w-full accent-[#185FA5]" + /> +
+ 500 AED + 5,000 AED +
+
+
+ {/* Filter and Search Panel */} -
-
- {/* Core Search bar */} +
+
+ {/* Search */}
- {/* Sort & Reset Actions */} -
- {/* Sort Selector */} -
+
+ {/* Sort */} +
setSelectedNationality(e.target.value)} - className="w-full px-3 py-2.5 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5] font-semibold text-slate-700 cursor-pointer" - > - {filtersMetadata.nationalities?.map(nat => ( - - ))} - + {/* Active filter tags */} + {activeFilterTags.length > 0 && ( +
+ Active: + {activeFilterTags.map(tag => ( + + ))} +
- - {/* Skills */} -
- - - {isSkillsOpen && ( -
- {filtersMetadata.skills?.slice(1).map(skill => ( - - ))} -
- )} -
- - {/* Language */} -
- - - {isLangOpen && ( -
- {filtersMetadata.languages?.slice(1).map(lang => ( - - ))} -
- )} -
- - {/* Visa Status */} -
- - - {isVisaOpen && ( -
- {filtersMetadata.visaStatuses?.slice(1).map(status => ( - - ))} -
- )} -
-
+ )}
{/* Listing metadata bar */} @@ -625,7 +787,8 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [], href={`/employer/workers/${worker.id}`} className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl h-10 font-bold text-xs flex items-center justify-center transition-colors shadow-xs" > - {t('open_full_profile')} + View Profile +
diff --git a/resources/js/Pages/Employer/Workers/Show.jsx b/resources/js/Pages/Employer/Workers/Show.jsx index df22064..32cb3d4 100644 --- a/resources/js/Pages/Employer/Workers/Show.jsx +++ b/resources/js/Pages/Employer/Workers/Show.jsx @@ -232,10 +232,12 @@ export default function Show({ worker }) { {worker.category}
-
- - {worker.rating} / 5.0 ({workerReviews.length} {t('reviews', 'reviews')}) -
+ {workerReviews.length > 0 && ( +
+ + {worker.rating} / 5.0 ({workerReviews.length} {t('reviews', 'reviews')}) +
+ )}
@@ -348,23 +350,13 @@ export default function Show({ worker }) { {worker.passport_status}
-
+
{t('visa_status', 'Visa Status')}
{worker.visa_status}
- -
-
- - {t('medical_health_test', 'Medical Health Test')} -
- - {t('passed_certified', 'PASSED CERTIFIED')} - -
{/* Languages Spoken with visual flags */} @@ -386,13 +378,7 @@ export default function Show({ worker }) { {/* Right Column: Bio & Skills */}
- {/* Professional Summary */} -
-

{t('professional_summary', 'Professional Summary')}

-

- "{worker.bio}" -

-
+ {/* Verified Skills & Competencies */}
@@ -562,40 +548,15 @@ export default function Show({ worker }) {

))} + {workerReviews.length === 0 && ( +
+ {t('no_reviews_yet', 'No reviews posted yet.')} +
+ )}
- {/* Similar Recommendations Slider */} -
-

{t('similar_recommended_workers', 'Similar Recommended Workers')}

-
- {worker.similar_workers?.map((sim) => ( -
-
-
-

{sim.name}

-

{sim.nationality} • {sim.category}

-
- {sim.verified && ( - - {t('verified', 'Verified')} - - )} -
-
- {sim.salary} {t('aed', 'AED')}/{t('mo', 'mo')} - - {t('view_profile', 'View Profile')} - - -
-
- ))} -
-
+
{/* Stage 4/5 Hired Flow Success Modal Overlay */} diff --git a/resources/js/components/Employer/FilterDrawer.jsx b/resources/js/components/Employer/FilterDrawer.jsx new file mode 100644 index 0000000..8996283 --- /dev/null +++ b/resources/js/components/Employer/FilterDrawer.jsx @@ -0,0 +1,213 @@ +import React, { useEffect } from 'react'; +import { X, RotateCcw, SlidersHorizontal } from 'lucide-react'; + +/** + * FilterDrawer — a right-side slide-in drawer for employer portal filter panels. + * + * Props: + * open {boolean} – controls visibility + * onClose {function} – called when backdrop or X is clicked + * onReset {function} – called when "Clear All" is clicked + * activeCount {number} – number of active filters (shown in badge) + * title {string} – drawer heading + * children {node} – filter form content + */ +export default function FilterDrawer({ open, onClose, onReset, activeCount = 0, title = 'Filters', children }) { + // Lock body scroll when drawer is open + useEffect(() => { + if (open) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + return () => { document.body.style.overflow = ''; }; + }, [open]); + + // Close on Escape key + useEffect(() => { + const handler = (e) => { if (e.key === 'Escape') onClose(); }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [onClose]); + + return ( + <> + {/* ── Backdrop ── */} +