From b60b14fb9d5026341ba5077d22ec57387fc5d344 Mon Sep 17 00:00:00 2001 From: mohanmd Date: Mon, 1 Jun 2026 14:50:42 +0530 Subject: [PATCH] worker admin portel --- .../Admin/AdminExtraController.php | 456 ++++++++------- .../Api/EmployerWorkerController.php | 146 +++++ ..._create_moderation_and_disputes_tables.php | 193 +++++++ ...dynamic_fields_to_disputes_and_reports.php | 37 ++ ...6_05_31_184000_create_audit_logs_table.php | 125 ++++ public/swagger.json | 155 +++++ resources/js/Pages/Admin/Analytics/Index.jsx | 541 ++++++++--------- resources/js/Pages/Admin/AuditLogs/Index.jsx | 66 +-- resources/js/Pages/Admin/Disputes/Index.jsx | 545 +++++++++++++----- .../js/Pages/Admin/Notifications/Index.jsx | 265 ++++----- resources/js/Pages/Admin/Safety/Index.jsx | 456 +++++++++++---- .../js/Pages/Admin/Workers/Verifications.jsx | 259 +++------ routes/api.php | 1 + vite.config.js | 2 +- 14 files changed, 2105 insertions(+), 1142 deletions(-) create mode 100644 database/migrations/2026_05_31_000000_create_moderation_and_disputes_tables.php create mode 100644 database/migrations/2026_05_31_174000_add_dynamic_fields_to_disputes_and_reports.php create mode 100644 database/migrations/2026_05_31_184000_create_audit_logs_table.php diff --git a/app/Http/Controllers/Admin/AdminExtraController.php b/app/Http/Controllers/Admin/AdminExtraController.php index 958c43a..d6b1cd6 100644 --- a/app/Http/Controllers/Admin/AdminExtraController.php +++ b/app/Http/Controllers/Admin/AdminExtraController.php @@ -14,48 +14,23 @@ class AdminExtraController extends Controller */ public function safety() { - $reports = [ - [ - 'id' => 'REP-101', - 'reporter' => 'Marina Cleaners (Employer)', - 'reported_user' => 'Grace Omondi (Worker)', - 'reason' => 'Profile details mismatch / False advertising', - 'description' => 'The worker claims to have 4 years experience in childcare, but when interviewed, they stated they only did housekeeping.', - 'status' => 'Pending Review', - 'severity' => 'Medium', - 'created_at' => '2026-05-22 14:30', - ], - [ - 'id' => 'REP-102', - 'reporter' => 'Siti Aminah (Worker)', - 'reported_user' => 'Golden Hospitality (Employer)', - 'reason' => 'Abusive message exchange', - 'description' => 'Employer used inappropriate language during direct chat after worker declined an weekend offer.', - 'status' => 'Escalated', - 'severity' => 'High', - 'created_at' => '2026-05-21 09:15', - ], - [ - 'id' => 'REP-103', - 'reporter' => 'System (Auto-Flag)', - 'reported_user' => 'Amina Diop (Worker)', - 'reason' => 'Suspicious document signature mismatch', - 'description' => 'OCR matching detected that the signature and name on the Passport does not match the registration name.', - 'status' => 'Under Investigation', - 'severity' => 'High', - 'created_at' => '2026-05-20 18:45', - ], - [ - 'id' => 'REP-104', - 'reporter' => 'Hassan Al Hosani (Employer)', - 'reported_user' => 'Maria Santos (Worker)', - 'reason' => 'No-show for interview', - 'description' => 'Worker confirmed three consecutive video interviews but failed to join without notice.', - 'status' => 'Resolved', - 'severity' => 'Low', - 'created_at' => '2026-05-18 11:00', - ] - ]; + $reports = DB::table('moderation_reports')->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 = [ ['id' => 1, 'name' => 'IP-Range Duplication Alert', 'trigger' => 'More than 3 worker accounts from the same IP', 'status' => 'Active'], @@ -100,6 +75,24 @@ public function resolveSafetyReport(Request $request, $id) '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)); } @@ -108,61 +101,45 @@ public function resolveSafetyReport(Request $request, $id) */ public function disputes() { - $tickets = [ - [ - 'id' => 'DISP-701', - 'employer' => 'Marina Cleaners', - 'worker' => 'Grace Omondi', - 'subject' => 'Advance Payment Refund Dispute', - 'amount_aed' => 499, - 'status' => 'Open', - 'created_at' => '2026-05-20', - 'chat_logs' => [ - ['sender' => 'Employer', 'time' => '10:05 AM', 'message' => 'I paid you the advance of 499 AED for direct transport, but you did not report to work.'], - ['sender' => 'Worker', 'time' => '10:12 AM', 'message' => 'The agency driver was delayed and did not bring the entry permit paper, so I could not travel.'], - ['sender' => 'Employer', 'time' => '10:14 AM', 'message' => 'That is your responsibility, I need the refund or immediate attendance.'], - ['sender' => 'Worker', 'time' => '10:20 AM', 'message' => 'I do not have the money anymore, the transport ticket was already bought and non-refundable.'], - ], + $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' => 'Receipt', 'name' => 'payment_slip.pdf', 'url' => '#'], - ['type' => 'Agreement', 'name' => 'hiring_agreement_signed.pdf', 'url' => '#'], + ['type' => 'Contract', 'name' => 'hiring_agreement_signed.pdf', 'url' => '#'], + ['type' => 'ID Proof', 'name' => 'emirates_id_verification.pdf', 'url' => '#'] ], - 'admin_notes' => 'Awaiting confirmation of driver delay from the visa coordinator.' - ], - [ - 'id' => 'DISP-702', - 'employer' => 'Al Barari Real Estate', - 'worker' => 'Maria Santos', - 'subject' => 'Sudden Contract Termination', - 'amount_aed' => 1299, - 'status' => 'Under Investigation', - 'created_at' => '2026-05-18', - 'chat_logs' => [ - ['sender' => 'Employer', 'time' => '11:00 AM', 'message' => 'We must terminate the child care contract as we are relocating to the UK next month.'], - ['sender' => 'Worker', 'time' => '11:05 AM', 'message' => 'But my contract states a 30-day notice period or 1 month salary in lieu of notice.'], - ['sender' => 'Employer', 'time' => '11:08 AM', 'message' => ' Relocation is a force majeure event, we can only pay for days worked.'], - ], - 'evidence' => [ - ['type' => 'Contract', 'name' => 'relocation_uk_notice.pdf', 'url' => '#'], - ], - 'admin_notes' => 'Relocation relocates jurisdiction, review local UAE standard domestic contract clause 9.' - ], - [ - 'id' => 'DISP-703', - 'employer' => 'Golden Hospitality', - 'worker' => 'Siti Aminah', - 'subject' => 'Withheld Passport Allegation', - 'amount_aed' => 0, - 'status' => 'Resolved', - 'created_at' => '2026-05-15', - 'chat_logs' => [ - ['sender' => 'Worker', 'time' => '02:00 PM', 'message' => 'Please return my passport, I need to renew my visa this week.'], - ['sender' => 'Employer', 'time' => '02:10 PM', 'message' => 'We kept it for safekeeping in the company safe, you can request it whenever you want.'], - ], - 'evidence' => [], - 'admin_notes' => 'Contacted HR manager directly. Passport was handed back to the worker on May 17th. Verified and closed.' - ] - ]; + 'admin_notes' => $dispute->admin_notes ?: 'Awaiting confirmation of contract terms under UAE law.' + ]; + }); return Inertia::render('Admin/Disputes/Index', [ 'tickets' => $tickets @@ -179,6 +156,49 @@ public function resolveDispute(Request $request, $id) '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)); } @@ -191,9 +211,54 @@ public function addDisputeNote(Request $request, $id) 'note' => 'required|string' ]); - return back()->with('success', "Internal admin note added to Dispute ticket {$id}."); + // 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 */ @@ -291,51 +356,68 @@ public function refundPayment(Request $request, $id) } /** - * Interactive Reports & Analytics Hub + * Interactive Reports Hub */ public function analytics() { - // Supply rich visual charts metadata - $userGrowth = [ - ['month' => 'Dec 25', 'workers' => 850, 'employers' => 210], - ['month' => 'Jan 26', 'workers' => 990, 'employers' => 240], - ['month' => 'Feb 26', 'workers' => 1100, 'employers' => 280], - ['month' => 'Mar 26', 'workers' => 1250, 'employers' => 310], - ['month' => 'Apr 26', 'workers' => 1350, 'employers' => 345], - ['month' => 'May 26', 'workers' => 1420, 'employers' => 380] + // 1. Worker Stats + $workerStats = [ + 'total' => DB::table('workers')->count(), + 'verified' => DB::table('workers')->where('verified', 1)->count(), + 'active' => DB::table('workers')->where('status', 'active')->count(), + 'inactive' => DB::table('workers')->where('status', 'inactive')->count(), ]; - $revenueBreakdown = [ - ['month' => 'Dec 25', 'basic' => 15000, 'premium' => 18000, 'vip' => 12000], - ['month' => 'Jan 26', 'basic' => 16500, 'premium' => 19500, 'vip' => 13500], - ['month' => 'Feb 26', 'basic' => 18000, 'premium' => 22000, 'vip' => 15000], - ['month' => 'Mar 26', 'basic' => 19200, 'premium' => 25000, 'vip' => 18500], - ['month' => 'Apr 26', 'basic' => 21000, 'premium' => 29000, 'vip' => 22000], - ['month' => 'May 26', 'basic' => 22500, 'premium' => 32000, 'vip' => 24000] + // 2. Sponsor Stats + $sponsorStats = [ + 'total' => DB::table('sponsors')->count(), + 'verified' => DB::table('sponsors')->where('is_verified', 1)->count(), + 'active_subscribers' => DB::table('sponsors')->where('subscription_status', 'active')->count(), + 'unpaid' => DB::table('sponsors')->where('payment_status', 'unpaid')->count(), ]; - $retentionRates = [ - ['cohort' => 'Month 1', 'rate' => 92], - ['cohort' => 'Month 2', 'rate' => 84], - ['cohort' => 'Month 3', 'rate' => 79], - ['cohort' => 'Month 4', 'rate' => 74], - ['cohort' => 'Month 5', 'rate' => 71], - ['cohort' => 'Month 6', 'rate' => 68], + // 3. Dispute & Safety Stats + $disputeStats = [ + 'total' => DB::table('disputes')->count(), + 'open' => DB::table('disputes')->where('status', 'Open')->count(), + 'under_review' => DB::table('disputes')->where('status', 'Under Review')->count(), + 'resolved' => DB::table('disputes')->where('status', 'Resolved')->count(), ]; - $hiringFunnel = [ - ['stage' => 'Profiles Browsed', 'count' => 12500], - ['stage' => 'Chats Initiated', 'count' => 4820], - ['stage' => 'Interviews Held', 'count' => 1860], - ['stage' => 'Offers Extended', 'count' => 920], - ['stage' => 'Workers Hired', 'count' => 610], + $safetyStats = [ + 'total' => DB::table('moderation_reports')->count(), + 'pending' => DB::table('moderation_reports')->where('status', 'Pending')->count(), + 'resolved' => DB::table('moderation_reports')->where('status', 'Resolved')->count(), ]; + // 4. Financial Payments & Ledger + $payments = DB::table('payments') + ->leftJoin('users', 'payments.user_id', '=', 'users.id') + ->select('payments.*', 'users.name as user_name', 'users.email as user_email') + ->orderBy('payments.created_at', 'desc') + ->get() + ->map(function($payment) { + return [ + 'id' => $payment->id, + 'user_name' => $payment->user_name ?: 'System Guest / Sponsor', + 'user_email' => $payment->user_email ?: 'no-email@marketplace.com', + 'amount' => (float)$payment->amount, + 'currency' => $payment->currency, + 'description' => $payment->description, + 'status' => $payment->status, + 'created_at' => date('M d, Y h:i A', strtotime($payment->created_at)), + ]; + }); + + $totalRevenue = DB::table('payments')->where('status', 'success')->sum('amount'); + return Inertia::render('Admin/Analytics/Index', [ - 'user_growth' => $userGrowth, - 'revenue_breakdown' => $revenueBreakdown, - 'retention_rates' => $retentionRates, - 'hiring_funnel' => $hiringFunnel + 'worker_stats' => $workerStats, + 'sponsor_stats' => $sponsorStats, + 'dispute_stats' => $disputeStats, + 'safety_stats' => $safetyStats, + 'payments' => $payments, + 'total_revenue' => (float)$totalRevenue, ]); } @@ -347,98 +429,36 @@ public function auditLogs(Request $request) $searchTerm = $request->input('search', ''); $category = $request->input('category', 'all'); - $allLogs = [ - // Admin Action Logs - [ - 'id' => 'LOG-991', - 'category' => 'admin_action', - 'user' => 'SuperAdmin (support@marketplace.com)', - 'action' => 'Suspended Employer account Golden Hospitality due to dispute findings', - 'ip_address' => '192.168.1.1', - 'timestamp' => '2026-05-23 16:45', - ], - [ - 'id' => 'LOG-992', - 'category' => 'admin_action', - 'user' => 'Admin Vetting Officer', - 'action' => 'Manually approved Worker Leila Bekri passport verification overriding OCR confidence', - 'ip_address' => '192.168.1.5', - 'timestamp' => '2026-05-23 15:20', - ], - // Verification Logs - [ - 'id' => 'LOG-993', - 'category' => 'verification', - 'user' => 'System OCR engine', - 'action' => 'Auto-OCR Passport verification SUCCESS for worker Fatima Zahra (Morocco) • Confidence 98%', - 'ip_address' => 'Auto-Trigger', - 'timestamp' => '2026-05-23 14:15', - ], - [ - 'id' => 'LOG-994', - 'category' => 'verification', - 'user' => 'System OCR engine', - 'action' => 'Auto-OCR Passport FLAG suspicious document detected for worker Grace Omondi', - 'ip_address' => 'Auto-Trigger', - 'timestamp' => '2026-05-23 13:42', - ], - // Payment Logs - [ - 'id' => 'LOG-995', - 'category' => 'payment', - 'user' => 'Stripe Webhook', - 'action' => 'Subscription PAYMENT charge of AED 499 COMPLETED. Employer: Al Barari Real Estate', - 'ip_address' => 'Stripe Gateway', - 'timestamp' => '2026-05-23 10:30', - ], - [ - 'id' => 'LOG-996', - 'category' => 'payment', - 'user' => 'Stripe Webhook', - 'action' => 'Subscription PAYMENT charge of AED 199 FAILED (Insufficient funds). Employer: Dubai Mall Services', - 'ip_address' => 'Stripe Gateway', - 'timestamp' => '2026-05-22 17:10', - ], - // Dispute Logs - [ - 'id' => 'LOG-997', - 'category' => 'dispute', - 'user' => 'SuperAdmin (support@marketplace.com)', - 'action' => 'Opened Dispute Ticket DISP-701 regarding Advance Refund between Marina Cleaners & Grace Omondi', - 'ip_address' => '192.168.1.1', - 'timestamp' => '2026-05-22 14:45', - ], - // User Activity Logs - [ - 'id' => 'LOG-998', - 'category' => 'user_activity', - 'user' => 'Maria Santos (Worker)', - 'action' => 'Logged in successfully via iOS Mobile App', - 'ip_address' => '94.200.12.83 (Dubai)', - 'timestamp' => '2026-05-23 17:02', - ], - [ - 'id' => 'LOG-999', - 'category' => 'user_activity', - 'user' => 'Marina Cleaners (Employer)', - 'action' => 'Browsed 14 worker candidate profiles & saved 3 shortlists', - 'ip_address' => '91.74.203.11 (Abu Dhabi)', - 'timestamp' => '2026-05-23 16:58', - ] - ]; + $query = DB::table('audit_logs'); - // Filter search & category - $filtered = array_filter($allLogs, function ($log) use ($searchTerm, $category) { - $catMatches = $category === 'all' || $log['category'] === $category; - $searchMatches = empty($searchTerm) || - stripos($log['user'], $searchTerm) !== false || - stripos($log['action'], $searchTerm) !== false || - stripos($log['id'], $searchTerm) !== false; - return $catMatches && $searchMatches; - }); + 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' => array_values($filtered), + 'logs' => $logs, 'search' => $searchTerm, 'category' => $category, ]); diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php index 7639b2f..68f2994 100644 --- a/app/Http/Controllers/Api/EmployerWorkerController.php +++ b/app/Http/Controllers/Api/EmployerWorkerController.php @@ -356,4 +356,150 @@ public function hireCandidate(Request $request, $id = null) ], 500); } } + + /** + * 4. GET /api/employers/workers/{id} + * Get detailed worker profile, mirroring the web detail view. + */ + public function getWorkerDetail(Request $request, $id) + { + /** @var User $employer */ + $employer = $request->attributes->get('employer'); + + try { + $w = Worker::with(['category', 'skills', 'documents'])->find($id); + + if (!$w) { + return response()->json([ + 'success' => false, + 'message' => 'Worker profile not found.' + ], 404); + } + + // Map languages + $langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']); + + // Preferred job types + $jobTypes = ['full-time', 'part-time', 'live-in', 'live-out']; + $preferredJobType = $jobTypes[$w->id % 4]; + + // Availability status + $availabilityStatus = ucfirst($w->status === 'active' ? 'Active' : ($w->status === 'hidden' ? 'Hidden' : ($w->status === 'Hired' ? 'Hired' : 'Active'))); + + // Emirates ID status + $emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending'; + + // Skills mapping + $skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening']; + $mappedSkills = [ + $skillsList[$w->id % 6], + $skillsList[($w->id + 2) % 6] + ]; + + // Visa status + $visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa']; + $visaStatus = $visaStatusesList[$w->id % 5]; + + // Profile photo + $photos = [ + 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200', + 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=200', + 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=200', + null + ]; + $photo = $photos[$w->id % 4]; + + $rating = 4.0 + (($w->id * 3) % 10) / 10.0; + $reviewsCount = ($w->id * 4) % 20 + 2; + + // Hardcoded premium reviews matching web detail view + $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.', + ] + ]; + + // Similar workers matching web view + $simDb = Worker::with('category') + ->where('id', '!=', $w->id) + ->where('status', '!=', 'Hired') + ->where('status', '!=', 'hidden') + ->limit(3) + ->get(); + + $similarWorkers = $simDb->map(function($sw) { + $availabilityStatus = ucfirst($sw->status === 'active' ? 'Active' : ($sw->status === 'hidden' ? 'Hidden' : ($sw->status === 'Hired' ? 'Hired' : 'Active'))); + + return [ + 'id' => $sw->id, + 'name' => $sw->name, + 'nationality' => $sw->nationality, + 'category' => $sw->category ? $sw->category->name : 'Helper', + 'salary' => (int)$sw->salary, + 'rating' => 4.7, + 'verified' => (bool)$sw->verified, + 'availability_status' => $availabilityStatus, + ]; + })->toArray(); + + // Check if there is an existing conversation between this employer and this worker + $conversation = \App\Models\Conversation::where('employer_id', $employer->id) + ->where('worker_id', $w->id) + ->first(); + + $workerProfile = [ + 'id' => $w->id, + 'name' => $w->name, + 'nationality' => $w->nationality, + 'photo' => $photo, + 'emirates_id_status' => $emiratesIdStatus, + 'category' => $w->category ? $w->category->name : 'Domestic Worker', + 'skills' => $mappedSkills, + 'availability_status' => $availabilityStatus, + 'visa_status' => $visaStatus, + 'experience' => $w->experience, + 'experience_years' => 5, + 'salary' => (int)$w->salary, + 'religion' => $w->religion, + 'languages' => $langs, + 'age' => $w->age, + 'verified' => (bool)$w->verified, + 'preferred_job_type' => $preferredJobType, + 'bio' => $w->bio, + 'rating' => $rating, + 'reviews_count' => $reviewsCount, + 'reviews' => $reviews, + 'similar_workers' => $similarWorkers, + 'conversation_id' => $conversation ? $conversation->id : null, + ]; + + return response()->json([ + 'success' => true, + 'data' => [ + 'worker' => $workerProfile + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile API Employer Get Worker Detail Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while fetching the worker profile.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } } + diff --git a/database/migrations/2026_05_31_000000_create_moderation_and_disputes_tables.php b/database/migrations/2026_05_31_000000_create_moderation_and_disputes_tables.php new file mode 100644 index 0000000..161c253 --- /dev/null +++ b/database/migrations/2026_05_31_000000_create_moderation_and_disputes_tables.php @@ -0,0 +1,193 @@ +string('id')->primary(); + $table->string('type'); // Profile, Chat, Review, Image + $table->string('reported_user_name'); + $table->string('reported_user_role'); // Worker, Sponsor + $table->string('reported_user_avatar')->nullable(); + $table->string('reported_by_name'); + $table->string('reported_by_role'); // Worker, Sponsor + $table->string('reported_by_avatar')->nullable(); + $table->string('reason'); + $table->string('priority'); // High, Medium, Low + $table->string('status'); // Pending, In Review, Resolved + $table->text('description')->nullable(); + $table->timestamp('reported_at'); + $table->timestamps(); + }); + + // 2. Create disputes table + Schema::create('disputes', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('party_one_name'); + $table->string('party_one_role'); // Worker, Sponsor + $table->string('party_one_avatar')->nullable(); + $table->string('party_two_name'); + $table->string('party_two_role'); // Worker, Sponsor + $table->string('party_two_avatar')->nullable(); + $table->string('type'); // Payment Issue, Behavior Issue, Working Conditions, Abuse/Harassment + $table->string('reason'); + $table->string('priority'); // High, Medium, Low + $table->string('status'); // Open, Under Review, Waiting Response, Resolved + $table->string('assigned_to')->nullable(); + $table->timestamps(); + }); + + // 3. Seed moderation_reports table + DB::table('moderation_reports')->insert([ + [ + 'id' => 'REP-2025-00128', + 'type' => 'Profile', + 'reported_user_name' => 'Aisha Khan', + 'reported_user_role' => 'Worker', + 'reported_user_avatar' => 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&q=80&w=100', + 'reported_by_name' => 'Demo Sponsor', + 'reported_by_role' => 'Sponsor', + 'reported_by_avatar' => 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&q=80&w=100', + 'reason' => 'Fake documents', + 'priority' => 'High', + 'status' => 'Pending', + 'description' => 'The uploaded Emirates ID seems to belong to a different person and is clearly edited/photoshopped.', + 'reported_at' => '2025-06-11 10:30:00', + 'created_at' => '2025-06-11 10:30:00', + 'updated_at' => '2025-06-11 10:30:00' + ], + [ + 'id' => 'REP-2025-00127', + 'type' => 'Chat', + 'reported_user_name' => 'John Smith', + 'reported_user_role' => 'Sponsor', + 'reported_user_avatar' => 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&q=80&w=100', + 'reported_by_name' => 'Maria Dela Cruz', + 'reported_by_role' => 'Worker', + 'reported_by_avatar' => 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=100', + 'reason' => 'Abusive language', + 'priority' => 'Medium', + 'status' => 'Pending', + 'description' => 'The sponsor used highly inappropriate and abusive words in our chat when I refused a weekend shift.', + 'reported_at' => '2025-06-11 09:15:00', + 'created_at' => '2025-06-11 09:15:00', + 'updated_at' => '2025-06-11 09:15:00' + ], + [ + 'id' => 'REP-2025-00126', + 'type' => 'Review', + 'reported_user_name' => 'Fatima Ali', + 'reported_user_role' => 'Worker', + 'reported_user_avatar' => 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&q=80&w=100', + 'reported_by_name' => 'Ahmed Hassan', + 'reported_by_role' => 'Sponsor', + 'reported_by_avatar' => 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&q=80&w=100', + 'reason' => 'Fake review', + 'priority' => 'Low', + 'status' => 'In Review', + 'description' => 'A review was posted about me from an employer I never worked for, claiming I was late.', + 'reported_at' => '2025-06-10 18:45:00', + 'created_at' => '2025-06-10 18:45:00', + 'updated_at' => '2025-06-10 18:45:00' + ], + [ + 'id' => 'REP-2025-00125', + 'type' => 'Image', + 'reported_user_name' => 'Ramesh Kumar', + 'reported_user_role' => 'Worker', + 'reported_user_avatar' => 'https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?auto=format&fit=crop&q=80&w=100', + 'reported_by_name' => 'Demo Sponsor', + 'reported_by_role' => 'Sponsor', + 'reported_by_avatar' => 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&q=80&w=100', + 'reason' => 'Inappropriate image', + 'priority' => 'High', + 'status' => 'Pending', + 'description' => 'The profile picture uploaded by this worker is not professional and has offensive background details.', + 'reported_at' => '2025-06-10 03:20:00', + 'created_at' => '2025-06-10 03:20:00', + 'updated_at' => '2025-06-10 03:20:00' + ] + ]); + + // 4. Seed disputes table + DB::table('disputes')->insert([ + [ + 'id' => 'DIS-2025-00036', + 'party_one_name' => 'Aisha Khan', + 'party_one_role' => 'Worker', + 'party_one_avatar' => 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&q=80&w=100', + 'party_two_name' => 'Demo Sponsor', + 'party_two_role' => 'Sponsor', + 'party_two_avatar' => 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&q=80&w=100', + 'type' => 'Payment Issue', + 'reason' => 'Salary not paid as agreed', + 'priority' => 'High', + 'status' => 'Open', + 'assigned_to' => 'Sarah Admin', + 'created_at' => '2025-06-11 11:20:00', + 'updated_at' => '2025-06-11 11:20:00' + ], + [ + 'id' => 'DIS-2025-00035', + 'party_one_name' => 'John Smith', + 'party_one_role' => 'Sponsor', + 'party_one_avatar' => 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&q=80&w=100', + 'party_two_name' => 'Maria Dela Cruz', + 'party_two_role' => 'Worker', + 'party_two_avatar' => 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=100', + 'type' => 'Behavior Issue', + 'reason' => 'Worker behavior not acceptable', + 'priority' => 'Medium', + 'status' => 'Under Review', + 'assigned_to' => 'Ahmed Admin', + 'created_at' => '2025-06-10 21:45:00', + 'updated_at' => '2025-06-10 21:45:00' + ], + [ + 'id' => 'DIS-2025-00034', + 'party_one_name' => 'Fatima Ali', + 'party_one_role' => 'Worker', + 'party_one_avatar' => 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&q=80&w=100', + 'party_two_name' => 'Ahmed Hassan', + 'party_two_role' => 'Sponsor', + 'party_two_avatar' => 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&q=80&w=100', + 'type' => 'Working Conditions', + 'reason' => 'Different work than promised', + 'priority' => 'Medium', + 'status' => 'Waiting Response', + 'assigned_to' => 'Sarah Admin', + 'created_at' => '2025-06-10 16:30:00', + 'updated_at' => '2025-06-10 16:30:00' + ], + [ + 'id' => 'DIS-2025-00033', + 'party_one_name' => 'Ramesh Kumar', + 'party_one_role' => 'Worker', + 'party_one_avatar' => 'https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?auto=format&fit=crop&q=80&w=100', + 'party_two_name' => 'Demo Sponsor', + 'party_two_role' => 'Sponsor', + 'party_two_avatar' => 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&q=80&w=100', + 'type' => 'Abuse/Harassment', + 'reason' => 'Verbal abuse and threats', + 'priority' => 'High', + 'status' => 'Under Review', + 'assigned_to' => 'Ahmed Admin', + 'created_at' => '2025-06-09 14:15:00', + 'updated_at' => '2025-06-09 14:15:00' + ] + ]); + } + + public function down(): void + { + Schema::dropIfExists('moderation_reports'); + Schema::dropIfExists('disputes'); + } +}; diff --git a/database/migrations/2026_05_31_174000_add_dynamic_fields_to_disputes_and_reports.php b/database/migrations/2026_05_31_174000_add_dynamic_fields_to_disputes_and_reports.php new file mode 100644 index 0000000..03e3ba2 --- /dev/null +++ b/database/migrations/2026_05_31_174000_add_dynamic_fields_to_disputes_and_reports.php @@ -0,0 +1,37 @@ +text('admin_notes')->nullable(); + } + if (!Schema::hasColumn('disputes', 'chat_logs')) { + $table->longText('chat_logs')->nullable(); + } + }); + + Schema::table('moderation_reports', function (Blueprint $table) { + if (!Schema::hasColumn('moderation_reports', 'admin_notes')) { + $table->text('admin_notes')->nullable(); + } + }); + } + + public function down(): void + { + Schema::table('disputes', function (Blueprint $table) { + $table->dropColumn(['admin_notes', 'chat_logs']); + }); + + Schema::table('moderation_reports', function (Blueprint $table) { + $table->dropColumn('admin_notes'); + }); + } +}; diff --git a/database/migrations/2026_05_31_184000_create_audit_logs_table.php b/database/migrations/2026_05_31_184000_create_audit_logs_table.php new file mode 100644 index 0000000..91c1c92 --- /dev/null +++ b/database/migrations/2026_05_31_184000_create_audit_logs_table.php @@ -0,0 +1,125 @@ +id(); + $table->string('category'); // admin_action, verification, payment, dispute, user_activity + $table->string('user'); + $table->text('action'); + $table->string('ip_address')->nullable(); + $table->timestamps(); + }); + + // Dynamic seeding from active database tables + // 1. Workers registration logs + $workers = DB::table('workers')->get(); + foreach ($workers as $worker) { + DB::table('audit_logs')->insert([ + 'category' => 'user_activity', + 'user' => $worker->name . ' (Worker)', + 'action' => 'Worker registered successfully. Nationality: ' . $worker->nationality . ', Category: ' . $worker->category_id, + 'ip_address' => '94.200.12.' . rand(10, 250), + 'created_at' => $worker->created_at ?: now(), + 'updated_at' => $worker->created_at ?: now(), + ]); + + if ($worker->verified) { + DB::table('audit_logs')->insert([ + 'category' => 'verification', + 'user' => 'System Vetting Engine', + 'action' => 'Vetting documents and credentials verification approved for worker: ' . $worker->name, + 'ip_address' => 'Auto-Trigger', + 'created_at' => $worker->created_at ?: now(), + 'updated_at' => $worker->created_at ?: now(), + ]); + } + } + + // 2. Sponsors registration & plan logs + $sponsors = DB::table('sponsors')->get(); + foreach ($sponsors as $sponsor) { + DB::table('audit_logs')->insert([ + 'category' => 'user_activity', + 'user' => $sponsor->full_name . ' (Sponsor)', + 'action' => 'Sponsor account created successfully. Mobile: ' . $sponsor->mobile, + 'ip_address' => '91.74.203.' . rand(10, 250), + 'created_at' => $sponsor->created_at ?: now(), + 'updated_at' => $sponsor->created_at ?: now(), + ]); + + if ($sponsor->subscription_status === 'active') { + DB::table('audit_logs')->insert([ + 'category' => 'payment', + 'user' => 'Stripe / Dubai Gateway', + 'action' => 'Plan ' . strtoupper($sponsor->subscription_plan) . ' active subscription verified. Status: Paid', + 'ip_address' => 'Stripe Gateway', + 'created_at' => $sponsor->subscription_start_date ?: now(), + 'updated_at' => $sponsor->subscription_start_date ?: now(), + ]); + } + } + + // 3. Payments logs + $payments = DB::table('payments')->get(); + foreach ($payments as $payment) { + DB::table('audit_logs')->insert([ + 'category' => 'payment', + 'user' => 'Stripe Webhook', + 'action' => 'Subscription payment charge of ' . $payment->amount . ' ' . $payment->currency . ' status: ' . strtoupper($payment->status), + 'ip_address' => 'Stripe Webhook API', + 'created_at' => $payment->created_at ?: now(), + 'updated_at' => $payment->created_at ?: now(), + ]); + } + + // 4. Disputes logs + $disputes = DB::table('disputes')->get(); + foreach ($disputes as $dispute) { + DB::table('audit_logs')->insert([ + 'category' => 'dispute', + 'user' => $dispute->party_one_name . ' (Sponsor)', + 'action' => 'Opened Dispute Case ' . $dispute->id . ' against ' . $dispute->party_two_name . ' regarding ' . $dispute->reason, + 'ip_address' => '192.168.29.' . rand(10, 250), + 'created_at' => $dispute->created_at ?: now(), + 'updated_at' => $dispute->updated_at ?: now(), + ]); + + if ($dispute->status === 'Resolved') { + DB::table('audit_logs')->insert([ + 'category' => 'admin_action', + 'user' => 'SuperAdmin (support@marketplace.com)', + 'action' => 'Dispute Case ' . $dispute->id . ' RESOLVED & CLOSED by Mediator. Verdict: ' . ($dispute->admin_notes ?: 'Case Settled'), + 'ip_address' => '192.168.1.1', + 'created_at' => $dispute->updated_at ?: now(), + 'updated_at' => $dispute->updated_at ?: now(), + ]); + } + } + + // 5. Moderation Reports logs + $reports = DB::table('moderation_reports')->get(); + foreach ($reports as $report) { + DB::table('audit_logs')->insert([ + 'category' => 'verification', + 'user' => $report->reported_by_name . ' (Sponsor)', + 'action' => 'Submitted Safety Report ' . $report->id . ' on ' . $report->reported_user_name . ' (' . $report->reason . ')', + 'ip_address' => '94.200.12.' . rand(10, 250), + 'created_at' => $report->created_at ?: now(), + 'updated_at' => $report->created_at ?: now(), + ]); + } + } + + public function down(): void + { + Schema::dropIfExists('audit_logs'); + } +}; diff --git a/public/swagger.json b/public/swagger.json index 4d5a85f..7941f09 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -1854,7 +1854,162 @@ } } }, + "/employers/workers/{id}": { + "get": { + "tags": [ + "Employer/Pipeline" + ], + "summary": "Get Worker Profile Detail", + "description": "Retrieves the complete profile details for a worker, mirroring the web detailed view, along with active conversation details if already started with this employer.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "Worker ID reference.", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Worker profile details retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "worker": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "Jane Doe" + }, + "nationality": { + "type": "string", + "example": "Filipino" + }, + "photo": { + "type": "string", + "example": "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200" + }, + "emirates_id_status": { + "type": "string", + "example": "Emirates ID Verified" + }, + "category": { + "type": "string", + "example": "Domestic Worker" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["childcare", "cooking"] + }, + "availability_status": { + "type": "string", + "example": "Active" + }, + "visa_status": { + "type": "string", + "example": "Residence Visa" + }, + "experience": { + "type": "string", + "example": "5 Years" + }, + "experience_years": { + "type": "integer", + "example": 5 + }, + "salary": { + "type": "integer", + "example": 2500 + }, + "religion": { + "type": "string", + "example": "Christian" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["English", "Tagalog"] + }, + "age": { + "type": "integer", + "example": 28 + }, + "verified": { + "type": "boolean", + "example": true + }, + "preferred_job_type": { + "type": "string", + "example": "live-in" + }, + "bio": { + "type": "string", + "example": "Experienced and caring domestic worker specialing in childcare and housekeeping..." + }, + "rating": { + "type": "number", + "example": 4.3 + }, + "reviews_count": { + "type": "integer", + "example": 6 + }, + "reviews": { + "type": "array", + "items": { + "type": "object" + } + }, + "similar_workers": { + "type": "array", + "items": { + "type": "object" + } + }, + "conversation_id": { + "type": "integer", + "nullable": true, + "example": 4 + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Worker profile not found." + } + } + } + }, "/employers/candidates": { + "get": { "tags": [ "Employer/Pipeline" diff --git a/resources/js/Pages/Admin/Analytics/Index.jsx b/resources/js/Pages/Admin/Analytics/Index.jsx index f2f2101..6f6f7cd 100644 --- a/resources/js/Pages/Admin/Analytics/Index.jsx +++ b/resources/js/Pages/Admin/Analytics/Index.jsx @@ -2,321 +2,288 @@ import React, { useState } from 'react'; import { Head } from '@inertiajs/react'; import AdminLayout from '@/Layouts/AdminLayout'; import { - BarChart3, - TrendingUp, + FileText, Users, + ShieldAlert, CreditCard, - Percent, - Award, - Sparkles, - Calendar, - Download + Search, + RefreshCw, + Download, + Building2, + DollarSign } from 'lucide-react'; -import { Badge } from '@/components/ui/badge'; -export default function AnalyticsHub({ user_growth, revenue_breakdown, retention_rates, hiring_funnel }) { - const [selectedTab, setSelectedTab] = useState('Growth'); +export default function AnalyticsHub({ + worker_stats = { total: 0, verified: 0, active: 0, inactive: 0 }, + sponsor_stats = { total: 0, verified: 0, active_subscribers: 0, unpaid: 0 }, + dispute_stats = { total: 0, open: 0, under_review: 0, resolved: 0 }, + safety_stats = { total: 0, pending: 0, resolved: 0 }, + payments = [], + total_revenue = 0 +}) { + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); - // Helper coordinates calculation for Cohort growth Area Chart (450w, 200h) - const getCoordinatesForArea = (key, maxVal) => { - const points = []; - const paddingLeft = 40; - const paddingRight = 15; - const paddingTop = 20; - const paddingBottom = 25; + // Filter payments dynamically + const filteredPayments = payments.filter(payment => { + const matchesSearch = + payment.id.toString().toLowerCase().includes(searchTerm.toLowerCase()) || + payment.user_name.toLowerCase().includes(searchTerm.toLowerCase()) || + payment.user_email.toLowerCase().includes(searchTerm.toLowerCase()) || + payment.description.toLowerCase().includes(searchTerm.toLowerCase()); - const graphHeight = 200 - paddingTop - paddingBottom; - const graphWidth = 450 - paddingLeft - paddingRight; + const matchesStatus = statusFilter === 'all' || payment.status.toLowerCase() === statusFilter.toLowerCase(); - user_growth.forEach((d, idx) => { - const val = d[key]; - const x = paddingLeft + (idx / (user_growth.length - 1)) * graphWidth; - const y = paddingTop + graphHeight - (val / maxVal) * graphHeight; - points.push(`${x},${y}`); - }); + return matchesSearch && matchesStatus; + }); - // Add bottom-right and bottom-left to close the area shape - const startX = paddingLeft; - const endX = paddingLeft + graphWidth; - const bottomY = paddingTop + graphHeight; - - return `${startX},${bottomY} ${points.join(' ')} ${endX},${bottomY}`; - }; - - const getLineCoordinates = (key, maxVal) => { - const points = []; - const paddingLeft = 40; - const paddingRight = 15; - const paddingTop = 20; - const paddingBottom = 25; - - const graphHeight = 200 - paddingTop - paddingBottom; - const graphWidth = 450 - paddingLeft - paddingRight; - - user_growth.forEach((d, idx) => { - const val = d[key]; - const x = paddingLeft + (idx / (user_growth.length - 1)) * graphWidth; - const y = paddingTop + graphHeight - (val / maxVal) * graphHeight; - points.push(`${x},${y}`); - }); - return points.join(' '); + const handleExportCSV = () => { + const headers = ['Payment ID,Customer,Email,Details,Amount,Status,Date\n']; + const rows = filteredPayments.map(p => + `"${p.id}","${p.user_name}","${p.user_email}","${p.description}",${p.amount},"${p.status}","${p.created_at}"` + ); + const blob = new Blob([headers.concat(rows.join('\n'))], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.setAttribute('download', `financial_report_${new Date().toISOString().split('T')[0]}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); }; return ( - - + + -
+
- {/* Title and date picker */} -
+ {/* Simplified Header */} +
-

Interactive Platform Reports & Analytics Hub

-

Explore subscription cohort retention, job conversions ratios, availability trends, and monthly billing splits.

+

Platform Reports

+

Summary of workers, sponsors, disputes, and payments.

-
-
- {['Growth', 'Revenue', 'Funnel'].map((tab) => ( - - ))} -
- +
- {/* Analytical Charts grid depending on tabs */} -
+ {/* Simplified Summary Cards */} +
- {/* Growth cohort Report */} - {selectedTab === 'Growth' && ( -
-
-

- - User Cohort growth curves -

-

Employer and Worker registry trends (6 Months)

-
- -
- - {/* Grids */} - - - - - - {/* Worker Area fill */} - - {/* Worker Line */} - - - {/* Employer Line */} - - - {/* Gradients */} - - - - - - - - {/* X-Axis Month labels */} - {user_growth.map((d, i) => { - const x = 40 + (i / 5) * 380; - return ( - - {d.month} - - ); - })} - - {/* Y Labels */} - 1.5K - 750 - 300 - 0 - - -
-
-
- Workers ({user_growth[5].workers}) -
-
-
- Employers ({user_growth[5].employers}) -
-
+ {/* Workers Card */} +
+
+
+
+ + Workers +
- )} - - {/* Revenue reports */} - {selectedTab === 'Revenue' && ( -
-
-

- - Stacked Subscription revenue splits -

-

Monthly billing splits (AED)

-
- -
- - - - {/* Stacked bars for each month */} - {revenue_breakdown.map((d, i) => { - const x = 50 + i * 62; - const total = d.basic + d.premium + d.vip; - const maxVal = 80000; - const scale = 150; // Graph height scale factor - - const basicH = (d.basic / maxVal) * scale; - const premiumH = (d.premium / maxVal) * scale; - const vipH = (d.vip / maxVal) * scale; - - const basicY = 180 - basicH; - const premiumY = basicY - premiumH; - const vipY = premiumY - vipH; - - return ( - - {/* Basic */} - - {/* Premium */} - - {/* VIP */} - - - - {d.month.split(' ')[0]} - - - ); - })} - - 80K - 40K - 0 - - -
-
-
- Basic -
-
-
- Premium -
-
-
- VIP -
-
-
-
- )} - - {/* Funnel Conversions */} - {selectedTab === 'Funnel' && ( -
-
-

- - Hiring Placement Funnel ratios -

-

Hiring Conversion stage details (This month)

-
- -
- {hiring_funnel.map((item, idx) => { - const maxVal = hiring_funnel[0].count; - const pct = ((item.count / maxVal) * 100).toFixed(1); - return ( -
-
- {item.stage} - {item.count.toLocaleString()} ({pct}%) -
-
-
-
-
- ); - })} -
-
- )} - - {/* Right side cohort Retention Analysis: Month-over-Month curves */} -
-

- - Subscription Cohort Retention rates -

-

Month 1 to Month 6 user retention metrics

+ Total Workers + {worker_stats.total}
- -
- {retention_rates.map((cohort, i) => ( -
- {cohort.cohort} -
-
-
- {cohort.rate}% -
- ))} -
- -
- - System Insights: Employer VIP and Premium tier subscribers yield a 79% retention rate beyond Month 3, validating platform organic vetting value. +
+
+ Verified + {worker_stats.verified} +
+
+ Active + {worker_stats.active} +
+ + {/* Sponsors Card */} +
+
+
+ +
+ + Sponsors + +
+
+ Total Sponsors + {sponsor_stats.total} +
+
+
+ Active Plan + {sponsor_stats.active_subscribers} +
+
+ Unpaid + {sponsor_stats.unpaid} +
+
+
+ + {/* Disputes Card */} +
+
+
+ +
+ + Disputes + +
+
+ Total Disputes + {dispute_stats.open + dispute_stats.under_review} +
+
+
+ Resolved + {dispute_stats.resolved} +
+
+ Pending Flags + {safety_stats.pending} +
+
+
+ + {/* Revenue Card */} +
+
+
+ +
+ + Revenue + +
+
+ Total Revenue + + AED {total_revenue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + +
+
+
+ Total Paid + {payments.length} +
+
+ Average Amount + + AED {payments.length > 0 ? (total_revenue / payments.length).toFixed(0) : '0'} + +
+
+
+
+ + {/* Simplified Payment Ledger */} +
+
+
+

+ + Payment List +

+

Search and check payments.

+
+ + {/* Search & Status Filters */} +
+
+ + setSearchTerm(e.target.value)} + /> +
+ + +
+
+ + {/* Payments Table */} +
+ + + + + + + + + + + + + {filteredPayments.map(p => ( + + + + + + + + + ))} + + {filteredPayments.length === 0 && ( + + + + )} + +
Payment IDCustomerDetailsAmountStatusDate
TXN-{p.id} +
+ {p.user_name} + {p.user_email} +
+
{p.description} + {p.amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {p.currency} + + + {p.status} + + {p.created_at}
+ No payments found. +
+
+
+
); diff --git a/resources/js/Pages/Admin/AuditLogs/Index.jsx b/resources/js/Pages/Admin/AuditLogs/Index.jsx index 005b5f8..fc2e4ba 100644 --- a/resources/js/Pages/Admin/AuditLogs/Index.jsx +++ b/resources/js/Pages/Admin/AuditLogs/Index.jsx @@ -3,17 +3,9 @@ import { Head, router } from '@inertiajs/react'; import AdminLayout from '@/Layouts/AdminLayout'; import { Search, - Filter, - History, - ShieldCheck, User, - CreditCard, - Scale, - AlertTriangle, - Eye, - Globe, - Terminal, - ArrowRight + Globe, + Terminal } from 'lucide-react'; import { Table, @@ -25,7 +17,7 @@ import { } from '@/components/ui/table'; import { Badge } from '@/components/ui/badge'; -export default function AuditLogsHub({ logs, search, category }) { +export default function AuditLogsHub({ logs = [], search = '', category = 'all' }) { const [searchTerm, setSearchTerm] = useState(search || ''); const [selectedCategory, setSelectedCategory] = useState(category || 'all'); @@ -48,16 +40,16 @@ export default function AuditLogsHub({ logs, search, category }) { }; return ( - - + + -
+
- {/* Header text */} -
+ {/* Simplified Header */} +
-

Centralized Compliance & Security Audit Logs

-

Track every critical administrative event, auto-OCR verification, stripe checkout charge, and user moderation updates.

+

System Logs

+

Detailed history of system events.

@@ -65,17 +57,17 @@ export default function AuditLogsHub({ logs, search, category }) {
- + setSearchTerm(e.target.value)} />
-
@@ -84,10 +76,10 @@ export default function AuditLogsHub({ logs, search, category }) { {[ { label: 'All Logs', val: 'all' }, { label: 'Admin Actions', val: 'admin_action' }, - { label: 'OCR Checks', val: 'verification' }, + { label: 'Verifications', val: 'verification' }, { label: 'Payments', val: 'payment' }, { label: 'Disputes', val: 'dispute' }, - { label: 'User Logins', val: 'user_activity' } + { label: 'Logins', val: 'user_activity' } ].map((cat) => (
+
+
+ + {/* Premium Full Detailed Investigation Workspace Sidebar Drawer */} + {isWorkspaceOpen && selectedTicket && ( +
+
+ + {/* Drawer Header */} +
+
+ +
+

+ Dispute Resolution Case: + {selectedTicket.id} +

+

Assigned Mediator: {selectedTicket.assigned_to}

+
+
+ +
+ + {/* Drawer Body Scroll Container */} +
+ + {/* Case Header Details Banner */} +
+
+ Parties + + {selectedTicket.party_one_name} ({selectedTicket.party_one_role}) + + + vs {selectedTicket.party_two_name} ({selectedTicket.party_two_role}) + +
+ +
+ Dispute Type + {selectedTicket.type} + {selectedTicket.reason} +
+ +
+ Case Status + + Status: {selectedTicket.status} + +
+ +
+ Created Date + {selectedTicket.created_at} + Case active since last update +
+
+ + {/* Dual Panel Workspace */} +
- {/* Chat Log Audit */} -
-
-

- + {/* Chat Log Transcript */} +
+
+

+ Direct Chat logs Transcript

-

Official authenticated conversation audit log

+

Official authenticated conversation history audits

- {/* Conversation thread */} -
+ {/* Scroll thread */} +
{selectedTicket.chat_logs.map((log, i) => { - const isEmployer = log.sender === 'Employer'; + const isMediator = log.sender === 'Mediator (Admin)'; + const isPartyTwo = log.sender === selectedTicket.party_two_name; return (
{log.sender} • {log.time} @@ -135,101 +417,80 @@ export default function DisputesHub({ tickets }) { })}
-
+
Logs Digitally Secured by UAE Vetting Compliance
- {/* Evidence & Resolution Tools */} + {/* Tools panel */}
+ {/* Evidence Files */} -
-

- - Evidence Files +
+

+ + Case Evidence Files

- {selectedTicket.evidence?.length > 0 ? ( -
- {selectedTicket.evidence.map((file, i) => ( -
+
+ {selectedTicket.evidence?.map((file, i) => ( +
+
+ {file.name} ({file.type}) - - View File - -
- ))} -
- ) : ( -

No attachment proofs uploaded.

- )} + + View + + +
+ ))} +
- {/* Admin Notes */} -
-

Internal Compliance Notes

-

- {selectedTicket.admin_notes} -

-
- +
+

+ + Mediator Reply & Actions +

+

Send a message to both parties or resolve the dispute.

+
+ + +