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 (
-
Explore subscription cohort retention, job conversions ratios, availability trends, and monthly billing splits.
+Summary of workers, sponsors, disputes, and payments.
Employer and Worker registry trends (6 Months)
-Monthly billing splits (AED)
-Hiring Conversion stage details (This month)
-Month 1 to Month 6 user retention metrics
+ Total Workers + {worker_stats.total}Search and check payments.
+| Payment ID | +Customer | +Details | +Amount | +Status | +Date | +
|---|---|---|---|---|---|
| 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. + | +|||||
Track every critical administrative event, auto-OCR verification, stripe checkout charge, and user moderation updates.
+Detailed history of system events.
| Dispute ID | +Raised By | +Type | +Reason | +Status | +Created At | +Actions | +||
|---|---|---|---|---|---|---|---|---|
| + {ticket.id} + | +
+
+
+
+
+ {ticket.party_one_name}
+
+
+ {ticket.party_one_role}
+
+
+ |
+ + + {ticket.type} + + | ++ {ticket.reason} + | ++ + {ticket.status} + + | ++ {ticket.created_at} + | +
+
+
+ |
+ ||
| + No active disputes found matching your criteria. + | +||||||||
Assigned Mediator: {selectedTicket.assigned_to}
+Official authenticated conversation audit log
+Official authenticated conversation history audits
No attachment proofs uploaded.
- )} + + View +- {selectedTicket.admin_notes} -
-Submit official dispute settlement outcome
-Reach users directly via SMS, WhatsApp, and Native Push notifications. Monitor open rates and clicks.
+ {/* Simplified Header */} +Send quick push or WhatsApp alerts to workers and sponsors.
Schedules immediate or recurring broadcasts
-Logs of recently delivered campaigns
-{camp.message}
-Meta pre-approved utility check-in templates
-{tpl.text}
-Send a dynamic message immediately
Manage user abuse reports, content moderation queues, and customize automatic fraud filters.
+Review and take action on reported content, users and reviews.
Escalated complaints filed by employers or workers
-{report.description}
-| + + | +Report ID | +Type | +Reported User | +Reported By | +Reason | +Status | +Reported At | +Actions | +|
|---|---|---|---|---|---|---|---|---|---|
| + + | ++ {report.id} + | ++ + {report.type} + + | +
+
+
+
+ {report.reported_user_name}
+ {report.reported_user_role}
+
+ |
+
+
+
+
+ {report.reported_by_name}
+ {report.reported_by_role}
+
+ |
+ + {report.reason} + | ++ + {report.status} + + | ++ {report.reported_at} + | +
+
+
+ |
+ |
| + No incident reports found matching your criteria. + | +|||||||||
Bio updates or pictures flagged for manual review
Parameters triggering system automated flags
@@ -183,30 +400,21 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {System triggered 14 keyword warning notices to employers for sharing email/phone details in chats prior to hiring offer completions today.
-