diff --git a/app/Http/Controllers/Admin/SupportTicketController.php b/app/Http/Controllers/Admin/SupportTicketController.php index 2e3f8cf..dc82100 100644 --- a/app/Http/Controllers/Admin/SupportTicketController.php +++ b/app/Http/Controllers/Admin/SupportTicketController.php @@ -56,7 +56,7 @@ public function index(Request $request) public function show($id) { - $ticket = SupportTicket::with(['user', 'worker'])->findOrFail($id); + $ticket = SupportTicket::with(['user', 'worker', 'reason'])->findOrFail($id); $replies = SupportTicketReply::where('support_ticket_id', $ticket->id) ->with(['user', 'worker']) @@ -79,6 +79,8 @@ public function show($id) 'ticket_number' => $ticket->ticket_number, 'subject' => $ticket->subject, 'description' => $ticket->description, + 'reason_name' => $ticket->reason ? $ticket->reason->reason : null, + 'voice_note_url' => $ticket->voice_note_path ? asset('storage/' . $ticket->voice_note_path) : null, 'status' => $ticket->status, 'priority' => $ticket->priority, 'user_type' => $ticket->user_id ? 'Employer' : 'Worker', diff --git a/app/Http/Controllers/Api/EmployerMessageController.php b/app/Http/Controllers/Api/EmployerMessageController.php index 854326a..23fff0f 100644 --- a/app/Http/Controllers/Api/EmployerMessageController.php +++ b/app/Http/Controllers/Api/EmployerMessageController.php @@ -269,6 +269,21 @@ public function sendMessage(Request $request, $id) $conv->touch(); }); + // Dispatch push notification to worker + $worker = $conv->worker; + if ($worker && $worker->fcm_token) { + \App\Services\FCMService::sendPushNotification( + $worker->fcm_token, + "New Message from " . ($employer->name ?? "Employer"), + $request->text ?: "Sent an attachment", + [ + 'conversation_id' => $conv->id, + 'sender_type' => 'employer', + 'sender_id' => $employer->id, + ] + ); + } + return response()->json([ 'success' => true, 'message' => 'Message sent successfully.', diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php index b6e1ac9..c8a8620 100644 --- a/app/Http/Controllers/Api/EmployerWorkerController.php +++ b/app/Http/Controllers/Api/EmployerWorkerController.php @@ -69,6 +69,7 @@ private function formatWorker(Worker $w) 'religion' => $w->religion, 'languages' => $langs, 'age' => $w->age, + 'gender' => $w->gender, 'verified' => (bool)$w->verified, 'preferred_job_type' => $preferredJobType, 'bio' => $w->bio, @@ -113,7 +114,14 @@ public function getWorkers(Request $request) $workersArray = $formattedWorkers->toArray(); - // Apply filters: skills, language/languages, nationality, preferred_location, job_type, live_in_out, in_country, visa_status + // Apply filters: gender, skills, language/languages, nationality, preferred_location, job_type, live_in_out, in_country, visa_status + if ($request->filled('gender')) { + $gender = strtolower($request->gender); + $workersArray = array_values(array_filter($workersArray, function ($c) use ($gender) { + return isset($c['gender']) && strtolower($c['gender']) === $gender; + })); + } + if ($request->filled('nationality')) { $natInput = $request->nationality; $natsArray = is_array($natInput) ? $natInput : array_filter(array_map('trim', explode(',', $natInput))); @@ -169,12 +177,22 @@ public function getWorkers(Request $request) $locsArray = is_array($prefLoc) ? $prefLoc : array_filter(array_map('trim', explode(',', $prefLoc))); $locsArray = array_map('strtolower', $locsArray); - $workersArray = array_values(array_filter($workersArray, function ($c) use ($locsArray) { + $locationMapping = [ + 'dubai' => ['dubai', 'marina', 'barsha', 'nahda', 'jumeirah', 'deira', 'downtown', 'silicon', 'sports', 'motor', 'jlt', 'jbr', 'meydan', 'ranches', 'bay', 'mirdif', 'quoz'], + 'abu dhabi' => ['abu dhabi', 'yas', 'khalifa', 'reem', 'saadiyat', 'raha', 'mussafah', 'zahiyah', 'karamah'], + 'oman' => ['oman', 'muscat', 'salalah', 'sohar', 'nizwa', 'sur', 'ibri', 'rustaq'] + ]; + + $workersArray = array_values(array_filter($workersArray, function ($c) use ($locsArray, $locationMapping) { if (!isset($c['preferred_location'])) return false; $wLoc = strtolower($c['preferred_location']); foreach ($locsArray as $l) { - if (str_contains($wLoc, $l)) { - return true; + if ($l === 'all') return true; + $allowedKeywords = $locationMapping[$l] ?? [$l]; + foreach ($allowedKeywords as $keyword) { + if (str_contains($wLoc, $keyword)) { + return true; + } } } return false; @@ -374,6 +392,7 @@ public function getCandidates(Request $request) 'live_in_out' => $w->live_in_out, 'in_country' => (bool)$w->in_country, 'visa_status' => $w->visa_status, + 'gender' => $w->gender, ]; })->filter()->values()->toArray(); @@ -414,6 +433,7 @@ public function getCandidates(Request $request) 'live_in_out' => $w->live_in_out, 'in_country' => (bool)$w->in_country, 'visa_status' => $w->visa_status, + 'gender' => $w->gender, ]; })->filter()->values()->toArray(); @@ -425,7 +445,14 @@ public function getCandidates(Request $request) return $c && $c['status'] === 'Hired'; })); - // Apply filters: skills, languages, nationality, availability, preferred_location, job_type, live_in_out, in_country, visa_status + // Apply filters: gender, skills, languages, nationality, availability, preferred_location, job_type, live_in_out, in_country, visa_status + if ($request->filled('gender')) { + $gender = strtolower($request->gender); + $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($gender) { + return isset($c['gender']) && strtolower($c['gender']) === $gender; + })); + } + if ($request->filled('nationality')) { $natInput = $request->nationality; $natsArray = is_array($natInput) ? $natInput : array_filter(array_map('trim', explode(',', $natInput))); @@ -876,6 +903,7 @@ public function getWorkerDetail(Request $request, $id) 'religion' => $w->religion, 'languages' => $langs, 'age' => $w->age, + 'gender' => $w->gender, 'verified' => (bool)$w->verified, 'preferred_job_type' => $preferredJobType, 'bio' => $w->bio, diff --git a/app/Http/Controllers/Api/SponsorAuthController.php b/app/Http/Controllers/Api/SponsorAuthController.php index 42fc782..2eecf04 100644 --- a/app/Http/Controllers/Api/SponsorAuthController.php +++ b/app/Http/Controllers/Api/SponsorAuthController.php @@ -27,12 +27,14 @@ public function register(Request $request) 'mobile' => 'required|string|max:50|unique:sponsors,mobile', 'password' => 'required|string|min:6', 'license_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240', + 'emirates_id_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240', 'organization_name' => 'nullable|string|max:255', 'email' => 'nullable|email|max:255|unique:sponsors,email', 'nationality' => 'nullable|string|max:100', 'city' => 'nullable|string|max:100', 'address' => 'nullable|string|max:255', 'country_code' => 'nullable|string|max:10', + 'license_expiry' => 'nullable|date_format:Y-m-d', ], [ 'mobile.unique' => 'This mobile number is already registered.', 'email.unique' => 'This email address is already registered.', @@ -55,7 +57,7 @@ public function register(Request $request) $email = "sponsor.{$mobileClean}." . rand(10, 99) . "@migrant.ae"; } - // Store license file + // Store files $destinationPath = public_path('uploads/licenses'); if (!file_exists($destinationPath)) { mkdir($destinationPath, 0755, true); @@ -66,9 +68,19 @@ public function register(Request $request) $licenseFile->move($destinationPath, $licenseFileName); $licensePath = 'uploads/licenses/' . $licenseFileName; + $emiratesIdPath = null; + if ($request->hasFile('emirates_id_file')) { + $emiratesIdFile = $request->file('emirates_id_file'); + $emiratesIdFileName = time() . '_emirates_id_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $emiratesIdFile->getClientOriginalName()); + $emiratesIdFile->move($destinationPath, $emiratesIdFileName); + $emiratesIdPath = 'uploads/licenses/' . $emiratesIdFileName; + } + $apiToken = Str::random(80); - $sponsor = DB::transaction(function () use ($request, $email, $licensePath, $apiToken) { + $licenseExpiry = $request->license_expiry ?? now()->addYears(3)->toDateString(); + + $sponsor = DB::transaction(function () use ($request, $email, $licensePath, $emiratesIdPath, $apiToken, $licenseExpiry) { return Sponsor::create([ 'full_name' => $request->full_name, 'organization_name' => $request->organization_name, @@ -80,6 +92,8 @@ public function register(Request $request) 'city' => $request->city, 'address' => $request->address, 'license_file' => $licensePath, + 'emirates_id_file' => $emiratesIdPath, + 'license_expiry' => $licenseExpiry, 'status' => 'active', 'is_verified' => false, 'subscription_status' => 'none', diff --git a/app/Http/Controllers/Api/SponsorController.php b/app/Http/Controllers/Api/SponsorController.php index 7da8172..5e59704 100644 --- a/app/Http/Controllers/Api/SponsorController.php +++ b/app/Http/Controllers/Api/SponsorController.php @@ -50,6 +50,9 @@ public function getDashboard(Request $request) 'is_verified' => $sponsor->is_verified, 'status' => $sponsor->status, 'license_file' => $sponsor->license_file ? asset($sponsor->license_file) : null, + 'emirates_id_file' => $sponsor->emirates_id_file ? asset($sponsor->emirates_id_file) : null, + 'license_expiry' => $sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : null, + 'validity' => $sponsor->license_expiry ? ($sponsor->license_expiry->isFuture() ? 'Valid' : 'Expired') : 'Pending Review', 'joined_at' => $sponsor->created_at->toIso8601String(), ], 'recent_charity_events' => $recentEvents, @@ -279,6 +282,9 @@ public function getProfile(Request $request) 'is_verified' => $sponsor->is_verified, 'status' => $sponsor->status, 'license_file' => $sponsor->license_file ? asset($sponsor->license_file) : null, + 'emirates_id_file' => $sponsor->emirates_id_file ? asset($sponsor->emirates_id_file) : null, + 'license_expiry' => $sponsor->license_expiry ? $sponsor->license_expiry->toDateString() : null, + 'validity' => $sponsor->license_expiry ? ($sponsor->license_expiry->isFuture() ? 'Valid' : 'Expired') : 'Pending Review', 'joined_at' => $sponsor->created_at->toIso8601String(), ] ] diff --git a/app/Http/Controllers/Api/SupportTicketController.php b/app/Http/Controllers/Api/SupportTicketController.php index cbd29c3..8a11e7b 100644 --- a/app/Http/Controllers/Api/SupportTicketController.php +++ b/app/Http/Controllers/Api/SupportTicketController.php @@ -22,7 +22,7 @@ public function getTicketsForWorker(Request $request) return response()->json(['success' => false, 'message' => 'Unauthorized.'], 401); } - $tickets = SupportTicket::where('worker_id', $worker->id) + $tickets = SupportTicket::with('reason')->where('worker_id', $worker->id) ->orderBy('created_at', 'desc') ->get() ->map(function ($ticket) { @@ -31,6 +31,9 @@ public function getTicketsForWorker(Request $request) 'ticket_number' => $ticket->ticket_number, 'subject' => $ticket->subject, 'description' => $ticket->description, + 'reason_id' => $ticket->reason_id, + 'reason_name' => $ticket->reason ? $ticket->reason->reason : null, + 'voice_note_url' => $ticket->voice_note_path ? asset('storage/' . $ticket->voice_note_path) : null, 'status' => $ticket->status, 'priority' => $ticket->priority, 'created_at' => $ticket->created_at->toIso8601String(), @@ -54,6 +57,8 @@ public function createTicketFromWorker(Request $request) 'subject' => 'required|string|max:255', 'description' => 'required|string', 'priority' => 'nullable|string|in:low,medium,high', + 'reason_id' => 'nullable|exists:report_reasons,id', + 'voice_note' => 'nullable|file|mimes:mp3,wav,m4a,ogg,webm,aac,3gp,amr|max:10240', // 10MB limit ]); if ($validator->fails()) { @@ -64,15 +69,24 @@ public function createTicketFromWorker(Request $request) ], 422); } + $voiceNotePath = null; + if ($request->hasFile('voice_note')) { + $voiceNotePath = $request->file('voice_note')->store('support_voice_notes', 'public'); + } + $ticket = SupportTicket::create([ 'ticket_number' => 'TKT-' . rand(100000, 999999), 'worker_id' => $worker->id, + 'reason_id' => $request->reason_id, 'subject' => $request->subject, 'description' => $request->description, + 'voice_note_path' => $voiceNotePath, 'priority' => $request->priority ?? 'medium', 'status' => 'open', ]); + $ticket->load('reason'); + return response()->json([ 'success' => true, 'message' => 'Ticket created successfully.', @@ -81,6 +95,9 @@ public function createTicketFromWorker(Request $request) 'ticket_number' => $ticket->ticket_number, 'subject' => $ticket->subject, 'description' => $ticket->description, + 'reason_id' => $ticket->reason_id, + 'reason_name' => $ticket->reason ? $ticket->reason->reason : null, + 'voice_note_url' => $ticket->voice_note_path ? asset('storage/' . $ticket->voice_note_path) : null, 'status' => $ticket->status, 'priority' => $ticket->priority, 'created_at' => $ticket->created_at->toIso8601String(), @@ -280,7 +297,7 @@ public function getTicketDetail(Request $request, $id) return response()->json(['success' => false, 'message' => 'Unauthorized.'], 401); } - $query = SupportTicket::where('id', $id); + $query = SupportTicket::with('reason')->where('id', $id); if ($worker) { $query->where('worker_id', $worker->id); } else { @@ -314,6 +331,9 @@ public function getTicketDetail(Request $request, $id) 'ticket_number' => $ticket->ticket_number, 'subject' => $ticket->subject, 'description' => $ticket->description, + 'reason_id' => $ticket->reason_id, + 'reason_name' => $ticket->reason ? $ticket->reason->reason : null, + 'voice_note_url' => $ticket->voice_note_path ? asset('storage/' . $ticket->voice_note_path) : null, 'status' => $ticket->status, 'priority' => $ticket->priority, 'created_at' => $ticket->created_at->toIso8601String(), diff --git a/app/Http/Controllers/Api/WorkerAuthController.php b/app/Http/Controllers/Api/WorkerAuthController.php index b85be59..a11689a 100644 --- a/app/Http/Controllers/Api/WorkerAuthController.php +++ b/app/Http/Controllers/Api/WorkerAuthController.php @@ -762,7 +762,10 @@ public function nationalities(Request $request) // Apply Pagination $page = (int) $request->input('page', 1); - $perPage = (int) $request->input('per_page', 15); + $perPage = (int) $request->input('per_page', 500); + if ($perPage === 15) { + $perPage = 500; + } $total = count($list); $offset = ($page - 1) * $perPage; $paginatedList = array_slice($list, $offset, $perPage); @@ -780,4 +783,211 @@ public function nationalities(Request $request) ], ], 200); } + + /** + * Get list of preferred locations translated to requested language. + * Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta). + */ + public function preferredLocations(Request $request) + { + $lang = strtolower($request->input('lang') ?? $request->header('Accept-Language') ?? 'en'); + if (str_contains($lang, ',')) { + $lang = explode(',', $lang)[0]; + } + if (str_contains($lang, '-')) { + $lang = explode('-', $lang)[0]; + } + + $langMap = [ + 'hindi' => 'hi', + 'sawahi' => 'sw', + 'swahili' => 'sw', + 'taglo' => 'tl', + 'tagalog' => 'tl', + 'tamil' => 'ta', + ]; + + if (isset($langMap[$lang])) { + $lang = $langMap[$lang]; + } + + if (!in_array($lang, ['en', 'hi', 'sw', 'tl', 'ta'])) { + $lang = 'en'; + } + + $rawLocations = [ + ['code' => 'dubai', 'name' => 'Dubai', 'hi' => 'दुबई', 'sw' => 'Dubai', 'tl' => 'Dubai', 'ta' => 'துபாய்'], + ['code' => 'abu dhabi', 'name' => 'Abu Dhabi', 'hi' => 'अबू धाबी', 'sw' => 'Abu Dhabi', 'tl' => 'Abu Dhabi', 'ta' => 'அபுதாபி'], + ['code' => 'oman', 'name' => 'Oman', 'hi' => 'ओमान', 'sw' => 'Omani', 'tl' => 'Oman', 'ta' => 'ஓமன்'] + ]; + + $list = []; + foreach ($rawLocations as $item) { + $list[] = [ + 'code' => $item['code'], + 'name' => $item[$lang] ?? $item['name'], + ]; + } + + $search = strtolower($request->input('search') ?? $request->input('q') ?? ''); + if ($search !== '') { + $list = array_filter($list, function ($item) use ($search) { + return stripos($item['name'], $search) !== false || stripos($item['code'], $search) !== false; + }); + $list = array_values($list); + } + + // Apply Pagination + $page = (int) $request->input('page', 1); + $perPage = (int) $request->input('per_page', 15); + $total = count($list); + $offset = ($page - 1) * $perPage; + $paginatedList = array_slice($list, $offset, $perPage); + + return response()->json([ + 'success' => true, + 'data' => [ + 'preferred_locations' => $paginatedList, + 'pagination' => [ + 'total' => $total, + 'per_page' => $perPage, + 'current_page' => $page, + 'last_page' => max(1, (int) ceil($total / $perPage)), + ] + ], + ], 200); + } + + /** + * Get list of areas for a specific preferred location translated to requested language. + * Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta). + */ + public function preferredLocationAreas(Request $request, $location = null) + { + // Get location from path or query parameter + $loc = strtolower(trim($location ?? $request->input('location') ?? $request->input('city') ?? '')); + + // Handle URL-friendly formats like abu-dhabi, abu_dhabi, abu dhabi + $loc = str_replace(['_', '-'], ' ', $loc); + + if (empty($loc)) { + return response()->json([ + 'success' => false, + 'message' => 'Please provide a valid location (e.g., dubai, abu dhabi, oman).' + ], 400); + } + + $lang = strtolower($request->input('lang') ?? $request->header('Accept-Language') ?? 'en'); + if (str_contains($lang, ',')) { + $lang = explode(',', $lang)[0]; + } + if (str_contains($lang, '-')) { + $lang = explode('-', $lang)[0]; + } + + $langMap = [ + 'hindi' => 'hi', + 'sawahi' => 'sw', + 'swahili' => 'sw', + 'taglo' => 'tl', + 'tagalog' => 'tl', + 'tamil' => 'ta', + ]; + + if (isset($langMap[$lang])) { + $lang = $langMap[$lang]; + } + + if (!in_array($lang, ['en', 'hi', 'sw', 'tl', 'ta'])) { + $lang = 'en'; + } + + $areasMapping = [ + 'dubai' => [ + ['code' => 'dubai', 'name' => 'Dubai', 'hi' => 'दुबई', 'sw' => 'Dubai', 'tl' => 'Dubai', 'ta' => 'துபாய்'], + ['code' => 'marina', 'name' => 'Dubai Marina', 'hi' => 'दुबई मरीना', 'sw' => 'Dubai Marina', 'tl' => 'Dubai Marina', 'ta' => 'துபாய் மெரினா'], + ['code' => 'barsha', 'name' => 'Al Barsha', 'hi' => 'अल बरशा', 'sw' => 'Al Barsha', 'tl' => 'Al Barsha', 'ta' => 'அல் பர்ஷா'], + ['code' => 'nahda', 'name' => 'Al Nahda', 'hi' => 'अल नहदा', 'sw' => 'Al Nahda', 'tl' => 'Al Nahda', 'ta' => 'அல் நஹ்தா'], + ['code' => 'jumeirah', 'name' => 'Jumeirah', 'hi' => 'जुमेराह', 'sw' => 'Jumeirah', 'tl' => 'Jumeirah', 'ta' => 'ஜுமேரா'], + ['code' => 'deira', 'name' => 'Deira', 'hi' => 'देइरा', 'sw' => 'Deira', 'tl' => 'Deira', 'ta' => 'தேரா'], + ['code' => 'downtown', 'name' => 'Downtown Dubai', 'hi' => 'डाउनटाउन दुबई', 'sw' => 'Downtown Dubai', 'tl' => 'Downtown Dubai', 'ta' => 'டவுன்டவுன் துபாய்'], + ['code' => 'silicon', 'name' => 'Silicon Oasis', 'hi' => 'सिलिकॉन ओएसिस', 'sw' => 'Silicon Oasis', 'tl' => 'Silicon Oasis', 'ta' => 'சிலிக்கான் ஓசிஸ்'], + ['code' => 'sports', 'name' => 'Sports City', 'hi' => 'स्पोर्ट्स सिटी', 'sw' => 'Sports City', 'tl' => 'Sports City', 'ta' => 'ஸ்போர்ட்ஸ் சிட்டி'], + ['code' => 'motor', 'name' => 'Motor City', 'hi' => 'मोटर सिटी', 'sw' => 'Motor City', 'tl' => 'Motor City', 'ta' => 'மோட்டார் சிட்டி'], + ['code' => 'jlt', 'name' => 'JLT', 'hi' => 'JLT', 'sw' => 'JLT', 'tl' => 'JLT', 'ta' => 'ஜே.எல்.டி'], + ['code' => 'jbr', 'name' => 'JBR', 'hi' => 'JBR', 'sw' => 'JBR', 'tl' => 'JBR', 'ta' => 'ஜே.பி.ஆர்'], + ['code' => 'meydan', 'name' => 'Meydan', 'hi' => 'मेदान', 'sw' => 'Meydan', 'tl' => 'Meydan', 'ta' => 'மெய்தான்'], + ['code' => 'ranches', 'name' => 'Arabian Ranches', 'hi' => 'अरेबियन रैंचेस', 'sw' => 'Arabian Ranches', 'tl' => 'Arabian Ranches', 'ta' => 'அரேபியன் ரான்சஸ்'], + ['code' => 'bay', 'name' => 'Business Bay', 'hi' => 'बिजनेस बे', 'sw' => 'Business Bay', 'tl' => 'Business Bay', 'ta' => 'பிசினஸ் பே'], + ['code' => 'mirdif', 'name' => 'Mirdif', 'hi' => 'मिर्डिफ', 'sw' => 'Mirdif', 'tl' => 'Mirdif', 'ta' => 'மிர்திஃப்'], + ['code' => 'quoz', 'name' => 'Al Quoz', 'hi' => 'अल क्वोज़', 'sw' => 'Al Quoz', 'tl' => 'Al Quoz', 'ta' => 'அல் கூஸ்'], + ], + 'abu dhabi' => [ + ['code' => 'abu dhabi', 'name' => 'Abu Dhabi City', 'hi' => 'अबू धाबी शहर', 'sw' => 'Abu Dhabi City', 'tl' => 'Abu Dhabi City', 'ta' => 'அபுதாபி நகரம்'], + ['code' => 'yas', 'name' => 'Yas Island', 'hi' => 'यास द्वीप', 'sw' => 'Yas Island', 'tl' => 'Yas Island', 'ta' => 'யாஸ் தீவு'], + ['code' => 'khalifa', 'name' => 'Khalifa City', 'hi' => 'खलीफा शहर', 'sw' => 'Khalifa City', 'tl' => 'Khalifa City', 'ta' => 'கலீஃபா நகரம்'], + ['code' => 'reem', 'name' => 'Reem Island', 'hi' => 'रीम द्वीप', 'sw' => 'Reem Island', 'tl' => 'Reem Island', 'ta' => 'ரீம் தீவு'], + ['code' => 'saadiyat', 'name' => 'Saadiyat Island', 'hi' => 'सादियात द्वीप', 'sw' => 'Saadiyat Island', 'tl' => 'Saadiyat Island', 'ta' => 'சாதியாத் தீவு'], + ['code' => 'raha', 'name' => 'Al Raha', 'hi' => 'अल राहा', 'sw' => 'Al Raha', 'tl' => 'Al Raha', 'ta' => 'அல் ராஹா'], + ['code' => 'mussafah', 'name' => 'Mussafah', 'hi' => 'मुसफ्फह', 'sw' => 'Mussafah', 'tl' => 'Mussafah', 'ta' => 'முஸாஃபா'], + ['code' => 'zahiyah', 'name' => 'Al Zahiyah', 'hi' => 'अल ज़ाहिया', 'sw' => 'Al Zahiyah', 'tl' => 'Al Zahiyah', 'ta' => 'அல் ஜாஹியா'], + ['code' => 'karamah', 'name' => 'Al Karamah', 'hi' => 'अल करमा', 'sw' => 'Al Karamah', 'tl' => 'Al Karamah', 'ta' => 'அல் கராமா'], + ], + 'oman' => [ + ['code' => 'oman', 'name' => 'Oman', 'hi' => 'ओमान', 'sw' => 'Omani', 'tl' => 'Oman', 'ta' => 'ஓமன்'], + ['code' => 'muscat', 'name' => 'Muscat', 'hi' => 'मस्कट', 'sw' => 'Muscat', 'tl' => 'Muscat', 'ta' => 'மस्कट'], + ['code' => 'salalah', 'name' => 'Salalah', 'hi' => 'सलालाह', 'sw' => 'Salalah', 'tl' => 'Salalah', 'ta' => 'சலாலா'], + ['code' => 'sohar', 'name' => 'Sohar', 'hi' => 'सोहर', 'sw' => 'Sohar', 'tl' => 'Sohar', 'ta' => 'சோஹார்'], + ['code' => 'nizwa', 'name' => 'Nizwa', 'hi' => 'निज़वा', 'sw' => 'Nizwa', 'tl' => 'Nizwa', 'ta' => 'நிஸ்வா'], + ['code' => 'sur', 'name' => 'Sur', 'hi' => 'सुर', 'sw' => 'Sur', 'tl' => 'Sur', 'ta' => 'சூர்'], + ['code' => 'ibri', 'name' => 'Ibri', 'hi' => 'इबरी', 'sw' => 'Ibri', 'tl' => 'Ibri', 'ta' => 'இப்ரி'], + ['code' => 'rustaq', 'name' => 'Rustaq', 'hi' => 'रुस्तक़', 'sw' => 'Rustaq', 'tl' => 'Rustaq', 'ta' => 'रुस्तक़'], + ] + ]; + + if (!isset($areasMapping[$loc])) { + return response()->json([ + 'success' => false, + 'message' => 'Areas for the specified location were not found. Supported locations: dubai, abu dhabi, oman.' + ], 404); + } + + $rawAreas = $areasMapping[$loc]; + $list = []; + foreach ($rawAreas as $item) { + $list[] = [ + 'code' => $item['code'], + 'name' => $item[$lang] ?? $item['name'], + ]; + } + + $search = strtolower($request->input('search') ?? $request->input('q') ?? ''); + if ($search !== '') { + $list = array_filter($list, function ($item) use ($search) { + return stripos($item['name'], $search) !== false || stripos($item['code'], $search) !== false; + }); + $list = array_values($list); + } + + // Apply Pagination + $page = (int) $request->input('page', 1); + $perPage = (int) $request->input('per_page', 15); + $total = count($list); + $offset = ($page - 1) * $perPage; + $paginatedList = array_slice($list, $offset, $perPage); + + return response()->json([ + 'success' => true, + 'data' => [ + 'location' => $loc, + 'areas' => $paginatedList, + 'pagination' => [ + 'total' => $total, + 'per_page' => $perPage, + 'current_page' => $page, + 'last_page' => max(1, (int) ceil($total / $perPage)), + ] + ], + ], 200); + } } diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index 2020713..c8d1577 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -71,6 +71,7 @@ public function updateProfile(Request $request) 'country' => 'nullable|string|max:100', 'city' => 'nullable|string|max:100', 'area' => 'nullable|string|max:100', + 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { @@ -102,7 +103,8 @@ public function updateProfile(Request $request) 'preferred_location', 'country', 'city', - 'area' + 'area', + 'fcm_token' ]); // Filter out null inputs if we want to support partial updates diff --git a/app/Http/Controllers/Employer/CandidateController.php b/app/Http/Controllers/Employer/CandidateController.php index f7e5577..4012932 100644 --- a/app/Http/Controllers/Employer/CandidateController.php +++ b/app/Http/Controllers/Employer/CandidateController.php @@ -124,12 +124,22 @@ public function index(Request $request) $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) { + $locationMapping = [ + 'dubai' => ['dubai', 'marina', 'barsha', 'nahda', 'jumeirah', 'deira', 'downtown', 'silicon', 'sports', 'motor', 'jlt', 'jbr', 'meydan', 'ranches', 'bay', 'mirdif', 'quoz'], + 'abu dhabi' => ['abu dhabi', 'yas', 'khalifa', 'reem', 'saadiyat', 'raha', 'mussafah', 'zahiyah', 'karamah'], + 'oman' => ['oman', 'muscat', 'salalah', 'sohar', 'nizwa', 'sur', 'ibri', 'rustaq'] + ]; + + $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($locsArray, $locationMapping) { if (!isset($c['preferred_location'])) return false; $wLoc = strtolower($c['preferred_location']); foreach ($locsArray as $l) { - if (str_contains($wLoc, $l)) { - return true; + if ($l === 'all') return true; + $allowedKeywords = $locationMapping[$l] ?? [$l]; + foreach ($allowedKeywords as $keyword) { + if (str_contains($wLoc, $keyword)) { + return true; + } } } return false; @@ -189,6 +199,13 @@ public function index(Request $request) })); } + if ($request->filled('gender')) { + $gender = strtolower($request->gender); + $mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($gender) { + return isset($c['gender']) && strtolower($c['gender']) === $gender; + })); + } + if ($request->has('in_country')) { $inCountryVal = $request->input('in_country'); if ($inCountryVal !== null && $inCountryVal !== '') { @@ -250,8 +267,13 @@ public function index(Request $request) })); } + $nationalitiesResponse = app(\App\Http\Controllers\Api\WorkerAuthController::class)->nationalities(new \Illuminate\Http\Request(['per_page' => 500])); + $nationalitiesData = json_decode($nationalitiesResponse->getContent(), true); + $allNationalities = collect($nationalitiesData['data']['nationalities'] ?? [])->pluck('name')->filter()->toArray(); + return Inertia::render('Employer/SelectedCandidates', [ 'selectedWorkers' => $mergedCandidates, + 'allNationalities' => $allNationalities, ]); } diff --git a/app/Http/Controllers/Employer/EmployerAuthController.php b/app/Http/Controllers/Employer/EmployerAuthController.php index 5a8b4a0..73c1903 100644 --- a/app/Http/Controllers/Employer/EmployerAuthController.php +++ b/app/Http/Controllers/Employer/EmployerAuthController.php @@ -85,6 +85,7 @@ public function register(Request $request) 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255', 'phone' => 'required|string|regex:/^[0-9]{7,15}$/', + 'emirates_id_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240', ], [ 'phone.regex' => 'The mobile number must be between 7 and 15 digits without any country code (e.g. 501234567).', ]); @@ -98,6 +99,19 @@ public function register(Request $request) ], 409); } + // Store file if provided + $emiratesIdPath = null; + if ($request->hasFile('emirates_id_file')) { + $destinationPath = public_path('uploads/licenses'); + if (!file_exists($destinationPath)) { + mkdir($destinationPath, 0755, true); + } + $emiratesIdFile = $request->file('emirates_id_file'); + $emiratesIdFileName = time() . '_emirates_id_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $emiratesIdFile->getClientOriginalName()); + $emiratesIdFile->move($destinationPath, $emiratesIdFileName); + $emiratesIdPath = 'uploads/licenses/' . $emiratesIdFileName; + } + // 3. Generate 6-digit OTP, hash & store with 10-min expiry $otp = (string) mt_rand(100000, 999999); $hashedOtp = Hash::make($otp); @@ -108,6 +122,7 @@ public function register(Request $request) 'name' => $request->name, 'email' => $request->email, 'phone' => $request->phone, + 'emirates_id_file' => $emiratesIdPath, ], 'employer_otp' => [ 'hash' => $hashedOtp, @@ -172,8 +187,8 @@ public function verifyEmail(Request $request) ]); } - // Hash comparison (allow 000000 or 111111 in local environment for automated testing) - $isLocalDebug = app()->environment('local') && ($request->otp === '000000' || $request->otp === '111111'); + // Hash comparison (allow 000000 or 111111 in local/testing environment for automated testing) + $isLocalDebug = (app()->environment('local') || app()->environment('testing')) && ($request->otp === '000000' || $request->otp === '111111'); if (!$isLocalDebug && !Hash::check($request->otp, $otpSession['hash'])) { $otpSession['attempts']++; session(['employer_otp' => $otpSession]); @@ -374,6 +389,7 @@ public function createPassword(Request $request) 'otp_verified_at' => now(), 'status' => 'active', 'last_login_at' => now(), + 'emirates_id_file' => $pending['emirates_id_file'] ?? null, ]); // Create active subscription in database diff --git a/app/Http/Controllers/Employer/MessageController.php b/app/Http/Controllers/Employer/MessageController.php index 12a0f35..87643ea 100644 --- a/app/Http/Controllers/Employer/MessageController.php +++ b/app/Http/Controllers/Employer/MessageController.php @@ -235,6 +235,21 @@ public function send(Request $request, $id) // Touch conversation updated_at for sorting $conv->touch(); + // Dispatch push notification to worker + $worker = $conv->worker; + if ($worker && $worker->fcm_token) { + \App\Services\FCMService::sendPushNotification( + $worker->fcm_token, + "New Message from " . ($user->name ?? "Employer"), + $request->text ?: "Sent an attachment", + [ + 'conversation_id' => $conv->id, + 'sender_type' => 'employer', + 'sender_id' => $user->id, + ] + ); + } + return back(); } diff --git a/app/Http/Controllers/Employer/WorkerController.php b/app/Http/Controllers/Employer/WorkerController.php index 9053c4d..14d03c6 100644 --- a/app/Http/Controllers/Employer/WorkerController.php +++ b/app/Http/Controllers/Employer/WorkerController.php @@ -123,12 +123,22 @@ public function index(Request $request) $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) { + $locationMapping = [ + 'dubai' => ['dubai', 'marina', 'barsha', 'nahda', 'jumeirah', 'deira', 'downtown', 'silicon', 'sports', 'motor', 'jlt', 'jbr', 'meydan', 'ranches', 'bay', 'mirdif', 'quoz'], + 'abu dhabi' => ['abu dhabi', 'yas', 'khalifa', 'reem', 'saadiyat', 'raha', 'mussafah', 'zahiyah', 'karamah'], + 'oman' => ['oman', 'muscat', 'salalah', 'sohar', 'nizwa', 'sur', 'ibri', 'rustaq'] + ]; + + $workers = array_values(array_filter($workers, function ($c) use ($locsArray, $locationMapping) { if (!isset($c['preferred_location'])) return false; $wLoc = strtolower($c['preferred_location']); foreach ($locsArray as $l) { - if (str_contains($wLoc, $l)) { - return true; + if ($l === 'all') return true; + $allowedKeywords = $locationMapping[$l] ?? [$l]; + foreach ($allowedKeywords as $keyword) { + if (str_contains($wLoc, $keyword)) { + return true; + } } } return false; @@ -187,7 +197,12 @@ public function index(Request $request) return false; })); } - + if ($request->filled('gender')) { + $gender = strtolower($request->gender); + $workers = array_values(array_filter($workers, function ($c) use ($gender) { + return isset($c['gender']) && strtolower($c['gender']) === $gender; + })); + } if ($request->has('in_country')) { $inCountryVal = $request->input('in_country'); if ($inCountryVal !== null && $inCountryVal !== '') { @@ -253,7 +268,9 @@ public function index(Request $request) $shortlistedIds = Shortlist::where('employer_id', $employerId)->pluck('worker_id')->toArray(); $dbCategories = WorkerCategory::pluck('name')->toArray(); - $dbNationalities = Worker::distinct()->pluck('nationality')->toArray(); + $nationalitiesResponse = app(\App\Http\Controllers\Api\WorkerAuthController::class)->nationalities(new \Illuminate\Http\Request(['per_page' => 500])); + $nationalitiesData = json_decode($nationalitiesResponse->getContent(), true); + $dbNationalities = collect($nationalitiesData['data']['nationalities'] ?? [])->pluck('name')->filter()->toArray(); $filtersMetadata = [ 'categories' => array_merge(['All Categories'], $dbCategories), diff --git a/app/Models/Sponsor.php b/app/Models/Sponsor.php index 418d827..e878105 100644 --- a/app/Models/Sponsor.php +++ b/app/Models/Sponsor.php @@ -32,6 +32,8 @@ class Sponsor extends Authenticatable 'last_login_at', 'api_token', 'license_file', + 'emirates_id_file', + 'license_expiry', ]; protected $hidden = [ @@ -45,6 +47,7 @@ class Sponsor extends Authenticatable 'subscription_start_date' => 'datetime', 'subscription_end_date' => 'datetime', 'last_login_at' => 'datetime', + 'license_expiry' => 'datetime', ]; public function hasActiveSubscription(): bool diff --git a/app/Models/SupportTicket.php b/app/Models/SupportTicket.php index 691a866..6df61e9 100644 --- a/app/Models/SupportTicket.php +++ b/app/Models/SupportTicket.php @@ -16,6 +16,7 @@ class SupportTicket extends Model 'worker_id', 'subject', 'description', + 'voice_note_path', 'status', 'priority', ]; diff --git a/app/Models/Worker.php b/app/Models/Worker.php index acc8b7a..28cbb7d 100644 --- a/app/Models/Worker.php +++ b/app/Models/Worker.php @@ -36,6 +36,7 @@ class Worker extends Model 'in_country', 'visa_status', 'preferred_job_type', + 'fcm_token', ]; protected $hidden = [ diff --git a/app/Services/FCMService.php b/app/Services/FCMService.php new file mode 100644 index 0000000..29706e0 --- /dev/null +++ b/app/Services/FCMService.php @@ -0,0 +1,155 @@ + 'RS256', 'typ' => 'JWT']); + + $now = time(); + $payload = json_encode([ + 'iss' => $clientEmail, + 'scope' => 'https://www.googleapis.com/auth/firebase.messaging', + 'aud' => 'https://oauth2.googleapis.com/token', + 'exp' => $now + 3600, + 'iat' => $now, + ]); + + $base64UrlHeader = self::base64UrlEncode($header); + $base64UrlPayload = self::base64UrlEncode($payload); + + $signatureInput = $base64UrlHeader . '.' . $base64UrlPayload; + $signature = ''; + + if (!openssl_sign($signatureInput, $signature, $privateKey, OPENSSL_ALGO_SHA256)) { + Log::error("Failed to sign JWT with private key using openssl_sign."); + return null; + } + + $base64UrlSignature = self::base64UrlEncode($signature); + $jwtToken = $signatureInput . '.' . $base64UrlSignature; + + $response = Http::asForm()->post('https://oauth2.googleapis.com/token', [ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => $jwtToken, + ]); + + if ($response->failed()) { + Log::error("FCM OAuth2 request failed: " . $response->body()); + return null; + } + + $data = $response->json(); + return $data['access_token'] ?? null; + + } catch (\Exception $e) { + Log::error("FCM Access Token Generation Error: " . $e->getMessage()); + return null; + } + }); + } + + /** + * Send a Push Notification to a specific FCM token. + * + * @param string $token + * @param string $title + * @param string $body + * @param array $data + * @return bool + */ + public static function sendPushNotification($token, $title, $body, array $data = []) + { + if (empty($token)) { + return false; + } + + try { + $accessToken = self::getAccessToken(); + if (!$accessToken) { + Log::error("Cannot send push notification: FCM Access Token is null."); + return false; + } + + // Load credentials to fetch the correct project_id + $credentialsPath = base_path('migrant-fe5a7-firebase-adminsdk-fbsvc-f8b365ecd0.json'); + $credentials = json_decode(file_get_contents($credentialsPath), true); + $projectId = $credentials['project_id'] ?? 'migrant-fe5a7'; + + // Convert all data array values to strings as required by FCM v1 + $formattedData = []; + foreach ($data as $key => $value) { + $formattedData[(string)$key] = (string)$value; + } + + $payload = [ + 'message' => [ + 'token' => $token, + 'notification' => [ + 'title' => $title, + 'body' => $body, + ], + ] + ]; + + if (!empty($formattedData)) { + $payload['message']['data'] = $formattedData; + } + + $response = Http::withToken($accessToken) + ->post("https://fcm.googleapis.com/v1/projects/{$projectId}/messages:send", $payload); + + if ($response->failed()) { + Log::error("FCM Push Notification failed: " . $response->body()); + return false; + } + + return true; + + } catch (\Exception $e) { + Log::error("FCM Notification Dispatch Failure: " . $e->getMessage()); + return false; + } + } + + /** + * Base64 Url Encode helper. + * + * @param string $data + * @return string + */ + private static function base64UrlEncode($data) + { + return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data)); + } +} diff --git a/database/migrations/2026_06_12_181000_add_emirates_id_file_to_sponsors_table.php b/database/migrations/2026_06_12_181000_add_emirates_id_file_to_sponsors_table.php new file mode 100644 index 0000000..96de29a --- /dev/null +++ b/database/migrations/2026_06_12_181000_add_emirates_id_file_to_sponsors_table.php @@ -0,0 +1,30 @@ +string('emirates_id_file')->nullable()->after('license_file'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('sponsors', function (Blueprint $table) { + $table->dropColumn('emirates_id_file'); + }); + } +}; diff --git a/database/migrations/2026_06_12_185000_add_license_expiry_to_sponsors_table.php b/database/migrations/2026_06_12_185000_add_license_expiry_to_sponsors_table.php new file mode 100644 index 0000000..2759045 --- /dev/null +++ b/database/migrations/2026_06_12_185000_add_license_expiry_to_sponsors_table.php @@ -0,0 +1,30 @@ +date('license_expiry')->nullable()->after('license_file'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('sponsors', function (Blueprint $table) { + $table->dropColumn('license_expiry'); + }); + } +}; diff --git a/database/migrations/2026_06_13_000000_add_fcm_token_to_workers_table.php b/database/migrations/2026_06_13_000000_add_fcm_token_to_workers_table.php new file mode 100644 index 0000000..2043ed9 --- /dev/null +++ b/database/migrations/2026_06_13_000000_add_fcm_token_to_workers_table.php @@ -0,0 +1,28 @@ +string('fcm_token')->nullable()->after('api_token'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('workers', function (Blueprint $table) { + $table->dropColumn('fcm_token'); + }); + } +}; diff --git a/database/migrations/2026_06_13_000001_add_voice_note_path_to_support_tickets_table.php b/database/migrations/2026_06_13_000001_add_voice_note_path_to_support_tickets_table.php new file mode 100644 index 0000000..eb3105c --- /dev/null +++ b/database/migrations/2026_06_13_000001_add_voice_note_path_to_support_tickets_table.php @@ -0,0 +1,28 @@ +string('voice_note_path')->nullable()->after('description'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('support_tickets', function (Blueprint $table) { + $table->dropColumn('voice_note_path'); + }); + } +}; diff --git a/migrant-fe5a7-firebase-adminsdk-fbsvc-f8b365ecd0.json b/migrant-fe5a7-firebase-adminsdk-fbsvc-f8b365ecd0.json new file mode 100644 index 0000000..3a8d331 --- /dev/null +++ b/migrant-fe5a7-firebase-adminsdk-fbsvc-f8b365ecd0.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "migrant-fe5a7", + "private_key_id": "f8b365ecd04e9a5382841a837826ec6f3a4ddab0", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDoXYB0utGboyNW\nQJcvU3ARTwYGXD4qBNNgWY7z3mJkFdbiaeGp0D1esTuucYb8h2ldS3CURREAZXVG\nS55y26kXTPi4leBnEtEy7h2Mdp03EKQg3rJhwKL+u9KjO6s15Pa85umVNkIW1bNt\n/NgToQ8nx7rZpHJMsJM9jgVpFa/7++ugpcGwz41OeurrGtoeAlwOLa9aEFCNIJJ5\nESu6qra1tIr3LPOhIwj2xhQ2DSxSepEKrQmwZbBT1ak78NnHDqn3KSZKRvM7AgqD\nCtJ3rfATHhriJzEOLaUMAn6lhDwfOwi79lVIUSRq7S9jyI8qsyzh+xNLf2x3R/tu\n4bDrHUnlAgMBAAECggEAFaEyhmsnfNHcRbigeh7NtUbCXFA01yt406Syil/ej8t6\no7vQbkK8m7ZqxEu8EYC3XaBam+qr8PoAxyjd2Ho7DLi1BFWrPF0DjTaeGIeoDcqm\nRIsGVtQcOBMawegZgVxNXXsvLh0NXNvKpofdQ0KASycr7PuCSkI7ioTQWx5SMeXt\ncSDwD/H9gs2xg2uToG2ggKWMA1akAV4JibNvGGolzgJ/BHUQv2IbPKTtw0k2oRVr\nir7ztVlZh2QRpdxQSvb4VMz0jtYKUIF2vw9QcH+nzgBzjR//htvz7kYSBO6euSIm\ng4r/+Q01bcWx2FHrf7B1FIGrXDgxXptQF/Ou1nGJxwKBgQD93H0ZnLWRohd/8Koa\nM1ll1+CUgFNQn2RwthJi5yEgYXtAB6uWYph2RL6586b6JXl786Si5fZ6WzPU/pZP\n09IRaHnpiPT2uHkXTCT+KkhpJvvu8nH2CplIqXSnCiwDO7Loq6fshl4em9Zg6+Tu\nk7ofJaeKqW5zmlTyI0cJT+XjpwKBgQDqUqbgJaPgvcYVGDseNYNCnd2fFUyl9T6f\nSU0ubU2yGjec70rCwb9GRNT//H0R30kGJNbSwDwapIha/QD7UxeHjPkUGdwVNCKM\nQV19MYH3O1mv/j8N3fBasmk8zWbMUWCRdnIOiIkF5Md4FdRm7UtP9Lxy4SgXJfrK\njGJz/E4HkwKBgGBN6u9ycbcDxOJ2TDGQVVO7Z6kuXWzyasoPaD447Go6UOVjg4aT\ndL85KRmmAyxWVxXcwhJCJxUX0Dv3MNKryr9r4QPlPvjx1o4uBsKdC8dIUL6/Hth2\nANx93JEZ3MSFO0PlCtlByCbYe6VdGAYh6LO0NzD1Qb99Rshs9Z/kvZN1AoGBAJJC\nkuz4MalXayvBmy0JA+xx82KX/ebdBICSVX20NjoESVBIwOZ93vFyh21dYYflUoRm\nPD0CRsHujzoUECfPvrEaWmKknY4So7neFwfM/i2euyWyUhNKw/sov613HEJOTTOe\ntiTCLp0iJyuanKC+XzMCNRqT1d1VFIyXQeDZzK7HAoGAGC98Oz+z4z57IRhqdpTN\nGA2D+xitrQtEVKcq0RWuOM8aIxAGPfaAmJ+ec7vagy1xpohOtF8bAne9GvSH01nN\nbegp001RqmcHPCoqv9SzqJUZcEMG7M89NBvn1uL6hfmnMkZIljFoA8u0b5NzGy58\nvp3dka7/U12lo5n1guQF1Hk=\n-----END PRIVATE KEY-----\n", + "client_email": "firebase-adminsdk-fbsvc@migrant-fe5a7.iam.gserviceaccount.com", + "client_id": "114003929019295227149", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40migrant-fe5a7.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/public/swagger.json b/public/swagger.json index 8ba9b42..b8fd2e1 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -52,6 +52,11 @@ "format": "binary", "description": "Organization or trade license document (jpg/png/pdf, max 10MB). (REQUIRED)" }, + "emirates_id_file": { + "type": "string", + "format": "binary", + "description": "Emirates ID document file (jpg/png/pdf, max 10MB). (OPTIONAL)" + }, "organization_name": { "type": "string", "example": "Al-Rashidi Charitable Foundation", @@ -787,9 +792,9 @@ "in": "query", "schema": { "type": "integer", - "default": 15 + "default": 500 }, - "description": "Number of nationalities to return per page" + "description": "Number of nationalities to return per page. Defaults to 500 on the backend to retrieve all nationalities by default." } ], "responses": { @@ -854,6 +859,386 @@ } } }, + "/preferred-locations": { + "get": { + "tags": [ + "General" + ], + "summary": "Get Localized Preferred Locations List", + "description": "Returns a list of preferred locations (Dubai, Abu Dhabi, Oman) translated to the requested language. Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta).", + "security": [], + "parameters": [ + { + "name": "lang", + "in": "query", + "schema": { + "type": "string", + "default": "en" + }, + "description": "Language locale code (e.g. 'en', 'hindi', 'sawahi', 'taglo', 'tamil')" + }, + { + "name": "Accept-Language", + "in": "header", + "schema": { + "type": "string" + }, + "description": "Fallback header locale (e.g. 'en', 'hi', 'sw', 'tl', 'ta')" + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Search keyword to filter locations by name or code" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "Page number for pagination" + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 15 + }, + "description": "Number of locations to return per page" + } + ], + "responses": { + "200": { + "description": "Preferred locations list retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "preferred_locations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "dubai" + }, + "name": { + "type": "string", + "example": "Dubai" + } + } + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "example": 3 + }, + "per_page": { + "type": "integer", + "example": 15 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "last_page": { + "type": "integer", + "example": 1 + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/preferred-locations/{location}/areas": { + "get": { + "tags": [ + "General" + ], + "summary": "Get Localized Areas for a Preferred Location (Path Parameter)", + "description": "Returns a list of localized neighborhoods or sub-locations mapped under a specific preferred location (Dubai, Abu Dhabi, Oman) using path routing.", + "security": [], + "parameters": [ + { + "name": "location", + "in": "path", + "required": true, + "description": "The preferred location name (e.g. dubai, abu dhabi, oman).", + "schema": { + "type": "string" + } + }, + { + "name": "lang", + "in": "query", + "schema": { + "type": "string", + "default": "en" + }, + "description": "Language locale code (e.g. 'en', 'hindi', 'sawahi', 'taglo', 'tamil')" + }, + { + "name": "Accept-Language", + "in": "header", + "schema": { + "type": "string" + }, + "description": "Fallback header locale (e.g. 'en', 'hi', 'sw', 'tl', 'ta')" + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Search keyword to filter areas by name or code" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "Page number for pagination" + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 15 + }, + "description": "Number of areas to return per page" + } + ], + "responses": { + "200": { + "description": "Areas retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "location": { + "type": "string", + "example": "dubai" + }, + "areas": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "marina" + }, + "name": { + "type": "string", + "example": "Dubai Marina" + } + } + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "example": 17 + }, + "per_page": { + "type": "integer", + "example": 15 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "last_page": { + "type": "integer", + "example": 2 + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/preferred-locations/areas": { + "get": { + "tags": [ + "General" + ], + "summary": "Get Localized Areas for a Preferred Location (Query Parameter)", + "description": "Returns a list of localized neighborhoods or sub-locations mapped under a specific preferred location (Dubai, Abu Dhabi, Oman) using query parameter routing.", + "security": [], + "parameters": [ + { + "name": "city", + "in": "query", + "required": false, + "description": "The preferred location name (e.g. dubai, abu dhabi, oman).", + "schema": { + "type": "string" + } + }, + { + "name": "location", + "in": "query", + "required": false, + "description": "Alternative key for location name (e.g. dubai, abu dhabi, oman).", + "schema": { + "type": "string" + } + }, + { + "name": "lang", + "in": "query", + "schema": { + "type": "string", + "default": "en" + }, + "description": "Language locale code (e.g. 'en', 'hindi', 'sawahi', 'taglo', 'tamil')" + }, + { + "name": "Accept-Language", + "in": "header", + "schema": { + "type": "string" + }, + "description": "Fallback header locale (e.g. 'en', 'hi', 'sw', 'tl', 'ta')" + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Search keyword to filter areas by name or code" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "Page number for pagination" + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 15 + }, + "description": "Number of areas to return per page" + } + ], + "responses": { + "200": { + "description": "Areas retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "location": { + "type": "string", + "example": "dubai" + }, + "areas": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "marina" + }, + "name": { + "type": "string", + "example": "Dubai Marina" + } + } + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "example": 17 + }, + "per_page": { + "type": "integer", + "example": 15 + }, + "current_page": { + "type": "integer", + "example": 1 + }, + "last_page": { + "type": "integer", + "example": 2 + } + } + } + } + } + } + } + } + } + } + } + } + }, "/workers/send-otp": { "post": { "tags": [ @@ -1180,6 +1565,11 @@ 6, 8 ] + }, + "fcm_token": { + "type": "string", + "example": "fcm-device-token-xyz-123", + "description": "Firebase Cloud Messaging token for push notifications." } } } @@ -1777,7 +2167,7 @@ "requestBody": { "required": true, "content": { - "application/json": { + "multipart/form-data": { "schema": { "type": "object", "required": [ @@ -1801,6 +2191,16 @@ "high" ], "example": "medium" + }, + "reason_id": { + "type": "integer", + "example": 1, + "description": "Optional report/support reason ID." + }, + "voice_note": { + "type": "string", + "format": "binary", + "description": "Optional audio/voice note file attachment." } } } @@ -2778,7 +3178,16 @@ "name": "nationality", "in": "query", "required": false, - "description": "Filter by nationality name.", + "description": "Filter by nationality name (comma-separated list for multiple choice, e.g. India,Kenya,Ethiopia).", + "schema": { + "type": "string" + } + }, + { + "name": "gender", + "in": "query", + "required": false, + "description": "Filter by gender (e.g. male, female).", "schema": { "type": "string" } @@ -3078,7 +3487,16 @@ "name": "nationality", "in": "query", "required": false, - "description": "Filter candidates by nationality name.", + "description": "Filter candidates by nationality name (comma-separated list for multiple choice, e.g. India,Kenya,Ethiopia).", + "schema": { + "type": "string" + } + }, + { + "name": "gender", + "in": "query", + "required": false, + "description": "Filter candidates by gender (e.g. male, female).", "schema": { "type": "string" } @@ -3964,6 +4382,7 @@ "is_verified": {"type": "boolean", "example": false}, "status": {"type": "string", "example": "active"}, "license_file": {"type": "string", "example": "uploads/licenses/1234567890_license_trade.pdf"}, + "emirates_id_file": {"type": "string", "example": "uploads/licenses/1234567890_emirates_id.pdf"}, "created_at": {"type": "string", "format": "date-time"} } }, diff --git a/resources/js/Pages/Admin/Support/Show.jsx b/resources/js/Pages/Admin/Support/Show.jsx index 573a4af..ac287d9 100644 --- a/resources/js/Pages/Admin/Support/Show.jsx +++ b/resources/js/Pages/Admin/Support/Show.jsx @@ -115,12 +115,34 @@ export default function Show({ ticket, replies }) {
+ {Array.isArray(errors.emirates_id_file) ? errors.emirates_id_file[0] : errors.emirates_id_file} +
+ )} +