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/EmployerMessageController.php b/app/Http/Controllers/Api/EmployerMessageController.php
index 7ad1f9d..52152d6 100644
--- a/app/Http/Controllers/Api/EmployerMessageController.php
+++ b/app/Http/Controllers/Api/EmployerMessageController.php
@@ -7,12 +7,69 @@
use App\Models\Message;
use App\Models\Worker;
use App\Models\User;
+use App\Models\JobOffer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class EmployerMessageController extends Controller
{
+ public static function processWorkerResponse($conv, $worker, $text)
+ {
+ $replyText = strtolower(trim($text));
+
+ if ($replyText === 'yes' || $replyText === 'no') {
+ // Find the last message sent by the employer in this conversation (excluding the current worker message)
+ $lastEmployerMessage = Message::where('conversation_id', $conv->id)
+ ->where('sender_type', 'employer')
+ ->latest()
+ ->first();
+
+ if ($lastEmployerMessage) {
+ $questionText = strtolower($lastEmployerMessage->text);
+ $isLookingJobQuestion = str_contains($questionText, 'looking job') ||
+ str_contains($questionText, 'looking for a job') ||
+ str_contains($questionText, 'looking for job') ||
+ str_contains($questionText, 'are you looking');
+
+ if ($isLookingJobQuestion) {
+ if ($replyText === 'yes') {
+ // S6 Outcome: Update status as Hired
+ $worker->update([
+ 'status' => 'Hired',
+ 'availability' => 'Hired'
+ ]);
+
+ // Accept existing direct offer or application
+ $offer = JobOffer::where('employer_id', $conv->employer_id)
+ ->where('worker_id', $worker->id)
+ ->first();
+
+ if ($offer) {
+ $offer->update(['status' => 'accepted']);
+ } else {
+ JobOffer::create([
+ 'employer_id' => $conv->employer_id,
+ 'worker_id' => $worker->id,
+ 'work_date' => now()->format('Y-m-d'),
+ 'location' => 'Dubai',
+ 'salary' => $worker->salary ?: 2000,
+ 'notes' => 'Hired via chat agreement.',
+ 'status' => 'accepted',
+ ]);
+ }
+ } else if ($replyText === 'no') {
+ // S5 Outcome: Update status as Hidden (Auto-Hidden from search)
+ $worker->update([
+ 'status' => 'hidden',
+ 'availability' => 'Not Available'
+ ]);
+ }
+ }
+ }
+ }
+ }
+
/**
* Get all conversations for the authorized employer.
*
@@ -186,6 +243,32 @@ public function sendMessage(Request $request, $id)
// Touch conversation updated_at for sorting
$conv->touch();
+
+ // Check if employer sent predefined message "are you looking job?"
+ $textLower = strtolower($request->text);
+ if (
+ str_contains($textLower, 'looking job') ||
+ str_contains($textLower, 'looking for a job') ||
+ str_contains($textLower, 'looking for job') ||
+ str_contains($textLower, 'are you looking')
+ ) {
+ // Automatically simulate worker response 'Yes' (triggers the S6 hired flow)
+ $workerResponseText = 'Yes';
+
+ // Create the message in database
+ Message::create([
+ 'conversation_id' => $conv->id,
+ 'sender_type' => 'worker',
+ 'sender_id' => $conv->worker_id,
+ 'text' => $workerResponseText,
+ ]);
+
+ // Process the worker response to update status to Hired!
+ $worker = Worker::find($conv->worker_id);
+ if ($worker) {
+ self::processWorkerResponse($conv, $worker, $workerResponseText);
+ }
+ }
});
return response()->json([
diff --git a/app/Http/Controllers/Api/EmployerReviewController.php b/app/Http/Controllers/Api/EmployerReviewController.php
new file mode 100644
index 0000000..eac3c0c
--- /dev/null
+++ b/app/Http/Controllers/Api/EmployerReviewController.php
@@ -0,0 +1,144 @@
+attributes->get('employer');
+
+ $validator = Validator::make($request->all(), [
+ 'worker_id' => 'required|exists:workers,id',
+ 'rating' => 'required|integer|min:1|max:5',
+ 'comment' => 'nullable|string|max:1000',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Validation error.',
+ 'errors' => $validator->errors()
+ ], 422);
+ }
+
+ try {
+ // Check if employer has already reviewed this worker
+ $existingReview = Review::where('employer_id', $employer->id)
+ ->where('worker_id', $request->worker_id)
+ ->first();
+
+ if ($existingReview) {
+ // If they already have a review, we can update it or direct them to the edit endpoint.
+ // To be robust, let's update it directly and inform them it was updated (acting as an edit).
+ $existingReview->update([
+ 'rating' => $request->rating,
+ 'comment' => $request->comment,
+ ]);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Review updated successfully (existing review updated).',
+ 'data' => [
+ 'review' => $existingReview
+ ]
+ ], 200);
+ }
+
+ // Create new review
+ $review = Review::create([
+ 'employer_id' => $employer->id,
+ 'worker_id' => $request->worker_id,
+ 'rating' => $request->rating,
+ 'comment' => $request->comment,
+ ]);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Review added successfully.',
+ 'data' => [
+ 'review' => $review
+ ]
+ ], 201);
+
+ } catch (\Exception $e) {
+ logger()->error('Mobile API Add Review Failure: ' . $e->getMessage());
+
+ return response()->json([
+ 'success' => false,
+ 'message' => 'An error occurred while adding the review.',
+ 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
+ ], 500);
+ }
+ }
+
+ /**
+ * Edit/update an existing review.
+ * PUT /api/employers/reviews/{id}
+ */
+ public function editReview(Request $request, $id)
+ {
+ /** @var User $employer */
+ $employer = $request->attributes->get('employer');
+
+ $validator = Validator::make($request->all(), [
+ 'rating' => 'required|integer|min:1|max:5',
+ 'comment' => 'nullable|string|max:1000',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Validation error.',
+ 'errors' => $validator->errors()
+ ], 422);
+ }
+
+ try {
+ $review = Review::where('id', $id)
+ ->where('employer_id', $employer->id)
+ ->first();
+
+ if (!$review) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Review not found or unauthorized.'
+ ], 404);
+ }
+
+ $review->update([
+ 'rating' => $request->rating,
+ 'comment' => $request->comment,
+ ]);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Review edited successfully.',
+ 'data' => [
+ 'review' => $review
+ ]
+ ], 200);
+
+ } catch (\Exception $e) {
+ logger()->error('Mobile API Edit Review Failure: ' . $e->getMessage());
+
+ return response()->json([
+ 'success' => false,
+ 'message' => 'An error occurred while updating the review.',
+ 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
+ ], 500);
+ }
+ }
+}
diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php
index 7639b2f..8773bf9 100644
--- a/app/Http/Controllers/Api/EmployerWorkerController.php
+++ b/app/Http/Controllers/Api/EmployerWorkerController.php
@@ -11,6 +11,8 @@
use App\Models\JobOffer;
use App\Models\JobApplication;
use App\Models\JobPost;
+use App\Models\Review;
+use App\Models\ProfileView;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
@@ -356,4 +358,173 @@ 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);
+ }
+
+ // Record employer profile view (Requirement 3)
+ if ($employer) {
+ ProfileView::updateOrCreate(
+ ['employer_id' => $employer->id, 'worker_id' => $w->id],
+ ['updated_at' => now()]
+ );
+ }
+
+ // 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];
+
+ // Fetch dynamic database reviews (Requirement 1)
+ $dbReviews = Review::where('worker_id', $w->id)->with('employer')->latest()->get();
+ $reviews = $dbReviews->map(function ($rev) {
+ return [
+ 'id' => $rev->id,
+ 'employer_name' => $rev->employer->name ?? 'Employer',
+ 'rating' => $rev->rating,
+ 'date' => $rev->created_at->format('M d, Y'),
+ 'comment' => $rev->comment,
+ ];
+ })->toArray();
+
+ $reviewsCount = count($reviews);
+ if ($reviewsCount > 0) {
+ $rating = round($dbReviews->avg('rating'), 1);
+ } else {
+ $rating = 4.0 + (($w->id * 3) % 10) / 10.0;
+ $reviewsCount = ($w->id * 4) % 20 + 2;
+ $reviews = [
+ [
+ 'id' => 1,
+ 'employer_name' => 'Fatima Al Mansoori',
+ 'rating' => 5,
+ 'date' => 'May 10, 2026',
+ 'comment' => 'Extremely reliable and respectful. Professional work and great communication.',
+ ],
+ [
+ 'id' => 2,
+ 'employer_name' => 'Michael Harrison',
+ 'rating' => 4,
+ 'date' => 'Mar 24, 2026',
+ 'comment' => 'Very punctual, did exactly what was expected. Highly recommend.',
+ ]
+ ];
+ }
+
+ // 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/app/Http/Controllers/Api/WorkerMessageController.php b/app/Http/Controllers/Api/WorkerMessageController.php
index b2b1553..597fd23 100644
--- a/app/Http/Controllers/Api/WorkerMessageController.php
+++ b/app/Http/Controllers/Api/WorkerMessageController.php
@@ -6,12 +6,69 @@
use App\Models\Conversation;
use App\Models\Message;
use App\Models\Worker;
+use App\Models\JobOffer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class WorkerMessageController extends Controller
{
+ public static function processWorkerResponse($conv, $worker, $text)
+ {
+ $replyText = strtolower(trim($text));
+
+ if ($replyText === 'yes' || $replyText === 'no') {
+ // Find the last message sent by the employer in this conversation (excluding the current worker message)
+ $lastEmployerMessage = Message::where('conversation_id', $conv->id)
+ ->where('sender_type', 'employer')
+ ->latest()
+ ->first();
+
+ if ($lastEmployerMessage) {
+ $questionText = strtolower($lastEmployerMessage->text);
+ $isLookingJobQuestion = str_contains($questionText, 'looking job') ||
+ str_contains($questionText, 'looking for a job') ||
+ str_contains($questionText, 'looking for job') ||
+ str_contains($questionText, 'are you looking');
+
+ if ($isLookingJobQuestion) {
+ if ($replyText === 'yes') {
+ // S6 Outcome: Update status as Hired
+ $worker->update([
+ 'status' => 'Hired',
+ 'availability' => 'Hired'
+ ]);
+
+ // Accept existing direct offer or application
+ $offer = JobOffer::where('employer_id', $conv->employer_id)
+ ->where('worker_id', $worker->id)
+ ->first();
+
+ if ($offer) {
+ $offer->update(['status' => 'accepted']);
+ } else {
+ JobOffer::create([
+ 'employer_id' => $conv->employer_id,
+ 'worker_id' => $worker->id,
+ 'work_date' => now()->format('Y-m-d'),
+ 'location' => 'Dubai',
+ 'salary' => $worker->salary ?: 2000,
+ 'notes' => 'Hired via chat agreement.',
+ 'status' => 'accepted',
+ ]);
+ }
+ } else if ($replyText === 'no') {
+ // S5 Outcome: Update status as Hidden (Auto-Hidden from search)
+ $worker->update([
+ 'status' => 'hidden',
+ 'availability' => 'Not Available'
+ ]);
+ }
+ }
+ }
+ }
+ }
+
/**
* Get all conversations for the authorized worker.
*
@@ -181,6 +238,9 @@ public function sendMessage(Request $request, $id)
// Touch conversation updated_at for sorting
$conv->touch();
+
+ // Process the worker response to update status to Hired or Hidden
+ self::processWorkerResponse($conv, $worker, $request->text);
});
return response()->json([
diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php
index cd0dcaf..a361806 100644
--- a/app/Http/Controllers/Api/WorkerProfileController.php
+++ b/app/Http/Controllers/Api/WorkerProfileController.php
@@ -6,6 +6,9 @@
use App\Models\JobOffer;
use App\Models\Worker;
use App\Models\WorkerDocument;
+use App\Models\ProfileView;
+use App\Models\EmployerProfile;
+use App\Models\Announcement;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
@@ -431,4 +434,140 @@ public function respondToOffer(Request $request, $id)
], 500);
}
}
+
+ /**
+ * Get profile view statistics and employer listing for the worker dashboard.
+ * GET /api/workers/dashboard/views
+ */
+ public function getProfileViews(Request $request)
+ {
+ /** @var Worker $worker */
+ $worker = $request->attributes->get('worker');
+
+ try {
+ // Get views count
+ $viewsCount = ProfileView::where('worker_id', $worker->id)->count();
+
+ // Get list of employers/sponsors who viewed, with their details
+ $views = ProfileView::where('worker_id', $worker->id)
+ ->with('employer')
+ ->latest('updated_at')
+ ->get();
+
+ $list = $views->map(function ($view) {
+ $emp = $view->employer;
+
+ $empProfile = null;
+ if ($emp) {
+ $empProfile = EmployerProfile::where('user_id', $emp->id)->first();
+ }
+
+ return [
+ 'id' => $view->id,
+ 'employer_id' => $view->employer_id,
+ 'employer_name' => $emp->name ?? 'Employer/Sponsor',
+ 'company_name' => $empProfile->company_name ?? 'Private Sponsor',
+ 'nationality' => $empProfile->nationality ?? 'UAE',
+ 'city' => $empProfile->city ?? 'Dubai',
+ 'viewed_at' => $view->updated_at->toIso8601String(),
+ 'viewed_at_formatted' => $view->updated_at->diffForHumans(),
+ ];
+ });
+
+ return response()->json([
+ 'success' => true,
+ 'data' => [
+ 'views_count' => $viewsCount,
+ 'views_list' => $list
+ ]
+ ], 200);
+
+ } catch (\Exception $e) {
+ logger()->error('Worker Dashboard Views API Failure: ' . $e->getMessage());
+
+ return response()->json([
+ 'success' => false,
+ 'message' => 'An error occurred while fetching profile views statistics.',
+ 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
+ ], 500);
+ }
+ }
+
+ /**
+ * Get consolidated worker dashboard details (Requirement: dashboard api).
+ * GET /api/workers/dashboard
+ */
+ public function getDashboard(Request $request)
+ {
+ /** @var Worker $worker */
+ $worker = $request->attributes->get('worker');
+
+ try {
+ // 1. Active status
+ $activeStatus = $worker->status;
+
+ // 2. Count of sponsors who viewed the profile
+ $viewsCount = ProfileView::where('worker_id', $worker->id)->count();
+
+ // 3. Currently working sponsor/employer
+ $currentSponsor = null;
+ $acceptedOffer = JobOffer::where('worker_id', $worker->id)
+ ->where('status', 'accepted')
+ ->with('employer.employerProfile')
+ ->first();
+
+ if ($acceptedOffer && $acceptedOffer->employer) {
+ $emp = $acceptedOffer->employer;
+ $empProfile = $emp->employerProfile;
+ $currentSponsor = [
+ 'id' => $emp->id,
+ 'name' => $emp->name,
+ 'company_name' => $empProfile->company_name ?? 'Private Sponsor',
+ 'nationality' => $empProfile->nationality ?? 'UAE',
+ 'city' => $empProfile->city ?? 'Dubai',
+ 'email' => $emp->email,
+ 'phone' => $emp->phone,
+ 'hired_at' => $acceptedOffer->updated_at->toIso8601String(),
+ 'hired_at_formatted' => $acceptedOffer->updated_at->format('M d, Y'),
+ ];
+ }
+
+ // 4. Latest charity events / announcements list
+ $charityEvents = Announcement::with('employer.employerProfile')
+ ->latest()
+ ->limit(5)
+ ->get()
+ ->map(function ($event) {
+ return [
+ 'id' => $event->id,
+ 'title' => $event->title,
+ 'body' => $event->body,
+ 'type' => $event->type,
+ 'employer_name' => $event->employer->name ?? 'System',
+ 'company_name' => $event->employer->employerProfile->company_name ?? 'Migrant Support',
+ 'created_at' => $event->created_at->toIso8601String(),
+ 'time_ago' => $event->created_at->diffForHumans(),
+ ];
+ });
+
+ return response()->json([
+ 'success' => true,
+ 'data' => [
+ 'active_status' => $activeStatus,
+ 'count_sponsors_viewed' => $viewsCount,
+ 'currently_working_sponsor' => $currentSponsor,
+ 'latest_charity_events_list' => $charityEvents
+ ]
+ ], 200);
+
+ } catch (\Exception $e) {
+ logger()->error('Worker Dashboard API Failure: ' . $e->getMessage());
+
+ return response()->json([
+ 'success' => false,
+ 'message' => 'An error occurred while loading the worker dashboard.',
+ 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
+ ], 500);
+ }
+ }
}
diff --git a/app/Http/Controllers/Employer/MessageController.php b/app/Http/Controllers/Employer/MessageController.php
index 14e5ab5..bf57155 100644
--- a/app/Http/Controllers/Employer/MessageController.php
+++ b/app/Http/Controllers/Employer/MessageController.php
@@ -9,6 +9,7 @@
use App\Models\Conversation;
use App\Models\Message;
use App\Models\Worker;
+use App\Models\JobOffer;
class MessageController extends Controller
{
@@ -36,6 +37,62 @@ private function resolveCurrentUser()
return null;
}
+ public static function processWorkerResponse($conv, $worker, $text)
+ {
+ $replyText = strtolower(trim($text));
+
+ if ($replyText === 'yes' || $replyText === 'no') {
+ // Find the last message sent by the employer in this conversation (excluding the current worker message)
+ $lastEmployerMessage = Message::where('conversation_id', $conv->id)
+ ->where('sender_type', 'employer')
+ ->latest()
+ ->first();
+
+ if ($lastEmployerMessage) {
+ $questionText = strtolower($lastEmployerMessage->text);
+ $isLookingJobQuestion = str_contains($questionText, 'looking job') ||
+ str_contains($questionText, 'looking for a job') ||
+ str_contains($questionText, 'looking for job') ||
+ str_contains($questionText, 'are you looking');
+
+ if ($isLookingJobQuestion) {
+ if ($replyText === 'yes') {
+ // S6 Outcome: Update status as Hired
+ $worker->update([
+ 'status' => 'Hired',
+ 'availability' => 'Hired'
+ ]);
+
+ // Accept existing direct offer or application
+ $offer = JobOffer::where('employer_id', $conv->employer_id)
+ ->where('worker_id', $worker->id)
+ ->first();
+
+ if ($offer) {
+ $offer->update(['status' => 'accepted']);
+ } else {
+ JobOffer::create([
+ 'employer_id' => $conv->employer_id,
+ 'worker_id' => $worker->id,
+ 'work_date' => now()->format('Y-m-d'),
+ 'location' => 'Dubai',
+ 'salary' => $worker->salary ?: 2000,
+ 'notes' => 'Hired via chat agreement.',
+ 'status' => 'accepted',
+ ]);
+ }
+ } else if ($replyText === 'no') {
+ // S5 Outcome: Update status as Hidden (Auto-Hidden from search)
+ $worker->update([
+ 'status' => 'hidden',
+ 'availability' => 'Not Available'
+ ]);
+ }
+ }
+ }
+ }
+ }
+
public function index(Request $request)
{
$user = $this->resolveCurrentUser();
@@ -150,6 +207,32 @@ public function send(Request $request, $id)
// Touch conversation updated_at for sorting
$conv->touch();
+ // Check if employer sent predefined message "are you looking job?"
+ $textLower = strtolower($request->text);
+ if (
+ str_contains($textLower, 'looking job') ||
+ str_contains($textLower, 'looking for a job') ||
+ str_contains($textLower, 'looking for job') ||
+ str_contains($textLower, 'are you looking')
+ ) {
+ // Automatically simulate worker response 'Yes' (triggers the S6 hired flow)
+ $workerResponseText = 'Yes';
+
+ // Create the message in database
+ Message::create([
+ 'conversation_id' => $conv->id,
+ 'sender_type' => 'worker',
+ 'sender_id' => $conv->worker_id,
+ 'text' => $workerResponseText,
+ ]);
+
+ // Process the worker response to update status to Hired!
+ $worker = Worker::find($conv->worker_id);
+ if ($worker) {
+ self::processWorkerResponse($conv, $worker, $workerResponseText);
+ }
+ }
+
return back();
}
diff --git a/app/Http/Controllers/Employer/WorkerController.php b/app/Http/Controllers/Employer/WorkerController.php
index a070c52..4cc5cd6 100644
--- a/app/Http/Controllers/Employer/WorkerController.php
+++ b/app/Http/Controllers/Employer/WorkerController.php
@@ -10,6 +10,8 @@
use App\Models\WorkerCategory;
use App\Models\Shortlist;
use App\Models\JobOffer;
+use App\Models\Review;
+use App\Models\ProfileView;
class WorkerController extends Controller
{
@@ -134,6 +136,15 @@ public function index(Request $request)
public function show($id)
{
$w = Worker::with(['category', 'skills', 'documents'])->findOrFail($id);
+
+ // Record employer profile view (Requirement 3)
+ $user = $this->resolveCurrentUser();
+ if ($user) {
+ ProfileView::updateOrCreate(
+ ['employer_id' => $user->id, 'worker_id' => $w->id],
+ ['updated_at' => now()]
+ );
+ }
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : ( ($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic'] );
@@ -158,25 +169,41 @@ public function show($id)
];
$photo = $photos[$w->id % 4];
- $rating = 4.0 + (($w->id * 3) % 10) / 10.0;
- $reviewsCount = ($w->id * 4) % 20 + 2;
+ // Fetch dynamic database reviews (Requirement 1)
+ $dbReviews = Review::where('worker_id', $w->id)->with('employer')->latest()->get();
+ $reviews = $dbReviews->map(function ($rev) {
+ return [
+ 'id' => $rev->id,
+ 'employer_name' => $rev->employer->name ?? 'Employer',
+ 'rating' => $rev->rating,
+ 'date' => $rev->created_at->format('M d, Y'),
+ 'comment' => $rev->comment,
+ ];
+ })->toArray();
- $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.',
- ]
- ];
+ $reviewsCount = count($reviews);
+ if ($reviewsCount > 0) {
+ $rating = round($dbReviews->avg('rating'), 1);
+ } else {
+ $rating = 4.0 + (($w->id * 3) % 10) / 10.0;
+ $reviewsCount = ($w->id * 4) % 20 + 2;
+ $reviews = [
+ [
+ 'id' => 1,
+ 'employer_name' => 'Fatima Al Mansoori',
+ 'rating' => 5,
+ 'date' => 'May 10, 2026',
+ 'comment' => 'Extremely reliable and respectful. Professional work and great communication.',
+ ],
+ [
+ 'id' => 2,
+ 'employer_name' => 'Michael Harrison',
+ 'rating' => 4,
+ 'date' => 'Mar 24, 2026',
+ 'comment' => 'Very punctual, did exactly what was expected. Highly recommend.',
+ ]
+ ];
+ }
$simDb = Worker::with('category')
->where('id', '!=', $w->id)
diff --git a/app/Models/ProfileView.php b/app/Models/ProfileView.php
new file mode 100644
index 0000000..c936b05
--- /dev/null
+++ b/app/Models/ProfileView.php
@@ -0,0 +1,26 @@
+belongsTo(User::class, 'employer_id');
+ }
+
+ public function worker()
+ {
+ return $this->belongsTo(Worker::class, 'worker_id');
+ }
+}
diff --git a/app/Models/Review.php b/app/Models/Review.php
new file mode 100644
index 0000000..ecf185d
--- /dev/null
+++ b/app/Models/Review.php
@@ -0,0 +1,28 @@
+belongsTo(User::class, 'employer_id');
+ }
+
+ public function worker()
+ {
+ return $this->belongsTo(Worker::class, 'worker_id');
+ }
+}
diff --git a/app/Models/Worker.php b/app/Models/Worker.php
index 4aef447..516c049 100644
--- a/app/Models/Worker.php
+++ b/app/Models/Worker.php
@@ -60,4 +60,14 @@ public function offers()
{
return $this->hasMany(JobOffer::class, 'worker_id');
}
+
+ public function reviews()
+ {
+ return $this->hasMany(Review::class);
+ }
+
+ public function profileViews()
+ {
+ return $this->hasMany(ProfileView::class);
+ }
}
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/database/migrations/2026_06_01_000001_create_reviews_table.php b/database/migrations/2026_06_01_000001_create_reviews_table.php
new file mode 100644
index 0000000..648d0eb
--- /dev/null
+++ b/database/migrations/2026_06_01_000001_create_reviews_table.php
@@ -0,0 +1,34 @@
+id();
+ $table->foreignId('employer_id')->constrained('users')->onDelete('cascade');
+ $table->foreignId('worker_id')->constrained('workers')->onDelete('cascade');
+ $table->integer('rating');
+ $table->text('comment')->nullable();
+ $table->timestamps();
+
+ // Ensure an employer can only review a worker once
+ $table->unique(['employer_id', 'worker_id']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('reviews');
+ }
+};
diff --git a/database/migrations/2026_06_01_000002_create_profile_views_table.php b/database/migrations/2026_06_01_000002_create_profile_views_table.php
new file mode 100644
index 0000000..7cddd97
--- /dev/null
+++ b/database/migrations/2026_06_01_000002_create_profile_views_table.php
@@ -0,0 +1,32 @@
+id();
+ $table->foreignId('employer_id')->constrained('users')->onDelete('cascade');
+ $table->foreignId('worker_id')->constrained('workers')->onDelete('cascade');
+ $table->timestamps();
+
+ // Unique combination to track unique employer-worker views and update updated_at on duplicate
+ $table->unique(['employer_id', 'worker_id']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('profile_views');
+ }
+};
diff --git a/public/swagger.json b/public/swagger.json
index 4d5a85f..692d210 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"
@@ -1907,6 +2062,318 @@
}
}
}
+ },
+ "/workers/dashboard/views": {
+ "get": {
+ "tags": [
+ "Worker/Dashboard"
+ ],
+ "summary": "Get Worker Profile Views Statistics",
+ "description": "Allows authenticated workers to see the total count and detailed list of employers/sponsors who viewed their full profile.",
+ "responses": {
+ "200": {
+ "description": "Profile views statistics retrieved successfully.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "example": true
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "views_count": {
+ "type": "integer",
+ "example": 5
+ },
+ "views_list": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "example": 1
+ },
+ "employer_id": {
+ "type": "integer",
+ "example": 2
+ },
+ "employer_name": {
+ "type": "string",
+ "example": "Ahmad"
+ },
+ "company_name": {
+ "type": "string",
+ "example": "Ahmad Tech Ltd"
+ },
+ "nationality": {
+ "type": "string",
+ "example": "UAE"
+ },
+ "city": {
+ "type": "string",
+ "example": "Dubai"
+ },
+ "viewed_at": {
+ "type": "string",
+ "format": "date-time",
+ "example": "2026-06-01T15:10:24.000000Z"
+ },
+ "viewed_at_formatted": {
+ "type": "string",
+ "example": "2 minutes ago"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/workers/dashboard": {
+ "get": {
+ "tags": [
+ "Worker/Dashboard"
+ ],
+ "summary": "Get Worker Consolidated Dashboard Data",
+ "description": "Allows authenticated workers to retrieve their dashboard metrics: active status, profile views count, currently working sponsor/employer details, and latest charity events/announcements.",
+ "responses": {
+ "200": {
+ "description": "Worker dashboard details retrieved successfully.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "example": true
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "active_status": {
+ "type": "string",
+ "example": "active"
+ },
+ "count_sponsors_viewed": {
+ "type": "integer",
+ "example": 5
+ },
+ "currently_working_sponsor": {
+ "type": "object",
+ "nullable": true,
+ "properties": {
+ "id": {
+ "type": "integer",
+ "example": 2
+ },
+ "name": {
+ "type": "string",
+ "example": "Ahmad"
+ },
+ "company_name": {
+ "type": "string",
+ "example": "Ahmad Tech Ltd"
+ },
+ "nationality": {
+ "type": "string",
+ "example": "UAE"
+ },
+ "city": {
+ "type": "string",
+ "example": "Dubai"
+ },
+ "email": {
+ "type": "string",
+ "example": "ahmad@example.com"
+ },
+ "phone": {
+ "type": "string",
+ "example": "+971509990001"
+ },
+ "hired_at": {
+ "type": "string",
+ "format": "date-time",
+ "example": "2026-06-01T15:10:24.000000Z"
+ },
+ "hired_at_formatted": {
+ "type": "string",
+ "example": "Jun 01, 2026"
+ }
+ }
+ },
+ "latest_charity_events_list": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "example": 1
+ },
+ "title": {
+ "type": "string",
+ "example": "Free Dental Checkup Camp"
+ },
+ "body": {
+ "type": "string",
+ "example": "Emirates Charity is providing free screening."
+ },
+ "type": {
+ "type": "string",
+ "example": "charity"
+ },
+ "employer_name": {
+ "type": "string",
+ "example": "Emirates Charity"
+ },
+ "company_name": {
+ "type": "string",
+ "example": "Emirates Charity Foundation"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time",
+ "example": "2026-06-01T10:00:00.000000Z"
+ },
+ "time_ago": {
+ "type": "string",
+ "example": "5 hours ago"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/employers/reviews": {
+ "post": {
+ "tags": [
+ "Employer/Reviews"
+ ],
+ "summary": "Add or Update Worker Review",
+ "description": "Allows employers to submit a review score and description comment for a worker. If the employer has already left a review, this will update it.",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "worker_id",
+ "rating"
+ ],
+ "properties": {
+ "worker_id": {
+ "type": "integer",
+ "example": 136,
+ "description": "Worker ID being reviewed."
+ },
+ "rating": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 5,
+ "example": 5,
+ "description": "Review score from 1 to 5."
+ },
+ "comment": {
+ "type": "string",
+ "example": "Highly professional, punctual and did a great job!",
+ "description": "Optional detailed comment."
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Review added successfully."
+ },
+ "200": {
+ "description": "Review updated successfully."
+ },
+ "422": {
+ "description": "Validation error."
+ }
+ }
+ }
+ },
+ "/employers/reviews/{id}": {
+ "put": {
+ "tags": [
+ "Employer/Reviews"
+ ],
+ "summary": "Edit Worker Review",
+ "description": "Allows employers to edit their previously created review score and comment by review ID.",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "Review ID reference.",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "rating"
+ ],
+ "properties": {
+ "rating": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 5,
+ "example": 4,
+ "description": "Updated rating score from 1 to 5."
+ },
+ "comment": {
+ "type": "string",
+ "example": "Updated feedback comment.",
+ "description": "Updated review comment description."
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Review edited successfully."
+ },
+ "404": {
+ "description": "Review not found or unauthorized."
+ },
+ "422": {
+ "description": "Validation error."
+ }
+ }
+ }
}
},
"components": {
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.
-