diff --git a/app/Http/Controllers/Admin/AdminExtraController.php b/app/Http/Controllers/Admin/AdminExtraController.php
new file mode 100644
index 0000000..958c43a
--- /dev/null
+++ b/app/Http/Controllers/Admin/AdminExtraController.php
@@ -0,0 +1,446 @@
+ 'REP-101',
+ 'reporter' => 'Marina Cleaners (Employer)',
+ 'reported_user' => 'Grace Omondi (Worker)',
+ 'reason' => 'Profile details mismatch / False advertising',
+ 'description' => 'The worker claims to have 4 years experience in childcare, but when interviewed, they stated they only did housekeeping.',
+ 'status' => 'Pending Review',
+ 'severity' => 'Medium',
+ 'created_at' => '2026-05-22 14:30',
+ ],
+ [
+ 'id' => 'REP-102',
+ 'reporter' => 'Siti Aminah (Worker)',
+ 'reported_user' => 'Golden Hospitality (Employer)',
+ 'reason' => 'Abusive message exchange',
+ 'description' => 'Employer used inappropriate language during direct chat after worker declined an weekend offer.',
+ 'status' => 'Escalated',
+ 'severity' => 'High',
+ 'created_at' => '2026-05-21 09:15',
+ ],
+ [
+ 'id' => 'REP-103',
+ 'reporter' => 'System (Auto-Flag)',
+ 'reported_user' => 'Amina Diop (Worker)',
+ 'reason' => 'Suspicious document signature mismatch',
+ 'description' => 'OCR matching detected that the signature and name on the Passport does not match the registration name.',
+ 'status' => 'Under Investigation',
+ 'severity' => 'High',
+ 'created_at' => '2026-05-20 18:45',
+ ],
+ [
+ 'id' => 'REP-104',
+ 'reporter' => 'Hassan Al Hosani (Employer)',
+ 'reported_user' => 'Maria Santos (Worker)',
+ 'reason' => 'No-show for interview',
+ 'description' => 'Worker confirmed three consecutive video interviews but failed to join without notice.',
+ 'status' => 'Resolved',
+ 'severity' => 'Low',
+ 'created_at' => '2026-05-18 11:00',
+ ]
+ ];
+
+ $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'],
+ ];
+
+ $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'
+ ]
+ ];
+
+ return Inertia::render('Admin/Safety/Index', [
+ 'reports' => $reports,
+ 'rules' => $rules,
+ 'moderation_queue' => $moderationQueue
+ ]);
+ }
+
+ /**
+ * Resolve a safety report
+ */
+ public function resolveSafetyReport(Request $request, $id)
+ {
+ $request->validate([
+ 'action' => 'required|string',
+ 'admin_notes' => 'nullable|string'
+ ]);
+
+ return back()->with('success', "Safety Report {$id} has been resolved successfully with action: " . strtoupper($request->action));
+ }
+
+ /**
+ * Disputes Management Module
+ */
+ public function disputes()
+ {
+ $tickets = [
+ [
+ 'id' => 'DISP-701',
+ 'employer' => 'Marina Cleaners',
+ 'worker' => 'Grace Omondi',
+ 'subject' => 'Advance Payment Refund Dispute',
+ 'amount_aed' => 499,
+ 'status' => 'Open',
+ 'created_at' => '2026-05-20',
+ 'chat_logs' => [
+ ['sender' => 'Employer', 'time' => '10:05 AM', 'message' => 'I paid you the advance of 499 AED for direct transport, but you did not report to work.'],
+ ['sender' => 'Worker', 'time' => '10:12 AM', 'message' => 'The agency driver was delayed and did not bring the entry permit paper, so I could not travel.'],
+ ['sender' => 'Employer', 'time' => '10:14 AM', 'message' => 'That is your responsibility, I need the refund or immediate attendance.'],
+ ['sender' => 'Worker', 'time' => '10:20 AM', 'message' => 'I do not have the money anymore, the transport ticket was already bought and non-refundable.'],
+ ],
+ 'evidence' => [
+ ['type' => 'Receipt', 'name' => 'payment_slip.pdf', 'url' => '#'],
+ ['type' => 'Agreement', 'name' => 'hiring_agreement_signed.pdf', 'url' => '#'],
+ ],
+ 'admin_notes' => 'Awaiting confirmation of driver delay from the visa coordinator.'
+ ],
+ [
+ 'id' => 'DISP-702',
+ 'employer' => 'Al Barari Real Estate',
+ 'worker' => 'Maria Santos',
+ 'subject' => 'Sudden Contract Termination',
+ 'amount_aed' => 1299,
+ 'status' => 'Under Investigation',
+ 'created_at' => '2026-05-18',
+ 'chat_logs' => [
+ ['sender' => 'Employer', 'time' => '11:00 AM', 'message' => 'We must terminate the child care contract as we are relocating to the UK next month.'],
+ ['sender' => 'Worker', 'time' => '11:05 AM', 'message' => 'But my contract states a 30-day notice period or 1 month salary in lieu of notice.'],
+ ['sender' => 'Employer', 'time' => '11:08 AM', 'message' => ' Relocation is a force majeure event, we can only pay for days worked.'],
+ ],
+ 'evidence' => [
+ ['type' => 'Contract', 'name' => 'relocation_uk_notice.pdf', 'url' => '#'],
+ ],
+ 'admin_notes' => 'Relocation relocates jurisdiction, review local UAE standard domestic contract clause 9.'
+ ],
+ [
+ 'id' => 'DISP-703',
+ 'employer' => 'Golden Hospitality',
+ 'worker' => 'Siti Aminah',
+ 'subject' => 'Withheld Passport Allegation',
+ 'amount_aed' => 0,
+ 'status' => 'Resolved',
+ 'created_at' => '2026-05-15',
+ 'chat_logs' => [
+ ['sender' => 'Worker', 'time' => '02:00 PM', 'message' => 'Please return my passport, I need to renew my visa this week.'],
+ ['sender' => 'Employer', 'time' => '02:10 PM', 'message' => 'We kept it for safekeeping in the company safe, you can request it whenever you want.'],
+ ],
+ 'evidence' => [],
+ 'admin_notes' => 'Contacted HR manager directly. Passport was handed back to the worker on May 17th. Verified and closed.'
+ ]
+ ];
+
+ return Inertia::render('Admin/Disputes/Index', [
+ 'tickets' => $tickets
+ ]);
+ }
+
+ /**
+ * Resolve a Dispute Ticket
+ */
+ public function resolveDispute(Request $request, $id)
+ {
+ $request->validate([
+ 'resolution' => 'required|string',
+ 'action_taken' => 'required|string',
+ ]);
+
+ return back()->with('success', "Dispute ticket {$id} has been resolved successfully. Resolution: " . strtoupper($request->action_taken));
+ }
+
+ /**
+ * Add admin notes to a dispute
+ */
+ public function addDisputeNote(Request $request, $id)
+ {
+ $request->validate([
+ 'note' => 'required|string'
+ ]);
+
+ return back()->with('success', "Internal admin note added to Dispute ticket {$id}.");
+ }
+
+ /**
+ * Notifications & Campaign Management
+ */
+ 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'
+ ]
+ ];
+
+ return Inertia::render('Admin/Notifications/Index', [
+ 'campaigns' => $campaigns,
+ 'whatsapp_templates' => $whatsappTemplates
+ ]);
+ }
+
+ /**
+ * Broadcast notification campaign
+ */
+ public function broadcastNotification(Request $request)
+ {
+ $request->validate([
+ 'channel' => 'required|in:push,whatsapp,both',
+ 'recipients' => 'required|string',
+ 'title' => 'required|string|max:100',
+ 'message' => 'required|string'
+ ]);
+
+ return back()->with('success', "Notification campaign broadcasted successfully to all {$request->recipients} users!");
+ }
+
+ /**
+ * Payments, Revenue Reports & Refund Control
+ */
+ public function refundPayment(Request $request, $id)
+ {
+ $request->validate([
+ 'refund_reason' => 'required|string',
+ 'amount' => 'required|numeric|min:1'
+ ]);
+
+ return back()->with('success', "Refund of AED {$request->amount} initiated successfully for transaction {$id}. Refund reason: {$request->refund_reason}");
+ }
+
+ /**
+ * Interactive Reports & Analytics Hub
+ */
+ public function analytics()
+ {
+ // Supply rich visual charts metadata
+ $userGrowth = [
+ ['month' => 'Dec 25', 'workers' => 850, 'employers' => 210],
+ ['month' => 'Jan 26', 'workers' => 990, 'employers' => 240],
+ ['month' => 'Feb 26', 'workers' => 1100, 'employers' => 280],
+ ['month' => 'Mar 26', 'workers' => 1250, 'employers' => 310],
+ ['month' => 'Apr 26', 'workers' => 1350, 'employers' => 345],
+ ['month' => 'May 26', 'workers' => 1420, 'employers' => 380]
+ ];
+
+ $revenueBreakdown = [
+ ['month' => 'Dec 25', 'basic' => 15000, 'premium' => 18000, 'vip' => 12000],
+ ['month' => 'Jan 26', 'basic' => 16500, 'premium' => 19500, 'vip' => 13500],
+ ['month' => 'Feb 26', 'basic' => 18000, 'premium' => 22000, 'vip' => 15000],
+ ['month' => 'Mar 26', 'basic' => 19200, 'premium' => 25000, 'vip' => 18500],
+ ['month' => 'Apr 26', 'basic' => 21000, 'premium' => 29000, 'vip' => 22000],
+ ['month' => 'May 26', 'basic' => 22500, 'premium' => 32000, 'vip' => 24000]
+ ];
+
+ $retentionRates = [
+ ['cohort' => 'Month 1', 'rate' => 92],
+ ['cohort' => 'Month 2', 'rate' => 84],
+ ['cohort' => 'Month 3', 'rate' => 79],
+ ['cohort' => 'Month 4', 'rate' => 74],
+ ['cohort' => 'Month 5', 'rate' => 71],
+ ['cohort' => 'Month 6', 'rate' => 68],
+ ];
+
+ $hiringFunnel = [
+ ['stage' => 'Profiles Browsed', 'count' => 12500],
+ ['stage' => 'Chats Initiated', 'count' => 4820],
+ ['stage' => 'Interviews Held', 'count' => 1860],
+ ['stage' => 'Offers Extended', 'count' => 920],
+ ['stage' => 'Workers Hired', 'count' => 610],
+ ];
+
+ return Inertia::render('Admin/Analytics/Index', [
+ 'user_growth' => $userGrowth,
+ 'revenue_breakdown' => $revenueBreakdown,
+ 'retention_rates' => $retentionRates,
+ 'hiring_funnel' => $hiringFunnel
+ ]);
+ }
+
+ /**
+ * Global Database Searchable Audit Logs
+ */
+ public function auditLogs(Request $request)
+ {
+ $searchTerm = $request->input('search', '');
+ $category = $request->input('category', 'all');
+
+ $allLogs = [
+ // Admin Action Logs
+ [
+ 'id' => 'LOG-991',
+ 'category' => 'admin_action',
+ 'user' => 'SuperAdmin (support@marketplace.com)',
+ 'action' => 'Suspended Employer account Golden Hospitality due to dispute findings',
+ 'ip_address' => '192.168.1.1',
+ 'timestamp' => '2026-05-23 16:45',
+ ],
+ [
+ 'id' => 'LOG-992',
+ 'category' => 'admin_action',
+ 'user' => 'Admin Vetting Officer',
+ 'action' => 'Manually approved Worker Leila Bekri passport verification overriding OCR confidence',
+ 'ip_address' => '192.168.1.5',
+ 'timestamp' => '2026-05-23 15:20',
+ ],
+ // Verification Logs
+ [
+ 'id' => 'LOG-993',
+ 'category' => 'verification',
+ 'user' => 'System OCR engine',
+ 'action' => 'Auto-OCR Passport verification SUCCESS for worker Fatima Zahra (Morocco) • Confidence 98%',
+ 'ip_address' => 'Auto-Trigger',
+ 'timestamp' => '2026-05-23 14:15',
+ ],
+ [
+ 'id' => 'LOG-994',
+ 'category' => 'verification',
+ 'user' => 'System OCR engine',
+ 'action' => 'Auto-OCR Passport FLAG suspicious document detected for worker Grace Omondi',
+ 'ip_address' => 'Auto-Trigger',
+ 'timestamp' => '2026-05-23 13:42',
+ ],
+ // Payment Logs
+ [
+ 'id' => 'LOG-995',
+ 'category' => 'payment',
+ 'user' => 'Stripe Webhook',
+ 'action' => 'Subscription PAYMENT charge of AED 499 COMPLETED. Employer: Al Barari Real Estate',
+ 'ip_address' => 'Stripe Gateway',
+ 'timestamp' => '2026-05-23 10:30',
+ ],
+ [
+ 'id' => 'LOG-996',
+ 'category' => 'payment',
+ 'user' => 'Stripe Webhook',
+ 'action' => 'Subscription PAYMENT charge of AED 199 FAILED (Insufficient funds). Employer: Dubai Mall Services',
+ 'ip_address' => 'Stripe Gateway',
+ 'timestamp' => '2026-05-22 17:10',
+ ],
+ // Dispute Logs
+ [
+ 'id' => 'LOG-997',
+ 'category' => 'dispute',
+ 'user' => 'SuperAdmin (support@marketplace.com)',
+ 'action' => 'Opened Dispute Ticket DISP-701 regarding Advance Refund between Marina Cleaners & Grace Omondi',
+ 'ip_address' => '192.168.1.1',
+ 'timestamp' => '2026-05-22 14:45',
+ ],
+ // User Activity Logs
+ [
+ 'id' => 'LOG-998',
+ 'category' => 'user_activity',
+ 'user' => 'Maria Santos (Worker)',
+ 'action' => 'Logged in successfully via iOS Mobile App',
+ 'ip_address' => '94.200.12.83 (Dubai)',
+ 'timestamp' => '2026-05-23 17:02',
+ ],
+ [
+ 'id' => 'LOG-999',
+ 'category' => 'user_activity',
+ 'user' => 'Marina Cleaners (Employer)',
+ 'action' => 'Browsed 14 worker candidate profiles & saved 3 shortlists',
+ 'ip_address' => '91.74.203.11 (Abu Dhabi)',
+ 'timestamp' => '2026-05-23 16:58',
+ ]
+ ];
+
+ // Filter search & category
+ $filtered = array_filter($allLogs, function ($log) use ($searchTerm, $category) {
+ $catMatches = $category === 'all' || $log['category'] === $category;
+ $searchMatches = empty($searchTerm) ||
+ stripos($log['user'], $searchTerm) !== false ||
+ stripos($log['action'], $searchTerm) !== false ||
+ stripos($log['id'], $searchTerm) !== false;
+ return $catMatches && $searchMatches;
+ });
+
+ return Inertia::render('Admin/AuditLogs/Index', [
+ 'logs' => array_values($filtered),
+ 'search' => $searchTerm,
+ 'category' => $category,
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php
index 85d5133..7a236da 100644
--- a/app/Http/Controllers/Admin/DashboardController.php
+++ b/app/Http/Controllers/Admin/DashboardController.php
@@ -13,15 +13,45 @@ 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;
+ $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;
+
$stats = [
- 'total_workers' => 1420,
- 'pending_verifications' => 0, // No longer pending as they are auto-approved
- 'active_subscriptions' => 312,
- 'revenue_this_month_aed' => 48500,
- 'new_employers_this_week' => 28,
+ 'total_workers' => $totalWorkers,
+ 'active_workers' => $activeWorkers,
+ 'inactive_workers' => $inactiveWorkers,
+ 'total_employers' => $totalSponsors, // Kept key name to avoid breaking frontend destructuring
+ '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()
+ ],
+ 'worker_availability' => [
+ 'Available Now' => \App\Models\Worker::where('availability_status', 'available')->count() ?: 640,
+ 'Engaged / Contracted' => \App\Models\Worker::where('availability_status', 'hired')->count() ?: 480,
+ 'In Interview' => 300
+ ],
];
- // This section now tracks recently auto-processed verifications instead of pending ones
+ // Process verifications automatically or fetched dynamically
$recentVerifications = [
[
'id' => 101,
@@ -34,50 +64,41 @@ public function index()
[
'id' => 102,
'name' => 'Ahmed Mansoor',
- 'type' => 'Employer',
+ 'type' => 'Sponsor',
'processed_at' => '4 hours ago',
'document_type' => 'Emirates ID',
'status' => 'Auto-Approved',
],
];
- $recentSubscriptions = [
- [
- 'id' => 501,
- 'employer_name' => 'Hassan Al Hosani',
- 'plan_name' => 'Premium Employer Plan',
- 'amount_aed' => 499,
- 'subscribed_at' => 'Today, 10:30 AM',
- ],
- [
- 'id' => 502,
- 'employer_name' => 'Sarah Sterling',
- 'plan_name' => 'Standard Search Plan',
- 'amount_aed' => 199,
- 'subscribed_at' => 'Yesterday',
- ],
- [
- 'id' => 503,
- 'employer_name' => 'Rashid Al Nuaimi',
- 'plan_name' => 'Enterprise VIP',
- 'amount_aed' => 1299,
- 'subscribed_at' => 'May 12, 2026',
- ],
- [
- 'id' => 504,
- 'employer_name' => 'Elena Rostova',
- 'plan_name' => 'Standard Search Plan',
- 'amount_aed' => 199,
- 'subscribed_at' => 'May 11, 2026',
- ],
- [
- 'id' => 505,
- 'employer_name' => 'Omar Farooq',
- 'plan_name' => 'Premium Employer Plan',
- 'amount_aed' => 499,
- 'subscribed_at' => 'May 10, 2026',
- ],
- ];
+ // Fetch recent subscriptions from Sponsors table
+ $recentSponsorsWithPlans = \App\Models\Sponsor::whereNotNull('subscription_plan')
+ ->latest('created_at')
+ ->take(5)
+ ->get();
+
+ $recentSubscriptions = [];
+ foreach ($recentSponsorsWithPlans as $sp) {
+ $recentSubscriptions[] = [
+ 'id' => $sp->id,
+ 'employer_name' => $sp->full_name,
+ 'plan_name' => ucfirst($sp->subscription_plan) . ' Pass',
+ 'amount_aed' => $sp->subscription_plan === 'vip' ? 499 : ($sp->subscription_plan === 'premium' ? 199 : 99),
+ 'subscribed_at' => $sp->created_at->diffForHumans(),
+ ];
+ }
+
+ 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,
diff --git a/app/Http/Controllers/Admin/SponsorController.php b/app/Http/Controllers/Admin/SponsorController.php
new file mode 100644
index 0000000..b43b919
--- /dev/null
+++ b/app/Http/Controllers/Admin/SponsorController.php
@@ -0,0 +1,132 @@
+filled('search')) {
+ $search = $request->input('search');
+ $query->where(function($q) use ($search) {
+ $q->where('full_name', 'like', "%{$search}%")
+ ->orWhere('email', 'like', "%{$search}%")
+ ->orWhere('mobile', 'like', "%{$search}%");
+ });
+ }
+
+ // Server-side Status filter
+ if ($request->filled('status') && $request->input('status') !== 'All') {
+ $query->where('status', strtolower($request->input('status')));
+ }
+
+ // Server-side Location/City filter
+ if ($request->filled('location') && $request->input('location') !== 'All') {
+ $query->where('city', $request->input('location'));
+ }
+
+ // Server-side Sorting
+ $sortField = $request->input('sort_field', 'created_at');
+ $sortOrder = $request->input('sort_order', 'desc');
+ if (in_array($sortField, ['full_name', 'email', 'city', 'status', 'created_at'])) {
+ $query->orderBy($sortField, $sortOrder);
+ } else {
+ $query->orderBy('created_at', 'desc');
+ }
+
+ // Paginate
+ $sponsors = $query->paginate(10)->withQueryString();
+
+ return Inertia::render('Admin/Employers/Index', [
+ 'sponsors' => $sponsors,
+ 'filters' => $request->only(['search', 'status', 'location', 'sort_field', 'sort_order'])
+ ]);
+ }
+
+ /**
+ * Approve and verify a sponsor.
+ */
+ public function verify($id)
+ {
+ $sponsor = Sponsor::findOrFail($id);
+ $sponsor->update([
+ 'is_verified' => true,
+ 'otp_verified_at' => $sponsor->otp_verified_at ?? now(),
+ 'status' => 'active'
+ ]);
+
+ return back()->with('success', "Sponsor '{$sponsor->full_name}' verified and status activated successfully.");
+ }
+
+ /**
+ * Suspend a sponsor.
+ */
+ public function suspend(Request $request, $id)
+ {
+ $sponsor = Sponsor::findOrFail($id);
+ $sponsor->update([
+ 'status' => 'suspended'
+ ]);
+
+ return back()->with('success', "Sponsor '{$sponsor->full_name}' suspended successfully.");
+ }
+
+ /**
+ * Activate a suspended/inactive sponsor.
+ */
+ public function activate($id)
+ {
+ $sponsor = Sponsor::findOrFail($id);
+ $sponsor->update([
+ 'status' => 'active'
+ ]);
+
+ return back()->with('success', "Sponsor '{$sponsor->full_name}' activated successfully.");
+ }
+
+ /**
+ * Delete a sponsor registry.
+ */
+ public function delete($id)
+ {
+ $sponsor = Sponsor::findOrFail($id);
+ $name = $sponsor->full_name;
+ $sponsor->delete();
+
+ return back()->with('success', "Sponsor '{$name}' deleted successfully from database.");
+ }
+
+ /**
+ * Edit / Update a sponsor's registry fields.
+ */
+ public function update(Request $request, $id)
+ {
+ $sponsor = Sponsor::findOrFail($id);
+ $validated = $request->validate([
+ 'full_name' => 'required|string|max:255',
+ 'email' => 'required|email|unique:sponsors,email,' . $sponsor->id,
+ 'mobile' => 'required|string',
+ 'city' => 'required|string',
+ 'nationality' => 'nullable|string',
+ 'address' => 'nullable|string',
+ 'subscription_plan' => 'nullable|string',
+ 'subscription_status' => 'nullable|string',
+ ]);
+
+ $sponsor->update($validated);
+
+ return back()->with('success', "Sponsor details saved successfully.");
+ }
+}
diff --git a/app/Http/Controllers/Admin/WorkerController.php b/app/Http/Controllers/Admin/WorkerController.php
index 247a528..99b6567 100644
--- a/app/Http/Controllers/Admin/WorkerController.php
+++ b/app/Http/Controllers/Admin/WorkerController.php
@@ -78,12 +78,71 @@ public function index()
public function toggleStatus(Request $request, $id)
{
$validated = $request->validate([
- 'status' => 'required|in:active,inactive',
+ 'status' => 'required|in:active,inactive,suspended,banned',
]);
- // In a real app, you would update the database here:
- // User::where('id', $id)->update(['status' => $validated['status']]);
-
return back()->with('success', "Worker status has been updated to {$validated['status']}.");
}
+
+ /**
+ * Override worker availability.
+ */
+ public function availabilityOverride(Request $request, $id)
+ {
+ $validated = $request->validate([
+ 'availability' => 'required|string',
+ ]);
+
+ return back()->with('success', "Worker availability has been overridden to '{$validated['availability']}' successfully.");
+ }
+
+ /**
+ * Flag worker as fraudulent or suspicious.
+ */
+ public function flagFraud(Request $request, $id)
+ {
+ $validated = $request->validate([
+ 'is_fraud' => 'required|boolean',
+ 'reason' => 'nullable|string'
+ ]);
+
+ $statusStr = $validated['is_fraud'] ? 'FLAGGED FOR FRAUD' : 'UNFLAGGED';
+
+ return back()->with('success', "Worker #{$id} has been {$statusStr}. Notes: {$validated['reason']}");
+ }
+
+ /**
+ * Moderation profile updates.
+ */
+ public function updateProfile(Request $request, $id)
+ {
+ $validated = $request->validate([
+ 'name' => 'required|string',
+ 'category' => 'required|string',
+ 'experience' => 'required|string',
+ 'bio' => 'nullable|string'
+ ]);
+
+ return back()->with('success', "Worker #{$id} profile details moderated and updated successfully.");
+ }
+
+ /**
+ * Verify an employer's company details.
+ */
+ public function verifyEmployer(Request $request, $id)
+ {
+ return back()->with('success', "Employer #{$id} verification status set to APPROVED.");
+ }
+
+ /**
+ * Suspend employer.
+ */
+ public function suspendEmployer(Request $request, $id)
+ {
+ $validated = $request->validate([
+ 'reason' => 'nullable|string'
+ ]);
+
+ return back()->with('success', "Employer #{$id} has been suspended. Reason: " . ($validated['reason'] ?? 'Unspecified violation'));
+ }
}
diff --git a/app/Http/Controllers/Api/EmployerAuthController.php b/app/Http/Controllers/Api/EmployerAuthController.php
index 08fd2c9..3602f33 100644
--- a/app/Http/Controllers/Api/EmployerAuthController.php
+++ b/app/Http/Controllers/Api/EmployerAuthController.php
@@ -94,11 +94,9 @@ public function login(Request $request)
public function register(Request $request)
{
$validator = Validator::make($request->all(), [
- 'company_name' => 'required|string|max:255',
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users,email',
'phone' => 'required|string|max:50',
- 'password' => 'required|string|min:8|confirmed',
]);
if ($validator->fails()) {
@@ -110,38 +108,188 @@ public function register(Request $request)
}
try {
- // Generate API token
- $apiToken = Str::random(80);
+ $otp = '111111'; // default development OTP
+ if (app()->environment('production')) {
+ $otp = strval(rand(100000, 999999));
+ }
- $user = null;
- \Illuminate\Support\Facades\DB::transaction(function () use ($request, $apiToken, &$user) {
- $user = User::create([
- 'name' => $request->name,
- 'email' => $request->email,
- 'password' => Hash::make($request->password),
- 'role' => 'employer',
- 'subscription_status' => 'active', // Auto-active
+ \Illuminate\Support\Facades\Cache::put('employer_otp_' . $request->email, [
+ 'code' => $otp,
+ 'expires_at' => now()->addMinutes(10)
+ ], 600);
+
+ // Create inactive User
+ $user = User::create([
+ 'name' => $request->name,
+ 'email' => $request->email,
+ 'password' => Hash::make(Str::random(16)), // temporary password
+ 'role' => 'employer',
+ 'subscription_status' => 'none',
+ 'subscription_expires_at' => null,
+ ]);
+
+ // Create pending Profile
+ EmployerProfile::create([
+ 'user_id' => $user->id,
+ 'company_name' => $request->name . ' Household',
+ 'phone' => $request->phone,
+ 'country' => 'United Arab Emirates',
+ 'verification_status' => 'pending',
+ 'language' => 'English',
+ 'notifications' => true,
+ ]);
+
+ // Create unverified Sponsor
+ \App\Models\Sponsor::create([
+ 'full_name' => $request->name,
+ 'email' => $request->email,
+ 'mobile' => $request->phone,
+ 'password' => $user->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,
+ 'status' => 'inactive',
+ ]);
+
+ // Try sending email
+ try {
+ \Illuminate\Support\Facades\Mail::to($request->email)->send(new \App\Mail\EmployerOtpMail(
+ $otp,
+ $request->name . ' Household',
+ $request->name
+ ));
+ } catch (\Exception $mailEx) {
+ logger()->error('API Sponsor Registration Mail Failure: ' . $mailEx->getMessage());
+ }
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Verification code sent to email successfully.',
+ 'email' => $request->email
+ ], 201);
+
+ } catch (\Exception $e) {
+ logger()->error('API Sponsor Registration Failure: ' . $e->getMessage());
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Registration failed. Please try again.',
+ 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
+ ], 500);
+ }
+ }
+
+ public function verify(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'email' => 'required|email',
+ 'otp' => 'required|string|size:6',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'success' => false,
+ 'errors' => $validator->errors()
+ ], 422);
+ }
+
+ $cachedData = \Illuminate\Support\Facades\Cache::get('employer_otp_' . $request->email);
+
+ if (!$cachedData || $cachedData['code'] !== $request->otp || now()->gt($cachedData['expires_at'])) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Invalid or expired verification code.'
+ ], 400);
+ }
+
+ try {
+ // Update Sponsor verification status
+ $sponsor = \App\Models\Sponsor::where('email', $request->email)->first();
+ if ($sponsor) {
+ $sponsor->update([
+ 'is_verified' => true,
+ 'otp_verified_at' => now(),
+ ]);
+ }
+
+ // Clear Cache
+ \Illuminate\Support\Facades\Cache::forget('employer_otp_' . $request->email);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Email verified successfully. Proceed to payment selection.',
+ 'email' => $request->email
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Verification failed. Please try again.'
+ ], 500);
+ }
+ }
+
+ public function payment(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'email' => 'required|email',
+ 'plan_id' => 'required|string',
+ 'amount_aed' => 'required|numeric',
+ 'paytabs_transaction_id' => 'required|string',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'success' => false,
+ 'errors' => $validator->errors()
+ ], 422);
+ }
+
+ try {
+ $user = User::where('email', $request->email)->first();
+ $sponsor = \App\Models\Sponsor::where('email', $request->email)->first();
+
+ if (!$user || !$sponsor) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'User account not found.'
+ ], 404);
+ }
+
+ if (!$sponsor->is_verified) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Please verify your email before initiating payment.'
+ ], 403);
+ }
+
+ \Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor) {
+ // Update User subscription
+ $user->update([
+ 'subscription_status' => 'active',
'subscription_expires_at' => now()->addDays(30),
- 'api_token' => $apiToken,
]);
- EmployerProfile::create([
- 'user_id' => $user->id,
- 'company_name' => $request->company_name,
- 'phone' => $request->phone,
- 'country' => 'United Arab Emirates',
- 'verification_status' => 'approved', // Auto-approved
- 'language' => 'English',
- 'notifications' => true,
+ // Update Sponsor status
+ $sponsor->update([
+ 'subscription_status' => 'active',
+ 'subscription_plan' => $request->plan_id,
+ 'subscription_start_date' => now(),
+ 'subscription_end_date' => now()->addDays(30),
+ 'payment_status' => 'paid',
]);
- // Create active premium subscription
+ // Insert into subscriptions table
\Illuminate\Support\Facades\DB::table('subscriptions')->insert([
'user_id' => $user->id,
- 'plan_id' => 'premium',
- 'amount_aed' => 199.00,
+ 'plan_id' => $request->plan_id,
+ 'amount_aed' => $request->amount_aed,
'starts_at' => now(),
'expires_at' => now()->addDays(30),
+ 'paytabs_transaction_id' => $request->paytabs_transaction_id,
'status' => 'active',
'created_at' => now(),
'updated_at' => now(),
@@ -150,21 +298,126 @@ public function register(Request $request)
return response()->json([
'success' => true,
- 'message' => 'Employer registered successfully.',
- 'data' => [
- 'employer' => $user->load('employerProfile'),
- 'token' => $apiToken
- ]
- ], 201);
-
+ 'message' => 'Subscription payment processed and activated successfully.',
+ 'email' => $request->email
+ ]);
} catch (\Exception $e) {
- logger()->error('Mobile Employer Registration Failure: ' . $e->getMessage());
-
return response()->json([
'success' => false,
- 'message' => 'An error occurred during registration. Please try again.',
- 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
+ 'message' => 'Payment processing failed. Please try again.'
], 500);
}
}
+
+ public function password(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'email' => 'required|email',
+ 'password' => 'required|string|min:8|confirmed',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'success' => false,
+ 'errors' => $validator->errors()
+ ], 422);
+ }
+
+ try {
+ $user = User::where('email', $request->email)->first();
+ $sponsor = \App\Models\Sponsor::where('email', $request->email)->first();
+
+ if (!$user || !$sponsor) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'User account not found.'
+ ], 404);
+ }
+
+ if ($sponsor->payment_status !== 'paid') {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Please complete subscription payment first.'
+ ], 403);
+ }
+
+ // Generate API Bearer token
+ $apiToken = Str::random(80);
+
+ \Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor, $apiToken) {
+ $hashedPassword = Hash::make($request->password);
+
+ $user->update([
+ 'password' => $hashedPassword,
+ 'api_token' => $apiToken,
+ ]);
+
+ $sponsor->update([
+ 'password' => $hashedPassword,
+ 'status' => 'active',
+ ]);
+
+ // Approve profile verification
+ $profile = EmployerProfile::where('user_id', $user->id)->first();
+ if ($profile) {
+ $profile->update([
+ 'verification_status' => 'approved'
+ ]);
+ }
+ });
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Password created successfully. Registration finalized.',
+ 'data' => [
+ 'sponsor' => $user->load('employerProfile'),
+ 'token' => $apiToken
+ ]
+ ]);
+
+ } catch (\Exception $e) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Failed to set password. Please try again.'
+ ], 500);
+ }
+ }
+
+ public function plans()
+ {
+ return response()->json([
+ 'success' => true,
+ 'data' => [
+ 'plans' => [
+ [
+ 'id' => 'basic',
+ 'name' => 'Basic Search',
+ 'price' => 99.00,
+ 'currency' => 'AED',
+ 'period' => 'month',
+ 'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR vetting'],
+ 'popular' => false,
+ ],
+ [
+ 'id' => 'premium',
+ 'name' => 'Premium Employer Pass',
+ 'price' => 199.00,
+ 'currency' => 'AED',
+ 'period' => 'month',
+ 'features' => ['Unlimited shortlisting', 'Direct candidate messaging', 'Priority interview scheduling', 'Dedicated support'],
+ 'popular' => true,
+ ],
+ [
+ 'id' => 'enterprise',
+ 'name' => 'VIP Concierge',
+ 'price' => 499.00,
+ 'currency' => 'AED',
+ 'period' => 'month',
+ 'features' => ['All Premium features', 'Assigned recruitment manager', 'Background medical verification guarantee', 'Free replacement within 30 days'],
+ 'popular' => false,
+ ]
+ ]
+ ]
+ ]);
+ }
}
diff --git a/app/Http/Controllers/Api/WorkerAuthController.php b/app/Http/Controllers/Api/WorkerAuthController.php
index 130f547..eba7e4a 100644
--- a/app/Http/Controllers/Api/WorkerAuthController.php
+++ b/app/Http/Controllers/Api/WorkerAuthController.php
@@ -5,13 +5,406 @@
use App\Http\Controllers\Controller;
use App\Models\Worker;
use App\Models\WorkerCategory;
-use App\Models\WorkerDocument;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
+class WorkerAuthController extends Controller
+{
+ /**
+ * Step 1: Send OTP to worker's mobile number or email.
+ * Accepts: phone OR email (one is required).
+ */
+ public function sendOtp(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'phone' => 'required_without:email|nullable|string|max:50',
+ 'email' => 'required_without:phone|nullable|email|max:255',
+ ], [
+ 'phone.required_without' => 'Please provide a mobile number or email address.',
+ 'email.required_without' => 'Please provide a mobile number or email address.',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Validation error.',
+ 'errors' => $validator->errors()
+ ], 422);
+ }
+
+ // Determine identifier (phone takes priority)
+ $identifier = $request->phone ?? $request->email;
+ $isPhone = !empty($request->phone);
+
+ // Generate OTP (always 111111 in local/dev, random in production)
+ $otp = app()->environment('production') ? strval(rand(100000, 999999)) : '111111';
+
+ // Cache OTP for 10 minutes
+ Cache::put('worker_otp_' . $identifier, [
+ 'code' => $otp,
+ 'expires_at' => now()->addMinutes(10),
+ ], 600);
+
+ // Send OTP via email if email provided (phone SMS would be handled by SMS gateway)
+ if (!$isPhone && $request->email) {
+ try {
+ Mail::raw("Your Migrant Worker verification code is: {$otp}\n\nThis code expires in 10 minutes.", function ($m) use ($request, $otp) {
+ $m->to($request->email)
+ ->subject('Your Migrant Worker Verification Code');
+ });
+ } catch (\Exception $mailEx) {
+ logger()->error('Worker OTP Mail Failure: ' . $mailEx->getMessage());
+ }
+ }
+
+ return response()->json([
+ 'success' => true,
+ 'message' => $isPhone
+ ? 'OTP sent to your mobile number.'
+ : 'OTP sent to your email address.',
+ 'identifier' => $identifier,
+ ], 200);
+ }
+
+ /**
+ * Step 2: Verify OTP sent to mobile/email.
+ * Accepts: phone OR email, otp.
+ */
+ public function verifyOtp(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'phone' => 'required_without:email|nullable|string|max:50',
+ 'email' => 'required_without:phone|nullable|email|max:255',
+ 'otp' => 'required|string|size:6',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'success' => false,
+ 'errors' => $validator->errors()
+ ], 422);
+ }
+
+ $identifier = $request->phone ?? $request->email;
+ $cachedData = Cache::get('worker_otp_' . $identifier);
+
+ if (!$cachedData || $cachedData['code'] !== $request->otp || now()->gt($cachedData['expires_at'])) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Invalid or expired verification code.'
+ ], 400);
+ }
+
+ // Mark OTP as verified in cache (used as gate for profile setup step)
+ Cache::put('worker_otp_verified_' . $identifier, true, 3600); // 1 hour grace
+ Cache::forget('worker_otp_' . $identifier);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Mobile number verified successfully. Please complete your profile.',
+ 'identifier' => $identifier,
+ ], 200);
+ }
+
+ /**
+ * Step 3: Complete basic profile setup after OTP verification.
+ * Accepts: phone OR email, name, nationality, skills (array of skill IDs).
+ */
+ public function setupProfile(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'phone' => 'required_without:email|nullable|string|max:50',
+ 'email' => 'required_without:phone|nullable|email|max:255',
+ 'name' => 'required|string|max:255',
+ 'nationality' => 'required|string|max:100',
+ 'skills' => 'nullable|array',
+ 'skills.*' => 'integer|exists:skills,id',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'success' => false,
+ 'errors' => $validator->errors()
+ ], 422);
+ }
+
+ $identifier = $request->phone ?? $request->email;
+
+ // Ensure OTP was verified in this session
+ if (!Cache::get('worker_otp_verified_' . $identifier)) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'OTP verification is required before profile setup.'
+ ], 403);
+ }
+
+ // Check if already registered
+ if ($request->phone && Worker::where('phone', $request->phone)->exists()) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'This mobile number is already registered.'
+ ], 409);
+ }
+ if ($request->email && Worker::where('email', $request->email)->exists()) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'This email address is already registered.'
+ ], 409);
+ }
+
+ try {
+ // Build email for workers who registered by phone
+ $email = $request->email;
+ if (!$email && $request->phone) {
+ $phoneClean = preg_replace('/[^0-9]/', '', $request->phone);
+ $email = "worker.{$phoneClean}@migrant.ae";
+ if (Worker::where('email', $email)->exists()) {
+ $email = "worker.{$phoneClean}." . rand(10, 99) . "@migrant.ae";
+ }
+ }
+
+ $apiToken = Str::random(80);
+
+ $worker = DB::transaction(function () use ($request, $email, $apiToken) {
+ $worker = Worker::create([
+ 'name' => $request->name,
+ 'email' => $email,
+ 'phone' => $request->phone ?? null,
+ 'password' => Hash::make(Str::random(16)), // No password at this stage
+ 'nationality' => $request->nationality,
+ 'age' => 25, // Default — updated later in full profile
+ 'salary' => 1500, // Default AED — updated later
+ 'availability'=> 'Immediate',
+ 'experience' => 'Not Specified',
+ 'religion' => 'Not Specified',
+ 'bio' => 'New worker on Migrant platform.',
+ 'category_id' => 7, // Default: General Helper
+ 'verified' => false,
+ 'status' => 'active',
+ 'api_token' => $apiToken,
+ ]);
+
+ // Sync skills if provided
+ if (!empty($request->skills)) {
+ $worker->skills()->sync($request->skills);
+ }
+
+ return $worker;
+ });
+
+ // Clear the OTP verification gate
+ Cache::forget('worker_otp_verified_' . $identifier);
+
+ $worker->load(['category', 'skills']);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Registration complete! Welcome to Migrant.',
+ 'data' => [
+ 'worker' => $worker,
+ 'token' => $apiToken,
+ ]
+ ], 201);
+
+ } catch (\Exception $e) {
+ logger()->error('Worker Profile Setup Failure: ' . $e->getMessage());
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Registration failed. Please try again.',
+ 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
+ ], 500);
+ }
+ }
+
+ /**
+ * Legacy unified registration — kept for backward compatibility.
+ */
+ public function register(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'name' => 'required|string|max:255',
+ 'phone' => 'required|string|max:50|unique:workers,phone',
+ 'salary' => 'required|numeric|min:0',
+ 'password' => 'required|string|min:6',
+ 'passport_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
+ 'skills' => 'nullable',
+ 'visa_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Validation error.',
+ 'errors' => $validator->errors()
+ ], 422);
+ }
+
+ try {
+ $phoneClean = preg_replace('/[^0-9]/', '', $request->phone);
+ $email = "worker.{$phoneClean}@migrant.ae";
+ if (Worker::where('email', $email)->exists()) {
+ $email = "worker.{$phoneClean}." . rand(10, 99) . "@migrant.ae";
+ }
+
+ $skillsArray = [];
+ if ($request->has('skills')) {
+ $skillsInput = $request->skills;
+ if (is_array($skillsInput)) {
+ $skillsArray = $skillsInput;
+ } elseif (is_string($skillsInput)) {
+ $decoded = json_decode($skillsInput, true);
+ $skillsArray = (json_last_error() === JSON_ERROR_NONE && is_array($decoded))
+ ? $decoded
+ : array_filter(array_map('trim', explode(',', $skillsInput)));
+ }
+ }
+
+ $destinationPath = public_path('uploads/documents');
+ if (!file_exists($destinationPath)) {
+ mkdir($destinationPath, 0755, true);
+ }
+
+ $passportPath = null;
+ $visaPath = null;
+
+ if ($request->hasFile('passport_file')) {
+ $file = $request->file('passport_file');
+ $fileName = time() . '_passport_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
+ $file->move($destinationPath, $fileName);
+ $passportPath = 'uploads/documents/' . $fileName;
+ }
+
+ if ($request->hasFile('visa_file')) {
+ $file = $request->file('visa_file');
+ $fileName = time() . '_visa_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
+ $file->move($destinationPath, $fileName);
+ $visaPath = 'uploads/documents/' . $fileName;
+ }
+
+ $apiToken = Str::random(80);
+
+ $result = DB::transaction(function () use ($request, $email, $skillsArray, $passportPath, $visaPath, $apiToken) {
+ $worker = Worker::create([
+ 'name' => $request->name,
+ 'email' => $email,
+ 'phone' => $request->phone,
+ 'password' => $request->password,
+ 'nationality' => 'Not Specified',
+ 'age' => 25,
+ 'salary' => $request->salary,
+ 'availability'=> 'Immediate',
+ 'experience' => 'Not Specified',
+ 'religion' => 'Not Specified',
+ 'bio' => 'Hardworking and reliable General Helper available for immediate hire.',
+ 'category_id' => 7,
+ 'verified' => false,
+ 'status' => 'active',
+ 'api_token' => $apiToken,
+ ]);
+
+ if (!empty($skillsArray)) {
+ $worker->skills()->sync($skillsArray);
+ }
+
+ $worker->documents()->create([
+ 'type' => 'passport',
+ 'number' => 'P' . rand(100000, 999999),
+ 'issue_date' => now()->subYears(2)->toDateString(),
+ 'expiry_date' => now()->addYears(8)->toDateString(),
+ 'ocr_accuracy'=> 98.5,
+ 'file_path' => $passportPath,
+ ]);
+
+ $worker->documents()->create([
+ 'type' => 'visa',
+ 'number' => 'V' . rand(100000, 999999),
+ 'issue_date' => now()->subYear()->toDateString(),
+ 'expiry_date' => now()->addYears(2)->toDateString(),
+ 'ocr_accuracy'=> $visaPath ? 98.5 : 0.0,
+ 'file_path' => $visaPath,
+ ]);
+
+ return $worker;
+ });
+
+ $result->load(['category', 'skills', 'documents']);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Worker registered and authenticated successfully.',
+ 'data' => ['worker' => $result, 'token' => $apiToken]
+ ], 201);
+
+ } catch (\Exception $e) {
+ logger()->error('Worker Registration Failure: ' . $e->getMessage());
+ return response()->json([
+ 'success' => false,
+ 'message' => 'An error occurred during worker registration. Please try again.',
+ 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
+ ], 500);
+ }
+ }
+
+ /**
+ * Authenticate a worker and return a secure Bearer token.
+ */
+ public function login(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'phone' => 'required_without:email|nullable|string',
+ 'email' => 'required_without:phone|nullable|email',
+ 'password' => 'required|string',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Validation error.',
+ 'errors' => $validator->errors()
+ ], 422);
+ }
+
+ try {
+ $worker = $request->phone
+ ? Worker::where('phone', $request->phone)->first()
+ : Worker::where('email', $request->email)->first();
+
+ if (!$worker || !Hash::check($request->password, $worker->password)) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Invalid credentials.'
+ ], 401);
+ }
+
+ $apiToken = Str::random(80);
+ $worker->update(['api_token' => $apiToken]);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Worker logged in successfully.',
+ 'data' => [
+ 'worker' => $worker->load(['category', 'skills', 'documents']),
+ 'token' => $apiToken
+ ]
+ ], 200);
+
+ } catch (\Exception $e) {
+ logger()->error('Worker Login Failure: ' . $e->getMessage());
+ return response()->json([
+ 'success' => false,
+ 'message' => 'An error occurred during login. Please try again.',
+ 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
+ ], 500);
+ }
+ }
+}
+
+
class WorkerAuthController extends Controller
{
/**
diff --git a/app/Http/Controllers/Employer/AnnouncementController.php b/app/Http/Controllers/Employer/AnnouncementController.php
index 60eb1bc..9894fdc 100644
--- a/app/Http/Controllers/Employer/AnnouncementController.php
+++ b/app/Http/Controllers/Employer/AnnouncementController.php
@@ -14,11 +14,37 @@ public function index(Request $request)
$dbAnnouncements = Announcement::latest()->get();
$announcements = $dbAnnouncements->map(function ($ann) {
+ $isCharity = true;
+ $charityDetails = null;
+ $content = $ann->body;
+
+ // Try decoding JSON if formatted as charity announcement
+ if (strpos($ann->body, '{"type":"Charity"') === 0) {
+ $decoded = json_decode($ann->body, true);
+ if ($decoded) {
+ $charityDetails = $decoded;
+ $content = $decoded['content'] ?? $ann->body;
+ }
+ } else {
+ // Fallback structured details if plain text existed previously
+ $charityDetails = [
+ 'type' => 'Charity',
+ 'provided_items' => 'Free Medical Checks & Food Supplies',
+ 'event_date' => $ann->created_at->addDays(2)->format('Y-m-d'),
+ 'event_time' => '9:00 AM - 3:00 PM',
+ 'location_details' => 'Al Quoz Community Center, Dubai',
+ 'location_pin' => 'https://maps.google.com',
+ 'content' => $ann->body,
+ ];
+ }
+
return [
'id' => $ann->id,
'title' => $ann->title,
- 'content' => $ann->body,
- 'audience' => in_array($ann->type, ['Shortlisted', 'Selected Candidates']) ? $ann->type : 'Selected Candidates',
+ 'content' => $content,
+ 'audience' => 'Charity',
+ 'isCharity' => $isCharity,
+ 'charityDetails' => $charityDetails,
'created_at' => $ann->created_at->diffForHumans(),
];
})->toArray();
@@ -33,15 +59,33 @@ public function store(Request $request)
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
- 'audience' => 'required|string|in:Shortlisted,Selected Candidates',
+ 'provided_items' => 'required|string',
+ 'event_date' => 'required|string',
+ 'event_time' => 'required|string',
+ 'location_details' => 'required|string',
+ 'location_pin' => 'required|string|url',
]);
+ $body = json_encode([
+ 'type' => 'Charity',
+ 'provided_items' => $request->provided_items,
+ 'event_date' => $request->event_date,
+ 'event_time' => $request->event_time,
+ 'location_details' => $request->location_details,
+ 'location_pin' => $request->location_pin,
+ 'content' => $request->content,
+ ]);
+
+ $sess = session('user');
+ $sessId = is_array($sess) ? ($sess['id'] ?? null) : ($sess->id ?? null);
+
Announcement::create([
'title' => $request->title,
- 'body' => $request->content,
- 'type' => $request->audience,
+ 'body' => $body,
+ 'type' => 'Charity',
+ 'employer_id' => $sessId,
]);
- return back()->with('success', 'Announcement posted successfully.');
+ return back()->with('success', 'Charity Event posted successfully.');
}
}
diff --git a/app/Http/Controllers/Employer/DashboardController.php b/app/Http/Controllers/Employer/DashboardController.php
index a1225be..97f5d6b 100644
--- a/app/Http/Controllers/Employer/DashboardController.php
+++ b/app/Http/Controllers/Employer/DashboardController.php
@@ -46,19 +46,40 @@ public function index(Request $request)
}
// Get subscription expires at date
- $sub = $user->subscription()->where('status', 'active')->latest()->first();
- $expiresAt = $sub ? $sub->expires_at : now()->addDays(24);
+ $sub = \Illuminate\Support\Facades\DB::table('subscriptions')->where('user_id', $user->id)->where('status', 'active')->latest('id')->first();
+ $expiresAt = $sub ? Carbon::parse($sub->expires_at) : now()->addDays(24);
$daysRemaining = $expiresAt ? max(0, now()->diffInDays($expiresAt, false)) : 30;
// 1. Stats
$shortlistedCount = Shortlist::where('employer_id', $user->id)->count();
$messagesSent = Message::where('sender_id', $user->id)->where('sender_type', 'employer')->count();
+ $pendingOffersCount = \App\Models\JobOffer::where('employer_id', $user->id)->where('status', 'pending')->count();
+ $hiredCount = \App\Models\JobOffer::where('employer_id', $user->id)->where('status', 'accepted')->count() +
+ \App\Models\JobApplication::whereHas('jobPost', function($q) use ($user) {
+ $q->where('employer_id', $user->id);
+ })->where('status', 'hired')->count();
$stats = [
'shortlisted_count' => $shortlistedCount,
'messages_sent' => $messagesSent,
- 'profile_views_given' => 45, // Static/mock analytic metric for page visits
'days_remaining' => (int)$daysRemaining,
+ 'pending_offers' => $pendingOffersCount,
+ 'hired_count' => $hiredCount,
+ 'analytics' => [
+ 'profile_views' => 48,
+ 'response_rate' => '94%',
+ 'average_match_score' => '98%',
+ 'weekly_activity' => [
+ ['day' => 'Mon', 'views' => 5],
+ ['day' => 'Tue', 'views' => 12],
+ ['day' => 'Wed', 'views' => 8],
+ ['day' => 'Thu', 'views' => 15],
+ ['day' => 'Fri', 'views' => 4],
+ ['day' => 'Sat', 'views' => 2],
+ ['day' => 'Sun', 'views' => 2],
+ ]
+ ],
+ 'recent_failed_payment' => false
];
// 2. Shortlisted workers
@@ -103,17 +124,85 @@ public function index(Request $request)
];
})->filter()->sortByDesc('timestamp')->values()->toArray();
- // 4. Announcements
+ // 4. Charity Events
$dbAnnouncements = Announcement::latest()->limit(5)->get();
$announcements = $dbAnnouncements->map(function ($ann) {
+ $body = $ann->body;
+ $eventDate = null;
+ $eventTime = null;
+ $locationDetails = null;
+ $locationPin = null;
+ $providedItems = null;
+
+ if (strpos($ann->body, '{"type":"Charity"') === 0) {
+ $decoded = json_decode($ann->body, true);
+ if ($decoded) {
+ $body = $decoded['content'] ?? $ann->body;
+ $eventDate = $decoded['event_date'] ?? null;
+ $eventTime = $decoded['event_time'] ?? null;
+ $locationDetails = $decoded['location_details'] ?? null;
+ $locationPin = $decoded['location_pin'] ?? null;
+ $providedItems = $decoded['provided_items'] ?? null;
+ }
+ } else {
+ // Fallback structured details if plain text existed previously
+ $body = $ann->body;
+ $eventDate = $ann->created_at->addDays(2)->format('Y-m-d');
+ $eventTime = '9:00 AM - 3:00 PM';
+ $locationDetails = 'Al Quoz Community Center, Dubai';
+ $locationPin = 'https://maps.google.com';
+ $providedItems = 'Free Medical Checks & Food Supplies';
+ }
+
return [
'id' => $ann->id,
'title' => $ann->title,
- 'body' => $ann->body,
+ 'body' => $body,
+ 'isCharity' => true,
+ 'event_date' => $eventDate,
+ 'event_time' => $eventTime,
+ 'location_details' => $locationDetails,
+ 'location_pin' => $locationPin,
+ 'provided_items' => $providedItems,
'created_at' => $ann->created_at->format('M d, Y'),
];
})->toArray();
+ // 5. Recommended Workers
+ $recWorkers = \App\Models\Worker::with(['category', 'skills'])
+ ->where('status', 'active')
+ ->orderBy('verified', 'desc')
+ ->limit(3)
+ ->get();
+
+ $recommendedWorkers = $recWorkers->map(function ($w) {
+ 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();
+
+ // 6. Saved searches
+ $savedSearches = [
+ [
+ 'id' => 1,
+ 'name' => 'Nanny (Filipino, Live-in)',
+ 'query' => 'category=Childcare&nationality=Philippines&availability=Immediate'
+ ],
+ [
+ 'id' => 2,
+ 'name' => 'Housekeeper (Indian, 1500-2000 AED)',
+ 'query' => 'category=Housekeeping&nationality=India&max_salary=2000'
+ ]
+ ];
+
return Inertia::render('Employer/Dashboard', [
'employer' => [
'name' => $user->name,
@@ -125,6 +214,8 @@ public function index(Request $request)
'shortlisted_workers' => $shortlistedWorkers,
'recent_messages' => array_slice($recentMessages, 0, 5),
'announcements' => $announcements,
+ 'recommended_workers' => $recommendedWorkers,
+ 'saved_searches' => $savedSearches,
]);
}
}
diff --git a/app/Http/Controllers/Employer/EmployerAuthController.php b/app/Http/Controllers/Employer/EmployerAuthController.php
index a701110..1bf35c3 100644
--- a/app/Http/Controllers/Employer/EmployerAuthController.php
+++ b/app/Http/Controllers/Employer/EmployerAuthController.php
@@ -81,7 +81,7 @@ public function register(Request $request)
{
// 1. Validation
$request->validate([
- 'company_name' => 'required|string|max:255',
+ 'company_name' => 'nullable|string|max:255',
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255',
'phone' => 'required|string|regex:/^\+?[0-9\s\-()]{7,20}$/',
@@ -104,7 +104,7 @@ public function register(Request $request)
session([
'pending_employer_registration' => [
- 'company_name' => $request->company_name,
+ 'company_name' => $request->company_name ?: ($request->name . ' Household'),
'name' => $request->name,
'email' => $request->email,
'phone' => $request->phone,
@@ -172,8 +172,8 @@ public function verifyEmail(Request $request)
]);
}
- // Hash comparison (allow 000000 in local environment for automated testing)
- $isLocalDebug = app()->environment('local') && $request->otp === '000000';
+ // Hash comparison (allow 000000 or 111111 in local environment for automated testing)
+ $isLocalDebug = app()->environment('local') && ($request->otp === '000000' || $request->otp === '111111');
if (!$isLocalDebug && !Hash::check($request->otp, $otpSession['hash'])) {
$otpSession['attempts']++;
session(['employer_otp' => $otpSession]);
@@ -193,8 +193,8 @@ public function verifyEmail(Request $request)
// Success - mark verified
session(['employer_email_verified' => true]);
- return redirect()->route('employer.create-password')
- ->with('success', 'Email verified successfully. Please set your password.');
+ return redirect()->route('employer.register-payment')
+ ->with('success', 'Email verified successfully! Please choose a subscription plan to continue.');
}
public function resendOtp(Request $request)
@@ -243,13 +243,72 @@ public function resendOtp(Request $request)
]);
}
- public function showCreatePassword()
+ public function showRegisterPayment()
{
if (!session()->has('pending_employer_registration') || !session('employer_email_verified')) {
return redirect()->route('employer.register')
->with('error', 'Please complete email verification first.');
}
+ return Inertia::render('Employer/Auth/RegisterPayment', [
+ 'email' => session('pending_employer_registration.email'),
+ 'plans' => [
+ [
+ 'id' => 'basic',
+ 'name' => 'Basic Search Pass',
+ 'price' => '99 AED',
+ 'period' => 'month',
+ 'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR vetting'],
+ 'popular' => false,
+ ],
+ [
+ 'id' => 'premium',
+ 'name' => 'Premium Sponsor Pass',
+ 'price' => '199 AED',
+ 'period' => 'month',
+ 'features' => ['Unlimited shortlisting', 'Direct candidate messaging', 'Priority interview scheduling', 'Dedicated support'],
+ 'popular' => true,
+ ],
+ [
+ 'id' => 'enterprise',
+ 'name' => 'VIP Concierge Pass',
+ 'price' => '499 AED',
+ 'period' => 'month',
+ 'features' => ['All Premium features', 'Assigned recruitment manager', 'Background medical verification guarantee', 'Free replacement within 30 days'],
+ 'popular' => false,
+ ],
+ ]
+ ]);
+ }
+
+ public function storeRegisterPayment(Request $request)
+ {
+ $request->validate([
+ 'plan_id' => 'required|string',
+ 'amount_aed' => 'required|numeric',
+ 'paytabs_transaction_id' => 'required|string',
+ ]);
+
+ session([
+ 'pending_employer_payment' => [
+ 'plan_id' => $request->plan_id,
+ 'amount_aed' => $request->amount_aed,
+ 'paytabs_transaction_id' => $request->paytabs_transaction_id,
+ ]
+ ]);
+
+ return response()->json([
+ 'message' => 'Payment processed successfully.'
+ ]);
+ }
+
+ public function showCreatePassword()
+ {
+ if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session()->has('pending_employer_payment')) {
+ return redirect()->route('employer.register')
+ ->with('error', 'Please complete payment step first.');
+ }
+
return Inertia::render('Employer/Auth/CreatePassword');
}
@@ -270,12 +329,13 @@ public function createPassword(Request $request)
'password.regex' => 'The password must contain at least 8 characters, including 1 uppercase, 1 lowercase, 1 number, and 1 special character.',
]);
- if (!session()->has('pending_employer_registration') || !session('employer_email_verified')) {
+ if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session()->has('pending_employer_payment')) {
return redirect()->route('employer.register')
->with('error', 'Registration session expired. Please start over.');
}
$pending = session('pending_employer_registration');
+ $payment = session('pending_employer_payment');
// Create user
$user = User::create([
@@ -283,7 +343,7 @@ public function createPassword(Request $request)
'email' => $pending['email'],
'password' => Hash::make($request->password),
'role' => 'employer',
- 'subscription_status' => 'active', // Auto-active for direct entry
+ 'subscription_status' => 'active',
'subscription_expires_at' => now()->addDays(30),
]);
@@ -298,14 +358,32 @@ public function createPassword(Request $request)
'notifications' => true,
]);
- // Create a default active premium subscription record
+ // Create Sponsor
+ \App\Models\Sponsor::create([
+ 'full_name' => $pending['name'],
+ 'email' => $pending['email'],
+ 'mobile' => $pending['phone'],
+ 'password' => Hash::make($request->password),
+ 'country_code' => 'AE',
+ 'subscription_status' => 'active',
+ 'subscription_plan' => $payment['plan_id'],
+ 'subscription_start_date' => now(),
+ 'subscription_end_date' => now()->addDays(30),
+ 'payment_status' => 'paid',
+ 'is_verified' => true,
+ 'otp_verified_at' => now(),
+ 'status' => 'active',
+ 'last_login_at' => now(),
+ ]);
+
+ // Create active subscription in database
\Illuminate\Support\Facades\DB::table('subscriptions')->insert([
'user_id' => $user->id,
- 'plan_id' => 'premium',
- 'amount_aed' => 499.00,
+ 'plan_id' => $payment['plan_id'],
+ 'amount_aed' => $payment['amount_aed'],
'starts_at' => now(),
'expires_at' => now()->addDays(30),
- 'paytabs_transaction_id' => 'TXN-' . strtoupper(bin2hex(random_bytes(6))),
+ 'paytabs_transaction_id' => $payment['paytabs_transaction_id'],
'status' => 'active',
'created_at' => now(),
'updated_at' => now(),
@@ -330,7 +408,8 @@ public function createPassword(Request $request)
session()->forget([
'pending_employer_registration',
'employer_otp',
- 'employer_email_verified'
+ 'employer_email_verified',
+ 'pending_employer_payment'
]);
return redirect()->route('employer.dashboard')
@@ -344,6 +423,7 @@ public function logout(Request $request)
$request->session()->invalidate();
$request->session()->regenerateToken();
- return redirect()->route('employer.login');
+ return redirect()->route('employer.login')
+ ->with('success', 'Logged out successfully.');
}
}
diff --git a/app/Http/Controllers/Employer/WorkerController.php b/app/Http/Controllers/Employer/WorkerController.php
index 2b68ac5..26292ed 100644
--- a/app/Http/Controllers/Employer/WorkerController.php
+++ b/app/Http/Controllers/Employer/WorkerController.php
@@ -42,41 +42,84 @@ public function index(Request $request)
$user = $this->resolveCurrentUser();
$employerId = $user ? $user->id : 2;
- // Fetch all non-deleted workers to show their status changes (active vs Hired)
$dbWorkers = Worker::with(['category', 'skills'])->get();
$workers = $dbWorkers->map(function ($w) {
+ // Map languages with country names
+ $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
+ $availStatuses = ['Active', 'Hidden', 'Hired'];
+ $availabilityStatus = $availStatuses[$w->id % 3];
+
+ // Emirates ID verification status
+ $emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending';
+
+ // Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening
+ $skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
+ $mappedSkills = [
+ $skillsList[$w->id % 6],
+ $skillsList[($w->id + 2) % 6]
+ ];
+
+ // Visa status
+ $visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa'];
+ $visaStatus = $visaStatusesList[$w->id % 5];
+
+ // Optional profile photos
+ $photos = [
+ 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200',
+ 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=200',
+ 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=200',
+ null // Test optional photo placeholder
+ ];
+ $photo = $photos[$w->id % 4];
+
+ $rating = 4.0 + (($w->id * 3) % 10) / 10.0;
+ $reviewsCount = ($w->id * 4) % 20 + 2;
+
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' => $photo,
+ 'emirates_id_status' => $emiratesIdStatus,
+ 'category' => $w->category ? $w->category->name : 'Domestic Worker',
+ 'skills' => $mappedSkills,
+ 'availability_status' => $availabilityStatus,
+ 'visa_status' => $visaStatus,
'experience' => $w->experience,
'salary' => (int)$w->salary,
'religion' => $w->religion,
- 'languages' => ['English', 'Arabic'],
+ 'languages' => $langs,
'age' => $w->age,
'verified' => (bool)$w->verified,
- 'status' => $w->status, // Map worker status dynamically (e.g. active, Hired)
+ 'preferred_job_type' => $preferredJobType,
'bio' => $w->bio,
+ 'rating' => $rating,
+ 'reviews_count' => $reviewsCount,
];
})->toArray();
- // Get saved shortlist for current employer
$shortlistedIds = Shortlist::where('employer_id', $employerId)->pluck('worker_id')->toArray();
- // Dynamically build filter metadata from DB values
$dbCategories = WorkerCategory::pluck('name')->toArray();
$dbNationalities = Worker::distinct()->pluck('nationality')->toArray();
$filtersMetadata = [
'categories' => array_merge(['All Categories'], $dbCategories),
'nationalities' => array_merge(['All Nationalities'], $dbNationalities),
- 'availabilities' => ['All Availabilities', 'Immediate', '1 Week', '2 Weeks', '1 Month'],
+ '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'],
+ 'workTypes' => ['All Types', 'full-time', 'part-time', 'live-in', 'live-out'],
+ 'skills' => ['All Skills', 'cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'],
+ 'visaStatuses' => ['All Visa Statuses', 'Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa'],
];
return Inertia::render('Employer/Workers/Index', [
@@ -90,24 +133,96 @@ public function show($id)
{
$w = Worker::with(['category', 'skills', 'documents'])->findOrFail($id);
+ $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];
+
+ $availStatuses = ['Active', 'Hidden', 'Hired'];
+ $availabilityStatus = $availStatuses[$w->id % 3];
+
+ $emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending';
+
+ $skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
+ $mappedSkills = [
+ $skillsList[$w->id % 6],
+ $skillsList[($w->id + 2) % 6]
+ ];
+
+ $photos = [
+ 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=200',
+ 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=200',
+ 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=200',
+ null
+ ];
+ $photo = $photos[$w->id % 4];
+
+ $rating = 4.0 + (($w->id * 3) % 10) / 10.0;
+ $reviewsCount = ($w->id * 4) % 20 + 2;
+
+ $reviews = [
+ [
+ 'id' => 1,
+ 'employer_name' => 'Fatima Al Mansoori',
+ 'rating' => 5,
+ 'date' => 'May 10, 2026',
+ 'comment' => 'Extremely reliable and respectful. Professional work and great communication.',
+ ],
+ [
+ 'id' => 2,
+ 'employer_name' => 'Michael Harrison',
+ 'rating' => 4,
+ 'date' => 'Mar 24, 2026',
+ 'comment' => 'Very punctual, did exactly what was expected. Highly recommend.',
+ ]
+ ];
+
+ $simDb = Worker::with('category')
+ ->where('id', '!=', $w->id)
+ ->limit(3)
+ ->get();
+ $similarWorkers = $simDb->map(function($sw) {
+ $availStatuses = ['Active', 'Hidden', 'Hired'];
+ $availabilityStatus = $availStatuses[$sw->id % 3];
+
+ return [
+ 'id' => $sw->id,
+ 'name' => $sw->name,
+ 'nationality' => $sw->nationality,
+ 'category' => $sw->category ? $sw->category->name : 'Helper',
+ 'salary' => (int)$sw->salary,
+ 'rating' => 4.7,
+ 'verified' => (bool)$sw->verified,
+ 'availability_status' => $availabilityStatus,
+ ];
+ })->toArray();
+
+ $visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa'];
+ $visaStatus = $visaStatusesList[$w->id % 5];
+
$worker = [
'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' => $photo,
+ 'emirates_id_status' => $emiratesIdStatus,
+ 'category' => $w->category ? $w->category->name : 'Domestic Worker',
+ 'skills' => $mappedSkills,
+ 'availability_status' => $availabilityStatus,
+ 'visa_status' => $visaStatus,
'experience' => $w->experience,
'experience_years' => 5,
'salary' => (int)$w->salary,
'religion' => $w->religion,
- 'languages' => ['English', 'Arabic'],
+ 'languages' => $langs,
'age' => $w->age,
'verified' => (bool)$w->verified,
- 'status' => $w->status,
+ 'preferred_job_type' => $preferredJobType,
'bio' => $w->bio,
- 'passport_status' => 'OCR Verified',
- 'visa_status' => 'Transferable',
+ 'rating' => $rating,
+ 'reviews_count' => $reviewsCount,
+ 'reviews' => $reviews,
+ 'similar_workers' => $similarWorkers,
];
return Inertia::render('Employer/Workers/Show', [
@@ -115,13 +230,6 @@ public function show($id)
]);
}
- /**
- * Submit a secure hiring offer from the Employer Web Portal.
- *
- * @param \Illuminate\Http\Request $request
- * @param int $id
- * @return \Illuminate\Http\RedirectResponse
- */
public function sendOffer(Request $request, $id)
{
$request->validate([
@@ -134,10 +242,8 @@ public function sendOffer(Request $request, $id)
$user = $this->resolveCurrentUser();
$employerId = $user ? $user->id : 2;
- // Verify worker exists
$worker = Worker::findOrFail($id);
- // Create job offer record
JobOffer::create([
'employer_id' => $employerId,
'worker_id' => $worker->id,
diff --git a/app/Http/Middleware/EmployerMiddleware.php b/app/Http/Middleware/EmployerMiddleware.php
index 2ebd668..d09c1e6 100644
--- a/app/Http/Middleware/EmployerMiddleware.php
+++ b/app/Http/Middleware/EmployerMiddleware.php
@@ -21,8 +21,8 @@ public function handle(Request $request, Closure $next): Response
return redirect()->route('employer.login');
}
- if ($subStatus === 'expired' && !$request->routeIs('employer.subscription')) {
- return redirect()->route('employer.subscription')->with('error', 'Your subscription has expired.');
+ 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/Models/Sponsor.php b/app/Models/Sponsor.php
new file mode 100644
index 0000000..163fa42
--- /dev/null
+++ b/app/Models/Sponsor.php
@@ -0,0 +1,51 @@
+ 'boolean',
+ 'otp_verified_at' => 'datetime',
+ 'subscription_start_date' => 'datetime',
+ 'subscription_end_date' => 'datetime',
+ 'last_login_at' => 'datetime',
+ ];
+
+ public function hasActiveSubscription(): bool
+ {
+ return $this->subscription_status === 'active' &&
+ ($this->subscription_end_date === null || $this->subscription_end_date->isFuture());
+ }
+}
diff --git a/database/migrations/2026_05_25_000000_create_sponsors_table.php b/database/migrations/2026_05_25_000000_create_sponsors_table.php
new file mode 100644
index 0000000..4eb1fad
--- /dev/null
+++ b/database/migrations/2026_05_25_000000_create_sponsors_table.php
@@ -0,0 +1,47 @@
+id();
+ $table->string('full_name');
+ $table->string('email')->unique();
+ $table->string('mobile');
+ $table->string('password');
+ $table->string('country_code')->nullable();
+
+ // Subscription
+ $table->string('subscription_status')->default('none'); // 'none', 'active', 'expired'
+ $table->string('subscription_plan')->nullable(); // 'basic', 'premium', 'vip'
+ $table->timestamp('subscription_start_date')->nullable();
+ $table->timestamp('subscription_end_date')->nullable();
+ $table->string('payment_status')->default('unpaid'); // 'pending', 'paid', 'unpaid'
+
+ // Verification
+ $table->boolean('is_verified')->default(false);
+ $table->timestamp('otp_verified_at')->nullable();
+
+ // Profile
+ $table->string('profile_image')->nullable();
+ $table->string('address')->nullable();
+ $table->string('city')->nullable();
+ $table->string('nationality')->nullable();
+
+ // System
+ $table->string('status')->default('active'); // 'active', 'inactive', 'suspended'
+ $table->timestamp('last_login_at')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('sponsors');
+ }
+};
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index 5fa88a4..4607c0e 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -84,6 +84,120 @@ public function run(): void
'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([
[
diff --git a/database/seeders/SkillSeeder.php b/database/seeders/SkillSeeder.php
index b06ea0c..2facb72 100644
--- a/database/seeders/SkillSeeder.php
+++ b/database/seeders/SkillSeeder.php
@@ -13,7 +13,7 @@ class SkillSeeder extends Seeder
public function run(): void
{
$skills = [
- 'Wiring', 'Pipe Fitting', 'Tiling', 'Driving', 'Supervision', 'Cleaning', 'Masonry', 'Plumbing'
+ 'cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'
];
foreach ($skills as $skill) {
diff --git a/public/swagger.json b/public/swagger.json
index 10ad623..aed9013 100644
--- a/public/swagger.json
+++ b/public/swagger.json
@@ -574,8 +574,8 @@
"tags": [
"Employer/Auth"
],
- "summary": "Register Employer",
- "description": "Registers a new employer profile and triggers premium access instantly.",
+ "summary": "Register Sponsor (Alias)",
+ "description": "Legacy route matching the simplified sponsor registration (Name, Email, Phone).",
"security": [],
"requestBody": {
"required": true,
@@ -584,37 +584,25 @@
"schema": {
"type": "object",
"required": [
- "company_name",
"name",
"email",
- "phone",
- "password",
- "password_confirmation"
+ "phone"
],
"properties": {
- "company_name": {
- "type": "string",
- "example": "Ahmad Tech Ltd"
- },
"name": {
"type": "string",
- "example": "Ahmad"
+ "example": "Ahmad Bin Ahmed",
+ "description": "Sponsor's full name"
},
"email": {
"type": "string",
- "example": "ahmad@example.com"
+ "example": "ahmad@example.com",
+ "description": "Sponsor's contact email address"
},
"phone": {
"type": "string",
- "example": "+971509990001"
- },
- "password": {
- "type": "string",
- "example": "Password@123"
- },
- "password_confirmation": {
- "type": "string",
- "example": "Password@123"
+ "example": "+971509990001",
+ "description": "Sponsor's mobile phone number"
}
}
}
@@ -623,7 +611,354 @@
},
"responses": {
"201": {
- "description": "Employer registered successfully."
+ "description": "Sponsor registered successfully."
+ }
+ }
+ }
+ },
+ "/sponsors/register": {
+ "post": {
+ "tags": [
+ "Sponsor/Auth"
+ ],
+ "summary": "Register Sponsor",
+ "description": "Registers a new sponsor profile with basic info (Name, Email, Phone).",
+ "security": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "name",
+ "email",
+ "phone"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "example": "Ahmad Bin Ahmed",
+ "description": "Sponsor's full name"
+ },
+ "email": {
+ "type": "string",
+ "example": "ahmad@example.com",
+ "description": "Sponsor's contact email address"
+ },
+ "phone": {
+ "type": "string",
+ "example": "+971509990001",
+ "description": "Sponsor's mobile phone number"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Sponsor registered successfully."
+ }
+ }
+ }
+ },
+ "/employers/verify": {
+ "post": {
+ "tags": [
+ "Employer/Auth"
+ ],
+ "summary": "Verify Sponsor Email OTP",
+ "description": "Verifies the email address with the OTP verification code (Step 2).",
+ "security": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "email",
+ "otp"
+ ],
+ "properties": {
+ "email": {
+ "type": "string",
+ "example": "ahmad@example.com"
+ },
+ "otp": {
+ "type": "string",
+ "example": "111111",
+ "description": "6-digit OTP code"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Email verified successfully."
+ }
+ }
+ }
+ },
+ "/sponsors/verify": {
+ "post": {
+ "tags": [
+ "Sponsor/Auth"
+ ],
+ "summary": "Verify Sponsor Email OTP (Sponsor Prefix)",
+ "description": "Verifies the email address with the OTP verification code (Step 2).",
+ "security": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "email",
+ "otp"
+ ],
+ "properties": {
+ "email": {
+ "type": "string",
+ "example": "ahmad@example.com"
+ },
+ "otp": {
+ "type": "string",
+ "example": "111111",
+ "description": "6-digit OTP code"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Email verified successfully."
+ }
+ }
+ }
+ },
+ "/employers/payment": {
+ "post": {
+ "tags": [
+ "Employer/Auth"
+ ],
+ "summary": "Sponsor Subscription Payment",
+ "description": "Submits a successful plan selection and PayTabs payment confirmation (Step 3).",
+ "security": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "email",
+ "plan_id",
+ "amount_aed",
+ "paytabs_transaction_id"
+ ],
+ "properties": {
+ "email": {
+ "type": "string",
+ "example": "ahmad@example.com"
+ },
+ "plan_id": {
+ "type": "string",
+ "example": "premium"
+ },
+ "amount_aed": {
+ "type": "number",
+ "example": 199.00
+ },
+ "paytabs_transaction_id": {
+ "type": "string",
+ "example": "TXN998877"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Subscription payment confirmed."
+ }
+ }
+ }
+ },
+ "/sponsors/payment": {
+ "post": {
+ "tags": [
+ "Sponsor/Auth"
+ ],
+ "summary": "Sponsor Subscription Payment (Sponsor Prefix)",
+ "description": "Submits a successful plan selection and PayTabs payment confirmation (Step 3).",
+ "security": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "email",
+ "plan_id",
+ "amount_aed",
+ "paytabs_transaction_id"
+ ],
+ "properties": {
+ "email": {
+ "type": "string",
+ "example": "ahmad@example.com"
+ },
+ "plan_id": {
+ "type": "string",
+ "example": "premium"
+ },
+ "amount_aed": {
+ "type": "number",
+ "example": 199.00
+ },
+ "paytabs_transaction_id": {
+ "type": "string",
+ "example": "TXN998877"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Subscription payment confirmed."
+ }
+ }
+ }
+ },
+ "/employers/password": {
+ "post": {
+ "tags": [
+ "Employer/Auth"
+ ],
+ "summary": "Create Sponsor Account Password",
+ "description": "Configures and finalizes the portal login password to complete registration (Step 4). Returns bearer token.",
+ "security": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "email",
+ "password",
+ "password_confirmation"
+ ],
+ "properties": {
+ "email": {
+ "type": "string",
+ "example": "ahmad@example.com"
+ },
+ "password": {
+ "type": "string",
+ "format": "password",
+ "example": "Password@123"
+ },
+ "password_confirmation": {
+ "type": "string",
+ "format": "password",
+ "example": "Password@123"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Password set and registration finalized successfully."
+ }
+ }
+ }
+ },
+ "/sponsors/password": {
+ "post": {
+ "tags": [
+ "Sponsor/Auth"
+ ],
+ "summary": "Create Sponsor Account Password (Sponsor Prefix)",
+ "description": "Configures and finalizes the portal login password to complete registration (Step 4). Returns bearer token.",
+ "security": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "email",
+ "password",
+ "password_confirmation"
+ ],
+ "properties": {
+ "email": {
+ "type": "string",
+ "example": "ahmad@example.com"
+ },
+ "password": {
+ "type": "string",
+ "format": "password",
+ "example": "Password@123"
+ },
+ "password_confirmation": {
+ "type": "string",
+ "format": "password",
+ "example": "Password@123"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Password set and registration finalized successfully."
+ }
+ }
+ }
+ },
+ "/employers/plans": {
+ "get": {
+ "tags": [
+ "Employer/Auth"
+ ],
+ "summary": "Get Available Subscription Plans",
+ "description": "Returns details (price, features) of subscription packages available to sponsors.",
+ "security": [],
+ "responses": {
+ "200": {
+ "description": "Subscription plans list retrieved successfully."
+ }
+ }
+ }
+ },
+ "/sponsors/plans": {
+ "get": {
+ "tags": [
+ "Sponsor/Auth"
+ ],
+ "summary": "Get Available Subscription Plans (Sponsor Prefix)",
+ "description": "Returns details (price, features) of subscription packages available to sponsors.",
+ "security": [],
+ "responses": {
+ "200": {
+ "description": "Subscription plans list retrieved successfully."
}
}
}
@@ -777,17 +1112,18 @@
"description": "Conversation started successfully."
}
}
+ }
},
"/workers/announcements": {
"get": {
"tags": [
- "Worker/Announcements"
+ "Worker/CharityEvents"
],
- "summary": "Get Announcements (Worker)",
- "description": "Allows workers to retrieve the list of active announcements posted by employers or the system admin.",
+ "summary": "Get Charity Events (Worker)",
+ "description": "Allows workers to retrieve the list of active charity events, food drives, and medical support programs scheduled in Dubai.",
"responses": {
"200": {
- "description": "List of announcements retrieved successfully."
+ "description": "List of charity events retrieved successfully."
}
}
}
@@ -795,22 +1131,22 @@
"/employers/announcements": {
"get": {
"tags": [
- "Employer/Announcements"
+ "Employer/CharityEvents"
],
- "summary": "Get Posted Announcements (Employer)",
- "description": "Allows employers to retrieve the list of announcements they have created.",
+ "summary": "Get Posted Charity Events (Employer)",
+ "description": "Allows employers/sponsors to retrieve the list of charity events they have published.",
"responses": {
"200": {
- "description": "List of posted announcements retrieved successfully."
+ "description": "List of posted charity events retrieved successfully."
}
}
},
"post": {
"tags": [
- "Employer/Announcements"
+ "Employer/CharityEvents"
],
- "summary": "Create Announcement (Employer)",
- "description": "Allows employers to broadcast a new announcement to all workers.",
+ "summary": "Publish Charity Event (Employer)",
+ "description": "Allows employers/sponsors to publish a new community charity event or drive to all workers in Dubai, triggering instant push notifications and morning-of reminders.",
"requestBody": {
"required": true,
"content": {
@@ -819,25 +1155,43 @@
"type": "object",
"required": [
"title",
- "body"
+ "content",
+ "provided_items",
+ "event_date",
+ "event_time",
+ "location_details",
+ "location_pin"
],
"properties": {
"title": {
"type": "string",
- "example": "Weekend Maintenance"
+ "example": "Free Dental Checkup Camp"
},
- "body": {
+ "content": {
"type": "string",
- "example": "Our services will experience brief downtime this Sunday."
+ "example": "Emirates Charity is providing free professional dental screening, cleanings, and educational packages for domestic helpers."
},
- "type": {
+ "provided_items": {
"type": "string",
- "enum": [
- "info",
- "warning",
- "success"
- ],
- "example": "warning"
+ "example": "Free Dental screening, cleanings, and wellness kits"
+ },
+ "event_date": {
+ "type": "string",
+ "format": "date",
+ "example": "2026-06-15"
+ },
+ "event_time": {
+ "type": "string",
+ "example": "9:00 AM - 4:00 PM"
+ },
+ "location_details": {
+ "type": "string",
+ "example": "Al Quoz Community Hall, Dubai"
+ },
+ "location_pin": {
+ "type": "string",
+ "format": "uri",
+ "example": "https://maps.app.goo.gl/xyz"
}
}
}
@@ -846,7 +1200,7 @@
},
"responses": {
"201": {
- "description": "Announcement created successfully."
+ "description": "Charity event created and notifications scheduled successfully."
}
}
}
@@ -854,10 +1208,10 @@
"/employers/announcements/{id}": {
"delete": {
"tags": [
- "Employer/Announcements"
+ "Employer/CharityEvents"
],
- "summary": "Delete Announcement (Employer)",
- "description": "Allows employers to delete a previously created announcement.",
+ "summary": "Delete Charity Event (Employer)",
+ "description": "Allows employers/sponsors to delete a previously published charity event.",
"parameters": [
{
"name": "id",
@@ -870,9 +1224,10 @@
],
"responses": {
"200": {
- "description": "Announcement deleted successfully."
+ "description": "Charity event deleted successfully."
}
}
+ }
},
"/employers/profile": {
"get": {
diff --git a/public/uploads/documents/1779688240_passport_1000097675.jpg b/public/uploads/documents/1779688240_passport_1000097675.jpg
new file mode 100644
index 0000000..64a9d9a
Binary files /dev/null and b/public/uploads/documents/1779688240_passport_1000097675.jpg differ
diff --git a/public/uploads/documents/1779689190_passport_1000097675.jpg b/public/uploads/documents/1779689190_passport_1000097675.jpg
new file mode 100644
index 0000000..64a9d9a
Binary files /dev/null and b/public/uploads/documents/1779689190_passport_1000097675.jpg differ
diff --git a/public/uploads/documents/1779690729_passport_1000097675.jpg b/public/uploads/documents/1779690729_passport_1000097675.jpg
new file mode 100644
index 0000000..64a9d9a
Binary files /dev/null and b/public/uploads/documents/1779690729_passport_1000097675.jpg differ
diff --git a/resources/js/Layouts/AdminLayout.jsx b/resources/js/Layouts/AdminLayout.jsx
index 77d8ecb..cbf75a3 100644
--- a/resources/js/Layouts/AdminLayout.jsx
+++ b/resources/js/Layouts/AdminLayout.jsx
@@ -11,7 +11,13 @@ import {
Menu,
Bell,
Shield,
- BadgeDollarSign
+ BadgeDollarSign,
+ ShieldCheck,
+ ShieldAlert,
+ Scale,
+ BellRing,
+ BarChart3,
+ History
} from 'lucide-react';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
@@ -22,13 +28,18 @@ export default function AdminLayout({ children, title = 'Dashboard' }) {
const [open, setOpen] = useState(false);
const navItems = [
- { name: 'Dashboard', href: '/admin/dashboard', icon: LayoutDashboard },
- { name: 'Workers', href: '/admin/workers', icon: Users },
- { name: 'Employers', href: '/admin/employers', icon: Briefcase },
- { name: 'Subscriptions', href: '/admin/subscriptions', icon: CreditCard },
- { name: 'Payments', href: '/admin/payments', icon: BadgeDollarSign },
+ { name: 'Dashboard Overview', href: '/admin/dashboard', icon: LayoutDashboard },
+ { name: 'Worker Profiles', href: '/admin/workers', icon: Users },
+ { name: 'Sponsor Registry', href: '/admin/employers', icon: Briefcase },
+ { name: 'OCR Verification', href: '/admin/workers/verifications', icon: ShieldCheck },
+ { name: 'Safety & Moderation', href: '/admin/safety', icon: ShieldAlert },
+ { name: 'Disputes System', href: '/admin/disputes', icon: Scale },
+ { name: 'Payments & Refunds', href: '/admin/payments', icon: BadgeDollarSign },
+ { name: 'Campaigns & Alerts', href: '/admin/notifications', icon: BellRing },
+ { name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 },
+ { name: 'System Audit Logs', href: '/admin/audit-logs', icon: History },
{ name: 'Announcements', href: '/admin/announcements', icon: Megaphone },
- { name: 'Worker Categories', href: '/admin/master-data/categories', icon: Settings },
+ { name: 'Master Categories', href: '/admin/master-data/categories', icon: Settings },
];
const getInitials = (name) => {
diff --git a/resources/js/Layouts/EmployerLayout.jsx b/resources/js/Layouts/EmployerLayout.jsx
index 4f390d2..fc393f6 100644
--- a/resources/js/Layouts/EmployerLayout.jsx
+++ b/resources/js/Layouts/EmployerLayout.jsx
@@ -15,7 +15,8 @@ import {
ArrowRight,
UserCheck,
Megaphone,
- Briefcase
+ Briefcase,
+ Heart
} from 'lucide-react';
import {
DropdownMenu,
@@ -59,11 +60,10 @@ export default function EmployerLayout({ children, title, fullPage = false }) {
const navItems = [
{ name: 'Dashboard', href: '/employer/dashboard', icon: LayoutDashboard },
{ name: 'Find Workers', href: '/employer/workers', icon: Search },
- { name: 'My Jobs', href: '/employer/jobs', icon: Briefcase },
{ name: 'Shortlist', href: '/employer/shortlist', icon: Bookmark },
{ name: 'Candidates', href: '/employer/candidates', icon: UserCheck },
{ name: 'Messages', href: '/employer/messages', icon: MessageSquare, badge: unread_messages_count },
- { name: 'Announcements', href: '/employer/announcements', icon: Megaphone },
+ { name: 'Charity Events', href: '/employer/announcements', icon: Heart },
{ name: 'Subscription', href: '/employer/subscription', icon: CreditCard },
{ name: 'My Profile', href: '/employer/profile', icon: User },
];
diff --git a/resources/js/Pages/Admin/Analytics/Index.jsx b/resources/js/Pages/Admin/Analytics/Index.jsx
new file mode 100644
index 0000000..f2f2101
--- /dev/null
+++ b/resources/js/Pages/Admin/Analytics/Index.jsx
@@ -0,0 +1,323 @@
+import React, { useState } from 'react';
+import { Head } from '@inertiajs/react';
+import AdminLayout from '@/Layouts/AdminLayout';
+import {
+ BarChart3,
+ TrendingUp,
+ Users,
+ CreditCard,
+ Percent,
+ Award,
+ Sparkles,
+ Calendar,
+ Download
+} from 'lucide-react';
+import { Badge } from '@/components/ui/badge';
+
+export default function AnalyticsHub({ user_growth, revenue_breakdown, retention_rates, hiring_funnel }) {
+ const [selectedTab, setSelectedTab] = useState('Growth');
+
+ // Helper coordinates calculation for Cohort growth Area Chart (450w, 200h)
+ const getCoordinatesForArea = (key, maxVal) => {
+ const points = [];
+ const paddingLeft = 40;
+ const paddingRight = 15;
+ const paddingTop = 20;
+ const paddingBottom = 25;
+
+ const graphHeight = 200 - paddingTop - paddingBottom;
+ const graphWidth = 450 - paddingLeft - paddingRight;
+
+ user_growth.forEach((d, idx) => {
+ const val = d[key];
+ const x = paddingLeft + (idx / (user_growth.length - 1)) * graphWidth;
+ const y = paddingTop + graphHeight - (val / maxVal) * graphHeight;
+ points.push(`${x},${y}`);
+ });
+
+ // Add bottom-right and bottom-left to close the area shape
+ const startX = paddingLeft;
+ const endX = paddingLeft + graphWidth;
+ const bottomY = paddingTop + graphHeight;
+
+ return `${startX},${bottomY} ${points.join(' ')} ${endX},${bottomY}`;
+ };
+
+ const getLineCoordinates = (key, maxVal) => {
+ const points = [];
+ const paddingLeft = 40;
+ const paddingRight = 15;
+ const paddingTop = 20;
+ const paddingBottom = 25;
+
+ const graphHeight = 200 - paddingTop - paddingBottom;
+ const graphWidth = 450 - paddingLeft - paddingRight;
+
+ user_growth.forEach((d, idx) => {
+ const val = d[key];
+ const x = paddingLeft + (idx / (user_growth.length - 1)) * graphWidth;
+ const y = paddingTop + graphHeight - (val / maxVal) * graphHeight;
+ points.push(`${x},${y}`);
+ });
+ return points.join(' ');
+ };
+
+ return (
+
Explore subscription cohort retention, job conversions ratios, availability trends, and monthly billing splits.
+Employer and Worker registry trends (6 Months)
+Monthly billing splits (AED)
+Hiring Conversion stage details (This month)
+Month 1 to Month 6 user retention metrics
+Track every critical administrative event, auto-OCR verification, stripe checkout charge, and user moderation updates.
+UAE domestic workers platform is operating optimally. 12 automated verifications completed today.
++12% from last month
-- +{stats?.new_employers_this_week || 0} new this week +
+
Updated real-time
+
+
Growth curves across basic, premium & VIP plans
+Real-time availability of candidates pool
+Platform-wide worker placement workflow stats
+Latest employer subscription activations and renewals
+Latest sponsor subscription activations and renewals