get()->map(function ($report) { return [ 'id' => $report->id, 'type' => $report->type, 'reported_user_name' => $report->reported_user_name, 'reported_user_role' => $report->reported_user_role, 'reported_user_avatar' => $report->reported_user_avatar, 'reported_by_name' => $report->reported_by_name, 'reported_by_role' => $report->reported_by_role, 'reported_by_avatar' => $report->reported_by_avatar, 'reason' => $report->reason, 'priority' => $report->priority, 'status' => $report->status, 'description' => $report->description, 'reported_at' => date('M d, Y h:i A', strtotime($report->reported_at)), ]; }); $rules = []; $moderationQueue = []; return Inertia::render('Admin/Safety/Index', [ 'reports' => $reports, 'rules' => $rules, 'moderation_queue' => $moderationQueue ]); } /** * Resolve a safety report */ public function resolveSafetyReport(Request $request, $id) { $request->validate([ 'action' => 'required|string', 'admin_notes' => 'nullable|string' ]); DB::table('moderation_reports') ->where('id', $id) ->update([ 'status' => 'Resolved', 'admin_notes' => $request->admin_notes ?: 'Resolved by administrator', 'updated_at' => now() ]); // Dynamically log event DB::table('audit_logs')->insert([ 'category' => 'admin_action', 'user' => 'SuperAdmin (support@marketplace.com)', 'action' => 'Resolved Safety Report ' . $id . ' with action: ' . strtoupper($request->action) . '. Notes: ' . ($request->admin_notes ?: 'none'), 'ip_address' => $request->ip() ?: '127.0.0.1', 'created_at' => now(), 'updated_at' => now() ]); return back()->with('success', "Safety Report {$id} has been resolved successfully with action: " . strtoupper($request->action)); } /** * Disputes Management Module */ public function disputes() { $tickets = DB::table('disputes')->get()->map(function ($dispute) { // Decode or initialize logs dynamically $logs = []; if ($dispute->chat_logs) { $logs = json_decode($dispute->chat_logs, true) ?: []; } else { $logs = [ ['sender' => $dispute->party_two_name, 'time' => '10:05 AM', 'message' => "Regarding the dispute on {$dispute->type}: we need to resolve this issue about {$dispute->reason} as soon as possible."], ['sender' => $dispute->party_one_name, 'time' => '10:12 AM', 'message' => 'I have already explained my side. Please check the contract agreement details.'], ['sender' => $dispute->party_two_name, 'time' => '10:14 AM', 'message' => 'That is not what we agreed upon. Let the admin mediator review the records.'] ]; // Save it back so it becomes persistent in DB immediately! DB::table('disputes')->where('id', $dispute->id)->update([ 'chat_logs' => json_encode($logs) ]); } return [ 'id' => $dispute->id, 'party_one_name' => $dispute->party_one_name, 'party_one_role' => $dispute->party_one_role, 'party_one_avatar' => $dispute->party_one_avatar, 'party_two_name' => $dispute->party_two_name, 'party_two_role' => $dispute->party_two_role, 'party_two_avatar' => $dispute->party_two_avatar, 'type' => $dispute->type, 'reason' => $dispute->reason, 'priority' => $dispute->priority, 'status' => $dispute->status, 'assigned_to' => $dispute->assigned_to, 'created_at' => date('M d, Y h:i A', strtotime($dispute->created_at)), 'chat_logs' => $logs, 'evidence' => [ ['type' => 'Contract', 'name' => 'hiring_agreement_signed.pdf', 'url' => '#'], ['type' => 'ID Proof', 'name' => 'emirates_id_verification.pdf', 'url' => '#'] ], 'admin_notes' => $dispute->admin_notes ?: 'Awaiting confirmation of contract terms under UAE law.' ]; }); return Inertia::render('Admin/Disputes/Index', [ 'tickets' => $tickets ]); } /** * Resolve a Dispute Ticket */ public function resolveDispute(Request $request, $id) { $request->validate([ 'resolution' => 'required|string', 'action_taken' => 'required|string', ]); $dispute = DB::table('disputes')->where('id', $id)->first(); if (!$dispute) { return back()->withErrors(['error' => 'Dispute not found']); } // Decode existing logs or set default $logs = []; if ($dispute->chat_logs) { $logs = json_decode($dispute->chat_logs, true) ?: []; } else { $logs = [ ['sender' => $dispute->party_two_name, 'time' => '10:05 AM', 'message' => "Regarding the dispute on {$dispute->type}: we need to resolve this issue about {$dispute->reason} as soon as possible."], ['sender' => $dispute->party_one_name, 'time' => '10:12 AM', 'message' => 'I have already explained my side. Please check the contract agreement details.'], ['sender' => $dispute->party_two_name, 'time' => '10:14 AM', 'message' => 'That is not what we agreed upon. Let the admin mediator review the records.'] ]; } // Add resolution details $logs[] = [ 'sender' => 'Mediator (Admin)', 'time' => date('h:i A'), 'message' => '⚠️ RESOLVED & CLOSED CASE: ' . $request->resolution ]; DB::table('disputes') ->where('id', $id) ->update([ 'status' => 'Resolved', 'admin_notes' => $dispute->admin_notes ? ($dispute->admin_notes . "\n• Resolution: " . $request->resolution) : ("• Resolution: " . $request->resolution), 'chat_logs' => json_encode($logs), 'updated_at' => now() ]); // Dynamically log event DB::table('audit_logs')->insert([ 'category' => 'admin_action', 'user' => 'SuperAdmin (support@marketplace.com)', 'action' => 'Resolved Dispute Ticket ' . $id . ' with resolution: ' . $request->resolution . ' (' . strtoupper($request->action_taken) . ')', 'ip_address' => $request->ip() ?: '127.0.0.1', 'created_at' => now(), 'updated_at' => now() ]); return back()->with('success', "Dispute ticket {$id} has been resolved successfully. Resolution: " . strtoupper($request->action_taken)); } /** * Add admin notes to a dispute */ public function addDisputeNote(Request $request, $id) { $request->validate([ 'note' => 'required|string' ]); // Retrieve existing dispute $dispute = DB::table('disputes')->where('id', $id)->first(); if (!$dispute) { return back()->withErrors(['error' => 'Dispute not found']); } // Decode existing logs or set default $logs = []; if ($dispute->chat_logs) { $logs = json_decode($dispute->chat_logs, true) ?: []; } else { $logs = [ ['sender' => $dispute->party_two_name, 'time' => '10:05 AM', 'message' => "Regarding the dispute on {$dispute->type}: we need to resolve this issue about {$dispute->reason} as soon as possible."], ['sender' => $dispute->party_one_name, 'time' => '10:12 AM', 'message' => 'I have already explained my side. Please check the contract agreement details.'], ['sender' => $dispute->party_two_name, 'time' => '10:14 AM', 'message' => 'That is not what we agreed upon. Let the admin mediator review the records.'] ]; } // Append new reply from mediator $logs[] = [ 'sender' => 'Mediator (Admin)', 'time' => date('h:i A'), 'message' => $request->note ]; // Update database DB::table('disputes') ->where('id', $id) ->update([ 'admin_notes' => $dispute->admin_notes ? ($dispute->admin_notes . "\n• " . $request->note) : ("• " . $request->note), 'chat_logs' => json_encode($logs), 'updated_at' => now() ]); // Dynamically log event DB::table('audit_logs')->insert([ 'category' => 'admin_action', 'user' => 'SuperAdmin (support@marketplace.com)', 'action' => 'Mediator posted a reply note to Dispute ticket ' . $id . ': "' . substr($request->note, 0, 100) . '..."', 'ip_address' => $request->ip() ?: '127.0.0.1', 'created_at' => now(), 'updated_at' => now() ]); return back()->with('success', "Mediator reply and logs successfully updated on Dispute ticket {$id}."); } /** * Notifications & Campaign Management */ public function notifications() { $campaigns = DB::table('audit_logs') ->where('category', 'campaign') ->orderBy('created_at', 'desc') ->get() ->map(function ($log) { $data = json_decode($log->action, true) ?: [ 'channel' => 'push', 'recipient_type' => 'All Active Workers', 'title' => 'Broadcast Alert', 'message' => $log->action ]; return [ 'id' => $log->id, 'channel' => $data['channel'] ?? 'push', 'recipient_type' => $data['recipient_type'] ?? 'All Active Workers', 'title' => $data['title'] ?? 'Broadcast Alert', 'message' => $data['message'] ?? '', 'sent_at' => date('Y-m-d H:i', strtotime($log->created_at)), ]; }); return Inertia::render('Admin/Notifications/Index', [ 'campaigns' => $campaigns, ]); } /** * Broadcast notification campaign */ public function broadcastNotification(Request $request) { $request->validate([ 'channel' => 'required|in:push,whatsapp,both', 'recipients' => 'required|string', 'title' => 'required|string|max:100', 'message' => 'required|string' ]); $campaignData = [ 'channel' => $request->channel, 'recipient_type' => $request->recipients, 'title' => $request->title, 'message' => $request->message, ]; DB::table('audit_logs')->insert([ 'category' => 'campaign', 'user' => auth()->user() ? auth()->user()->email : 'Admin', 'action' => json_encode($campaignData), 'ip_address' => $request->ip(), 'created_at' => now(), 'updated_at' => now(), ]); // Send FCM push notifications for broadcast if ($request->channel === 'push' || $request->channel === 'both') { $recipientsLower = strtolower($request->recipients); if (str_contains($recipientsLower, 'worker') || $recipientsLower === 'all') { $workers = \App\Models\Worker::whereNotNull('fcm_token')->get(); foreach ($workers as $worker) { \App\Services\FCMService::sendPushNotification( $worker->fcm_token, $request->title, $request->message, ['type' => 'broadcast'] ); } } if (str_contains($recipientsLower, 'employer') || $recipientsLower === 'all') { $employers = \App\Models\User::where('role', 'employer')->whereNotNull('fcm_token')->get(); foreach ($employers as $employer) { \App\Services\FCMService::sendPushNotification( $employer->fcm_token, $request->title, $request->message, ['type' => 'broadcast'] ); } } } return back()->with('success', "Notification campaign broadcasted successfully to all {$request->recipients} users!"); } /** * Payments, Revenue Reports & Refund Control */ public function refundPayment(Request $request, $id) { $request->validate([ 'refund_reason' => 'required|string', 'amount' => 'required|numeric|min:1' ]); $payment = DB::table('subscriptions')->where('id', $id)->first(); if ($payment) { DB::table('subscriptions')->where('id', $id)->update([ 'status' => 'refunded', 'updated_at' => now() ]); DB::table('audit_logs')->insert([ 'category' => 'admin_action', 'user' => auth()->user() ? auth()->user()->email : 'Admin', 'action' => "Refunded payment #{$id} of amount AED {$request->amount}. Reason: {$request->refund_reason}", 'ip_address' => $request->ip() ?: '127.0.0.1', 'created_at' => now(), 'updated_at' => now() ]); } return back()->with('success', "Refund of AED {$request->amount} initiated successfully for transaction {$id}. Refund reason: {$request->refund_reason}"); } /** * Interactive Reports Hub */ public function analytics(Request $request) { $reportType = $request->input('report_type', 'workers'); $startDate = $request->input('start_date'); $endDate = $request->input('end_date'); $status = $request->input('status', 'all'); $search = $request->input('search'); $nationality = $request->input('nationality', 'all'); $export = $request->input('export'); $perPage = (int)$request->input('per_page', 10); if ($perPage < 1) $perPage = 10; $headers = []; $query = null; $orderColumn = 'created_at'; $mapFn = null; if ($reportType === 'workers') { $query = DB::table('workers'); if ($startDate) { $query->whereDate('created_at', '>=', $startDate); } if ($endDate) { $query->whereDate('created_at', '<=', $endDate); } if ($status && $status !== 'all') { $query->where('status', $status); } if ($nationality && $nationality !== 'all') { $query->where('nationality', $nationality); } if ($search) { $query->where(function($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('email', 'like', "%{$search}%") ->orWhere('nationality', 'like', "%{$search}%"); }); } $orderColumn = 'created_at'; $headers = ['Worker ID', 'Name', 'Email', 'Phone', 'Nationality', 'Status', 'Salary (AED)', 'Verified', 'Joined Date']; $mapFn = function($w) { return [ 'id' => 'WRK-' . str_pad($w->id, 4, '0', STR_PAD_LEFT), 'name' => $w->name, 'email' => $w->email, 'phone' => $w->phone ?: 'N/A', 'nationality' => $w->nationality ?: 'N/A', 'status' => $w->status, 'salary' => (int)$w->salary, 'verified' => $w->verified ? 'Yes' : 'No', 'joined_at' => date('Y-m-d', strtotime($w->created_at)), ]; }; } elseif ($reportType === 'employers') { $query = DB::table('users') ->leftJoin('employer_profiles', 'users.id', '=', 'employer_profiles.user_id') ->leftJoin('sponsors', 'users.email', '=', 'sponsors.email') ->where('users.role', 'employer') ->select( 'users.id', 'users.name', 'users.email', 'sponsors.status as sponsor_status', 'sponsors.subscription_status', 'users.created_at' ); if ($startDate) { $query->whereDate('users.created_at', '>=', $startDate); } if ($endDate) { $query->whereDate('users.created_at', '<=', $endDate); } if ($status && $status !== 'all') { $query->where('sponsors.subscription_status', $status); } if ($search) { $query->where(function($q) use ($search) { $q->where('users.name', 'like', "%{$search}%") ->orWhere('users.email', 'like', "%{$search}%"); }); } $orderColumn = 'users.created_at'; $headers = ['Employer ID', 'Name', 'Email', 'Status', 'Subscription Status', 'Total Spent', 'Joined Date']; $mapFn = function($emp) { $totalPaid = DB::table('subscriptions')->where('user_id', $emp->id)->where('status', 'active')->sum('amount_aed'); return [ 'id' => 'EMP-' . str_pad($emp->id, 4, '0', STR_PAD_LEFT), 'name' => $emp->name, 'email' => $emp->email, 'status' => ucfirst($emp->sponsor_status ?? 'Active'), 'subscription' => ucfirst($emp->subscription_status ?? 'Inactive'), 'total_spent' => number_format($totalPaid, 2) . ' AED', 'joined_at' => date('Y-m-d', strtotime($emp->created_at)), ]; }; } elseif ($reportType === 'payments') { $query = DB::table('subscriptions') ->leftJoin('users', 'subscriptions.user_id', '=', 'users.id') ->select('subscriptions.*', 'users.name as user_name', 'users.email as user_email'); if ($startDate) { $query->whereDate('subscriptions.created_at', '>=', $startDate); } if ($endDate) { $query->whereDate('subscriptions.created_at', '<=', $endDate); } if ($status && $status !== 'all') { $query->where('subscriptions.status', $status === 'success' || $status === 'Completed' ? 'active' : $status); } if ($search) { $query->where(function($q) use ($search) { $q->where('users.name', 'like', "%{$search}%") ->orWhere('subscriptions.plan_id', 'like', "%{$search}%"); }); } $orderColumn = 'subscriptions.created_at'; $headers = ['Payment ID', 'Employer Name', 'Employer Email', 'Amount', 'Description', 'Status', 'Date']; $mapFn = function($pay) { $planLabel = ucfirst($pay->plan_id) . ' Sponsor Pass'; return [ 'id' => 'PAY-' . str_pad($pay->id, 4, '0', STR_PAD_LEFT), 'employer_name' => $pay->user_name ?: 'System Guest / Sponsor', 'employer_email' => $pay->user_email ?: 'no-email@marketplace.com', 'amount' => number_format((float)$pay->amount_aed, 2) . ' AED', 'description' => $planLabel, 'status' => $pay->status === 'active' ? 'Success' : ucfirst($pay->status), 'date' => date('Y-m-d H:i', strtotime($pay->created_at)), ]; }; } elseif ($reportType === 'tickets') { $query = DB::table('support_tickets') ->leftJoin('users', 'support_tickets.user_id', '=', 'users.id') ->leftJoin('report_reasons', 'support_tickets.reason_id', '=', 'report_reasons.id') ->select('support_tickets.*', 'users.name as user_name', 'users.email as user_email', 'report_reasons.reason as reason_name'); if ($startDate) { $query->whereDate('support_tickets.created_at', '>=', $startDate); } if ($endDate) { $query->whereDate('support_tickets.created_at', '<=', $endDate); } if ($status && $status !== 'all') { $query->where('support_tickets.status', $status); } if ($search) { $query->where(function($q) use ($search) { $q->where('users.name', 'like', "%{$search}%") ->orWhere('support_tickets.subject', 'like', "%{$search}%"); }); } $orderColumn = 'support_tickets.created_at'; $headers = ['Ticket ID', 'User Name', 'User Email', 'Subject', 'Reason', 'Status', 'Voice Note Attached', 'Created Date']; $mapFn = function($tick) { return [ 'id' => 'TCK-' . str_pad($tick->id, 4, '0', STR_PAD_LEFT), 'user_name' => $tick->user_name ?: 'System User', 'user_email' => $tick->user_email ?: 'N/A', 'subject' => $tick->subject, 'reason' => $tick->reason_name ?: 'General Support', 'status' => ucfirst($tick->status), 'has_voice_note' => $tick->voice_note_path ? 'Yes' : 'No', 'created_at' => date('Y-m-d H:i', strtotime($tick->created_at)), ]; }; } elseif ($reportType === 'safety') { $query = DB::table('moderation_reports'); if ($startDate) { $query->whereDate('reported_at', '>=', $startDate); } if ($endDate) { $query->whereDate('reported_at', '<=', $endDate); } if ($status && $status !== 'all') { $query->where('status', $status); } if ($search) { $query->where(function($q) use ($search) { $q->where('reported_user_name', 'like', "%{$search}%") ->orWhere('reported_by_name', 'like', "%{$search}%"); }); } $orderColumn = 'reported_at'; $headers = ['Report ID', 'Reported User', 'Reported By', 'Reason', 'Status', 'Reported At']; $mapFn = function($rep) { return [ 'id' => $rep->id, 'reported_user' => $rep->reported_user_name . ' (' . $rep->reported_user_role . ')', 'reported_by' => $rep->reported_by_name . ' (' . $rep->reported_by_role . ')', 'reason' => $rep->reason, 'status' => ucfirst($rep->status), 'reported_at' => date('Y-m-d H:i', strtotime($rep->reported_at)), ]; }; } // Add sorting $query->orderBy($orderColumn, 'desc'); if ($export === 'csv') { $data = $query->get()->map($mapFn); $filename = "report_" . $reportType . "_" . date('Ymd_His') . ".csv"; $callback = function() use ($data, $headers) { $file = fopen('php://output', 'w'); // UTF-8 BOM fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF)); fputcsv($file, $headers); foreach ($data as $row) { fputcsv($file, array_values((array)$row)); } fclose($file); }; return response()->stream($callback, 200, [ "Content-type" => "text/csv; charset=UTF-8", "Content-Disposition" => "attachment; filename={$filename}", "Pragma" => "no-cache", "Cache-Control" => "must-revalidate, post-check=0, pre-check=0", "Expires" => "0" ]); } // Normal paginated view $paginated = $query->paginate($perPage)->withQueryString(); $paginated->getCollection()->transform($mapFn); // Fetch distinct nationalities for filter $nationalities = DB::table('workers') ->whereNotNull('nationality') ->where('nationality', '!=', '') ->distinct() ->pluck('nationality') ->toArray(); return Inertia::render('Admin/Analytics/Index', [ 'reportType' => $reportType, 'startDate' => $startDate ?: '', 'endDate' => $endDate ?: '', 'status' => $status, 'search' => $search ?: '', 'nationality' => $nationality, 'headers' => $headers, 'reportData' => $paginated, 'nationalities' => $nationalities, 'perPage' => $perPage, ]); } /** * Global Database Searchable Audit Logs */ public function auditLogs(Request $request) { $searchTerm = $request->input('search', ''); $category = $request->input('category', 'all'); $query = DB::table('audit_logs'); if ($category !== 'all') { $query->where('category', $category); } if (!empty($searchTerm)) { $query->where(function($q) use ($searchTerm) { $q->where('user', 'like', '%' . $searchTerm . '%') ->orWhere('action', 'like', '%' . $searchTerm . '%') ->orWhere('id', 'like', '%' . $searchTerm . '%'); }); } $logs = $query->orderBy('created_at', 'desc') ->orderBy('id', 'desc') ->get() ->map(function ($log) { return [ 'id' => 'LOG-' . str_pad($log->id, 5, '0', STR_PAD_LEFT), 'category' => $log->category, 'user' => $log->user, 'action' => $log->action, 'ip_address' => $log->ip_address ?: 'Unknown', 'timestamp' => date('Y-m-d H:i', strtotime($log->created_at)), ]; }); return Inertia::render('Admin/AuditLogs/Index', [ 'logs' => $logs, 'search' => $searchTerm, 'category' => $category, ]); } /** * Charity Events & Drives management */ public function announcements() { $announcements = \App\Models\Announcement::with(['employer.employerProfile', 'sponsor'])->latest()->get()->map(function ($ann) { $postedBy = 'System'; $organization = 'Migrant Support'; if ($ann->sponsor_id) { $postedBy = $ann->sponsor->full_name; $organization = $ann->sponsor->organization_name; } elseif ($ann->employer_id) { $postedBy = $ann->employer->name; $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' => $content, 'type' => $ann->type ?? 'Charity', 'posted_by' => $postedBy, 'organization' => $organization, 'posted_by_email' => $ann->sponsor ? $ann->sponsor->email : ($ann->employer ? $ann->employer->email : null), 'status' => $ann->status ?? 'pending', 'remarks' => $ann->remarks, 'created_at' => $ann->created_at ? $ann->created_at->format('M d, Y h:i A') : 'Just now', 'charityDetails' => $charityDetails, ]; }); return Inertia::render('Admin/Events/Index', [ 'initialAnnouncements' => $announcements, ]); } /** * Store admin charity event */ public function storeAnnouncement(Request $request) { $request->validate([ 'title' => 'required|string|max:255', 'content' => 'required|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, ]); $ann = \App\Models\Announcement::create([ 'title' => $request->title, 'body' => $body, 'type' => 'Charity', 'status' => 'approved', ]); $this->notifyUsersOfEvent($ann); return back()->with('success', 'Charity Event created successfully.'); } /** * Approve charity event */ public function approveAnnouncement(Request $request, $id) { $ann = \App\Models\Announcement::findOrFail($id); $ann->update(['status' => 'approved']); $this->notifyUsersOfEvent($ann); return back()->with('success', 'Charity Event approved successfully.'); } /** * Reject charity event */ public function rejectAnnouncement(Request $request, $id) { $request->validate([ 'remarks' => 'required|string|max:1000', ]); $ann = \App\Models\Announcement::findOrFail($id); $ann->update([ 'status' => 'rejected', 'remarks' => $request->remarks, ]); return back()->with('success', 'Charity Event rejected successfully.'); } /** * Delete charity event */ public function deleteAnnouncement(Request $request, $id) { $ann = \App\Models\Announcement::findOrFail($id); $ann->delete(); return back()->with('success', 'Charity Event deleted successfully.'); } /** * Broadcast FCM Push Notification for new Event/Announcement. */ protected function notifyUsersOfEvent($ann) { $title = "New Event: " . $ann->title; $body = $ann->body; if (str_starts_with($ann->body, '{"type":"Charity"')) { $decoded = json_decode($ann->body, true); if ($decoded && isset($decoded['content'])) { $body = $decoded['content']; } } // Notify all workers $workers = \App\Models\Worker::whereNotNull('fcm_token')->get(); foreach ($workers as $worker) { \App\Services\FCMService::sendPushNotification( $worker->fcm_token, $title, $body, [ 'type' => 'announcement', 'announcement_id' => $ann->id, ] ); } // Notify all employers $employers = \App\Models\User::where('role', 'employer')->whereNotNull('fcm_token')->get(); foreach ($employers as $employer) { \App\Services\FCMService::sendPushNotification( $employer->fcm_token, $title, $body, [ 'type' => 'announcement', 'announcement_id' => $ann->id, ] ); } } }