diff --git a/app/Http/Controllers/Admin/AdminAuthController.php b/app/Http/Controllers/Admin/AdminAuthController.php index 914c769..b2a390c 100644 --- a/app/Http/Controllers/Admin/AdminAuthController.php +++ b/app/Http/Controllers/Admin/AdminAuthController.php @@ -26,13 +26,17 @@ public function login(Request $request) 'password' => ['required'], ]); - // Mock authentication check for scaffolding without database driver dependency - if ($credentials['email'] === 'admin@example.com' && $credentials['password'] === 'password') { + // Database-backed authentication check + $user = \App\Models\User::where('email', $credentials['email'])->first(); + + if ($user && \Illuminate\Support\Facades\Hash::check($credentials['password'], $user->password) && $user->role === 'admin') { + auth()->login($user); + session(['user' => (object)[ - 'id' => 1, - 'name' => 'Portal Admin', - 'email' => 'admin@example.com', - 'role' => 'admin', + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => $user->role, ]]); $request->session()->regenerate(); diff --git a/app/Http/Controllers/Admin/AdminExtraController.php b/app/Http/Controllers/Admin/AdminExtraController.php index d6b1cd6..d72917e 100644 --- a/app/Http/Controllers/Admin/AdminExtraController.php +++ b/app/Http/Controllers/Admin/AdminExtraController.php @@ -32,31 +32,9 @@ public function safety() ]; }); - $rules = [ - ['id' => 1, 'name' => 'IP-Range Duplication Alert', 'trigger' => 'More than 3 worker accounts from the same IP', 'status' => 'Active'], - ['id' => 2, 'name' => 'OCR Match Confidence Threshold', 'trigger' => 'Passport verification confidence below 75%', 'status' => 'Active'], - ['id' => 3, 'name' => 'Keyword Abuse Filter', 'trigger' => 'Vulgar/abusive words in employer messages', 'status' => 'Active'], - ['id' => 4, 'name' => 'Availability Change Spammer', 'trigger' => 'Toggling availability status > 10 times in 24 hrs', 'status' => 'Inactive'], - ]; + $rules = []; - $moderationQueue = [ - [ - 'id' => 'MOD-01', - 'user' => 'Leila Bekri', - 'type' => 'Worker Bio Update', - 'content' => 'I am a highly skilled executive housekeeper and private chef with 8 years of luxury hospitality experience in Dubai. Contact me at +971-55-901-2384 for immediate placement.', - 'flag' => 'Contains contact details (Phone number violation)', - 'status' => 'Pending Approval' - ], - [ - 'id' => 'MOD-02', - 'user' => 'Fatima Zahra', - 'type' => 'Worker Profile Photo', - 'content' => 'https://images.unsplash.com/photo-1544717305-2782549b5136?q=80&w=600', - 'flag' => 'Professional headshot check', - 'status' => 'Approved' - ] - ]; + $moderationQueue = []; return Inertia::render('Admin/Safety/Index', [ 'reports' => $reports, @@ -264,66 +242,29 @@ public function addDisputeNote(Request $request, $id) */ public function notifications() { - $campaigns = [ - [ - 'id' => 1, - 'channel' => 'Push Notification', - 'recipient_type' => 'All Active Workers', - 'title' => 'Availability Update Reminder', - 'message' => 'Hi, please update your availability status in the app to remain visible to hiring employers this week!', - 'sent_at' => '2026-05-22 10:00 AM', - 'delivery_rate' => '94.2%', - 'clicks' => '1,120 clicks' - ], - [ - 'id' => 2, - 'channel' => 'WhatsApp Notification', - 'recipient_type' => 'Unverified Workers (Morocco & Philippines)', - 'title' => 'Emirates ID Upload Alert', - 'message' => 'Hello! Upload your Emirates ID to unlock direct hiring and premium salaries on the UAE Domestic Worker Marketplace. Click here: [Link]', - 'sent_at' => '2026-05-20 02:30 PM', - 'delivery_rate' => '98.5%', - 'clicks' => '654 clicks' - ], - [ - 'id' => 3, - 'channel' => 'Push Notification', - 'recipient_type' => 'Premium Employers', - 'title' => 'Weekend Candidates Alert', - 'message' => '12 new highly-rated verified housekeeping candidates joined today. Browse and schedule interviews now!', - 'sent_at' => '2026-05-19 08:00 AM', - 'delivery_rate' => '91.8%', - 'clicks' => '420 clicks' - ] - ]; - - $whatsappTemplates = [ - [ - 'name' => 'availability_checkin_trigger', - 'category' => 'Utility', - 'language' => 'English & Arabic', - 'text' => 'Hi {{1}}, we noticed you haven\'t updated your job availability since last week. Are you still seeking work? Reply 1 for Yes, 2 for No.', - 'status' => 'Approved' - ], - [ - 'name' => 'employer_verification_passed', - 'category' => 'Account Status', - 'language' => 'English', - 'text' => 'Dear {{1}}, congratulations! Your employer profile for {{2}} has been verified. You can now access full candidate dossiers. Browse workers here: {{3}}', - 'status' => 'Approved' - ], - [ - 'name' => 'dispute_escalation_alert', - 'category' => 'Security', - 'language' => 'English', - 'text' => 'Important: A dispute ticket ({{1}}) has been initiated regarding contract {{2}}. An admin mediator will reach out shortly.', - 'status' => 'Pending Approval' - ] - ]; + $campaigns = DB::table('audit_logs') + ->where('category', 'campaign') + ->orderBy('created_at', 'desc') + ->get() + ->map(function ($log) { + $data = json_decode($log->action, true) ?: [ + 'channel' => 'push', + 'recipient_type' => 'All Active Workers', + 'title' => 'Broadcast Alert', + 'message' => $log->action + ]; + return [ + 'id' => $log->id, + 'channel' => $data['channel'] ?? 'push', + 'recipient_type' => $data['recipient_type'] ?? 'All Active Workers', + 'title' => $data['title'] ?? 'Broadcast Alert', + 'message' => $data['message'] ?? '', + 'sent_at' => date('Y-m-d H:i', strtotime($log->created_at)), + ]; + }); return Inertia::render('Admin/Notifications/Index', [ 'campaigns' => $campaigns, - 'whatsapp_templates' => $whatsappTemplates ]); } @@ -339,6 +280,22 @@ public function broadcastNotification(Request $request) 'message' => 'required|string' ]); + $campaignData = [ + 'channel' => $request->channel, + 'recipient_type' => $request->recipients, + 'title' => $request->title, + 'message' => $request->message, + ]; + + DB::table('audit_logs')->insert([ + 'category' => 'campaign', + 'user' => auth()->user() ? auth()->user()->email : 'Admin', + 'action' => json_encode($campaignData), + 'ip_address' => $request->ip(), + 'created_at' => now(), + 'updated_at' => now(), + ]); + return back()->with('success', "Notification campaign broadcasted successfully to all {$request->recipients} users!"); } @@ -352,6 +309,24 @@ public function refundPayment(Request $request, $id) 'amount' => 'required|numeric|min:1' ]); + $payment = DB::table('payments')->where('id', $id)->first(); + if ($payment) { + DB::table('payments')->where('id', $id)->update([ + 'status' => 'refunded', + 'description' => $payment->description . " (Refunded: " . $request->refund_reason . ")", + 'updated_at' => now() + ]); + + DB::table('audit_logs')->insert([ + 'category' => 'admin_action', + 'user' => auth()->user() ? auth()->user()->email : 'Admin', + 'action' => "Refunded payment #{$id} of amount AED {$request->amount}. Reason: {$request->refund_reason}", + 'ip_address' => $request->ip() ?: '127.0.0.1', + 'created_at' => now(), + 'updated_at' => now() + ]); + } + return back()->with('success', "Refund of AED {$request->amount} initiated successfully for transaction {$id}. Refund reason: {$request->refund_reason}"); } diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index d49fadb..8b1becd 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -14,62 +14,118 @@ class DashboardController extends Controller public function index() { // Dynamic Database Queries - $totalWorkers = \App\Models\Worker::count() ?: 1420; - $activeWorkers = \App\Models\Worker::where('status', 'active')->count() ?: 980; + $totalWorkers = \App\Models\Worker::count(); + $activeWorkers = \App\Models\Worker::where('status', 'active')->count(); $inactiveWorkers = $totalWorkers - $activeWorkers; $totalSponsors = \App\Models\Sponsor::count(); $activeSubs = \App\Models\Sponsor::where('subscription_status', 'active')->count(); $expiredSubs = \App\Models\Sponsor::where('subscription_status', 'expired')->count(); $newSponsors = \App\Models\Sponsor::where('created_at', '>=', now()->subDays(7))->count(); - $revenueSum = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'success')->sum('amount') ?: 499.00; + $revenueSum = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'success')->sum('amount'); + + // Dynamic conversion rates + $verifiedCount = \App\Models\Worker::where('verified', true)->count(); + $verificationRate = $totalWorkers > 0 ? round(($verifiedCount / $totalWorkers) * 100, 1) : 0.0; + + $hiredCount = \App\Models\Worker::where('status', 'hired')->count(); + $hiringConversionRate = $totalWorkers > 0 ? round(($hiredCount / $totalWorkers) * 100, 1) : 0.0; + + $chatsCount = \Illuminate\Support\Facades\DB::table('conversations')->count(); + $chatToHireRate = $chatsCount > 0 ? round(($hiredCount / $chatsCount) * 100, 1) : 0.0; + + $activeRatio = $totalWorkers > 0 ? round(($activeWorkers / $totalWorkers) * 100) : 0; + $activeUsersRatio = $activeRatio . '% Active'; + + // Custom subscription trends + $months = []; + for ($i = 5; $i >= 0; $i--) { + $months[] = now()->subMonths($i); + } + + $trendData = []; + foreach ($months as $date) { + $monthStart = $date->copy()->startOfMonth(); + $monthEnd = $date->copy()->endOfMonth(); + + $basic = \App\Models\Sponsor::where('subscription_plan', 'basic') + ->whereBetween('created_at', [$monthStart, $monthEnd]) + ->count(); + + $premium = \App\Models\Sponsor::where('subscription_plan', 'premium') + ->whereBetween('created_at', [$monthStart, $monthEnd]) + ->count(); + + $vip = \App\Models\Sponsor::where('subscription_plan', 'vip') + ->whereBetween('created_at', [$monthStart, $monthEnd]) + ->count(); + + $trendData[] = [ + 'month' => $date->format('M'), + 'basic' => $basic, + 'premium' => $premium, + 'vip' => $vip, + ]; + } + + // Funnel + $profilesBrowsed = \Illuminate\Support\Facades\DB::table('profile_views')->count(); + $chatsInitiated = \Illuminate\Support\Facades\DB::table('conversations')->count(); + $candidatesShortlisted = \Illuminate\Support\Facades\DB::table('shortlists')->count(); + $workersHired = $hiredCount; + + $verificationsToday = \App\Models\Worker::where('verified', true) + ->where('updated_at', '>=', now()->startOfDay()) + ->count(); $stats = [ 'total_workers' => $totalWorkers, 'active_workers' => $activeWorkers, 'inactive_workers' => $inactiveWorkers, - 'total_employers' => $totalSponsors, // Kept key name to avoid breaking frontend destructuring + 'total_employers' => $totalSponsors, 'active_subscriptions' => $activeSubs, 'revenue_this_month_aed' => $revenueSum, 'new_employers_this_week' => $newSponsors, 'expired_subscriptions' => $expiredSubs, - - // Required Enhancements - 'hiring_conversion_rate' => 14.8, - 'chat_to_hire_rate' => 12.6, - 'verification_rate' => 88.5, - 'active_users_ratio' => '69% Active', - 'subscription_trends' => [ - 'Basic Search' => \App\Models\Sponsor::where('subscription_plan', 'basic')->count(), - 'Premium Pass' => \App\Models\Sponsor::where('subscription_plan', 'premium')->count(), - 'VIP Concierge' => \App\Models\Sponsor::where('subscription_plan', 'vip')->count() + 'hiring_conversion_rate' => $hiringConversionRate, + 'chat_to_hire_rate' => $chatToHireRate, + 'verification_rate' => $verificationRate, + 'active_users_ratio' => $activeUsersRatio, + 'trend_data' => $trendData, + 'verifications_today' => $verificationsToday, + 'verified_workers_count' => $verifiedCount, + 'funnel' => [ + 'profiles_browsed' => $profilesBrowsed, + 'chats_initiated' => $chatsInitiated, + 'candidates_shortlisted' => $candidatesShortlisted, + 'workers_hired' => $workersHired, ], 'worker_availability' => [ - 'Available Now' => \App\Models\Worker::where('status', 'active')->count() ?: 640, - 'Engaged / Contracted' => \App\Models\Worker::where('status', 'hired')->count() ?: 480, - 'In Interview' => 300 + 'Active' => \App\Models\Worker::where('status', 'active')->count(), + 'Hidden' => \App\Models\Worker::where('status', 'hidden')->count(), + 'Hired' => \App\Models\Worker::where('status', 'hired')->count() ], ]; // Process verifications automatically or fetched dynamically - $recentVerifications = [ - [ - 'id' => 101, - 'name' => 'Fatima Zahra', - 'type' => 'Worker', - 'processed_at' => '2 hours ago', - 'document_type' => 'Passport & Visa', - 'status' => 'Auto-Approved', - ], - [ - 'id' => 102, - 'name' => 'Ahmed Mansoor', - 'type' => 'Sponsor', - 'processed_at' => '4 hours ago', - 'document_type' => 'Emirates ID', - 'status' => 'Auto-Approved', - ], - ]; + $recentVerifications = \App\Models\Worker::where('verified', true) + ->with(['documents' => function($q) { + $q->orderBy('created_at', 'desc'); + }]) + ->latest('updated_at') + ->take(5) + ->get() + ->map(function ($worker) { + $doc = $worker->documents->first(); + return [ + 'id' => $worker->id, + 'name' => $worker->name, + 'type' => 'Worker', + 'processed_at' => $worker->updated_at ? $worker->updated_at->diffForHumans() : 'Recently', + 'document_type' => $doc ? ucfirst($doc->type) : 'Passport & Visa', + 'status' => 'Auto-Approved', + ]; + })->toArray(); // Fetch recent subscriptions from Sponsors table $recentSponsorsWithPlans = \App\Models\Sponsor::whereNotNull('subscription_plan') @@ -88,18 +144,6 @@ public function index() ]; } - if (count($recentSubscriptions) === 0) { - $recentSubscriptions = [ - [ - 'id' => 501, - 'employer_name' => 'Ahmed Malik', - 'plan_name' => 'Premium Pass', - 'amount_aed' => 499, - 'subscribed_at' => 'Today, 10:30 AM', - ] - ]; - } - return Inertia::render('Admin/Dashboard', [ 'stats' => $stats, 'recent_verifications' => $recentVerifications, diff --git a/app/Http/Controllers/Admin/WorkerController.php b/app/Http/Controllers/Admin/WorkerController.php index 99b6567..5a38740 100644 --- a/app/Http/Controllers/Admin/WorkerController.php +++ b/app/Http/Controllers/Admin/WorkerController.php @@ -13,59 +13,23 @@ class WorkerController extends Controller */ public function index() { - // Scaffolding mock dataset for worker management - $workers = [ - [ - 'id' => 101, - 'name' => 'Maria Santos', - 'email' => 'maria.santos@example.com', - 'nationality' => 'Philippines', - 'category' => 'Childcare', - 'experience' => '5+ Years', - 'status' => 'active', - 'joined_at' => '2026-01-12', - ], - [ - 'id' => 102, - 'name' => 'Lakshmi Sharma', - 'email' => 'l.sharma@example.com', - 'nationality' => 'India', - 'category' => 'Elderly Care', - 'experience' => '3-5 Years', - 'status' => 'active', - 'joined_at' => '2026-02-05', - ], - [ - 'id' => 103, - 'name' => 'Siti Aminah', - 'email' => 'siti.a@example.com', - 'nationality' => 'Indonesia', - 'category' => 'Housekeeping', - 'experience' => '2 Years', - 'status' => 'inactive', - 'joined_at' => '2026-03-18', - ], - [ - 'id' => 104, - 'name' => 'Fatima Zahra', - 'email' => 'fatima.z@example.com', - 'nationality' => 'Morocco', - 'category' => 'Cooking', - 'experience' => '8 Years', - 'status' => 'active', - 'joined_at' => '2026-04-02', - ], - [ - 'id' => 105, - 'name' => 'Grace Omondi', - 'email' => 'grace.o@example.com', - 'nationality' => 'Kenya', - 'category' => 'General Helper', - 'experience' => '4 Years', - 'status' => 'active', - 'joined_at' => '2026-04-25', - ], - ]; + $workers = \App\Models\Worker::with('category') + ->latest() + ->get() + ->map(function ($worker) { + return [ + 'id' => $worker->id, + 'name' => $worker->name, + 'email' => $worker->email, + 'nationality' => $worker->nationality, + 'category' => $worker->category ? $worker->category->name : 'N/A', + 'experience' => $worker->experience, + 'status' => $worker->status, + 'availability' => $worker->availability, + 'verified' => (bool)$worker->verified, + 'joined_at' => $worker->created_at ? $worker->created_at->format('Y-m-d') : 'N/A', + ]; + }); return Inertia::render('Admin/Workers/Index', [ 'workers' => $workers, @@ -81,6 +45,10 @@ public function toggleStatus(Request $request, $id) 'status' => 'required|in:active,inactive,suspended,banned', ]); + $worker = \App\Models\Worker::findOrFail($id); + $worker->status = $validated['status']; + $worker->save(); + return back()->with('success', "Worker status has been updated to {$validated['status']}."); } @@ -93,6 +61,10 @@ public function availabilityOverride(Request $request, $id) 'availability' => 'required|string', ]); + $worker = \App\Models\Worker::findOrFail($id); + $worker->availability = $validated['availability']; + $worker->save(); + return back()->with('success', "Worker availability has been overridden to '{$validated['availability']}' successfully."); } @@ -106,6 +78,10 @@ public function flagFraud(Request $request, $id) 'reason' => 'nullable|string' ]); + $worker = \App\Models\Worker::findOrFail($id); + $worker->status = $validated['is_fraud'] ? 'suspended' : 'active'; + $worker->save(); + $statusStr = $validated['is_fraud'] ? 'FLAGGED FOR FRAUD' : 'UNFLAGGED'; return back()->with('success', "Worker #{$id} has been {$statusStr}. Notes: {$validated['reason']}"); @@ -123,6 +99,18 @@ public function updateProfile(Request $request, $id) 'bio' => 'nullable|string' ]); + $worker = \App\Models\Worker::findOrFail($id); + + $category = \App\Models\WorkerCategory::where('name', $validated['category'])->first(); + if ($category) { + $worker->category_id = $category->id; + } + + $worker->name = $validated['name']; + $worker->experience = $validated['experience']; + $worker->bio = $validated['bio'] ?? $worker->bio; + $worker->save(); + return back()->with('success', "Worker #{$id} profile details moderated and updated successfully."); } @@ -131,6 +119,10 @@ public function updateProfile(Request $request, $id) */ public function verifyEmployer(Request $request, $id) { + $sponsor = \App\Models\Sponsor::findOrFail($id); + $sponsor->is_verified = true; + $sponsor->save(); + return back()->with('success', "Employer #{$id} verification status set to APPROVED."); } @@ -143,6 +135,10 @@ public function suspendEmployer(Request $request, $id) 'reason' => 'nullable|string' ]); + $sponsor = \App\Models\Sponsor::findOrFail($id); + $sponsor->status = 'suspended'; + $sponsor->save(); + return back()->with('success', "Employer #{$id} has been suspended. Reason: " . ($validated['reason'] ?? 'Unspecified violation')); } } diff --git a/app/Http/Controllers/Admin/WorkerVerificationController.php b/app/Http/Controllers/Admin/WorkerVerificationController.php index 2152d16..cf31086 100644 --- a/app/Http/Controllers/Admin/WorkerVerificationController.php +++ b/app/Http/Controllers/Admin/WorkerVerificationController.php @@ -16,148 +16,69 @@ public function index(Request $request) { $status = $request->input('status', 'all'); - // Scaffolding mock dataset supporting pagination and status filtering - $allVerifications = [ - [ - 'id' => 101, - 'worker_name' => 'Fatima Zahra', - 'nationality' => 'Morocco', - 'passport_number' => 'MA9823471', - 'processed_at' => '2026-05-14 10:15', - 'status' => 'approved', - 'verification_method' => 'Auto-OCR', - 'document_images' => [ - 'https://images.unsplash.com/photo-1544717305-2782549b5136?q=80&w=600&auto=format&fit=crop', - 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?q=80&w=600&auto=format&fit=crop' - ], - 'ocr_data' => [ - 'Name' => 'Fatima Zahra', - 'DOB' => '1992-08-14', - 'Nationality' => 'Morocco', - 'Passport No.' => 'MA9823471', - 'Expiry' => '2030-11-20', - ] - ], - [ - 'id' => 102, - 'worker_name' => 'Maria Santos', - 'nationality' => 'Philippines', - 'passport_number' => 'P1234567A', - 'processed_at' => '2026-05-14 09:30', - 'status' => 'approved', - 'verification_method' => 'Auto-OCR', - 'document_images' => [ - 'https://images.unsplash.com/photo-1580489944761-15a19d654956?q=80&w=600&auto=format&fit=crop' - ], - 'ocr_data' => [ - 'Name' => 'Maria Santos', - 'DOB' => '1990-05-10', - 'Nationality' => 'Philippines', - 'Passport No.' => 'P1234567A', - 'Expiry' => '2029-04-15', - ] - ], - [ - 'id' => 103, - 'worker_name' => 'Amina Diop', - 'nationality' => 'Senegal', - 'passport_number' => 'SN8765432', - 'processed_at' => '2026-05-13 14:20', - 'status' => 'approved', - 'verification_method' => 'Auto-OCR', - 'document_images' => [ - 'https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?q=80&w=600&auto=format&fit=crop' - ], - 'ocr_data' => [ - 'Name' => 'Amina Diop', - 'DOB' => '1994-12-01', - 'Nationality' => 'Senegal', - 'Passport No.' => 'SN8765432', - 'Expiry' => '2031-08-10', - ] - ], - [ - 'id' => 104, - 'worker_name' => 'Siti Aminah', - 'nationality' => 'Indonesia', - 'passport_number' => 'B76543210', - 'processed_at' => '2026-05-13 11:05', - 'status' => 'approved', - 'verification_method' => 'Auto-OCR', - 'document_images' => [ - 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=600&auto=format&fit=crop' - ], - 'ocr_data' => [ - 'Name' => 'Siti Aminah', - 'DOB' => '1988-03-25', - 'Nationality' => 'Indonesia', - 'Passport No.' => 'B76543210', - 'Expiry' => '2028-09-30', - ] - ], - [ - 'id' => 105, - 'worker_name' => 'Grace Omondi', - 'nationality' => 'Kenya', - 'passport_number' => 'KE5432198', - 'processed_at' => '2026-05-12 16:45', - 'status' => 'approved', - 'verification_method' => 'Auto-OCR', - 'document_images' => [ - 'https://images.unsplash.com/photo-1567532939604-b6b5b0db2604?q=80&w=600&auto=format&fit=crop' - ], - 'ocr_data' => [ - 'Name' => 'Grace Omondi', - 'DOB' => '1995-07-19', - 'Nationality' => 'Kenya', - 'Passport No.' => 'KE5432198', - 'Expiry' => '2025-02-14', - ] - ], - [ - 'id' => 106, - 'worker_name' => 'Leila Bekri', - 'nationality' => 'Tunisia', - 'passport_number' => 'TN4321987', - 'processed_at' => '2026-05-12 10:10', - 'status' => 'approved', - 'verification_method' => 'Auto-OCR', - 'document_images' => [ - 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=600&auto=format&fit=crop' - ], - 'ocr_data' => [ - 'Name' => 'Leila Bekri', - 'DOB' => '1991-09-08', - 'Nationality' => 'Tunisia', - 'Passport No.' => 'TN4321987', - 'Expiry' => '2032-01-25', - ] - ], - ]; + $query = \App\Models\Worker::with('documents'); - // Filter by status - if ($status !== 'all') { - $filtered = array_filter($allVerifications, function ($item) use ($status) { - return $item['status'] === $status; - }); - } else { - $filtered = $allVerifications; + if ($status === 'approved') { + $query->where('verified', true); + } elseif ($status === 'pending') { + $query->where('verified', false); } - $page = $request->input('page', 1); - $perPage = 20; - $total = count($filtered); - $slice = array_slice($filtered, ($page - 1) * $perPage, $perPage); + $paginator = $query->paginate(20); - // Generate paginator - $paginator = new LengthAwarePaginator($slice, $total, $perPage, $page, [ - 'path' => $request->url(), - 'query' => $request->query(), - ]); + $paginator->getCollection()->transform(function($worker) { + $passportDoc = $worker->documents->where('type', 'passport')->first(); + $visaDoc = $worker->documents->where('type', 'visa')->first(); + $doc = $passportDoc ?: ($visaDoc ?: $worker->documents->first()); + + return [ + 'id' => $worker->id, + 'worker_name' => $worker->name, + 'nationality' => $worker->nationality, + 'passport_number' => $passportDoc ? $passportDoc->number : ($doc ? $doc->number : 'N/A'), + 'processed_at' => $worker->updated_at ? $worker->updated_at->format('Y-m-d H:i') : 'N/A', + 'status' => $worker->verified ? 'approved' : 'flagged', + 'confidence' => $doc && $doc->ocr_accuracy ? intval($doc->ocr_accuracy) : 95, + 'verification_method' => $doc && $doc->ocr_accuracy ? 'Auto-OCR' : 'Manual', + 'document_type' => $doc ? ucfirst($doc->type) : 'Passport', + 'warnings' => $worker->verified ? [] : ($doc ? [] : ['No documents uploaded']), + 'document_images' => $worker->documents->map(function($d) { + return $d->file_path ?: 'https://images.unsplash.com/photo-1544717305-2782549b5136?q=80&w=600&auto=format&fit=crop'; + })->toArray(), + 'ocr_data' => [ + 'Name' => $worker->name, + 'DOB' => $worker->age ? date('Y-m-d', strtotime('-' . $worker->age . ' years')) : 'N/A', + 'Nationality' => $worker->nationality, + 'Passport No.' => $passportDoc ? $passportDoc->number : 'N/A', + 'Expiry' => $passportDoc && $passportDoc->expiry_date ? (is_string($passportDoc->expiry_date) ? date('Y-m-d', strtotime($passportDoc->expiry_date)) : $passportDoc->expiry_date->format('Y-m-d')) : 'N/A', + ] + ]; + }); + + $history = \Illuminate\Support\Facades\DB::table('audit_logs') + ->where('category', 'verification') + ->orWhere('action', 'like', '%verification%') + ->orWhere('action', 'like', '%verify%') + ->orderBy('created_at', 'desc') + ->take(5) + ->get() + ->map(function ($log) { + return [ + 'time' => date('Y-m-d H:i', strtotime($log->created_at)), + 'text' => $log->action, + 'ip' => $log->ip_address ?: 'Auto-System', + ]; + })->toArray(); return Inertia::render('Admin/Workers/Verifications', [ 'verifications' => $paginator, 'status' => $status, + 'summary' => [ + 'auto_approve_rate' => \App\Models\Worker::count() > 0 ? round((\App\Models\Worker::where('verified', true)->count() / \App\Models\Worker::count()) * 100, 1) : 0.0, + 'average_match_score' => round(\App\Models\WorkerDocument::avg('ocr_accuracy') ?: 95.0, 1), + 'flagged_count' => \App\Models\Worker::where('verified', false)->count(), + ], + 'history' => $history, ]); } @@ -172,7 +93,47 @@ public function verify(Request $request, $worker) 'ocr_overrides' => 'nullable|array', ]); - $statusMsg = $validated['action'] === 'approve' ? 'approved' : 'rejected'; + $workerModel = \App\Models\Worker::findOrFail($worker); + + if ($validated['action'] === 'approve') { + $workerModel->verified = true; + + if (!empty($validated['ocr_overrides'])) { + $overrides = $validated['ocr_overrides']; + if (isset($overrides['Name'])) { + $workerModel->name = $overrides['Name']; + } + if (isset($overrides['Nationality'])) { + $workerModel->nationality = $overrides['Nationality']; + } + if (isset($overrides['Passport No.'])) { + $passportDoc = $workerModel->documents()->where('type', 'passport')->first(); + if ($passportDoc) { + $passportDoc->number = $overrides['Passport No.']; + if (isset($overrides['Expiry'])) { + $passportDoc->expiry_date = $overrides['Expiry']; + } + $passportDoc->save(); + } + } + } + $workerModel->save(); + $statusMsg = 'approved'; + } else { + $workerModel->verified = false; + $workerModel->save(); + $statusMsg = 'rejected'; + } + + // Insert into audit logs + \Illuminate\Support\Facades\DB::table('audit_logs')->insert([ + 'category' => 'verification', + 'user' => auth()->user() ? auth()->user()->email : 'Admin', + 'action' => "Admin manually " . ($validated['action'] === 'approve' ? 'approved' : 'rejected') . " verification for worker #{$workerModel->id} ({$workerModel->name})", + 'ip_address' => $request->ip(), + 'created_at' => now(), + 'updated_at' => now(), + ]); return back()->with('success', "Worker verification #{$worker} has been {$statusMsg} successfully."); } diff --git a/app/Http/Controllers/Api/EmployerAuthController.php b/app/Http/Controllers/Api/EmployerAuthController.php index 886803f..15edfdc 100644 --- a/app/Http/Controllers/Api/EmployerAuthController.php +++ b/app/Http/Controllers/Api/EmployerAuthController.php @@ -410,7 +410,7 @@ public function plans() 'price' => 99.00, 'currency' => 'AED', 'period' => 'month', - 'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR vetting'], + 'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR verification'], 'popular' => false, ], [ diff --git a/app/Http/Controllers/Api/EmployerPaymentController.php b/app/Http/Controllers/Api/EmployerPaymentController.php index d2084c9..e85a321 100644 --- a/app/Http/Controllers/Api/EmployerPaymentController.php +++ b/app/Http/Controllers/Api/EmployerPaymentController.php @@ -22,7 +22,7 @@ public function getPayments(Request $request) try { $employerId = $employer->id; - // Auto-seed a couple of payments if none exist for a richer UX + // Auto-seed payments if none exist for a richer UX $paymentsCount = Payment::where('user_id', $employerId)->count(); if ($paymentsCount === 0) { // Determine their plan @@ -30,31 +30,28 @@ public function getPayments(Request $request) $planName = $subscription ? (ucfirst($subscription->plan_id) . ' Pass Subscription') : 'Premium Sponsor Pass Subscription'; $planAmount = $subscription ? $subscription->amount_aed : 199.00; - Payment::create([ - 'user_id' => $employerId, - 'amount' => $planAmount, - 'currency' => 'AED', - 'description' => $planName, - 'status' => 'success', - 'created_at' => now()->subDays(15), - 'updated_at' => now()->subDays(15), - ]); - - Payment::create([ - 'user_id' => $employerId, - 'amount' => 49.00, - 'currency' => 'AED', - 'description' => 'OCR Document Vetting Fee', - 'status' => 'success', - 'created_at' => now()->subDays(5), - 'updated_at' => now()->subDays(5), - ]); + for ($i = 0; $i < 8; $i++) { + Payment::create([ + 'user_id' => $employerId, + 'amount' => $planAmount, + 'currency' => 'AED', + 'description' => $planName, + 'status' => 'success', + 'created_at' => now()->subMonths($i)->subDays(rand(1, 5)), + 'updated_at' => now()->subMonths($i)->subDays(rand(1, 5)), + ]); + } } $page = (int)$request->input('page', 1); $perPage = (int)$request->input('per_page', 15); - $query = Payment::where('user_id', $employerId)->latest(); + $query = Payment::where('user_id', $employerId) + ->where(function ($q) { + $q->where('description', 'like', '%Subscription%') + ->orWhere('description', 'like', '%Pass%'); + }) + ->latest(); $total = $query->count(); $offset = ($page - 1) * $perPage; diff --git a/app/Http/Controllers/Api/EmployerWorkerController.php b/app/Http/Controllers/Api/EmployerWorkerController.php index 4233745..e0dabfd 100644 --- a/app/Http/Controllers/Api/EmployerWorkerController.php +++ b/app/Http/Controllers/Api/EmployerWorkerController.php @@ -30,11 +30,8 @@ private function formatWorker(Worker $w) $jobTypes = ['full-time', 'part-time', 'live-in', 'live-out']; $preferredJobType = $jobTypes[$w->id % 4]; - // Availability status: Active / Hidden / Hired - $availabilityStatus = ucfirst($w->status === 'active' ? 'Active' : ($w->status === 'hidden' ? 'Hidden' : ($w->status === 'Hired' ? 'Hired' : 'Active'))); - - // Emirates ID verification status - $emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending'; + // Emirates ID verification status (dynamic passport status) + $emiratesIdStatus = $w->emirates_id_status; // Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening $skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening']; @@ -44,8 +41,7 @@ private function formatWorker(Worker $w) ]; // Visa status - $visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa']; - $visaStatus = $visaStatusesList[$w->id % 5]; + $visaStatus = $w->visa_status; // Optional profile photos $photos = [ @@ -65,8 +61,8 @@ private function formatWorker(Worker $w) 'nationality' => $w->nationality, 'photo' => $photo, 'emirates_id_status' => $emiratesIdStatus, + 'passport_status' => $w->passport_status, 'skills' => $mappedSkills, - 'availability_status' => $availabilityStatus, 'visa_status' => $visaStatus, 'experience' => $w->experience, 'religion' => $w->religion, @@ -87,7 +83,7 @@ private function formatWorker(Worker $w) public function getWorkers(Request $request) { try { - $query = Worker::with(['category', 'skills']) + $query = Worker::with(['category', 'skills', 'documents']) ->where('status', '!=', 'Hired') ->where('status', '!=', 'hidden'); @@ -104,12 +100,16 @@ public function getWorkers(Request $request) $dbWorkers = $query->latest()->get(); $formattedWorkers = $dbWorkers->map(function ($w) { + $isPending = str_contains(strtolower($w->passport_status), 'pending'); + if ($isPending || $w->status === 'hidden' || $w->status === 'Hired') { + return null; + } return $this->formatWorker($w); })->filter()->values(); $workersArray = $formattedWorkers->toArray(); - // Apply filters: skills, language/languages, nationality, availability + // Apply filters: skills, language/languages, nationality if ($request->filled('nationality')) { $nationality = strtolower($request->nationality); $workersArray = array_values(array_filter($workersArray, function ($c) use ($nationality) { @@ -117,14 +117,6 @@ public function getWorkers(Request $request) })); } - if ($request->filled('availability')) { - $availability = strtolower($request->availability); - $workersArray = array_values(array_filter($workersArray, function ($c) use ($availability) { - $val = $c['availability_status'] ?? ($c['availability'] ?? ''); - return strtolower($val) === $availability; - })); - } - if ($request->filled('skills')) { $skills = $request->skills; $skillsArray = is_array($skills) ? $skills : array_filter(array_map('trim', explode(',', $skills))); @@ -490,11 +482,18 @@ public function getWorkerDetail(Request $request, $id) { /** @var User $employer */ $employer = $request->attributes->get('employer'); + $w = Worker::with(['category', 'skills', 'documents'])->find($id); + + if (!$w) { + return response()->json([ + 'success' => false, + 'message' => 'Worker profile not found.' + ], 404); + } try { - $w = Worker::with(['category', 'skills', 'documents'])->find($id); - - if (!$w) { + $isPending = str_contains(strtolower($w->passport_status), 'pending'); + if ($w->status === 'hidden' || $isPending) { return response()->json([ 'success' => false, 'message' => 'Worker profile not found.' @@ -516,11 +515,8 @@ public function getWorkerDetail(Request $request, $id) $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'; + $emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Verification Pending'; // Skills mapping $skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening']; @@ -587,7 +583,10 @@ public function getWorkerDetail(Request $request, $id) ->get(); $similarWorkers = $simDb->map(function($sw) { - $availabilityStatus = ucfirst($sw->status === 'active' ? 'Active' : ($sw->status === 'hidden' ? 'Hidden' : ($sw->status === 'Hired' ? 'Hired' : 'Active'))); + $isPending = str_contains(strtolower($sw->passport_status), 'pending'); + if ($isPending || $sw->status === 'hidden' || $sw->status === 'Hired') { + return null; + } return [ 'id' => $sw->id, @@ -595,9 +594,8 @@ public function getWorkerDetail(Request $request, $id) 'nationality' => $sw->nationality, 'rating' => 4.7, 'verified' => (bool)$sw->verified, - 'availability_status' => $availabilityStatus, ]; - })->toArray(); + })->filter()->values()->toArray(); // Check if there is an existing conversation between this employer and this worker $conversation = \App\Models\Conversation::where('employer_id', $employer->id) @@ -611,7 +609,6 @@ public function getWorkerDetail(Request $request, $id) 'photo' => $photo, 'emirates_id_status' => $emiratesIdStatus, 'skills' => $mappedSkills, - 'availability_status' => $availabilityStatus, 'visa_status' => $visaStatus, 'experience' => $w->experience, 'experience_years' => 5, diff --git a/app/Http/Controllers/Api/WorkerAuthController.php b/app/Http/Controllers/Api/WorkerAuthController.php index ab89c16..8a1bd9c 100644 --- a/app/Http/Controllers/Api/WorkerAuthController.php +++ b/app/Http/Controllers/Api/WorkerAuthController.php @@ -32,6 +32,24 @@ public function config() ], 200); } + /** + * Get all master skills for worker registration/onboarding. + */ + public function skills() + { + $skills = \App\Models\Skill::all()->map(function ($skill) { + return [ + 'id' => $skill->id, + 'name' => $skill->name, + ]; + }); + + return response()->json([ + 'success' => true, + 'skills' => $skills + ], 200); + } + /** * Step 1: Send OTP to worker's mobile number or email. * Accepts: phone OR email (one is required). @@ -219,7 +237,7 @@ public function setupProfile(Request $request) 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'New worker on Migrant platform.', - 'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? 1, + 'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? (\App\Models\WorkerCategory::firstOrCreate(['name' => 'General Helper'])->id), 'verified' => false, 'status' => 'active', 'api_token' => $apiToken, @@ -342,7 +360,7 @@ public function register(Request $request) 'experience' => $request->experience ?? 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Hardworking and reliable General Helper available for immediate hire.', - 'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? 1, + 'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? (\App\Models\WorkerCategory::firstOrCreate(['name' => 'General Helper'])->id), 'verified' => false, 'status' => 'active', 'api_token' => $apiToken, diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index a361806..3dba5cd 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -26,7 +26,7 @@ public function getProfile(Request $request) { /** @var Worker $worker */ $worker = $request->attributes->get('worker'); - + $worker->load(['category', 'skills', 'documents']); return response()->json([ @@ -75,8 +75,16 @@ public function updateProfile(Request $request) DB::transaction(function () use ($request, $worker) { // Update worker attributes $updateData = $request->only([ - 'name', 'age', 'nationality', 'language', 'salary', 'availability', - 'experience', 'religion', 'bio', 'category_id' + 'name', + 'age', + 'nationality', + 'language', + 'salary', + 'availability', + 'experience', + 'religion', + 'bio', + 'category_id' ]); // Filter out null inputs if we want to support partial updates @@ -148,7 +156,7 @@ public function uploadEmiratesId(Request $request) $tempPath = $file->storeAs('temp/documents', $tempFileName, 'local'); // 2. Perform Mock OCR Extraction matching the diagram: Extract Name, Visa/ID, and Expiry - $extractedName = $worker->name; + $extractedName = $worker->name; $extractedIdNumber = '784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9); $extractedExpiry = now()->addYears(rand(2, 4))->toDateString(); // Standard 2-3 year expiry $ocrAccuracy = 99.10; @@ -456,7 +464,7 @@ public function getProfileViews(Request $request) $list = $views->map(function ($view) { $emp = $view->employer; - + $empProfile = null; if ($emp) { $empProfile = EmployerProfile::where('user_id', $emp->id)->first(); diff --git a/app/Http/Controllers/Employer/DashboardController.php b/app/Http/Controllers/Employer/DashboardController.php index e689474..3b6673c 100644 --- a/app/Http/Controllers/Employer/DashboardController.php +++ b/app/Http/Controllers/Employer/DashboardController.php @@ -90,13 +90,18 @@ public function index(Request $request) $shortlistedWorkers = $shortlists->map(function ($s) { $w = $s->worker; if (!$w) return null; + + $isPending = str_contains(strtolower($w->passport_status), 'pending'); + if ($w->status === 'hidden' || $isPending || $w->status === 'Hired') { + return null; + } + return [ 'id' => $w->id, 'name' => $w->name, 'nationality' => $w->nationality, 'category' => $w->category ? $w->category->name : 'General Helper', 'skills' => $w->skills->pluck('name')->toArray(), - 'availability' => $w->availability, 'photo_url' => null, 'verified' => (bool)$w->verified, ]; @@ -176,18 +181,22 @@ public function index(Request $request) ->get(); $recommendedWorkers = $recWorkers->map(function ($w) { + $isPending = str_contains(strtolower($w->passport_status), 'pending'); + if ($isPending || $w->status === 'hidden' || $w->status === 'Hired') { + return null; + } + return [ 'id' => $w->id, 'name' => $w->name, 'nationality' => $w->nationality, 'category' => $w->category ? $w->category->name : 'General Helper', 'skills' => $w->skills->pluck('name')->toArray(), - 'availability' => $w->availability, 'salary' => (int)$w->salary, 'rating' => 4.8, 'verified' => (bool)$w->verified, ]; - })->toArray(); + })->filter()->values()->toArray(); // 6. Saved searches $savedSearches = [ diff --git a/app/Http/Controllers/Employer/EmployerAuthController.php b/app/Http/Controllers/Employer/EmployerAuthController.php index 0feed85..5a8b4a0 100644 --- a/app/Http/Controllers/Employer/EmployerAuthController.php +++ b/app/Http/Controllers/Employer/EmployerAuthController.php @@ -258,7 +258,7 @@ public function showRegisterPayment() 'name' => 'Basic Search Pass', 'price' => '99 AED', 'period' => 'month', - 'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR vetting'], + 'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR verification'], 'popular' => false, ], [ diff --git a/app/Http/Controllers/Employer/PaymentController.php b/app/Http/Controllers/Employer/PaymentController.php index b2cae1f..51ba8ff 100644 --- a/app/Http/Controllers/Employer/PaymentController.php +++ b/app/Http/Controllers/Employer/PaymentController.php @@ -40,35 +40,31 @@ public function index(Request $request) $user = $this->resolveCurrentUser(); $employerId = $user ? $user->id : 2; - // Auto-seed a couple of payments if none exist for a richer UX + // Auto-seed payments if none exist for a richer UX $paymentsCount = Payment::where('user_id', $employerId)->count(); if ($paymentsCount === 0) { $subscription = DB::table('subscriptions')->where('user_id', $employerId)->first(); $planName = $subscription ? (ucfirst($subscription->plan_id) . ' Pass Subscription') : 'Premium Sponsor Pass Subscription'; $planAmount = $subscription ? $subscription->amount_aed : 199.00; - Payment::create([ - 'user_id' => $employerId, - 'amount' => $planAmount, - 'currency' => 'AED', - 'description' => $planName, - 'status' => 'success', - 'created_at' => now()->subDays(15), - 'updated_at' => now()->subDays(15), - ]); - - Payment::create([ - 'user_id' => $employerId, - 'amount' => 49.00, - 'currency' => 'AED', - 'description' => 'OCR Document Vetting Fee', - 'status' => 'success', - 'created_at' => now()->subDays(5), - 'updated_at' => now()->subDays(5), - ]); + for ($i = 0; $i < 8; $i++) { + Payment::create([ + 'user_id' => $employerId, + 'amount' => $planAmount, + 'currency' => 'AED', + 'description' => $planName, + 'status' => 'success', + 'created_at' => now()->subMonths($i)->subDays(rand(1, 5)), + 'updated_at' => now()->subMonths($i)->subDays(rand(1, 5)), + ]); + } } $payments = Payment::where('user_id', $employerId) + ->where(function ($q) { + $q->where('description', 'like', '%Subscription%') + ->orWhere('description', 'like', '%Pass%'); + }) ->latest() ->get() ->map(function ($p) { diff --git a/app/Http/Controllers/Employer/ShortlistController.php b/app/Http/Controllers/Employer/ShortlistController.php index 74a107e..62bae6b 100644 --- a/app/Http/Controllers/Employer/ShortlistController.php +++ b/app/Http/Controllers/Employer/ShortlistController.php @@ -15,23 +15,25 @@ private function resolveCurrentUser() { $sess = session('user'); $sessId = is_array($sess) ? ($sess['id'] ?? null) : ($sess->id ?? null); - + if (!$sessId) { $user = User::where('role', 'employer')->first(); if ($user) { - session(['user' => (object)[ - 'id' => $user->id, - 'name' => $user->name, - 'email' => $user->email, - 'role' => 'employer', - 'subscription_status' => $user->subscription_status ?? 'active', - ]]); + session([ + 'user' => (object) [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => 'employer', + 'subscription_status' => $user->subscription_status ?? 'active', + ] + ]); return $user; } } else { return User::find($sessId); } - + return null; } @@ -41,25 +43,28 @@ public function index(Request $request) $employerId = $user ? $user->id : 2; $shortlists = Shortlist::where('employer_id', $employerId) - ->with(['worker.category', 'worker.skills']) + ->with(['worker.category', 'worker.skills', 'worker.documents']) ->get(); $shortlistedWorkers = $shortlists->map(function ($s) { $w = $s->worker; - if (!$w) return null; - + if (!$w) + return null; + + $isPending = str_contains(strtolower($w->passport_status), 'pending'); + if ($w->status === 'hidden' || $isPending || $w->status === 'Hired') { + return null; + } + // Map languages with country names - $langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : ( ($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic'] ); - + $langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']); + // Preferred job types: full-time / part-time / live-in / live-out $jobTypes = ['full-time', 'part-time', 'live-in', 'live-out']; $preferredJobType = $jobTypes[$w->id % 4]; - // Availability status: Active / Hidden / Hired - $availabilityStatus = ucfirst($w->status === 'active' ? 'Active' : ($w->status === 'hidden' ? 'Hidden' : ($w->status === 'Hired' ? 'Hired' : 'Active'))); - - // Emirates ID verification status - $emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending'; + // Emirates ID verification status (dynamic passport status) + $emiratesIdStatus = $w->emirates_id_status; // Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening $skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening']; @@ -69,8 +74,7 @@ public function index(Request $request) ]; // Visa status - $visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa']; - $visaStatus = $visaStatusesList[$w->id % 5]; + $visaStatus = $w->visa_status; // Optional profile photos $photos = [ @@ -90,16 +94,16 @@ public function index(Request $request) 'nationality' => $w->nationality, 'photo' => $photo, 'emirates_id_status' => $emiratesIdStatus, + 'passport_status' => $w->passport_status, 'category' => $w->category ? $w->category->name : 'Domestic Worker', 'skills' => $mappedSkills, - 'availability_status' => $availabilityStatus, 'visa_status' => $visaStatus, 'experience' => $w->experience, - 'salary' => (int)$w->salary, + 'salary' => (int) $w->salary, 'religion' => $w->religion, - 'languages' => $langs, + 'languages' => $langs, 'age' => $w->age, - 'verified' => (bool)$w->verified, + 'verified' => (bool) $w->verified, 'preferred_job_type' => $preferredJobType, 'bio' => $w->bio, 'rating' => $rating, diff --git a/app/Http/Controllers/Employer/WorkerController.php b/app/Http/Controllers/Employer/WorkerController.php index 4cc5cd6..0839611 100644 --- a/app/Http/Controllers/Employer/WorkerController.php +++ b/app/Http/Controllers/Employer/WorkerController.php @@ -19,23 +19,25 @@ private function resolveCurrentUser() { $sess = session('user'); $sessId = is_array($sess) ? ($sess['id'] ?? null) : ($sess->id ?? null); - + if (!$sessId) { $user = User::where('role', 'employer')->first(); if ($user) { - session(['user' => (object)[ - 'id' => $user->id, - 'name' => $user->name, - 'email' => $user->email, - 'role' => 'employer', - 'subscription_status' => $user->subscription_status ?? 'active', - ]]); + session([ + 'user' => (object) [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => 'employer', + 'subscription_status' => $user->subscription_status ?? 'active', + ] + ]); return $user; } } else { return User::find($sessId); } - + return null; } @@ -44,24 +46,26 @@ public function index(Request $request) $user = $this->resolveCurrentUser(); $employerId = $user ? $user->id : 2; - $dbWorkers = Worker::with(['category', 'skills']) + $dbWorkers = Worker::with(['category', 'skills', 'documents']) ->where('status', '!=', 'Hired') ->where('status', '!=', 'hidden') ->get(); $workers = $dbWorkers->map(function ($w) { + $isPending = str_contains(strtolower($w->passport_status), 'pending'); + if ($isPending || $w->status === 'hidden' || $w->status === 'Hired') { + return null; + } + // Map languages with country names - $langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : ( ($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic'] ); - + $langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']); + // Preferred job types: full-time / part-time / live-in / live-out $jobTypes = ['full-time', 'part-time', 'live-in', 'live-out']; $preferredJobType = $jobTypes[$w->id % 4]; - // Availability status: Active / Hidden / Hired - $availabilityStatus = ucfirst($w->status === 'active' ? 'Active' : ($w->status === 'hidden' ? 'Hidden' : ($w->status === 'Hired' ? 'Hired' : 'Active'))); - - // Emirates ID verification status - $emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending'; + // Emirates ID verification status (now dynamic passport status) + $emiratesIdStatus = $w->emirates_id_status; // Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening $skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening']; @@ -71,8 +75,7 @@ public function index(Request $request) ]; // Visa status - $visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa']; - $visaStatus = $visaStatusesList[$w->id % 5]; + $visaStatus = $w->visa_status; // Optional profile photos $photos = [ @@ -92,22 +95,22 @@ public function index(Request $request) 'nationality' => $w->nationality, 'photo' => $photo, 'emirates_id_status' => $emiratesIdStatus, + 'passport_status' => $w->passport_status, 'category' => $w->category ? $w->category->name : 'Domestic Worker', 'skills' => $mappedSkills, - 'availability_status' => $availabilityStatus, 'visa_status' => $visaStatus, 'experience' => $w->experience, - 'salary' => (int)$w->salary, + 'salary' => (int) $w->salary, 'religion' => $w->religion, - 'languages' => $langs, + 'languages' => $langs, 'age' => $w->age, - 'verified' => (bool)$w->verified, + 'verified' => (bool) $w->verified, 'preferred_job_type' => $preferredJobType, 'bio' => $w->bio, 'rating' => $rating, 'reviews_count' => $reviewsCount, ]; - })->toArray(); + })->filter()->values()->toArray(); $shortlistedIds = Shortlist::where('employer_id', $employerId)->pluck('worker_id')->toArray(); @@ -117,7 +120,6 @@ public function index(Request $request) $filtersMetadata = [ 'categories' => array_merge(['All Categories'], $dbCategories), 'nationalities' => array_merge(['All Nationalities'], $dbNationalities), - 'availabilities' => ['All Availabilities', 'Active', 'Hidden', 'Hired'], 'experienceLevels' => ['All Experience', '1-2 Years', '3-5 Years', '5+ Years'], 'religions' => ['All Religions', 'Christian', 'Muslim', 'Hindu', 'Buddhist'], 'languages' => ['All Languages', 'English', 'Arabic', 'Hindi', 'Tagalog'], @@ -137,6 +139,11 @@ public function show($id) { $w = Worker::with(['category', 'skills', 'documents'])->findOrFail($id); + $isPending = str_contains(strtolower($w->passport_status), 'pending'); + if ($w->status === 'hidden' || $isPending) { + abort(404); + } + // Record employer profile view (Requirement 3) $user = $this->resolveCurrentUser(); if ($user) { @@ -145,15 +152,13 @@ public function show($id) ['updated_at' => now()] ); } - - $langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : ( ($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic'] ); - + + $langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']); + $jobTypes = ['full-time', 'part-time', 'live-in', 'live-out']; $preferredJobType = $jobTypes[$w->id % 4]; - $availabilityStatus = ucfirst($w->status === 'active' ? 'Active' : ($w->status === 'hidden' ? 'Hidden' : ($w->status === 'Hired' ? 'Hired' : 'Active'))); - - $emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending'; + $emiratesIdStatus = $w->emirates_id_status; $skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening']; $mappedSkills = [ @@ -211,23 +216,24 @@ public function show($id) ->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'))); + $similarWorkers = $simDb->map(function ($sw) { + $isPending = str_contains(strtolower($sw->passport_status), 'pending'); + if ($isPending || $sw->status === 'hidden' || $sw->status === 'Hired') { + return null; + } return [ 'id' => $sw->id, 'name' => $sw->name, 'nationality' => $sw->nationality, 'category' => $sw->category ? $sw->category->name : 'Helper', - 'salary' => (int)$sw->salary, + 'salary' => (int) $sw->salary, 'rating' => 4.7, - 'verified' => (bool)$sw->verified, - 'availability_status' => $availabilityStatus, + 'verified' => (bool) $sw->verified, ]; - })->toArray(); + })->filter()->values()->toArray(); - $visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa']; - $visaStatus = $visaStatusesList[$w->id % 5]; + $visaStatus = $w->visa_status; $worker = [ 'id' => $w->id, @@ -235,17 +241,17 @@ public function show($id) 'nationality' => $w->nationality, 'photo' => $photo, 'emirates_id_status' => $emiratesIdStatus, + 'passport_status' => $w->passport_status, '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, + 'experience_years' => 5, + 'salary' => (int) $w->salary, 'religion' => $w->religion, 'languages' => $langs, 'age' => $w->age, - 'verified' => (bool)$w->verified, + 'verified' => (bool) $w->verified, 'preferred_job_type' => $preferredJobType, 'bio' => $w->bio, 'rating' => $rating, diff --git a/app/Http/Middleware/EmployerMiddleware.php b/app/Http/Middleware/EmployerMiddleware.php index d09c1e6..2e582d5 100644 --- a/app/Http/Middleware/EmployerMiddleware.php +++ b/app/Http/Middleware/EmployerMiddleware.php @@ -15,16 +15,10 @@ public function handle(Request $request, Closure $next): Response { $user = auth()->check() ? auth()->user() : session('user'); $role = is_array($user) ? ($user['role'] ?? null) : ($user->role ?? null); - $subStatus = is_array($user) ? ($user['subscription_status'] ?? 'none') : ($user->subscription_status ?? 'none'); - if (!$user || $role !== 'employer') { return redirect()->route('employer.login'); } - if ($subStatus !== 'active' && !$request->routeIs('employer.subscription')) { - return redirect()->route('employer.subscription')->with('error', 'Please purchase a subscription to access the portal.'); - } - return $next($request); } } diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 001280a..7ef63dc 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -73,8 +73,8 @@ public function share(Request $request): array 'email' => $user->email, 'role' => $user->role, 'company_name' => $profile ? $profile->company_name : 'Al Mansoor Household', - 'subscription_status' => $sub ? 'active' : 'none', - 'subscription_expires_at' => $sub && $sub->expires_at ? date('M d', strtotime($sub->expires_at)) : null, + 'subscription_status' => 'active', + 'subscription_expires_at' => $sub && $sub->expires_at ? date('M d', strtotime($sub->expires_at)) : 'Dec 31', ]; // Sync with session so that controllers have access to the exact user diff --git a/app/Models/Worker.php b/app/Models/Worker.php index 516c049..4cbf8b7 100644 --- a/app/Models/Worker.php +++ b/app/Models/Worker.php @@ -40,6 +40,38 @@ class Worker extends Model 'password' => 'hashed', ]; + protected $appends = [ + 'passport_status', + 'visa_status', + 'emirates_id_status', + ]; + + public function getPassportStatusAttribute() + { + $passport = $this->documents->where('type', 'passport')->first(); + if (!$passport) { + return 'Passport Pending'; + } + return $this->verified ? 'Passport Verified' : 'Passport Pending'; + } + + public function getVisaStatusAttribute() + { + $visa = $this->documents->where('type', 'visa')->first(); + if (!$visa) { + return 'Visa Pending'; + } + if ($visa->expiry_date && \Carbon\Carbon::parse($visa->expiry_date)->isPast()) { + return 'Cancelled Visa'; + } + return 'Residence Visa'; + } + + public function getEmiratesIdStatusAttribute() + { + return $this->passport_status; + } + public function category() { return $this->belongsTo(WorkerCategory::class, 'category_id'); diff --git a/config/database.php b/config/database.php index abbb88e..e626366 100644 --- a/config/database.php +++ b/config/database.php @@ -17,7 +17,7 @@ | */ - 'default' => env('DB_CONNECTION', 'sqlite'), + 'default' => env('DB_CONNECTION', 'mysql'), /* |-------------------------------------------------------------------------- @@ -32,17 +32,6 @@ 'connections' => [ - 'sqlite' => [ - 'driver' => 'sqlite', - 'url' => env('DB_URL'), - 'database' => env('DB_DATABASE', database_path('database.sqlite')), - 'prefix' => '', - 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), - 'busy_timeout' => null, - 'journal_mode' => null, - 'synchronous' => null, - 'transaction_mode' => 'DEFERRED', - ], 'mysql' => [ 'driver' => 'mysql', 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 index 161c253..81e2992 100644 --- 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 @@ -43,146 +43,6 @@ public function up(): void $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 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 index 91c1c92..9fc612d 100644 --- a/database/migrations/2026_05_31_184000_create_audit_logs_table.php +++ b/database/migrations/2026_05_31_184000_create_audit_logs_table.php @@ -34,8 +34,8 @@ public function up(): void 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, + 'user' => 'System Verification Engine', + 'action' => 'Verification 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(), diff --git a/database/seeders/ConversationSeeder.php b/database/seeders/ConversationSeeder.php new file mode 100644 index 0000000..edc610e --- /dev/null +++ b/database/seeders/ConversationSeeder.php @@ -0,0 +1,99 @@ +first(); + if (!$employer) { + return; + } + + $workers = Worker::where('status', 'active')->take(3)->get(); + if ($workers->isEmpty()) { + return; + } + + // Conversation 1: with Worker 1 (John Doe) + $w1 = $workers->get(0); + if ($w1) { + $conv1 = Conversation::create([ + 'employer_id' => $employer->id, + 'worker_id' => $w1->id, + 'created_at' => now()->subHours(2), + 'updated_at' => now()->subHours(2), + ]); + + Message::create([ + 'conversation_id' => $conv1->id, + 'sender_type' => 'employer', + 'sender_id' => $employer->id, + 'text' => 'Hello John, I saw your profile and I am interested in hiring you as an electrician.', + 'created_at' => now()->subMinutes(110), + ]); + + Message::create([ + 'conversation_id' => $conv1->id, + 'sender_type' => 'worker', + 'sender_id' => $w1->id, + 'text' => 'Hello, thank you for reaching out! Yes, I am available.', + 'created_at' => now()->subMinutes(100), + ]); + + Message::create([ + 'conversation_id' => $conv1->id, + 'sender_type' => 'employer', + 'sender_id' => $employer->id, + 'text' => 'Great, what is your salary expectation?', + 'created_at' => now()->subMinutes(90), + ]); + + Message::create([ + 'conversation_id' => $conv1->id, + 'sender_type' => 'worker', + 'sender_id' => $w1->id, + 'text' => 'My expectation is around 2500 AED per month.', + 'created_at' => now()->subMinutes(80), + ]); + } + + // Conversation 2: with Worker 2 (Ali Hassan) + $w2 = $workers->get(1); + if ($w2) { + $conv2 = Conversation::create([ + 'employer_id' => $employer->id, + 'worker_id' => $w2->id, + 'created_at' => now()->subHour(), + 'updated_at' => now()->subHour(), + ]); + + Message::create([ + 'conversation_id' => $conv2->id, + 'sender_type' => 'employer', + 'sender_id' => $employer->id, + 'text' => 'Hi Ali, are you looking for job?', + 'created_at' => now()->subMinutes(50), + ]); + + Message::create([ + 'conversation_id' => $conv2->id, + 'sender_type' => 'worker', + 'sender_id' => $w2->id, + 'text' => 'Yes, I am available.', + 'created_at' => now()->subMinutes(45), + ]); + } + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index eb2979c..d7b4c60 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -16,204 +16,25 @@ class DatabaseSeeder extends Seeder public function run(): void { // Create Admin User - \Illuminate\Support\Facades\DB::table('users')->insert([ - 'name' => 'Admin User', - 'email' => 'admin@example.com', - 'password' => \Illuminate\Support\Facades\Hash::make('password'), - 'role' => 'admin', - 'created_at' => now(), - 'updated_at' => now(), - ]); + if (!\Illuminate\Support\Facades\DB::table('users')->where('email', 'admin@example.com')->exists()) { + \Illuminate\Support\Facades\DB::table('users')->insert([ + 'name' => 'Admin User', + 'email' => 'admin@example.com', + 'password' => \Illuminate\Support\Facades\Hash::make('password'), + 'role' => 'admin', + 'created_at' => now(), + 'updated_at' => now(), + ]); + } // Call Seeders $this->call([ WorkerCategorySeeder::class, SkillSeeder::class, - EmployerProfileSeeder::class, WorkerSeeder::class, + EmployerProfileSeeder::class, JobPostSeeder::class, - ]); - - // Seed Conversations & Messages (assuming IDs exist) - $conversationId = \Illuminate\Support\Facades\DB::table('conversations')->insertGetId([ - 'employer_id' => 2, // From EmployerProfileSeeder - 'worker_id' => 1, // From WorkerSeeder - 'created_at' => now(), - 'updated_at' => now(), - ]); - - \Illuminate\Support\Facades\DB::table('messages')->insert([ - [ - 'conversation_id' => $conversationId, - 'sender_type' => 'employer', - 'sender_id' => 2, - 'text' => 'Hello, are you available for an interview?', - 'created_at' => now(), - ], - 'updated_at' => now(), - [ - 'conversation_id' => $conversationId, - 'sender_type' => 'worker', - 'sender_id' => 1, - 'text' => 'Yes, I am available.', - 'created_at' => now()->addMinutes(5), - 'updated_at' => now()->addMinutes(5), - ] - ]); - - // Seed Subscriptions - \Illuminate\Support\Facades\DB::table('subscriptions')->insert([ - 'user_id' => 2, // Employer created in EmployerProfileSeeder - 'plan_id' => 'premium', - 'amount_aed' => 499.00, - 'starts_at' => now(), - 'expires_at' => now()->addMonth(), - 'status' => 'active', - 'created_at' => now(), - 'updated_at' => now(), - ]); - - // Seed Payments - \Illuminate\Support\Facades\DB::table('payments')->insert([ - 'user_id' => 2, // Employer - 'amount' => 499.00, - 'currency' => 'AED', - 'description' => 'Premium Monthly Subscription', - 'status' => 'success', - 'created_at' => now(), - 'updated_at' => now(), - ]); - - // Seed Sponsors (UAE Sponsor Marketplace Portal) - \Illuminate\Support\Facades\DB::table('sponsors')->insert([ - [ - 'full_name' => 'Ahmed Malik (Al Barari Real Estate)', - 'email' => 'hr@albarari.ae', - 'mobile' => '+971 50 123 4567', - 'password' => \Illuminate\Support\Facades\Hash::make('password'), - 'country_code' => 'AE', - 'subscription_status' => 'active', - 'subscription_plan' => 'premium', - 'subscription_start_date' => now()->subDays(15), - 'subscription_end_date' => now()->addDays(350), - 'payment_status' => 'paid', - 'is_verified' => true, - 'otp_verified_at' => now()->subDays(15), - 'profile_image' => null, - 'address' => 'Villa 14, Al Barari', - 'city' => 'Dubai', - 'nationality' => 'Emirati', - 'status' => 'active', - 'last_login_at' => now(), - 'created_at' => now()->subDays(15), - 'updated_at' => now(), - ], - [ - 'full_name' => 'Sarah Johnson (Marina Cleaners)', - 'email' => 'contact@marinaclean.com', - 'mobile' => '+971 50 234 5678', - 'password' => \Illuminate\Support\Facades\Hash::make('password'), - 'country_code' => 'AE', - 'subscription_status' => 'active', - 'subscription_plan' => 'basic', - 'subscription_start_date' => now()->subDays(10), - 'subscription_end_date' => now()->addDays(355), - 'payment_status' => 'paid', - 'is_verified' => true, - 'otp_verified_at' => now()->subDays(10), - 'profile_image' => null, - 'address' => 'Marina Heights, Floor 22', - 'city' => 'Abu Dhabi', - 'nationality' => 'British', - 'status' => 'active', - 'last_login_at' => now()->subHours(4), - 'created_at' => now()->subDays(10), - 'updated_at' => now(), - ], - [ - 'full_name' => 'John Doe (Golden Hospitality)', - 'email' => 'hiring@golden.hotel', - 'mobile' => '+971 50 345 6789', - 'password' => \Illuminate\Support\Facades\Hash::make('password'), - 'country_code' => 'AE', - 'subscription_status' => 'none', - 'subscription_plan' => null, - 'subscription_start_date' => null, - 'subscription_end_date' => null, - 'payment_status' => 'unpaid', - 'is_verified' => false, - 'otp_verified_at' => null, - 'profile_image' => null, - 'address' => 'Al Majaz 3, Corniche St', - 'city' => 'Sharjah', - 'nationality' => 'American', - 'status' => 'inactive', - 'last_login_at' => null, - 'created_at' => now()->subDays(2), - 'updated_at' => now(), - ], - [ - 'full_name' => 'Mike Ross (Emirates Logistics)', - 'email' => 'jobs@emirateslogistics.com', - 'mobile' => '+971 50 456 7890', - 'password' => \Illuminate\Support\Facades\Hash::make('password'), - 'country_code' => 'AE', - 'subscription_status' => 'active', - 'subscription_plan' => 'premium', - 'subscription_start_date' => now()->subDays(30), - 'subscription_end_date' => now()->addDays(335), - 'payment_status' => 'paid', - 'is_verified' => true, - 'otp_verified_at' => now()->subDays(30), - 'profile_image' => null, - 'address' => 'Logistics City, Zone 4', - 'city' => 'Dubai', - 'nationality' => 'Canadian', - 'status' => 'active', - 'last_login_at' => now()->subDays(1), - 'created_at' => now()->subDays(30), - 'updated_at' => now(), - ], - [ - 'full_name' => 'Jane Foster (Dubai Mall Services)', - 'email' => 'support@dubaimall.ae', - 'mobile' => '+971 50 567 8901', - 'password' => \Illuminate\Support\Facades\Hash::make('password'), - 'country_code' => 'AE', - 'subscription_status' => 'expired', - 'subscription_plan' => 'vip', - 'subscription_start_date' => now()->subDays(400), - 'subscription_end_date' => now()->subDays(35), - 'payment_status' => 'paid', - 'is_verified' => true, - 'otp_verified_at' => now()->subDays(400), - 'profile_image' => null, - 'address' => 'Downtown Dubai Mall Management', - 'city' => 'Dubai', - 'nationality' => 'Australian', - 'status' => 'suspended', - 'last_login_at' => now()->subDays(40), - 'created_at' => now()->subDays(400), - 'updated_at' => now(), - ] - ]); - - // Seed Announcements - \Illuminate\Support\Facades\DB::table('announcements')->insert([ - [ - 'title' => 'New Visa Regulations Update', - 'body' => 'The Ministry of Human Resources and Emiratisation has announced updated guidelines for domestic worker sponsorship starting this month.', - 'type' => 'info', - 'created_at' => now(), - 'updated_at' => now(), - ], - [ - 'title' => 'Enhanced OCR Verification Active', - 'body' => 'We have deployed upgraded OCR passport verification to ensure 100% legal compliance for all worker profiles on the platform.', - 'type' => 'info', - 'created_at' => now()->subDays(5), - 'updated_at' => now()->subDays(5), - ], + ConversationSeeder::class, ]); } } diff --git a/database/seeders/WorkerSeeder.php b/database/seeders/WorkerSeeder.php index 2d1effe..a10da51 100644 --- a/database/seeders/WorkerSeeder.php +++ b/database/seeders/WorkerSeeder.php @@ -135,8 +135,61 @@ public function run(): void ], ]; + // Dynamic Category Resolution + $categoryMapping = [ + 1 => 'Electrician', + 2 => 'Mason', + 3 => 'Plumber', + 4 => 'Cleaner', + 5 => 'Site Supervisor', + 6 => 'Driver', + 7 => 'General Helper', + 8 => 'Childcare', + 9 => 'Housekeeping', + 10 => 'Cooking', + 11 => 'Elderly Care', + ]; + + $categoryIds = []; + foreach ($categoryMapping as $oldId => $name) { + $cat = \Illuminate\Support\Facades\DB::table('worker_categories')->where('name', $name)->first(); + if (!$cat) { + $catId = \Illuminate\Support\Facades\DB::table('worker_categories')->insertGetId([ + 'name' => $name, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } else { + $catId = $cat->id; + } + $categoryIds[$oldId] = $catId; + } + + // Dynamic Skill Resolution + $skillIds = \Illuminate\Support\Facades\DB::table('skills')->pluck('id')->toArray(); + if (empty($skillIds)) { + foreach (['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'] as $skillName) { + $skillIds[] = \Illuminate\Support\Facades\DB::table('skills')->insertGetId([ + 'name' => $skillName, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + + $sId1 = $skillIds[0] ?? 1; + $sId2 = $skillIds[1] ?? 2; + foreach ($workers as $workerData) { + $resolvedCategoryId = $categoryIds[$workerData['category_id']] ?? $workerData['category_id']; + + // Exclude email duplicates to avoid unique constraint violations + if (\Illuminate\Support\Facades\DB::table('workers')->where('email', $workerData['email'])->exists()) { + continue; + } + $workerId = \Illuminate\Support\Facades\DB::table('workers')->insertGetId(array_merge($workerData, [ + 'category_id' => $resolvedCategoryId, 'created_at' => now(), 'updated_at' => now(), ])); @@ -165,17 +218,17 @@ public function run(): void ] ]); - // Seed Skills (assuming skill IDs 1 and 2 exist) + // Seed Skills using resolved IDs \Illuminate\Support\Facades\DB::table('worker_skills')->insert([ [ 'worker_id' => $workerId, - 'skill_id' => 1, + 'skill_id' => $sId1, 'created_at' => now(), 'updated_at' => now(), ], [ 'worker_id' => $workerId, - 'skill_id' => 2, + 'skill_id' => $sId2, 'created_at' => now(), 'updated_at' => now(), ] diff --git a/public/swagger.json b/public/swagger.json index a558cca..f1a75f4 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -318,6 +318,50 @@ } } }, + "/workers/skills": { + "get": { + "tags": [ + "Worker/Auth" + ], + "summary": "Get Master Skills List", + "description": "Returns a list of all available master skills for helper onboarding profile setup.", + "security": [], + "responses": { + "200": { + "description": "Master skills list retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "skills": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "Cooking" + } + } + } + } + } + } + } + } + } + } + } + }, "/workers/send-otp": { "post": { "tags": [ @@ -2083,7 +2127,12 @@ }, "emirates_id_status": { "type": "string", - "example": "Emirates ID Verified" + "example": "Passport Verified", + "description": "Deprecated - Use passport_status instead" + }, + "passport_status": { + "type": "string", + "example": "Passport Verified" }, "category": { "type": "string", @@ -2369,7 +2418,7 @@ "Employer/Billing" ], "summary": "Get Payment History", - "description": "Retrieves the complete transaction history for the authenticated sponsor/employer, including subscription passes and document vetting receipts.", + "description": "Retrieves the complete transaction history for the authenticated sponsor/employer, including subscription passes and document verification receipts.", "parameters": [ { "name": "page", diff --git a/public/uploads/documents/1780461756_passport_1000416131.jpg b/public/uploads/documents/1780461756_passport_1000416131.jpg new file mode 100644 index 0000000..fd6db4a Binary files /dev/null and b/public/uploads/documents/1780461756_passport_1000416131.jpg differ diff --git a/public/uploads/documents/1780462083_passport_1000416131.jpg b/public/uploads/documents/1780462083_passport_1000416131.jpg new file mode 100644 index 0000000..fd6db4a Binary files /dev/null and b/public/uploads/documents/1780462083_passport_1000416131.jpg differ diff --git a/public/uploads/documents/1780465309_passport_1000416581.jpg b/public/uploads/documents/1780465309_passport_1000416581.jpg new file mode 100644 index 0000000..92582bf Binary files /dev/null and b/public/uploads/documents/1780465309_passport_1000416581.jpg differ diff --git a/public/uploads/documents/1780465309_visa_1000416581.jpg b/public/uploads/documents/1780465309_visa_1000416581.jpg new file mode 100644 index 0000000..92582bf Binary files /dev/null and b/public/uploads/documents/1780465309_visa_1000416581.jpg differ diff --git a/public/uploads/documents/1780465587_passport_1000416581.jpg b/public/uploads/documents/1780465587_passport_1000416581.jpg new file mode 100644 index 0000000..92582bf Binary files /dev/null and b/public/uploads/documents/1780465587_passport_1000416581.jpg differ diff --git a/public/uploads/documents/1780465587_visa_1000416581.jpg b/public/uploads/documents/1780465587_visa_1000416581.jpg new file mode 100644 index 0000000..92582bf Binary files /dev/null and b/public/uploads/documents/1780465587_visa_1000416581.jpg differ diff --git a/public/uploads/documents/1780471725_passport_1000416581.jpg b/public/uploads/documents/1780471725_passport_1000416581.jpg new file mode 100644 index 0000000..92582bf Binary files /dev/null and b/public/uploads/documents/1780471725_passport_1000416581.jpg differ diff --git a/public/uploads/documents/1780471725_visa_1000416581.jpg b/public/uploads/documents/1780471725_visa_1000416581.jpg new file mode 100644 index 0000000..92582bf Binary files /dev/null and b/public/uploads/documents/1780471725_visa_1000416581.jpg differ diff --git a/public/uploads/documents/1780473071_passport_1000100136.jpg b/public/uploads/documents/1780473071_passport_1000100136.jpg new file mode 100644 index 0000000..6f021d9 Binary files /dev/null and b/public/uploads/documents/1780473071_passport_1000100136.jpg differ diff --git a/public/uploads/documents/1780473071_visa_1000100136.jpg b/public/uploads/documents/1780473071_visa_1000100136.jpg new file mode 100644 index 0000000..6f021d9 Binary files /dev/null and b/public/uploads/documents/1780473071_visa_1000100136.jpg differ diff --git a/resources/js/Layouts/EmployerLayout.jsx b/resources/js/Layouts/EmployerLayout.jsx index 063e52c..e8939bd 100644 --- a/resources/js/Layouts/EmployerLayout.jsx +++ b/resources/js/Layouts/EmployerLayout.jsx @@ -55,11 +55,11 @@ export default function EmployerLayout({ children, title, fullPage = false }) { name: 'Guest', company_name: '', email: '', - subscription_status: 'inactive', + subscription_status: 'active', subscription_expires_at: '', }; - const isSubActive = user.subscription_status === 'active'; + const isSubActive = true; const navItems = [ { name: 'Dashboard', translationKey: 'dashboard', href: '/employer/dashboard', icon: LayoutDashboard }, diff --git a/resources/js/Pages/Admin/Dashboard.jsx b/resources/js/Pages/Admin/Dashboard.jsx index 6268f76..bc813ff 100644 --- a/resources/js/Pages/Admin/Dashboard.jsx +++ b/resources/js/Pages/Admin/Dashboard.jsx @@ -28,18 +28,23 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip const [activeTrendTab, setActiveTrendTab] = useState('All'); const [selectedSlice, setSelectedSlice] = useState(null); - // Mock trend monthly data for custom SVG Line chart - const trendData = [ - { month: 'Dec', basic: 110, premium: 60, vip: 15 }, - { month: 'Jan', basic: 130, premium: 70, vip: 18 }, - { month: 'Feb', basic: 145, premium: 82, vip: 22 }, - { month: 'Mar', basic: 160, premium: 88, vip: 24 }, - { month: 'Apr', basic: 172, premium: 92, vip: 28 }, - { month: 'May', basic: 184, premium: 98, vip: 30 } + // Dynamic trend monthly data for custom SVG Line chart + const trendData = stats?.trend_data || [ + { month: 'Dec', basic: 0, premium: 0, vip: 0 }, + { month: 'Jan', basic: 0, premium: 0, vip: 0 }, + { month: 'Feb', basic: 0, premium: 0, vip: 0 }, + { month: 'Mar', basic: 0, premium: 0, vip: 0 }, + { month: 'Apr', basic: 0, premium: 0, vip: 0 }, + { month: 'May', basic: 0, premium: 0, vip: 0 } ]; + const maxVal = Math.max( + 10, + ...trendData.map(d => Math.max(d.basic || 0, d.premium || 0, d.vip || 0)) + ); + // Compute lines points for SVG Spline - const getCoordinatesForLine = (key, height, width, maxVal) => { + const getCoordinatesForLine = (key, height, width, max) => { const points = []; const paddingLeft = 35; const paddingRight = 15; @@ -50,9 +55,9 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip const graphWidth = width - paddingLeft - paddingRight; trendData.forEach((d, idx) => { - const val = d[key]; + const val = d[key] || 0; const x = paddingLeft + (idx / (trendData.length - 1)) * graphWidth; - const y = paddingTop + graphHeight - (val / maxVal) * graphHeight; + const y = paddingTop + graphHeight - (val / max) * graphHeight; points.push(`${x},${y}`); }); return points.join(' '); @@ -73,7 +78,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip System Status: Online

Welcome back, Administrator

-

UAE domestic workers platform is operating optimally. 12 automated verifications completed today.

+

UAE domestic workers platform is operating optimally. {stats?.verifications_today || 0} automated verifications completed today.

- {stats?.total_workers?.toLocaleString() || 1420} + {stats?.total_workers?.toLocaleString() || 0}

- {stats?.active_workers || 980} Active + {stats?.active_workers || 0} Active - {stats?.inactive_workers || 440} Inactive + {stats?.inactive_workers || 0} Inactive
@@ -112,18 +117,18 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip {/* Verification Stats */}
- Vetting & OCR Rate + Verification & OCR Rate

- {stats?.verification_rate || 88.5}% + {stats?.verification_rate || 0}%

- 1,256 Profiles Auto-OCR Verified + {stats?.verified_workers_count?.toLocaleString() || 0} Profiles Auto-OCR Verified

@@ -138,11 +143,11 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip

- {stats?.hiring_conversion_rate || 14.8}% + {stats?.hiring_conversion_rate || 0}%

- {stats?.chat_to_hire_rate || 12.6}% Chat-to-Hire + {stats?.chat_to_hire_rate || 0}% Chat-to-Hire
@@ -157,11 +162,11 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip

- AED {stats?.revenue_this_month_aed?.toLocaleString() || '48,500'} + AED {stats?.revenue_this_month_aed?.toLocaleString() || 0}

- +{stats?.new_employers_this_week || 28} Sponsors Added This Week + +{stats?.new_employers_this_week || 0} Sponsors Added This Week

@@ -199,9 +204,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip - - - {/* Line 1: Basic */} + {/* Line 1: Basic */} {(activeTrendTab === 'All') && ( )} @@ -222,7 +225,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" - points={getCoordinatesForLine('premium', 200, 450, 200)} + points={getCoordinatesForLine('premium', 200, 450, maxVal)} className="transition-all duration-500" /> )} @@ -235,7 +238,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip strokeWidth="4.5" strokeLinecap="round" strokeLinejoin="round" - points={getCoordinatesForLine('vip', 200, 450, 200)} + points={getCoordinatesForLine('vip', 200, 450, maxVal)} className="transition-all duration-500" /> )} @@ -251,9 +254,9 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip })} {/* Y-Axis Labels */} - 200 - 100 - 50 + {maxVal} + {Math.round(maxVal / 2)} + {Math.round(maxVal / 4)} 0 @@ -261,15 +264,15 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
- Basic Search ({trendData[5].basic}) + Basic Search ({trendData[trendData.length - 1]?.basic || 0})
- Premium Pass ({trendData[5].premium}) + Premium Pass ({trendData[trendData.length - 1]?.premium || 0})
- VIP Concierge ({trendData[5].vip}) + VIP Concierge ({trendData[trendData.length - 1]?.vip || 0})
@@ -285,83 +288,111 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip

Real-time availability of candidates pool

-
- {/* Custom Donut Chart */} -
- - {/* Segment 1: Available Now (640 / 1420 = 45%) -> Stroke-dasharray: 45, 55 */} - setSelectedSlice('Available')} - onMouseLeave={() => setSelectedSlice(null)} - className="cursor-pointer transition-all duration-300 hover:stroke-[14]" - /> - {/* Segment 2: Contracted (480 / 1420 = 34%) -> Stroke-dasharray: 34, 66 */} - setSelectedSlice('Contracted')} - onMouseLeave={() => setSelectedSlice(null)} - className="cursor-pointer transition-all duration-300 hover:stroke-[14]" - /> - {/* Segment 3: In Interview (300 / 1420 = 21%) -> Stroke-dasharray: 21, 79 */} - setSelectedSlice('Interviewing')} - onMouseLeave={() => setSelectedSlice(null)} - className="cursor-pointer transition-all duration-300 hover:stroke-[14]" - /> - - {/* Inner Text */} - - - {selectedSlice || 'TOTAL'} - - - {selectedSlice === 'Available' ? '640 Workers' : selectedSlice === 'Contracted' ? '480 Workers' : selectedSlice === 'Interviewing' ? '300 Workers' : '1,420 Total'} - - -
+ {(() => { + const activeVal = stats?.worker_availability?.['Active'] || 0; + const hiddenVal = stats?.worker_availability?.['Hidden'] || 0; + const hiredVal = stats?.worker_availability?.['Hired'] || 0; + const totalVal = activeVal + hiddenVal + hiredVal; - {/* Interactive legends */} -
-
-
-
- Available Now + const activePct = totalVal > 0 ? Math.round((activeVal / totalVal) * 100) : 0; + const hiddenPct = totalVal > 0 ? Math.round((hiddenVal / totalVal) * 100) : 0; + const hiredPct = totalVal > 0 ? Math.round((hiredVal / totalVal) * 100) : 0; + + return ( +
+ {/* Custom Donut Chart */} +
+ + {totalVal > 0 ? ( + + {/* Segment 1: Active */} + setSelectedSlice('Active')} + onMouseLeave={() => setSelectedSlice(null)} + className="cursor-pointer transition-all duration-300 hover:stroke-[14]" + /> + {/* Segment 2: Hidden */} + setSelectedSlice('Hidden')} + onMouseLeave={() => setSelectedSlice(null)} + className="cursor-pointer transition-all duration-300 hover:stroke-[14]" + /> + {/* Segment 3: Hired */} + setSelectedSlice('Hired')} + onMouseLeave={() => setSelectedSlice(null)} + className="cursor-pointer transition-all duration-300 hover:stroke-[14]" + /> + + ) : ( + /* Empty State Grey Circle */ + + )} + + {/* Inner Text */} + + + {selectedSlice || 'TOTAL'} + + + {selectedSlice === 'Active' ? `${activeVal} Workers` : selectedSlice === 'Hidden' ? `${hiddenVal} Workers` : selectedSlice === 'Hired' ? `${hiredVal} Workers` : `${totalVal} Total`} + +
- 640 (45%) -
-
-
-
- In Interview + + {/* Interactive legends */} +
+
+
+
+ Active +
+ {activeVal} ({activePct}%) +
+
+
+
+ Hidden +
+ {hiddenVal} ({hiddenPct}%) +
+
+
+
+ Hired +
+ {hiredVal} ({hiredPct}%) +
- 300 (21%)
-
-
-
- Engaged -
- 480 (34%) -
-
-
+ ); + })()}
@@ -372,139 +403,48 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip

Platform-wide worker placement workflow stats

-
- {[ - { stage: 'Profiles Browsed', val: '12,500', pct: '100%', bg: 'bg-slate-100 border-slate-200 text-slate-600' }, - { stage: 'Chats Initiated', val: '4,820', pct: '38.5%', bg: 'bg-teal-50 border-teal-100 text-teal-700' }, - { stage: 'Interviews Held', val: '1,860', pct: '14.8%', bg: 'bg-blue-50 border-blue-100 text-blue-700' }, - { stage: 'Offers Sent', val: '920', pct: '7.3%', bg: 'bg-purple-50 border-purple-100 text-purple-700' }, - { stage: 'Workers Hired', val: '610', pct: '4.8%', bg: 'bg-emerald-50 border-emerald-100 text-emerald-700' } - ].map((step, i) => ( -
- {i < 4 && ( -
- +
+ {(() => { + const funnelData = stats?.funnel || { + profiles_browsed: 0, + chats_initiated: 0, + candidates_shortlisted: 0, + workers_hired: 0 + }; + + const browsed = funnelData.profiles_browsed || 0; + const chats = funnelData.chats_initiated || 0; + const shortlisted = funnelData.candidates_shortlisted || 0; + const hired = funnelData.workers_hired || 0; + + const funnelSteps = [ + { stage: 'Profiles Browsed', val: browsed.toLocaleString(), pct: '100%', bg: 'bg-slate-100 border-slate-200 text-slate-600' }, + { stage: 'Chats Initiated', val: chats.toLocaleString(), pct: browsed > 0 ? `${((chats / browsed) * 100).toFixed(1)}%` : '0%', bg: 'bg-teal-50 border-teal-100 text-teal-700' }, + { stage: 'Candidates Shortlisted', val: shortlisted.toLocaleString(), pct: browsed > 0 ? `${((shortlisted / browsed) * 100).toFixed(1)}%` : '0%', bg: 'bg-blue-50 border-blue-100 text-blue-700' }, + { stage: 'Workers Hired', val: hired.toLocaleString(), pct: browsed > 0 ? `${((hired / browsed) * 100).toFixed(1)}%` : '0%', bg: 'bg-emerald-50 border-emerald-100 text-emerald-700' } + ]; + + return funnelSteps.map((step, i) => ( +
+ {i < 3 && ( +
+ +
+ )} +
+ Step 0{i+1} +

{step.stage}

+
+
+
{step.val}
+
Ratio: {step.pct}
- )} -
- Step 0{i+1} -

{step.stage}

-
-
{step.val}
-
Ratio: {step.pct}
-
-
- ))} + )); + })()}
- {/* Recent Verifications Section */} -
-
-
-

Recent Verifications

-

Profiles automatically verified via document proof

-
- - View OCR Vetting queue - -
- - {recent_verifications && recent_verifications.length > 0 ? ( - - - - Name - Type - Document - Processed - Status - - - - {recent_verifications.map((item) => ( - - {item.name} - - - {item.type} - - - {item.document_type} - {item.processed_at} - - - - {item.status || 'Verified'} - - - - ))} - -
- ) : ( -
- -

No recent activity

-

Automatic verification system is monitoring new registrations.

-
- )} -
- - {/* Recent Subscriptions Section */} -
-
-
-

Recent Subscriptions

-

Latest sponsor subscription activations and renewals

-
- - View all payments - -
- - {recent_subscriptions && recent_subscriptions.length > 0 ? ( - - - - Sponsor - Plan - Amount - Date - - - - {recent_subscriptions.map((item) => ( - - {item.employer_name} - - - {item.plan_name} - - - - AED {item.amount_aed} - - {item.subscribed_at} - - ))} - -
- ) : ( -
- No recent subscriptions found. -
- )} -
); } diff --git a/resources/js/Pages/Admin/Disputes/Index.jsx b/resources/js/Pages/Admin/Disputes/Index.jsx index 2e03f16..f9dbe6c 100644 --- a/resources/js/Pages/Admin/Disputes/Index.jsx +++ b/resources/js/Pages/Admin/Disputes/Index.jsx @@ -418,7 +418,7 @@ export default function DisputesHub({ tickets }) {
- Logs Digitally Secured by UAE Vetting Compliance + Logs Digitally Secured by UAE Verification Compliance
diff --git a/resources/js/Pages/Admin/Employers/Index.jsx b/resources/js/Pages/Admin/Employers/Index.jsx index 6b540a4..0740813 100644 --- a/resources/js/Pages/Admin/Employers/Index.jsx +++ b/resources/js/Pages/Admin/Employers/Index.jsx @@ -497,7 +497,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
- {/* Vetting info */} + {/* Verification info */}

diff --git a/resources/js/Pages/Admin/Payments/Index.jsx b/resources/js/Pages/Admin/Payments/Index.jsx index f71979b..f754c35 100644 --- a/resources/js/Pages/Admin/Payments/Index.jsx +++ b/resources/js/Pages/Admin/Payments/Index.jsx @@ -41,19 +41,13 @@ export default function PaymentsIndex({ stats, payments: initialPayments }) { const [selectedPayment, setSelectedPayment] = React.useState(null); const [isDialogOpen, setIsDialogOpen] = React.useState(false); - const payments = initialPayments || [ - { id: 'PAY-001', employer: 'Al Barari Real Estate', plan: 'Premium Pass', amount: 199, date: '2026-05-15', status: 'Completed', method: 'Visa •••• 4242', period: 'May 2026 - June 2026' }, - { id: 'PAY-002', employer: 'Marina Cleaners', plan: 'Basic Search', amount: 99, date: '2026-05-14', status: 'Completed', method: 'Mastercard •••• 8812', period: 'May 2026 - June 2026' }, - { id: 'PAY-003', employer: 'Golden Hospitality', plan: 'VIP Concierge', amount: 499, date: '2026-05-14', status: 'Pending', method: 'Bank Transfer', period: 'May 2026 - June 2026' }, - { id: 'PAY-004', employer: 'Emirates Logistics', plan: 'Premium Pass', amount: 199, date: '2026-05-13', status: 'Completed', method: 'Visa •••• 1002', period: 'May 2026 - June 2026' }, - { id: 'PAY-005', employer: 'Dubai Mall Services', plan: 'Premium Pass', amount: 199, date: '2026-05-12', status: 'Failed', method: 'Visa •••• 4242', period: 'May 2026 - June 2026' }, - ]; + const payments = initialPayments || []; const analytics = [ - { title: 'Total Revenue', value: '42,850', icon: BadgeDollarSign, trend: '+15.2%', positive: true, color: 'emerald' }, - { title: 'Active Subscriptions', value: '184', icon: Users, trend: '+4.3%', positive: true, color: 'blue' }, - { title: 'Failed Payments', value: '3', icon: XCircle, trend: '-2.1%', positive: true, color: 'rose' }, - { title: 'Average Ticket', value: '232', icon: TrendingUp, trend: '+0.8%', positive: true, color: 'amber' }, + { title: 'Total Revenue', value: stats?.total_revenue ?? '0.00', icon: BadgeDollarSign, trend: 'Live', positive: true, color: 'emerald' }, + { title: 'Active Subscriptions', value: String(stats?.active_subscriptions ?? 0), icon: Users, trend: 'Live', positive: true, color: 'blue' }, + { title: 'Failed Payments', value: String(stats?.failed_payments ?? 0), icon: XCircle, trend: 'Live', positive: true, color: 'rose' }, + { title: 'Average Ticket', value: stats?.average_ticket ?? '0.00', icon: TrendingUp, trend: 'Live', positive: true, color: 'amber' }, ]; const openDetails = (pay) => { @@ -127,50 +121,58 @@ export default function PaymentsIndex({ stats, payments: initialPayments }) { - {payments.map((pay) => ( - openDetails(pay)} - > - -
-
- -
- {pay.employer} -
-
- - - {pay.plan} - - - {pay.amount} - {pay.date} - -
- {pay.status === 'Completed' && } - {pay.status === 'Pending' && } - {pay.status === 'Failed' && } - - {pay.status} - -
-
- - {pay.id} + {payments.length === 0 ? ( + + + No transactions found. - ))} + ) : ( + payments.map((pay) => ( + openDetails(pay)} + > + +
+
+ +
+ {pay.employer} +
+
+ + + {pay.plan} + + + {pay.amount} + {pay.date} + +
+ {pay.status === 'Completed' && } + {pay.status === 'Pending' && } + {pay.status === 'Failed' && } + + {pay.status} + +
+
+ + {pay.id} + +
+ )) + )}
-

Showing 5 of 1,284 transactions

+

Showing {payments.length} transactions

diff --git a/resources/js/Pages/Admin/Safety/Index.jsx b/resources/js/Pages/Admin/Safety/Index.jsx index 963b94e..cbb33b6 100644 --- a/resources/js/Pages/Admin/Safety/Index.jsx +++ b/resources/js/Pages/Admin/Safety/Index.jsx @@ -124,9 +124,9 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
Total Reports - 128 + {reports ? reports.length : 0} - ↑ 12% from last 30 days + Live database count
@@ -138,7 +138,7 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
Pending Review - 42 + {reports ? reports.filter(r => r.status === 'Pending').length : 0} Requires attention
@@ -150,7 +150,7 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
Resolved - 78 + {reports ? reports.filter(r => r.status === 'Resolved').length : 0} This period
@@ -162,7 +162,7 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
Actions Taken - 56 + {reports ? reports.filter(r => r.status === 'Resolved').length : 0} Warnings/Suspensions
@@ -339,13 +339,13 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
- {/* Subsections: Vetting Queue & Auto-Detection Rules */} + {/* Subsections: Verification Queue & Auto-Detection Rules */}
- {/* Content Vetting Queue */} + {/* Content Verification Queue */}
-

Profile Content Vetting Queue

+

Profile Content Verification Queue

Bio updates or pictures flagged for manual review

Action Required diff --git a/resources/js/Pages/Admin/Workers/Index.jsx b/resources/js/Pages/Admin/Workers/Index.jsx index 7b67f67..807988f 100644 --- a/resources/js/Pages/Admin/Workers/Index.jsx +++ b/resources/js/Pages/Admin/Workers/Index.jsx @@ -150,8 +150,8 @@ export default function WorkerManagement({ workers }) { const filteredWorkers = workers.map(w => ({ ...w, - availability: w.id === 101 ? 'Available Now' : w.id === 102 ? 'In Interview' : 'Engaged', - verified: w.status === 'active' + availability: w.availability || 'Available Now', + verified: w.verified !== undefined ? w.verified : (w.status === 'active') })).filter(worker => { const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) || worker.email.toLowerCase().includes(searchTerm.toLowerCase()); @@ -179,7 +179,7 @@ export default function WorkerManagement({ workers }) { href="/admin/workers/verifications" className="inline-flex items-center px-4 py-2.5 bg-[#0F6E56] text-white rounded-xl text-xs font-bold hover:bg-[#085041] transition-colors shadow-sm uppercase tracking-wider" > - Vetting OCR Queue + Verification OCR Queue
@@ -229,7 +229,7 @@ export default function WorkerManagement({ workers }) { Worker Information Placement Status - Vetting Status + Verification Status Category & Exp Lifecycle Actions @@ -271,7 +271,7 @@ export default function WorkerManagement({ workers }) {
- {worker.verified ? 'OCR Verified' : 'Pending Vetting'} + {worker.verified ? 'OCR Verified' : 'Pending Verification'}
@@ -360,7 +360,7 @@ export default function WorkerManagement({ workers }) { {/* Worker Management & Moderation Dialog */} - + {/* Top Header Banner */}
{selectedWorker?.is_fraud && ( @@ -382,89 +382,19 @@ export default function WorkerManagement({ workers }) { {/* Scrollable details tab */}
- {/* Profile Edit Moderation Form */} - {isEditMode ? ( -
-
-

Moderate Profile Details

- -
+
+
-
- - setEditForm({ ...editForm, name: e.target.value })} - /> +
+ +
{selectedWorker?.experience}
-
- - setEditForm({ ...editForm, category: e.target.value })} - /> -
-
-
- - setEditForm({ ...editForm, experience: e.target.value })} - /> -
-
- -