announcement, employe registery

This commit is contained in:
mohanmd 2026-05-25 14:43:05 +05:30
parent 7aecb53065
commit 451c12b073
45 changed files with 8620 additions and 1562 deletions

View File

@ -0,0 +1,446 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Illuminate\Support\Facades\DB;
class AdminExtraController extends Controller
{
/**
* Safety & Moderation Dashboard
*/
public function safety()
{
$reports = [
[
'id' => 'REP-101',
'reporter' => 'Marina Cleaners (Employer)',
'reported_user' => 'Grace Omondi (Worker)',
'reason' => 'Profile details mismatch / False advertising',
'description' => 'The worker claims to have 4 years experience in childcare, but when interviewed, they stated they only did housekeeping.',
'status' => 'Pending Review',
'severity' => 'Medium',
'created_at' => '2026-05-22 14:30',
],
[
'id' => 'REP-102',
'reporter' => 'Siti Aminah (Worker)',
'reported_user' => 'Golden Hospitality (Employer)',
'reason' => 'Abusive message exchange',
'description' => 'Employer used inappropriate language during direct chat after worker declined an weekend offer.',
'status' => 'Escalated',
'severity' => 'High',
'created_at' => '2026-05-21 09:15',
],
[
'id' => 'REP-103',
'reporter' => 'System (Auto-Flag)',
'reported_user' => 'Amina Diop (Worker)',
'reason' => 'Suspicious document signature mismatch',
'description' => 'OCR matching detected that the signature and name on the Passport does not match the registration name.',
'status' => 'Under Investigation',
'severity' => 'High',
'created_at' => '2026-05-20 18:45',
],
[
'id' => 'REP-104',
'reporter' => 'Hassan Al Hosani (Employer)',
'reported_user' => 'Maria Santos (Worker)',
'reason' => 'No-show for interview',
'description' => 'Worker confirmed three consecutive video interviews but failed to join without notice.',
'status' => 'Resolved',
'severity' => 'Low',
'created_at' => '2026-05-18 11:00',
]
];
$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,
]);
}
}

View File

@ -13,15 +13,45 @@ class DashboardController extends Controller
*/ */
public function index() 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 = [ $stats = [
'total_workers' => 1420, 'total_workers' => $totalWorkers,
'pending_verifications' => 0, // No longer pending as they are auto-approved 'active_workers' => $activeWorkers,
'active_subscriptions' => 312, 'inactive_workers' => $inactiveWorkers,
'revenue_this_month_aed' => 48500, 'total_employers' => $totalSponsors, // Kept key name to avoid breaking frontend destructuring
'new_employers_this_week' => 28, '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 = [ $recentVerifications = [
[ [
'id' => 101, 'id' => 101,
@ -34,50 +64,41 @@ public function index()
[ [
'id' => 102, 'id' => 102,
'name' => 'Ahmed Mansoor', 'name' => 'Ahmed Mansoor',
'type' => 'Employer', 'type' => 'Sponsor',
'processed_at' => '4 hours ago', 'processed_at' => '4 hours ago',
'document_type' => 'Emirates ID', 'document_type' => 'Emirates ID',
'status' => 'Auto-Approved', 'status' => 'Auto-Approved',
], ],
]; ];
// 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 = [ $recentSubscriptions = [
[ [
'id' => 501, 'id' => 501,
'employer_name' => 'Hassan Al Hosani', 'employer_name' => 'Ahmed Malik',
'plan_name' => 'Premium Employer Plan', 'plan_name' => 'Premium Pass',
'amount_aed' => 499, 'amount_aed' => 499,
'subscribed_at' => 'Today, 10:30 AM', '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',
],
]; ];
}
return Inertia::render('Admin/Dashboard', [ return Inertia::render('Admin/Dashboard', [
'stats' => $stats, 'stats' => $stats,

View File

@ -0,0 +1,132 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Sponsor;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Illuminate\Support\Facades\Hash;
class SponsorController extends Controller
{
/**
* List all sponsors with filters, search, and sorting.
*/
public function index(Request $request)
{
$query = Sponsor::query();
// Server-side Search
if ($request->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.");
}
}

View File

@ -78,12 +78,71 @@ public function index()
public function toggleStatus(Request $request, $id) public function toggleStatus(Request $request, $id)
{ {
$validated = $request->validate([ $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']}."); 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'));
}
} }

View File

@ -94,11 +94,9 @@ public function login(Request $request)
public function register(Request $request) public function register(Request $request)
{ {
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
'company_name' => 'required|string|max:255',
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users,email', 'email' => 'required|string|email|max:255|unique:users,email',
'phone' => 'required|string|max:50', 'phone' => 'required|string|max:50',
'password' => 'required|string|min:8|confirmed',
]); ]);
if ($validator->fails()) { if ($validator->fails()) {
@ -110,38 +108,188 @@ public function register(Request $request)
} }
try { try {
// Generate API token $otp = '111111'; // default development OTP
$apiToken = Str::random(80); if (app()->environment('production')) {
$otp = strval(rand(100000, 999999));
}
$user = null; \Illuminate\Support\Facades\Cache::put('employer_otp_' . $request->email, [
\Illuminate\Support\Facades\DB::transaction(function () use ($request, $apiToken, &$user) { 'code' => $otp,
'expires_at' => now()->addMinutes(10)
], 600);
// Create inactive User
$user = User::create([ $user = User::create([
'name' => $request->name, 'name' => $request->name,
'email' => $request->email, 'email' => $request->email,
'password' => Hash::make($request->password), 'password' => Hash::make(Str::random(16)), // temporary password
'role' => 'employer', 'role' => 'employer',
'subscription_status' => 'active', // Auto-active 'subscription_status' => 'none',
'subscription_expires_at' => now()->addDays(30), 'subscription_expires_at' => null,
'api_token' => $apiToken,
]); ]);
// Create pending Profile
EmployerProfile::create([ EmployerProfile::create([
'user_id' => $user->id, 'user_id' => $user->id,
'company_name' => $request->company_name, 'company_name' => $request->name . ' Household',
'phone' => $request->phone, 'phone' => $request->phone,
'country' => 'United Arab Emirates', 'country' => 'United Arab Emirates',
'verification_status' => 'approved', // Auto-approved 'verification_status' => 'pending',
'language' => 'English', 'language' => 'English',
'notifications' => true, 'notifications' => true,
]); ]);
// Create active premium subscription // 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),
]);
// 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',
]);
// Insert into subscriptions table
\Illuminate\Support\Facades\DB::table('subscriptions')->insert([ \Illuminate\Support\Facades\DB::table('subscriptions')->insert([
'user_id' => $user->id, 'user_id' => $user->id,
'plan_id' => 'premium', 'plan_id' => $request->plan_id,
'amount_aed' => 199.00, 'amount_aed' => $request->amount_aed,
'starts_at' => now(), 'starts_at' => now(),
'expires_at' => now()->addDays(30), 'expires_at' => now()->addDays(30),
'paytabs_transaction_id' => $request->paytabs_transaction_id,
'status' => 'active', 'status' => 'active',
'created_at' => now(), 'created_at' => now(),
'updated_at' => now(), 'updated_at' => now(),
@ -150,21 +298,126 @@ public function register(Request $request)
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Employer registered successfully.', 'message' => 'Subscription payment processed and activated successfully.',
'data' => [ 'email' => $request->email
'employer' => $user->load('employerProfile'), ]);
'token' => $apiToken
]
], 201);
} catch (\Exception $e) { } catch (\Exception $e) {
logger()->error('Mobile Employer Registration Failure: ' . $e->getMessage());
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'An error occurred during registration. Please try again.', 'message' => 'Payment processing failed. Please try again.'
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500); ], 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,
]
]
]
]);
}
} }

View File

@ -5,13 +5,406 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Worker; use App\Models\Worker;
use App\Models\WorkerCategory; use App\Models\WorkerCategory;
use App\Models\WorkerDocument;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str; 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 class WorkerAuthController extends Controller
{ {
/** /**

View File

@ -14,11 +14,37 @@ public function index(Request $request)
$dbAnnouncements = Announcement::latest()->get(); $dbAnnouncements = Announcement::latest()->get();
$announcements = $dbAnnouncements->map(function ($ann) { $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 [ return [
'id' => $ann->id, 'id' => $ann->id,
'title' => $ann->title, 'title' => $ann->title,
'content' => $ann->body, 'content' => $content,
'audience' => in_array($ann->type, ['Shortlisted', 'Selected Candidates']) ? $ann->type : 'Selected Candidates', 'audience' => 'Charity',
'isCharity' => $isCharity,
'charityDetails' => $charityDetails,
'created_at' => $ann->created_at->diffForHumans(), 'created_at' => $ann->created_at->diffForHumans(),
]; ];
})->toArray(); })->toArray();
@ -33,15 +59,33 @@ public function store(Request $request)
$request->validate([ $request->validate([
'title' => 'required|string|max:255', 'title' => 'required|string|max:255',
'content' => 'required|string', '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([ Announcement::create([
'title' => $request->title, 'title' => $request->title,
'body' => $request->content, 'body' => $body,
'type' => $request->audience, 'type' => 'Charity',
'employer_id' => $sessId,
]); ]);
return back()->with('success', 'Announcement posted successfully.'); return back()->with('success', 'Charity Event posted successfully.');
} }
} }

View File

@ -46,19 +46,40 @@ public function index(Request $request)
} }
// Get subscription expires at date // Get subscription expires at date
$sub = $user->subscription()->where('status', 'active')->latest()->first(); $sub = \Illuminate\Support\Facades\DB::table('subscriptions')->where('user_id', $user->id)->where('status', 'active')->latest('id')->first();
$expiresAt = $sub ? $sub->expires_at : now()->addDays(24); $expiresAt = $sub ? Carbon::parse($sub->expires_at) : now()->addDays(24);
$daysRemaining = $expiresAt ? max(0, now()->diffInDays($expiresAt, false)) : 30; $daysRemaining = $expiresAt ? max(0, now()->diffInDays($expiresAt, false)) : 30;
// 1. Stats // 1. Stats
$shortlistedCount = Shortlist::where('employer_id', $user->id)->count(); $shortlistedCount = Shortlist::where('employer_id', $user->id)->count();
$messagesSent = Message::where('sender_id', $user->id)->where('sender_type', 'employer')->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 = [ $stats = [
'shortlisted_count' => $shortlistedCount, 'shortlisted_count' => $shortlistedCount,
'messages_sent' => $messagesSent, 'messages_sent' => $messagesSent,
'profile_views_given' => 45, // Static/mock analytic metric for page visits
'days_remaining' => (int)$daysRemaining, '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 // 2. Shortlisted workers
@ -103,17 +124,85 @@ public function index(Request $request)
]; ];
})->filter()->sortByDesc('timestamp')->values()->toArray(); })->filter()->sortByDesc('timestamp')->values()->toArray();
// 4. Announcements // 4. Charity Events
$dbAnnouncements = Announcement::latest()->limit(5)->get(); $dbAnnouncements = Announcement::latest()->limit(5)->get();
$announcements = $dbAnnouncements->map(function ($ann) { $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 [ return [
'id' => $ann->id, 'id' => $ann->id,
'title' => $ann->title, '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'), 'created_at' => $ann->created_at->format('M d, Y'),
]; ];
})->toArray(); })->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', [ return Inertia::render('Employer/Dashboard', [
'employer' => [ 'employer' => [
'name' => $user->name, 'name' => $user->name,
@ -125,6 +214,8 @@ public function index(Request $request)
'shortlisted_workers' => $shortlistedWorkers, 'shortlisted_workers' => $shortlistedWorkers,
'recent_messages' => array_slice($recentMessages, 0, 5), 'recent_messages' => array_slice($recentMessages, 0, 5),
'announcements' => $announcements, 'announcements' => $announcements,
'recommended_workers' => $recommendedWorkers,
'saved_searches' => $savedSearches,
]); ]);
} }
} }

View File

@ -81,7 +81,7 @@ public function register(Request $request)
{ {
// 1. Validation // 1. Validation
$request->validate([ $request->validate([
'company_name' => 'required|string|max:255', 'company_name' => 'nullable|string|max:255',
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255', 'email' => 'required|string|email|max:255',
'phone' => 'required|string|regex:/^\+?[0-9\s\-()]{7,20}$/', 'phone' => 'required|string|regex:/^\+?[0-9\s\-()]{7,20}$/',
@ -104,7 +104,7 @@ public function register(Request $request)
session([ session([
'pending_employer_registration' => [ 'pending_employer_registration' => [
'company_name' => $request->company_name, 'company_name' => $request->company_name ?: ($request->name . ' Household'),
'name' => $request->name, 'name' => $request->name,
'email' => $request->email, 'email' => $request->email,
'phone' => $request->phone, 'phone' => $request->phone,
@ -172,8 +172,8 @@ public function verifyEmail(Request $request)
]); ]);
} }
// Hash comparison (allow 000000 in local environment for automated testing) // Hash comparison (allow 000000 or 111111 in local environment for automated testing)
$isLocalDebug = app()->environment('local') && $request->otp === '000000'; $isLocalDebug = app()->environment('local') && ($request->otp === '000000' || $request->otp === '111111');
if (!$isLocalDebug && !Hash::check($request->otp, $otpSession['hash'])) { if (!$isLocalDebug && !Hash::check($request->otp, $otpSession['hash'])) {
$otpSession['attempts']++; $otpSession['attempts']++;
session(['employer_otp' => $otpSession]); session(['employer_otp' => $otpSession]);
@ -193,8 +193,8 @@ public function verifyEmail(Request $request)
// Success - mark verified // Success - mark verified
session(['employer_email_verified' => true]); session(['employer_email_verified' => true]);
return redirect()->route('employer.create-password') return redirect()->route('employer.register-payment')
->with('success', 'Email verified successfully. Please set your password.'); ->with('success', 'Email verified successfully! Please choose a subscription plan to continue.');
} }
public function resendOtp(Request $request) 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')) { if (!session()->has('pending_employer_registration') || !session('employer_email_verified')) {
return redirect()->route('employer.register') return redirect()->route('employer.register')
->with('error', 'Please complete email verification first.'); ->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'); 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.', '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') return redirect()->route('employer.register')
->with('error', 'Registration session expired. Please start over.'); ->with('error', 'Registration session expired. Please start over.');
} }
$pending = session('pending_employer_registration'); $pending = session('pending_employer_registration');
$payment = session('pending_employer_payment');
// Create user // Create user
$user = User::create([ $user = User::create([
@ -283,7 +343,7 @@ public function createPassword(Request $request)
'email' => $pending['email'], 'email' => $pending['email'],
'password' => Hash::make($request->password), 'password' => Hash::make($request->password),
'role' => 'employer', 'role' => 'employer',
'subscription_status' => 'active', // Auto-active for direct entry 'subscription_status' => 'active',
'subscription_expires_at' => now()->addDays(30), 'subscription_expires_at' => now()->addDays(30),
]); ]);
@ -298,14 +358,32 @@ public function createPassword(Request $request)
'notifications' => true, '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([ \Illuminate\Support\Facades\DB::table('subscriptions')->insert([
'user_id' => $user->id, 'user_id' => $user->id,
'plan_id' => 'premium', 'plan_id' => $payment['plan_id'],
'amount_aed' => 499.00, 'amount_aed' => $payment['amount_aed'],
'starts_at' => now(), 'starts_at' => now(),
'expires_at' => now()->addDays(30), 'expires_at' => now()->addDays(30),
'paytabs_transaction_id' => 'TXN-' . strtoupper(bin2hex(random_bytes(6))), 'paytabs_transaction_id' => $payment['paytabs_transaction_id'],
'status' => 'active', 'status' => 'active',
'created_at' => now(), 'created_at' => now(),
'updated_at' => now(), 'updated_at' => now(),
@ -330,7 +408,8 @@ public function createPassword(Request $request)
session()->forget([ session()->forget([
'pending_employer_registration', 'pending_employer_registration',
'employer_otp', 'employer_otp',
'employer_email_verified' 'employer_email_verified',
'pending_employer_payment'
]); ]);
return redirect()->route('employer.dashboard') return redirect()->route('employer.dashboard')
@ -344,6 +423,7 @@ public function logout(Request $request)
$request->session()->invalidate(); $request->session()->invalidate();
$request->session()->regenerateToken(); $request->session()->regenerateToken();
return redirect()->route('employer.login'); return redirect()->route('employer.login')
->with('success', 'Logged out successfully.');
} }
} }

View File

@ -42,41 +42,84 @@ public function index(Request $request)
$user = $this->resolveCurrentUser(); $user = $this->resolveCurrentUser();
$employerId = $user ? $user->id : 2; $employerId = $user ? $user->id : 2;
// Fetch all non-deleted workers to show their status changes (active vs Hired)
$dbWorkers = Worker::with(['category', 'skills'])->get(); $dbWorkers = Worker::with(['category', 'skills'])->get();
$workers = $dbWorkers->map(function ($w) { $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 [ return [
'id' => $w->id, 'id' => $w->id,
'name' => $w->name, 'name' => $w->name,
'nationality' => $w->nationality, 'nationality' => $w->nationality,
'category' => $w->category ? $w->category->name : 'General Helper', 'photo' => $photo,
'skills' => $w->skills->pluck('name')->toArray(), 'emirates_id_status' => $emiratesIdStatus,
'availability' => $w->availability, 'category' => $w->category ? $w->category->name : 'Domestic Worker',
'skills' => $mappedSkills,
'availability_status' => $availabilityStatus,
'visa_status' => $visaStatus,
'experience' => $w->experience, 'experience' => $w->experience,
'salary' => (int)$w->salary, 'salary' => (int)$w->salary,
'religion' => $w->religion, 'religion' => $w->religion,
'languages' => ['English', 'Arabic'], 'languages' => $langs,
'age' => $w->age, 'age' => $w->age,
'verified' => (bool)$w->verified, 'verified' => (bool)$w->verified,
'status' => $w->status, // Map worker status dynamically (e.g. active, Hired) 'preferred_job_type' => $preferredJobType,
'bio' => $w->bio, 'bio' => $w->bio,
'rating' => $rating,
'reviews_count' => $reviewsCount,
]; ];
})->toArray(); })->toArray();
// Get saved shortlist for current employer
$shortlistedIds = Shortlist::where('employer_id', $employerId)->pluck('worker_id')->toArray(); $shortlistedIds = Shortlist::where('employer_id', $employerId)->pluck('worker_id')->toArray();
// Dynamically build filter metadata from DB values
$dbCategories = WorkerCategory::pluck('name')->toArray(); $dbCategories = WorkerCategory::pluck('name')->toArray();
$dbNationalities = Worker::distinct()->pluck('nationality')->toArray(); $dbNationalities = Worker::distinct()->pluck('nationality')->toArray();
$filtersMetadata = [ $filtersMetadata = [
'categories' => array_merge(['All Categories'], $dbCategories), 'categories' => array_merge(['All Categories'], $dbCategories),
'nationalities' => array_merge(['All Nationalities'], $dbNationalities), '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'], 'experienceLevels' => ['All Experience', '1-2 Years', '3-5 Years', '5+ Years'],
'religions' => ['All Religions', 'Christian', 'Muslim', 'Hindu', 'Buddhist'], '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', [ return Inertia::render('Employer/Workers/Index', [
@ -90,24 +133,96 @@ public function show($id)
{ {
$w = Worker::with(['category', 'skills', 'documents'])->findOrFail($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 = [ $worker = [
'id' => $w->id, 'id' => $w->id,
'name' => $w->name, 'name' => $w->name,
'nationality' => $w->nationality, 'nationality' => $w->nationality,
'category' => $w->category ? $w->category->name : 'General Helper', 'photo' => $photo,
'skills' => $w->skills->pluck('name')->toArray(), 'emirates_id_status' => $emiratesIdStatus,
'availability' => $w->availability, 'category' => $w->category ? $w->category->name : 'Domestic Worker',
'skills' => $mappedSkills,
'availability_status' => $availabilityStatus,
'visa_status' => $visaStatus,
'experience' => $w->experience, 'experience' => $w->experience,
'experience_years' => 5, 'experience_years' => 5,
'salary' => (int)$w->salary, 'salary' => (int)$w->salary,
'religion' => $w->religion, 'religion' => $w->religion,
'languages' => ['English', 'Arabic'], 'languages' => $langs,
'age' => $w->age, 'age' => $w->age,
'verified' => (bool)$w->verified, 'verified' => (bool)$w->verified,
'status' => $w->status, 'preferred_job_type' => $preferredJobType,
'bio' => $w->bio, 'bio' => $w->bio,
'passport_status' => 'OCR Verified', 'rating' => $rating,
'visa_status' => 'Transferable', 'reviews_count' => $reviewsCount,
'reviews' => $reviews,
'similar_workers' => $similarWorkers,
]; ];
return Inertia::render('Employer/Workers/Show', [ 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) public function sendOffer(Request $request, $id)
{ {
$request->validate([ $request->validate([
@ -134,10 +242,8 @@ public function sendOffer(Request $request, $id)
$user = $this->resolveCurrentUser(); $user = $this->resolveCurrentUser();
$employerId = $user ? $user->id : 2; $employerId = $user ? $user->id : 2;
// Verify worker exists
$worker = Worker::findOrFail($id); $worker = Worker::findOrFail($id);
// Create job offer record
JobOffer::create([ JobOffer::create([
'employer_id' => $employerId, 'employer_id' => $employerId,
'worker_id' => $worker->id, 'worker_id' => $worker->id,

View File

@ -21,8 +21,8 @@ public function handle(Request $request, Closure $next): Response
return redirect()->route('employer.login'); return redirect()->route('employer.login');
} }
if ($subStatus === 'expired' && !$request->routeIs('employer.subscription')) { if ($subStatus !== 'active' && !$request->routeIs('employer.subscription')) {
return redirect()->route('employer.subscription')->with('error', 'Your subscription has expired.'); return redirect()->route('employer.subscription')->with('error', 'Please purchase a subscription to access the portal.');
} }
return $next($request); return $next($request);

51
app/Models/Sponsor.php Normal file
View File

@ -0,0 +1,51 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class Sponsor extends Authenticatable
{
use HasFactory, Notifiable;
protected $fillable = [
'full_name',
'email',
'mobile',
'password',
'country_code',
'subscription_status',
'subscription_plan',
'subscription_start_date',
'subscription_end_date',
'payment_status',
'is_verified',
'otp_verified_at',
'profile_image',
'address',
'city',
'nationality',
'status',
'last_login_at',
];
protected $hidden = [
'password',
];
protected $casts = [
'is_verified' => '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());
}
}

View File

@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('sponsors', function (Blueprint $table) {
$table->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');
}
};

View File

@ -84,6 +84,120 @@ public function run(): void
'updated_at' => now(), 'updated_at' => now(),
]); ]);
// Seed Sponsors (UAE Sponsor Marketplace Portal)
\Illuminate\Support\Facades\DB::table('sponsors')->insert([
[
'full_name' => 'Ahmed Malik (Al Barari Real Estate)',
'email' => 'hr@albarari.ae',
'mobile' => '+971 50 123 4567',
'password' => \Illuminate\Support\Facades\Hash::make('password'),
'country_code' => 'AE',
'subscription_status' => 'active',
'subscription_plan' => 'premium',
'subscription_start_date' => now()->subDays(15),
'subscription_end_date' => now()->addDays(350),
'payment_status' => 'paid',
'is_verified' => true,
'otp_verified_at' => now()->subDays(15),
'profile_image' => null,
'address' => 'Villa 14, Al Barari',
'city' => 'Dubai',
'nationality' => 'Emirati',
'status' => 'active',
'last_login_at' => now(),
'created_at' => now()->subDays(15),
'updated_at' => now(),
],
[
'full_name' => 'Sarah Johnson (Marina Cleaners)',
'email' => 'contact@marinaclean.com',
'mobile' => '+971 50 234 5678',
'password' => \Illuminate\Support\Facades\Hash::make('password'),
'country_code' => 'AE',
'subscription_status' => 'active',
'subscription_plan' => 'basic',
'subscription_start_date' => now()->subDays(10),
'subscription_end_date' => now()->addDays(355),
'payment_status' => 'paid',
'is_verified' => true,
'otp_verified_at' => now()->subDays(10),
'profile_image' => null,
'address' => 'Marina Heights, Floor 22',
'city' => 'Abu Dhabi',
'nationality' => 'British',
'status' => 'active',
'last_login_at' => now()->subHours(4),
'created_at' => now()->subDays(10),
'updated_at' => now(),
],
[
'full_name' => 'John Doe (Golden Hospitality)',
'email' => 'hiring@golden.hotel',
'mobile' => '+971 50 345 6789',
'password' => \Illuminate\Support\Facades\Hash::make('password'),
'country_code' => 'AE',
'subscription_status' => 'none',
'subscription_plan' => null,
'subscription_start_date' => null,
'subscription_end_date' => null,
'payment_status' => 'unpaid',
'is_verified' => false,
'otp_verified_at' => null,
'profile_image' => null,
'address' => 'Al Majaz 3, Corniche St',
'city' => 'Sharjah',
'nationality' => 'American',
'status' => 'inactive',
'last_login_at' => null,
'created_at' => now()->subDays(2),
'updated_at' => now(),
],
[
'full_name' => 'Mike Ross (Emirates Logistics)',
'email' => 'jobs@emirateslogistics.com',
'mobile' => '+971 50 456 7890',
'password' => \Illuminate\Support\Facades\Hash::make('password'),
'country_code' => 'AE',
'subscription_status' => 'active',
'subscription_plan' => 'premium',
'subscription_start_date' => now()->subDays(30),
'subscription_end_date' => now()->addDays(335),
'payment_status' => 'paid',
'is_verified' => true,
'otp_verified_at' => now()->subDays(30),
'profile_image' => null,
'address' => 'Logistics City, Zone 4',
'city' => 'Dubai',
'nationality' => 'Canadian',
'status' => 'active',
'last_login_at' => now()->subDays(1),
'created_at' => now()->subDays(30),
'updated_at' => now(),
],
[
'full_name' => 'Jane Foster (Dubai Mall Services)',
'email' => 'support@dubaimall.ae',
'mobile' => '+971 50 567 8901',
'password' => \Illuminate\Support\Facades\Hash::make('password'),
'country_code' => 'AE',
'subscription_status' => 'expired',
'subscription_plan' => 'vip',
'subscription_start_date' => now()->subDays(400),
'subscription_end_date' => now()->subDays(35),
'payment_status' => 'paid',
'is_verified' => true,
'otp_verified_at' => now()->subDays(400),
'profile_image' => null,
'address' => 'Downtown Dubai Mall Management',
'city' => 'Dubai',
'nationality' => 'Australian',
'status' => 'suspended',
'last_login_at' => now()->subDays(40),
'created_at' => now()->subDays(400),
'updated_at' => now(),
]
]);
// Seed Announcements // Seed Announcements
\Illuminate\Support\Facades\DB::table('announcements')->insert([ \Illuminate\Support\Facades\DB::table('announcements')->insert([
[ [

View File

@ -13,7 +13,7 @@ class SkillSeeder extends Seeder
public function run(): void public function run(): void
{ {
$skills = [ $skills = [
'Wiring', 'Pipe Fitting', 'Tiling', 'Driving', 'Supervision', 'Cleaning', 'Masonry', 'Plumbing' 'cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'
]; ];
foreach ($skills as $skill) { foreach ($skills as $skill) {

View File

@ -574,8 +574,8 @@
"tags": [ "tags": [
"Employer/Auth" "Employer/Auth"
], ],
"summary": "Register Employer", "summary": "Register Sponsor (Alias)",
"description": "Registers a new employer profile and triggers premium access instantly.", "description": "Legacy route matching the simplified sponsor registration (Name, Email, Phone).",
"security": [], "security": [],
"requestBody": { "requestBody": {
"required": true, "required": true,
@ -584,37 +584,25 @@
"schema": { "schema": {
"type": "object", "type": "object",
"required": [ "required": [
"company_name",
"name", "name",
"email", "email",
"phone", "phone"
"password",
"password_confirmation"
], ],
"properties": { "properties": {
"company_name": {
"type": "string",
"example": "Ahmad Tech Ltd"
},
"name": { "name": {
"type": "string", "type": "string",
"example": "Ahmad" "example": "Ahmad Bin Ahmed",
"description": "Sponsor's full name"
}, },
"email": { "email": {
"type": "string", "type": "string",
"example": "ahmad@example.com" "example": "ahmad@example.com",
"description": "Sponsor's contact email address"
}, },
"phone": { "phone": {
"type": "string", "type": "string",
"example": "+971509990001" "example": "+971509990001",
}, "description": "Sponsor's mobile phone number"
"password": {
"type": "string",
"example": "Password@123"
},
"password_confirmation": {
"type": "string",
"example": "Password@123"
} }
} }
} }
@ -623,7 +611,354 @@
}, },
"responses": { "responses": {
"201": { "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." "description": "Conversation started successfully."
} }
} }
}
}, },
"/workers/announcements": { "/workers/announcements": {
"get": { "get": {
"tags": [ "tags": [
"Worker/Announcements" "Worker/CharityEvents"
], ],
"summary": "Get Announcements (Worker)", "summary": "Get Charity Events (Worker)",
"description": "Allows workers to retrieve the list of active announcements posted by employers or the system admin.", "description": "Allows workers to retrieve the list of active charity events, food drives, and medical support programs scheduled in Dubai.",
"responses": { "responses": {
"200": { "200": {
"description": "List of announcements retrieved successfully." "description": "List of charity events retrieved successfully."
} }
} }
} }
@ -795,22 +1131,22 @@
"/employers/announcements": { "/employers/announcements": {
"get": { "get": {
"tags": [ "tags": [
"Employer/Announcements" "Employer/CharityEvents"
], ],
"summary": "Get Posted Announcements (Employer)", "summary": "Get Posted Charity Events (Employer)",
"description": "Allows employers to retrieve the list of announcements they have created.", "description": "Allows employers/sponsors to retrieve the list of charity events they have published.",
"responses": { "responses": {
"200": { "200": {
"description": "List of posted announcements retrieved successfully." "description": "List of posted charity events retrieved successfully."
} }
} }
}, },
"post": { "post": {
"tags": [ "tags": [
"Employer/Announcements" "Employer/CharityEvents"
], ],
"summary": "Create Announcement (Employer)", "summary": "Publish Charity Event (Employer)",
"description": "Allows employers to broadcast a new announcement to all workers.", "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": { "requestBody": {
"required": true, "required": true,
"content": { "content": {
@ -819,25 +1155,43 @@
"type": "object", "type": "object",
"required": [ "required": [
"title", "title",
"body" "content",
"provided_items",
"event_date",
"event_time",
"location_details",
"location_pin"
], ],
"properties": { "properties": {
"title": { "title": {
"type": "string", "type": "string",
"example": "Weekend Maintenance" "example": "Free Dental Checkup Camp"
}, },
"body": { "content": {
"type": "string", "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", "type": "string",
"enum": [ "example": "Free Dental screening, cleanings, and wellness kits"
"info", },
"warning", "event_date": {
"success" "type": "string",
], "format": "date",
"example": "warning" "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": { "responses": {
"201": { "201": {
"description": "Announcement created successfully." "description": "Charity event created and notifications scheduled successfully."
} }
} }
} }
@ -854,10 +1208,10 @@
"/employers/announcements/{id}": { "/employers/announcements/{id}": {
"delete": { "delete": {
"tags": [ "tags": [
"Employer/Announcements" "Employer/CharityEvents"
], ],
"summary": "Delete Announcement (Employer)", "summary": "Delete Charity Event (Employer)",
"description": "Allows employers to delete a previously created announcement.", "description": "Allows employers/sponsors to delete a previously published charity event.",
"parameters": [ "parameters": [
{ {
"name": "id", "name": "id",
@ -870,7 +1224,8 @@
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Announcement deleted successfully." "description": "Charity event deleted successfully."
}
} }
} }
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -11,7 +11,13 @@ import {
Menu, Menu,
Bell, Bell,
Shield, Shield,
BadgeDollarSign BadgeDollarSign,
ShieldCheck,
ShieldAlert,
Scale,
BellRing,
BarChart3,
History
} from 'lucide-react'; } from 'lucide-react';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; 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 [open, setOpen] = useState(false);
const navItems = [ const navItems = [
{ name: 'Dashboard', href: '/admin/dashboard', icon: LayoutDashboard }, { name: 'Dashboard Overview', href: '/admin/dashboard', icon: LayoutDashboard },
{ name: 'Workers', href: '/admin/workers', icon: Users }, { name: 'Worker Profiles', href: '/admin/workers', icon: Users },
{ name: 'Employers', href: '/admin/employers', icon: Briefcase }, { name: 'Sponsor Registry', href: '/admin/employers', icon: Briefcase },
{ name: 'Subscriptions', href: '/admin/subscriptions', icon: CreditCard }, { name: 'OCR Verification', href: '/admin/workers/verifications', icon: ShieldCheck },
{ name: 'Payments', href: '/admin/payments', icon: BadgeDollarSign }, { 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: '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) => { const getInitials = (name) => {

View File

@ -15,7 +15,8 @@ import {
ArrowRight, ArrowRight,
UserCheck, UserCheck,
Megaphone, Megaphone,
Briefcase Briefcase,
Heart
} from 'lucide-react'; } from 'lucide-react';
import { import {
DropdownMenu, DropdownMenu,
@ -59,11 +60,10 @@ export default function EmployerLayout({ children, title, fullPage = false }) {
const navItems = [ const navItems = [
{ name: 'Dashboard', href: '/employer/dashboard', icon: LayoutDashboard }, { name: 'Dashboard', href: '/employer/dashboard', icon: LayoutDashboard },
{ name: 'Find Workers', href: '/employer/workers', icon: Search }, { name: 'Find Workers', href: '/employer/workers', icon: Search },
{ name: 'My Jobs', href: '/employer/jobs', icon: Briefcase },
{ name: 'Shortlist', href: '/employer/shortlist', icon: Bookmark }, { name: 'Shortlist', href: '/employer/shortlist', icon: Bookmark },
{ name: 'Candidates', href: '/employer/candidates', icon: UserCheck }, { name: 'Candidates', href: '/employer/candidates', icon: UserCheck },
{ name: 'Messages', href: '/employer/messages', icon: MessageSquare, badge: unread_messages_count }, { 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: 'Subscription', href: '/employer/subscription', icon: CreditCard },
{ name: 'My Profile', href: '/employer/profile', icon: User }, { name: 'My Profile', href: '/employer/profile', icon: User },
]; ];

View File

@ -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 (
<AdminLayout title="Reports & Analytics Hub">
<Head title="System Analytics" />
<div className="font-sans max-w-7xl mx-auto space-y-8">
{/* Title and date picker */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-xl font-bold text-gray-900 tracking-tight">Interactive Platform Reports & Analytics Hub</h1>
<p className="text-xs text-gray-500 mt-0.5">Explore subscription cohort retention, job conversions ratios, availability trends, and monthly billing splits.</p>
</div>
<div className="flex items-center gap-3">
<div className="flex bg-slate-200/60 p-1 rounded-xl w-fit border border-slate-200">
{['Growth', 'Revenue', 'Funnel'].map((tab) => (
<button
key={tab}
type="button"
onClick={() => setSelectedTab(tab)}
className={`px-4 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all ${
selectedTab === tab
? 'bg-white text-[#0F6E56] shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
{tab} Report
</button>
))}
</div>
<button className="inline-flex items-center px-4 py-2.5 bg-slate-950 text-white rounded-xl text-xs font-bold hover:bg-slate-900 transition-colors shadow-sm space-x-2 uppercase tracking-wider">
<Download className="w-4 h-4" />
<span>Export PDF</span>
</button>
</div>
</div>
{/* Analytical Charts grid depending on tabs */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Growth cohort Report */}
{selectedTab === 'Growth' && (
<div className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm space-y-6">
<div>
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest flex items-center gap-1.5">
<TrendingUp className="w-4.5 h-4.5 text-[#0F6E56]" />
<span>User Cohort growth curves</span>
</h3>
<p className="text-[10px] text-slate-400 mt-0.5 uppercase tracking-wider font-bold">Employer and Worker registry trends (6 Months)</p>
</div>
<div className="relative">
<svg className="w-full h-56" viewBox="0 0 450 200">
{/* Grids */}
<line x1="40" y1="20" x2="435" y2="20" stroke="#f8fafc" strokeWidth="1.5" />
<line x1="40" y1="75" x2="435" y2="75" stroke="#f8fafc" strokeWidth="1.5" />
<line x1="40" y1="130" x2="435" y2="130" stroke="#f8fafc" strokeWidth="1.5" />
<line x1="40" y1="180" x2="435" y2="180" stroke="#cbd5e1" strokeWidth="1.5" />
{/* Worker Area fill */}
<polygon
fill="url(#workerGrad)"
points={getCoordinatesForArea('workers', 1500)}
className="opacity-45"
/>
{/* Worker Line */}
<polyline
fill="none"
stroke="#10b981"
strokeWidth="4"
points={getLineCoordinates('workers', 1500)}
/>
{/* Employer Line */}
<polyline
fill="none"
stroke="#3b82f6"
strokeWidth="3.5"
strokeDasharray="4 4"
points={getLineCoordinates('employers', 1500)}
/>
{/* Gradients */}
<defs>
<linearGradient id="workerGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#10b981" />
<stop offset="100%" stopColor="#ffffff" />
</linearGradient>
</defs>
{/* X-Axis Month labels */}
{user_growth.map((d, i) => {
const x = 40 + (i / 5) * 380;
return (
<text key={i} x={x} y="195" textAnchor="middle" className="text-[9px] font-bold fill-slate-400 font-mono">
{d.month}
</text>
);
})}
{/* Y Labels */}
<text x="30" y="24" textAnchor="end" className="text-[9px] font-black fill-slate-300">1.5K</text>
<text x="30" y="80" textAnchor="end" className="text-[9px] font-black fill-slate-300">750</text>
<text x="30" y="135" textAnchor="end" className="text-[9px] font-black fill-slate-300">300</text>
<text x="30" y="185" textAnchor="end" className="text-[9px] font-black fill-slate-300">0</text>
</svg>
<div className="flex items-center justify-center space-x-6 mt-4">
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
<div className="w-3.5 h-3.5 bg-emerald-500 rounded" />
<span>Workers ({user_growth[5].workers})</span>
</div>
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
<div className="w-3.5 h-1 border-t-2 border-dashed border-blue-500" />
<span>Employers ({user_growth[5].employers})</span>
</div>
</div>
</div>
</div>
)}
{/* Revenue reports */}
{selectedTab === 'Revenue' && (
<div className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm space-y-6">
<div>
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest flex items-center gap-1.5">
<CreditCard className="w-4.5 h-4.5 text-[#0F6E56]" />
<span>Stacked Subscription revenue splits</span>
</h3>
<p className="text-[10px] text-slate-400 mt-0.5 uppercase tracking-wider font-bold">Monthly billing splits (AED)</p>
</div>
<div className="relative">
<svg className="w-full h-56" viewBox="0 0 450 200">
<line x1="40" y1="180" x2="435" y2="180" stroke="#cbd5e1" strokeWidth="1.5" />
{/* Stacked bars for each month */}
{revenue_breakdown.map((d, i) => {
const x = 50 + i * 62;
const total = d.basic + d.premium + d.vip;
const maxVal = 80000;
const scale = 150; // Graph height scale factor
const basicH = (d.basic / maxVal) * scale;
const premiumH = (d.premium / maxVal) * scale;
const vipH = (d.vip / maxVal) * scale;
const basicY = 180 - basicH;
const premiumY = basicY - premiumH;
const vipY = premiumY - vipH;
return (
<g key={i} className="cursor-pointer group">
{/* Basic */}
<rect x={x} y={basicY} width="22" height={basicH} fill="#94a3b8" rx="2" />
{/* Premium */}
<rect x={x} y={premiumY} width="22" height={premiumH} fill="#3b82f6" />
{/* VIP */}
<rect x={x} y={vipY} width="22" height={vipH} fill="#10b981" rx="2" />
<text x={x + 11} y="195" textAnchor="middle" className="text-[9px] font-bold fill-slate-400 font-mono">
{d.month.split(' ')[0]}
</text>
</g>
);
})}
<text x="30" y="30" textAnchor="end" className="text-[9px] font-black fill-slate-300">80K</text>
<text x="30" y="105" textAnchor="end" className="text-[9px] font-black fill-slate-300">40K</text>
<text x="30" y="180" textAnchor="end" className="text-[9px] font-black fill-slate-300">0</text>
</svg>
<div className="flex items-center justify-center space-x-6 mt-4">
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
<div className="w-3 h-3 bg-slate-400 rounded-sm" />
<span>Basic</span>
</div>
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
<div className="w-3 h-3 bg-blue-500 rounded-sm" />
<span>Premium</span>
</div>
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
<div className="w-3 h-3 bg-emerald-500 rounded-sm" />
<span>VIP</span>
</div>
</div>
</div>
</div>
)}
{/* Funnel Conversions */}
{selectedTab === 'Funnel' && (
<div className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm space-y-6">
<div>
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest flex items-center gap-1.5">
<Percent className="w-4.5 h-4.5 text-[#0F6E56]" />
<span>Hiring Placement Funnel ratios</span>
</h3>
<p className="text-[10px] text-slate-400 mt-0.5 uppercase tracking-wider font-bold">Hiring Conversion stage details (This month)</p>
</div>
<div className="space-y-4">
{hiring_funnel.map((item, idx) => {
const maxVal = hiring_funnel[0].count;
const pct = ((item.count / maxVal) * 100).toFixed(1);
return (
<div key={idx} className="space-y-2 text-xs font-bold text-slate-700">
<div className="flex items-center justify-between">
<span>{item.stage}</span>
<span>{item.count.toLocaleString()} ({pct}%)</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-3.5 overflow-hidden">
<div
className="h-full rounded-full bg-gradient-to-r from-teal-600 to-teal-500 transition-all duration-500"
style={{ width: `${pct}%` }}
/>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Right side cohort Retention Analysis: Month-over-Month curves */}
<div className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm flex flex-col justify-between">
<div>
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest flex items-center gap-1.5">
<Award className="w-4.5 h-4.5 text-[#0F6E56]" />
<span>Subscription Cohort Retention rates</span>
</h3>
<p className="text-[10px] text-slate-400 mt-0.5 uppercase tracking-wider font-bold">Month 1 to Month 6 user retention metrics</p>
</div>
<div className="space-y-4 my-6">
{retention_rates.map((cohort, i) => (
<div key={i} className="flex items-center justify-between text-xs font-bold text-slate-700">
<span className="w-20">{cohort.cohort}</span>
<div className="flex-1 mx-4 bg-slate-100 rounded-full h-3 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-blue-600 to-blue-500 rounded-full"
style={{ width: `${cohort.rate}%` }}
/>
</div>
<span className="text-blue-600 w-10 text-right">{cohort.rate}%</span>
</div>
))}
</div>
<div className="bg-emerald-50/50 p-4 border border-emerald-100 rounded-2xl text-[11px] font-bold text-teal-800 leading-relaxed shadow-sm">
<Sparkles className="w-4 h-4 text-emerald-600 mb-1" />
<span>System Insights: Employer VIP and Premium tier subscribers yield a 79% retention rate beyond Month 3, validating platform organic vetting value.</span>
</div>
</div>
</div>
</div>
</AdminLayout>
);
}

View File

@ -0,0 +1,166 @@
import React, { useState } from 'react';
import { Head, router } from '@inertiajs/react';
import AdminLayout from '@/Layouts/AdminLayout';
import {
Search,
Filter,
History,
ShieldCheck,
User,
CreditCard,
Scale,
AlertTriangle,
Eye,
Globe,
Terminal,
ArrowRight
} from 'lucide-react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
export default function AuditLogsHub({ logs, search, category }) {
const [searchTerm, setSearchTerm] = useState(search || '');
const [selectedCategory, setSelectedCategory] = useState(category || 'all');
const handleSearchSubmit = (e) => {
e.preventDefault();
router.get('/admin/audit-logs', {
search: searchTerm,
category: selectedCategory
}, {
preserveState: true
});
};
const handleCategorySelect = (cat) => {
setSelectedCategory(cat);
router.get('/admin/audit-logs', {
search: searchTerm,
category: cat
});
};
return (
<AdminLayout title="System Compliance Audit Logs">
<Head title="System Audit Logs" />
<div className="font-sans max-w-7xl mx-auto space-y-6">
{/* Header text */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-xl font-bold text-gray-900 tracking-tight">Centralized Compliance & Security Audit Logs</h1>
<p className="text-xs text-gray-500 mt-0.5">Track every critical administrative event, auto-OCR verification, stripe checkout charge, and user moderation updates.</p>
</div>
</div>
{/* Filters Row */}
<div className="bg-white p-4 rounded-2xl border border-slate-200 shadow-sm flex flex-col md:flex-row gap-4 items-center justify-between">
<form onSubmit={handleSearchSubmit} className="flex flex-1 items-center space-x-3 w-full md:w-auto">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search logs by IP, actor, message or ticket ID..."
className="w-full pl-10 pr-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-medium focus:bg-white focus:ring-4 focus:ring-teal-500/10 outline-none transition-all"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<button type="submit" className="px-4 py-2.5 bg-slate-900 hover:bg-slate-800 text-white rounded-xl text-xs font-black uppercase tracking-wider">
Search Logs
</button>
</form>
{/* Category tabs */}
<div className="flex items-center gap-1 bg-slate-100 p-1 rounded-xl overflow-x-auto w-full md:w-auto">
{[
{ label: 'All Logs', val: 'all' },
{ label: 'Admin Actions', val: 'admin_action' },
{ label: 'OCR Checks', val: 'verification' },
{ label: 'Payments', val: 'payment' },
{ label: 'Disputes', val: 'dispute' },
{ label: 'User Logins', val: 'user_activity' }
].map((cat) => (
<button
key={cat.val}
onClick={() => handleCategorySelect(cat.val)}
className={`px-3 py-1.5 rounded-lg text-[9px] font-black uppercase tracking-widest whitespace-nowrap transition-all ${
selectedCategory === cat.val
? 'bg-white text-[#0F6E56] shadow-sm'
: 'text-gray-500 hover:text-gray-900'
}`}
>
{cat.label}
</button>
))}
</div>
</div>
{/* Audit Grid table */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8">Event ID / Category</TableHead>
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Actor Account</TableHead>
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Activity Description</TableHead>
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">IP Address</TableHead>
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 text-right pr-8">Timestamp</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs && logs.length > 0 ? (
logs.map((log) => (
<TableRow key={log.id} className="hover:bg-slate-50/50 transition-colors">
<TableCell className="py-4 pl-8">
<div className="flex items-center space-x-2">
<Badge className={`border-none text-[8px] font-black uppercase tracking-wider ${
log.category === 'admin_action' ? 'bg-red-50 text-red-700' :
log.category === 'verification' ? 'bg-blue-50 text-blue-700' :
log.category === 'payment' ? 'bg-emerald-50 text-emerald-700' :
log.category === 'dispute' ? 'bg-amber-50 text-amber-700' : 'bg-slate-100 text-slate-600'
}`}>{log.category.replace('_', ' ')}</Badge>
<span className="font-mono text-[9px] text-slate-400 font-bold">{log.id}</span>
</div>
</TableCell>
<TableCell className="text-xs font-bold text-slate-700">
<div className="flex items-center space-x-2">
<User className="w-3.5 h-3.5 text-slate-400" />
<span>{log.user}</span>
</div>
</TableCell>
<TableCell className="text-xs font-semibold text-slate-600 leading-normal max-w-sm">
{log.action}
</TableCell>
<TableCell className="text-xs font-mono font-medium text-slate-400">
<div className="flex items-center space-x-1">
<Globe className="w-3.5 h-3.5 text-slate-300" />
<span>{log.ip_address}</span>
</div>
</TableCell>
<TableCell className="text-xs text-slate-400 text-right pr-8 font-mono">{log.timestamp}</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="py-20 text-center">
<Terminal className="w-12 h-12 text-slate-200 mx-auto mb-3" />
<div className="font-bold text-slate-400">No security audit logs found matching your criteria.</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</AdminLayout>
);
}

View File

@ -1,13 +1,19 @@
import React from 'react'; import React, { useState } from 'react';
import { Head, Link, router } from '@inertiajs/react'; import { Head, Link, router } from '@inertiajs/react';
import AdminLayout from '@/Layouts/AdminLayout'; import AdminLayout from '@/Layouts/AdminLayout';
import { import {
Users, Users,
ShieldAlert, ShieldCheck,
CreditCard, CreditCard,
BadgeDollarSign, BadgeDollarSign,
CheckCircle, CheckCircle,
ArrowRight ArrowRight,
ArrowUpRight,
MessageSquare,
TrendingUp,
Percent,
PieChart,
Sparkles
} from 'lucide-react'; } from 'lucide-react';
import { import {
Table, Table,
@ -19,89 +25,380 @@ import {
} from '@/components/ui/table'; } from '@/components/ui/table';
export default function Dashboard({ stats, recent_verifications, recent_subscriptions }) { export default function Dashboard({ stats, recent_verifications, recent_subscriptions }) {
const handleApprove = (id) => { const [activeTrendTab, setActiveTrendTab] = useState('All');
router.post('/admin/verifications/approve', { id }); const [selectedSlice, setSelectedSlice] = useState(null);
};
const handleReject = (id) => { // Mock trend monthly data for custom SVG Line chart
router.post('/admin/verifications/reject', { id }); const trendData = [
{ month: 'Dec', basic: 110, premium: 60, vip: 15 },
{ month: 'Jan', basic: 130, premium: 70, vip: 18 },
{ month: 'Feb', basic: 145, premium: 82, vip: 22 },
{ month: 'Mar', basic: 160, premium: 88, vip: 24 },
{ month: 'Apr', basic: 172, premium: 92, vip: 28 },
{ month: 'May', basic: 184, premium: 98, vip: 30 }
];
// Compute lines points for SVG Spline
const getCoordinatesForLine = (key, height, width, maxVal) => {
const points = [];
const paddingLeft = 35;
const paddingRight = 15;
const paddingTop = 20;
const paddingBottom = 25;
const graphHeight = height - paddingTop - paddingBottom;
const graphWidth = width - paddingLeft - paddingRight;
trendData.forEach((d, idx) => {
const val = d[key];
const x = paddingLeft + (idx / (trendData.length - 1)) * graphWidth;
const y = paddingTop + graphHeight - (val / maxVal) * graphHeight;
points.push(`${x},${y}`);
});
return points.join(' ');
}; };
return ( return (
<AdminLayout title="Dashboard Overview"> <AdminLayout title="Dashboard Overview">
<Head title="Admin Dashboard" /> <Head title="Admin Dashboard" />
{/* Welcome banner */}
<div className="bg-slate-900 rounded-3xl p-6 text-white mb-8 relative overflow-hidden font-sans shadow-lg shadow-slate-900/10">
<div className="absolute top-0 right-0 w-64 h-64 bg-teal-500/10 rounded-full blur-3xl -translate-y-12 translate-x-12" />
<div className="absolute bottom-0 left-1/3 w-48 h-48 bg-teal-700/15 rounded-full blur-2xl" />
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="space-y-1">
<div className="flex items-center space-x-2 bg-teal-500/20 text-teal-300 px-3 py-1 rounded-full text-xs font-bold w-fit border border-teal-500/20">
<Sparkles className="w-3.5 h-3.5" />
<span>System Status: Online</span>
</div>
<h2 className="text-2xl font-black tracking-tight mt-2">Welcome back, Administrator</h2>
<p className="text-sm text-slate-400 font-medium">UAE domestic workers platform is operating optimally. 12 automated verifications completed today.</p>
</div>
<div className="flex items-center gap-2">
<Link
href="/admin/analytics"
className="bg-teal-600 hover:bg-teal-500 text-white font-bold text-xs uppercase tracking-wider px-5 py-3 rounded-xl transition-all shadow-md shadow-teal-600/20 flex items-center gap-1.5"
>
<TrendingUp className="w-4 h-4" />
<span>Full Analytics Hub</span>
</Link>
</div>
</div>
</div>
{/* Stat Cards Row */} {/* Stat Cards Row */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 mb-8 font-sans"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 font-sans">
{/* Total Workers */} {/* Total Workers & active/inactive */}
<div className="bg-white rounded-xl p-5 border border-gray-200 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow"> <div className="bg-white rounded-2xl p-6 border border-slate-100 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-500">Total Workers</span> <span className="text-xs font-black text-slate-400 uppercase tracking-wider">Workers Registered</span>
<div className="w-10 h-10 bg-[#0F6E56]/10 rounded-lg flex items-center justify-center shadow-inner"> <div className="w-10 h-10 bg-teal-50 rounded-xl flex items-center justify-center shadow-inner">
<Users className="w-5 h-5 text-[#0F6E56]" /> <Users className="w-5 h-5 text-[#0F6E56]" />
</div> </div>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<h3 className="text-2xl font-bold text-gray-900 tracking-tight"> <h3 className="text-3xl font-black text-slate-900 tracking-tight">
{stats?.total_workers?.toLocaleString() || 0} {stats?.total_workers?.toLocaleString() || 1420}
</h3> </h3>
<p className="text-xs text-emerald-600 font-medium mt-1">+12% from last month</p> <div className="flex items-center space-x-2 mt-2 text-xs font-bold">
<span className="text-emerald-600">{stats?.active_workers || 980} Active</span>
<span className="text-slate-300"></span>
<span className="text-slate-400">{stats?.inactive_workers || 440} Inactive</span>
</div>
</div> </div>
</div> </div>
{/* Verified Today */} {/* Verification Stats */}
<div className="bg-white rounded-xl p-5 border border-gray-200 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow"> <div className="bg-white rounded-2xl p-6 border border-slate-100 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-500">Auto-Verified Today</span> <span className="text-xs font-black text-slate-400 uppercase tracking-wider">Vetting & OCR Rate</span>
<div className="w-10 h-10 bg-emerald-50 rounded-lg flex items-center justify-center relative shadow-inner"> <div className="w-10 h-10 bg-emerald-50 rounded-lg flex items-center justify-center shadow-inner">
<CheckCircle className="w-5 h-5 text-emerald-600" /> <ShieldCheck className="w-5 h-5 text-emerald-600" />
</div>
</div>
<div className="mt-4 flex items-baseline justify-between">
<h3 className="text-2xl font-bold text-gray-900 tracking-tight">
12
</h3>
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-emerald-100 text-emerald-800">
Automatic
</span>
</div>
</div>
{/* Active Subscriptions */}
<div className="bg-white rounded-xl p-5 border border-gray-200 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-500">Active Subscriptions</span>
<div className="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center shadow-inner">
<CreditCard className="w-5 h-5 text-blue-600" />
</div> </div>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<h3 className="text-2xl font-bold text-gray-900 tracking-tight"> <h3 className="text-3xl font-black text-slate-900 tracking-tight">
{stats?.active_subscriptions?.toLocaleString() || 0} {stats?.verification_rate || 88.5}%
</h3> </h3>
<p className="text-xs text-blue-600 font-medium mt-1"> <p className="text-xs text-slate-500 font-medium mt-2 flex items-center">
+{stats?.new_employers_this_week || 0} new this week <CheckCircle className="w-3.5 h-3.5 text-emerald-500 mr-1 inline" />
<span>1,256 Profiles Auto-OCR Verified</span>
</p> </p>
</div> </div>
</div> </div>
{/* Monthly Revenue */} {/* Chat-To-Hire and Hiring Conversion */}
<div className="bg-white rounded-xl p-5 border border-gray-200 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow"> <div className="bg-white rounded-2xl p-6 border border-slate-100 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-500">Revenue This Month</span> <span className="text-xs font-black text-slate-400 uppercase tracking-wider">Hiring Conversion</span>
<div className="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center shadow-inner">
<Percent className="w-5 h-5 text-blue-600" />
</div>
</div>
<div className="mt-4">
<h3 className="text-3xl font-black text-slate-900 tracking-tight">
{stats?.hiring_conversion_rate || 14.8}%
</h3>
<div className="flex items-center space-x-2 mt-2 text-xs font-bold text-blue-600">
<MessageSquare className="w-3.5 h-3.5" />
<span>{stats?.chat_to_hire_rate || 12.6}% Chat-to-Hire</span>
</div>
</div>
</div>
{/* Subscription Revenue */}
<div className="bg-white rounded-2xl p-6 border border-slate-100 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<span className="text-xs font-black text-slate-400 uppercase tracking-wider">Monthly Subscription Revenue</span>
<div className="w-10 h-10 bg-emerald-50 rounded-lg flex items-center justify-center shadow-inner"> <div className="w-10 h-10 bg-emerald-50 rounded-lg flex items-center justify-center shadow-inner">
<BadgeDollarSign className="w-5 h-5 text-emerald-600" /> <BadgeDollarSign className="w-5 h-5 text-emerald-600" />
</div> </div>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<h3 className="text-2xl font-bold text-gray-900 tracking-tight"> <h3 className="text-3xl font-black text-slate-900 tracking-tight">
AED {stats?.revenue_this_month_aed?.toLocaleString() || 0} AED {stats?.revenue_this_month_aed?.toLocaleString() || '48,500'}
</h3> </h3>
<p className="text-xs text-emerald-600 font-medium mt-1">Updated real-time</p> <p className="text-xs text-emerald-600 font-bold mt-2 flex items-center">
<ArrowUpRight className="w-3.5 h-3.5 mr-0.5" />
<span>+{stats?.new_employers_this_week || 28} Sponsors Added This Week</span>
</p>
</div> </div>
</div> </div>
</div> </div>
{/* Required Analytics and Graphical reports */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8 font-sans">
{/* 1. Subscription Trends Spline Chart */}
<div className="bg-white rounded-3xl p-6 border border-slate-200 shadow-sm">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-base font-black text-slate-900 tracking-tight">Subscription Sign-ups Trend</h3>
<p className="text-xs text-slate-500 font-medium uppercase tracking-widest mt-1">Growth curves across basic, premium & VIP plans</p>
</div>
{/* Selector Tabs */}
<div className="flex bg-slate-100 p-1 rounded-xl">
{['All', 'Premium', 'VIP'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTrendTab(tab)}
className={`px-3 py-1 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all ${
activeTrendTab === tab ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-900'
}`}
>
{tab}
</button>
))}
</div>
</div>
{/* Custom SVG Line Chart */}
<div className="relative">
<svg className="w-full h-56" viewBox="0 0 450 200">
{/* Grid Lines */}
<line x1="35" y1="20" x2="435" y2="20" stroke="#f1f5f9" strokeWidth="1" />
<line x1="35" y1="75" x2="435" y2="75" stroke="#f1f5f9" strokeWidth="1" />
<line x1="35" y1="130" x2="435" y2="130" stroke="#f1f5f9" strokeWidth="1" />
<line x1="35" y1="180" x2="435" y2="180" stroke="#cbd5e1" strokeWidth="1" />
{/* Line 1: Basic */}
{(activeTrendTab === 'All') && (
<polyline
fill="none"
stroke="#94a3b8"
strokeWidth="3.5"
strokeLinecap="round"
strokeLinejoin="round"
points={getCoordinatesForLine('basic', 200, 450, 200)}
className="transition-all duration-500 opacity-60"
/>
)}
{/* Line 2: Premium */}
{(activeTrendTab === 'All' || activeTrendTab === 'Premium') && (
<polyline
fill="none"
stroke="#3b82f6"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
points={getCoordinatesForLine('premium', 200, 450, 200)}
className="transition-all duration-500"
/>
)}
{/* Line 3: VIP */}
{(activeTrendTab === 'All' || activeTrendTab === 'VIP') && (
<polyline
fill="none"
stroke="#10b981"
strokeWidth="4.5"
strokeLinecap="round"
strokeLinejoin="round"
points={getCoordinatesForLine('vip', 200, 450, 200)}
className="transition-all duration-500"
/>
)}
{/* X-Axis Labels */}
{trendData.map((d, i) => {
const x = 35 + (i / 5) * 385;
return (
<text key={i} x={x} y="195" textAnchor="middle" className="text-[10px] font-bold fill-slate-400 font-mono">
{d.month}
</text>
);
})}
{/* Y-Axis Labels */}
<text x="25" y="25" textAnchor="end" className="text-[10px] font-black fill-slate-300">200</text>
<text x="25" y="80" textAnchor="end" className="text-[10px] font-black fill-slate-300">100</text>
<text x="25" y="135" textAnchor="end" className="text-[10px] font-black fill-slate-300">50</text>
<text x="25" y="185" textAnchor="end" className="text-[10px] font-black fill-slate-300">0</text>
</svg>
{/* Custom Legend */}
<div className="flex items-center justify-center space-x-6 mt-4">
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
<div className="w-3 h-1 bg-slate-400 rounded" />
<span>Basic Search ({trendData[5].basic})</span>
</div>
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
<div className="w-3 h-1 bg-blue-500 rounded" />
<span>Premium Pass ({trendData[5].premium})</span>
</div>
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
<div className="w-3 h-1 bg-emerald-500 rounded" />
<span>VIP Concierge ({trendData[5].vip})</span>
</div>
</div>
</div>
</div>
{/* 2. Worker Availability Report Donut Chart */}
<div className="bg-white rounded-3xl p-6 border border-slate-200 shadow-sm flex flex-col justify-between">
<div>
<h3 className="text-base font-black text-slate-900 tracking-tight flex items-center gap-1.5">
<PieChart className="w-4 h-4 text-[#0F6E56]" />
<span>Worker Availability Status Reports</span>
</h3>
<p className="text-xs text-slate-500 font-medium uppercase tracking-widest mt-1">Real-time availability of candidates pool</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 items-center my-4">
{/* Custom Donut Chart */}
<div className="flex justify-center relative">
<svg className="w-40 h-40" viewBox="0 0 100 100">
{/* Segment 1: Available Now (640 / 1420 = 45%) -> Stroke-dasharray: 45, 55 */}
<circle
cx="50" cy="50" r="40"
fill="transparent"
stroke="#10b981"
strokeWidth="12"
strokeDasharray="45 55"
strokeDashoffset="25"
onMouseEnter={() => setSelectedSlice('Available')}
onMouseLeave={() => setSelectedSlice(null)}
className="cursor-pointer transition-all duration-300 hover:stroke-[14]"
/>
{/* Segment 2: Contracted (480 / 1420 = 34%) -> Stroke-dasharray: 34, 66 */}
<circle
cx="50" cy="50" r="40"
fill="transparent"
stroke="#94a3b8"
strokeWidth="12"
strokeDasharray="34 66"
strokeDashoffset="-20"
onMouseEnter={() => setSelectedSlice('Contracted')}
onMouseLeave={() => setSelectedSlice(null)}
className="cursor-pointer transition-all duration-300 hover:stroke-[14]"
/>
{/* Segment 3: In Interview (300 / 1420 = 21%) -> Stroke-dasharray: 21, 79 */}
<circle
cx="50" cy="50" r="40"
fill="transparent"
stroke="#3b82f6"
strokeWidth="12"
strokeDasharray="21 79"
strokeDashoffset="-54"
onMouseEnter={() => setSelectedSlice('Interviewing')}
onMouseLeave={() => setSelectedSlice(null)}
className="cursor-pointer transition-all duration-300 hover:stroke-[14]"
/>
{/* Inner Text */}
<circle cx="50" cy="50" r="28" fill="white" />
<text x="50" y="47" textAnchor="middle" className="text-[10px] font-black fill-slate-800">
{selectedSlice || 'TOTAL'}
</text>
<text x="50" y="60" textAnchor="middle" className="text-[8px] font-black fill-slate-400 tracking-wider">
{selectedSlice === 'Available' ? '640 Workers' : selectedSlice === 'Contracted' ? '480 Workers' : selectedSlice === 'Interviewing' ? '300 Workers' : '1,420 Total'}
</text>
</svg>
</div>
{/* Interactive legends */}
<div className="space-y-3">
<div className="p-3 bg-emerald-50/50 border border-emerald-100 rounded-xl flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="w-2.5 h-2.5 bg-emerald-500 rounded-full" />
<span className="text-xs font-bold text-slate-700">Available Now</span>
</div>
<span className="text-xs font-black text-slate-900">640 (45%)</span>
</div>
<div className="p-3 bg-blue-50/50 border border-blue-100 rounded-xl flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="w-2.5 h-2.5 bg-blue-500 rounded-full" />
<span className="text-xs font-bold text-slate-700">In Interview</span>
</div>
<span className="text-xs font-black text-slate-900">300 (21%)</span>
</div>
<div className="p-3 bg-slate-50 border border-slate-100 rounded-xl flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="w-2.5 h-2.5 bg-slate-400 rounded-full" />
<span className="text-xs font-bold text-slate-700">Engaged</span>
</div>
<span className="text-xs font-black text-slate-900">480 (34%)</span>
</div>
</div>
</div>
</div>
</div>
{/* 3. Hiring Funnel & Chat-To-Hire Conversion */}
<div className="bg-white rounded-3xl p-6 border border-slate-200 shadow-sm mb-8 font-sans">
<div className="mb-6">
<h3 className="text-base font-black text-slate-900 tracking-tight">Hiring Conversion & Chat-to-Hire funnel</h3>
<p className="text-xs text-slate-500 font-medium uppercase tracking-widest mt-1">Platform-wide worker placement workflow stats</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{[
{ stage: 'Profiles Browsed', val: '12,500', pct: '100%', bg: 'bg-slate-100 border-slate-200 text-slate-600' },
{ stage: 'Chats Initiated', val: '4,820', pct: '38.5%', bg: 'bg-teal-50 border-teal-100 text-teal-700' },
{ stage: 'Interviews Held', val: '1,860', pct: '14.8%', bg: 'bg-blue-50 border-blue-100 text-blue-700' },
{ stage: 'Offers Sent', val: '920', pct: '7.3%', bg: 'bg-purple-50 border-purple-100 text-purple-700' },
{ stage: 'Workers Hired', val: '610', pct: '4.8%', bg: 'bg-emerald-50 border-emerald-100 text-emerald-700' }
].map((step, i) => (
<div key={i} className={`p-4 border rounded-2xl ${step.bg} flex flex-col justify-between relative`}>
{i < 4 && (
<div className="hidden md:block absolute -right-3 top-1/2 -translate-y-1/2 z-10 w-6 h-6 bg-white border border-slate-100 rounded-full flex items-center justify-center shadow-sm">
<ArrowRight className="w-3.5 h-3.5 text-slate-400" />
</div>
)}
<div>
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400 block mb-1">Step 0{i+1}</span>
<h4 className="text-xs font-black tracking-tight">{step.stage}</h4>
</div>
<div className="mt-4">
<div className="text-xl font-black tracking-tight">{step.val}</div>
<div className="text-[10px] font-bold mt-0.5">Ratio: {step.pct}</div>
</div>
</div>
))}
</div>
</div>
{/* Recent Verifications Section */} {/* Recent Verifications Section */}
<div className="bg-white rounded-xl border border-gray-200 shadow-sm mb-8 overflow-hidden font-sans"> <div className="bg-white rounded-xl border border-gray-200 shadow-sm mb-8 overflow-hidden font-sans">
<div className="p-5 border-b border-gray-100 flex items-center justify-between bg-white"> <div className="p-5 border-b border-gray-100 flex items-center justify-between bg-white">
@ -113,7 +410,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
href="/admin/workers/verifications" href="/admin/workers/verifications"
className="text-xs font-semibold text-[#0F6E56] hover:text-[#085041] flex items-center transition-colors" className="text-xs font-semibold text-[#0F6E56] hover:text-[#085041] flex items-center transition-colors"
> >
View audit logs <ArrowRight className="w-3.5 h-3.5 ml-1" /> View OCR Vetting queue <ArrowRight className="w-3.5 h-3.5 ml-1" />
</Link> </Link>
</div> </div>
@ -165,13 +462,13 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
<div className="p-5 border-b border-gray-100 flex items-center justify-between bg-white"> <div className="p-5 border-b border-gray-100 flex items-center justify-between bg-white">
<div> <div>
<h2 className="text-base font-semibold text-gray-800 tracking-tight">Recent Subscriptions</h2> <h2 className="text-base font-semibold text-gray-800 tracking-tight">Recent Subscriptions</h2>
<p className="text-xs text-gray-500 mt-0.5">Latest employer subscription activations and renewals</p> <p className="text-xs text-gray-500 mt-0.5">Latest sponsor subscription activations and renewals</p>
</div> </div>
<Link <Link
href="/admin/subscriptions" href="/admin/payments"
className="text-xs font-semibold text-[#0F6E56] hover:text-[#085041] flex items-center transition-colors" className="text-xs font-semibold text-[#0F6E56] hover:text-[#085041] flex items-center transition-colors"
> >
View all <ArrowRight className="w-3.5 h-3.5 ml-1" /> View all payments <ArrowRight className="w-3.5 h-3.5 ml-1" />
</Link> </Link>
</div> </div>
@ -179,7 +476,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50"> <TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
<TableHead className="font-semibold text-slate-600 text-xs h-10">Employer</TableHead> <TableHead className="font-semibold text-slate-600 text-xs h-10">Sponsor</TableHead>
<TableHead className="font-semibold text-slate-600 text-xs h-10">Plan</TableHead> <TableHead className="font-semibold text-slate-600 text-xs h-10">Plan</TableHead>
<TableHead className="font-semibold text-slate-600 text-xs h-10">Amount</TableHead> <TableHead className="font-semibold text-slate-600 text-xs h-10">Amount</TableHead>
<TableHead className="font-semibold text-slate-600 text-xs h-10 text-right pr-6">Date</TableHead> <TableHead className="font-semibold text-slate-600 text-xs h-10 text-right pr-6">Date</TableHead>

View File

@ -0,0 +1,235 @@
import React, { useState } from 'react';
import { Head, router } from '@inertiajs/react';
import AdminLayout from '@/Layouts/AdminLayout';
import {
Scale,
MessageSquare,
Download,
BadgeDollarSign,
AlertCircle,
CheckCircle,
FileText,
ArrowRight,
ArrowUpRight,
User,
ShieldCheck,
Send
} from 'lucide-react';
import { Badge } from '@/components/ui/badge';
export default function DisputesHub({ tickets }) {
const [selectedTicket, setSelectedTicket] = useState(tickets[0] || null);
// Notes / Resolution states
const [internalNote, setInternalNote] = useState('');
const [resolutionAction, setResolutionAction] = useState('refund_employer');
const [resolutionSummary, setResolutionSummary] = useState('');
const handleAddNote = (e) => {
e.preventDefault();
if (!internalNote.trim()) return;
router.post(`/admin/disputes/${selectedTicket.id}/add-note`, { note: internalNote }, {
onSuccess: () => {
if (selectedTicket) {
selectedTicket.admin_notes += "\n• " + internalNote;
}
setInternalNote('');
}
});
};
const handleResolve = (e) => {
e.preventDefault();
router.post(`/admin/disputes/${selectedTicket.id}/resolve`, {
resolution: resolutionSummary,
action_taken: resolutionAction
}, {
onSuccess: () => {
if (selectedTicket) {
selectedTicket.status = 'Resolved';
}
}
});
};
return (
<AdminLayout title="Disputes Management Hub">
<Head title="Disputes Management" />
<div className="font-sans max-w-7xl mx-auto space-y-6">
{/* Header Row */}
<div>
<h1 className="text-xl font-bold text-gray-900 tracking-tight">Contract & Payment Dispute Resolution Center</h1>
<p className="text-xs text-gray-500 mt-0.5">Audit employer-worker chat history transcripts, review visa fee dispute receipts, and settle escrow refund transfers.</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-start">
{/* Column 1: Disputes List */}
<div className="space-y-4">
<div className="bg-white p-4 border border-slate-200 rounded-2xl shadow-sm">
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-3">Open Dispute Tickets</span>
<div className="space-y-2">
{tickets.map((ticket) => (
<div
key={ticket.id}
onClick={() => setSelectedTicket(ticket)}
className={`p-4 rounded-xl border cursor-pointer transition-all ${
selectedTicket?.id === ticket.id
? 'bg-emerald-50/50 border-[#0F6E56] shadow-sm'
: 'bg-white border-slate-100 hover:bg-slate-50'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="font-mono text-[9px] text-slate-400 font-bold">{ticket.id}</span>
<Badge className={`border-none text-[8px] font-black uppercase ${
ticket.status === 'Open' ? 'bg-amber-100 text-amber-700' :
ticket.status === 'Resolved' ? 'bg-emerald-100 text-emerald-800' : 'bg-blue-100 text-blue-700'
}`}>{ticket.status}</Badge>
</div>
<h4 className="text-xs font-black text-slate-900 tracking-tight leading-tight">{ticket.subject}</h4>
<div className="flex items-center justify-between mt-3 text-[10px] text-slate-400 font-bold">
<span>AED {ticket.amount_aed}</span>
<span>{ticket.created_at}</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* Column 2: Audit Logs & Chat Logs */}
<div className="lg:col-span-2 space-y-6">
{selectedTicket ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Chat Log Audit */}
<div className="bg-white border border-slate-200 rounded-3xl p-5 shadow-sm flex flex-col justify-between h-[520px]">
<div>
<h3 className="text-xs font-black text-slate-800 uppercase tracking-widest flex items-center gap-1">
<MessageSquare className="w-4 h-4 text-[#0F6E56]" />
<span>Direct Chat logs Transcript</span>
</h3>
<p className="text-[9px] text-slate-400 font-semibold mt-1">Official authenticated conversation audit log</p>
</div>
{/* Conversation thread */}
<div className="flex-1 overflow-y-auto space-y-3 my-4 pr-2 max-h-[360px]">
{selectedTicket.chat_logs.map((log, i) => {
const isEmployer = log.sender === 'Employer';
return (
<div
key={i}
className={`max-w-[85%] p-3 rounded-2xl text-[11px] font-bold shadow-sm ${
isEmployer
? 'bg-blue-50 text-blue-900 mr-auto border border-blue-100'
: 'bg-teal-50 text-teal-900 ml-auto border border-teal-100'
}`}
>
<span className="text-[8px] font-black uppercase text-slate-400 block mb-1">{log.sender} {log.time}</span>
<p className="font-semibold leading-relaxed">{log.message}</p>
</div>
);
})}
</div>
<div className="text-[10px] bg-slate-50 p-2.5 rounded-xl border border-slate-100 text-slate-400 font-semibold text-center uppercase tracking-wide">
Logs Digitally Secured by UAE Vetting Compliance
</div>
</div>
{/* Evidence & Resolution Tools */}
<div className="space-y-6">
{/* Evidence Files */}
<div className="bg-white border border-slate-200 rounded-3xl p-5 shadow-sm space-y-3">
<h3 className="text-xs font-black text-slate-800 uppercase tracking-widest flex items-center gap-1">
<FileText className="w-4 h-4 text-[#0F6E56]" />
<span>Evidence Files</span>
</h3>
{selectedTicket.evidence?.length > 0 ? (
<div className="space-y-2">
{selectedTicket.evidence.map((file, i) => (
<div key={i} className="flex items-center justify-between p-2.5 bg-slate-50 rounded-xl border text-xs font-bold text-slate-700">
<span>{file.name} ({file.type})</span>
<a href={file.url} className="text-[#0F6E56] hover:underline flex items-center gap-0.5">
<span>View File</span>
<ArrowUpRight className="w-3.5 h-3.5" />
</a>
</div>
))}
</div>
) : (
<p className="text-[10px] text-slate-400 font-semibold uppercase">No attachment proofs uploaded.</p>
)}
</div>
{/* Admin Notes */}
<div className="bg-yellow-50/50 border border-yellow-200 rounded-3xl p-5 shadow-sm space-y-3">
<h3 className="text-xs font-black text-yellow-800 uppercase tracking-widest">Internal Compliance Notes</h3>
<p className="text-[11px] text-slate-600 bg-white p-3 rounded-xl border border-yellow-100 font-semibold leading-relaxed whitespace-pre-line">
{selectedTicket.admin_notes}
</p>
<form onSubmit={handleAddNote} className="flex gap-2">
<input
type="text"
className="flex-1 bg-white border border-slate-200 rounded-xl px-3 py-2 text-xs font-bold focus:ring-2 focus:ring-yellow-500/10 outline-none"
placeholder="Write compliance log note..."
value={internalNote}
onChange={e => setInternalNote(e.target.value)}
/>
<button type="submit" className="p-2 bg-[#0F6E56] text-white rounded-xl">
<Send className="w-4.5 h-4.5" />
</button>
</form>
</div>
{/* Resolution Form */}
{selectedTicket.status !== 'Resolved' && (
<div className="bg-white border border-slate-200 rounded-3xl p-5 shadow-sm space-y-4">
<div>
<h3 className="text-xs font-black text-slate-800 uppercase tracking-widest">Settle Ticket Resolution</h3>
<p className="text-[10px] text-slate-400 font-semibold mt-1">Submit official dispute settlement outcome</p>
</div>
<form onSubmit={handleResolve} className="space-y-3 text-xs font-bold text-slate-700">
<div className="space-y-1">
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Resolution Verdict</label>
<select
className="w-full bg-slate-50 border rounded-xl px-3 py-2.5 outline-none cursor-pointer focus:bg-white"
value={resolutionAction}
onChange={e => setResolutionAction(e.target.value)}
>
<option value="refund_employer">Approve Full Refund to Employer (AED {selectedTicket.amount_aed})</option>
<option value="partial_refund">Approve 50% split partial refund</option>
<option value="payout_worker">Dismiss dispute & release payout to worker</option>
</select>
</div>
<div className="space-y-1">
<textarea
rows="2"
placeholder="Provide brief explanation for payment release/refund decisions..."
className="w-full bg-white border rounded-xl p-3 font-semibold focus:ring-2 focus:ring-[#0F6E56]/10 outline-none"
value={resolutionSummary}
onChange={e => setResolutionSummary(e.target.value)}
/>
</div>
<button type="submit" className="w-full py-2.5 bg-slate-900 hover:bg-slate-800 text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md">
Submit Official Verdict
</button>
</form>
</div>
)}
</div>
</div>
) : (
<div className="bg-white rounded-3xl border p-20 text-center text-slate-400 font-bold">
Select a dispute ticket from the left panel.
</div>
)}
</div>
</div>
</div>
</AdminLayout>
);
}

View File

@ -1,11 +1,11 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { Head } from '@inertiajs/react'; import { Head, router, Link } from '@inertiajs/react';
import AdminLayout from '@/Layouts/AdminLayout'; import AdminLayout from '@/Layouts/AdminLayout';
import { import {
Search, Search,
Filter, Filter,
Download, Download,
Briefcase, Users,
Mail, Mail,
MapPin, MapPin,
Calendar, Calendar,
@ -14,7 +14,16 @@ import {
XCircle, XCircle,
ExternalLink, ExternalLink,
MoreVertical, MoreVertical,
BadgeDollarSign BadgeDollarSign,
ShieldAlert,
TrendingUp,
AlertOctagon,
Award,
Edit2,
Trash2,
Check,
Phone,
ShieldCheck
} from 'lucide-react'; } from 'lucide-react';
import { import {
Table, Table,
@ -37,50 +46,179 @@ import {
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogFooter,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Badge } from '@/components/ui/badge';
export default function EmployersIndex({ employers: initialEmployers }) { export default function SponsorsIndex({ sponsors, filters = {} }) {
const [searchTerm, setSearchTerm] = React.useState(''); const [searchTerm, setSearchTerm] = useState(filters.search || '');
const [statusFilter, setStatusFilter] = React.useState('All'); const [statusFilter, setStatusFilter] = useState(filters.status || 'All');
const [locationFilter, setLocationFilter] = React.useState('All'); const [locationFilter, setLocationFilter] = useState(filters.location || 'All');
const [selectedEmployer, setSelectedEmployer] = React.useState(null); const [sortField, setSortField] = useState(filters.sort_field || 'created_at');
const [isDialogOpen, setIsDialogOpen] = React.useState(false); const [sortOrder, setSortOrder] = useState(filters.sort_order || 'desc');
const [employers, setEmployers] = React.useState(initialEmployers || [ const [selectedSponsor, setSelectedSponsor] = useState(null);
{ id: 1, name: 'Al Barari Real Estate', email: 'hr@albarari.ae', plan: 'Premium Pass', status: 'Active', location: 'Dubai', join_date: '2026-04-10', phone: '+971 50 123 4567', contact_person: 'Ahmed Malik', website: 'www.albarari.ae', company_id: 'TRD-991283' }, const [isDetailsOpen, setIsDetailsOpen] = useState(false);
{ id: 2, name: 'Marina Cleaners', email: 'contact@marinaclean.com', plan: 'Basic Search', status: 'Active', location: 'Abu Dhabi', join_date: '2026-04-12', phone: '+971 50 234 5678', contact_person: 'Sarah Johnson', website: 'marinaclean.com', company_id: 'TRD-102938' }, const [isEditOpen, setIsEditOpen] = useState(false);
{ id: 3, name: 'Golden Hospitality', email: 'hiring@golden.hotel', plan: 'None', status: 'Pending', location: 'Sharjah', join_date: '2026-05-01', phone: '+971 50 345 6789', contact_person: 'John Doe', website: 'golden.hotel', company_id: 'TRD-882731' }, const [editForm, setEditForm] = useState({
{ id: 4, name: 'Emirates Logistics', email: 'jobs@emirateslogistics.com', plan: 'Premium Pass', status: 'Active', location: 'Dubai', join_date: '2026-03-20', phone: '+971 50 456 7890', contact_person: 'Mike Ross', website: 'emirateslogistics.com', company_id: 'TRD-771625' }, id: '',
{ id: 5, name: 'Dubai Mall Services', email: 'support@dubaimall.ae', plan: 'VIP Concierge', status: 'Suspended', location: 'Dubai', join_date: '2026-02-15', phone: '+971 50 567 8901', contact_person: 'Jane Foster', website: 'dubaimall.ae', company_id: 'TRD-660514' }, full_name: '',
]); email: '',
mobile: '',
const filteredEmployers = React.useMemo(() => { city: '',
return employers.filter(emp => { nationality: '',
const matchesSearch = emp.name.toLowerCase().includes(searchTerm.toLowerCase()) || address: '',
emp.email.toLowerCase().includes(searchTerm.toLowerCase()); subscription_plan: 'premium',
const matchesStatus = statusFilter === 'All' || emp.status === statusFilter; subscription_status: 'active'
const matchesLocation = locationFilter === 'All' || emp.location === locationFilter;
return matchesSearch && matchesStatus && matchesLocation;
}); });
}, [employers, searchTerm, statusFilter, locationFilter]);
const handleApprove = (id) => { // Handle search/filter queries
setEmployers(employers.map(emp => const triggerSearch = (searchVal, statusVal, locationVal, sortF, sortO) => {
emp.id === id ? { ...emp, status: 'Active' } : emp router.get('/admin/employers', {
)); search: searchVal,
status: statusVal,
location: locationVal,
sort_field: sortF,
sort_order: sortO
}, {
preserveState: true,
preserveScroll: true,
replace: true
});
}; };
const handleAction = (emp) => { useEffect(() => {
setSelectedEmployer(emp); const delayDebounceFn = setTimeout(() => {
setIsDialogOpen(true); if (searchTerm !== (filters.search || '')) {
triggerSearch(searchTerm, statusFilter, locationFilter, sortField, sortOrder);
}
}, 500);
return () => clearTimeout(delayDebounceFn);
}, [searchTerm]);
const handleFilterChange = (status, location) => {
setStatusFilter(status);
setLocationFilter(location);
triggerSearch(searchTerm, status, location, sortField, sortOrder);
};
const handleSort = (field) => {
const order = sortField === field && sortOrder === 'desc' ? 'asc' : 'desc';
setSortField(field);
setSortOrder(order);
triggerSearch(searchTerm, statusFilter, locationFilter, field, order);
};
const handleApprove = (id) => {
router.post(`/admin/employers/${id}/verify`, {}, {
onSuccess: () => {
setIsDetailsOpen(false);
}
});
};
const handleSuspend = (id) => {
router.post(`/admin/employers/${id}/suspend`, {}, {
onSuccess: () => {
setIsDetailsOpen(false);
}
});
};
const handleActivate = (id) => {
router.post(`/admin/employers/${id}/activate`, {}, {
onSuccess: () => {
setIsDetailsOpen(false);
}
});
};
const handleDelete = (id) => {
if (confirm('Are you absolutely sure you want to permanently delete this sponsor registry?')) {
router.delete(`/admin/employers/${id}`);
}
};
const handleEditClick = (sponsor) => {
setEditForm({
id: sponsor.id,
full_name: sponsor.full_name,
email: sponsor.email,
mobile: sponsor.mobile,
city: sponsor.city || 'Dubai',
nationality: sponsor.nationality || '',
address: sponsor.address || '',
subscription_plan: sponsor.subscription_plan || 'premium',
subscription_status: sponsor.subscription_status || 'active'
});
setIsEditOpen(true);
};
const handleEditSubmit = (e) => {
e.preventDefault();
router.post(`/admin/employers/${editForm.id}/update`, editForm, {
onSuccess: () => {
setIsEditOpen(false);
}
});
};
const viewDetails = (sponsor) => {
setSelectedSponsor(sponsor);
setIsDetailsOpen(true);
}; };
return ( return (
<AdminLayout title="Employer Management"> <AdminLayout title="Sponsor Registry & Moderation">
<Head title="Employers" /> <Head title="Sponsor Management" />
<div className="space-y-6 font-sans">
{/* Statistics Ribbons */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-5">
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center justify-between">
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Total Sponsors</span>
<h3 className="text-2xl font-black text-slate-900">{sponsors.total || 0}</h3>
</div>
<div className="p-3 bg-teal-50 rounded-xl">
<Users className="w-6 h-6 text-teal-600" />
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center justify-between">
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Active Passes</span>
<h3 className="text-2xl font-black text-blue-600">
{sponsors.data.filter(s => s.subscription_status === 'active').length}
</h3>
</div>
<div className="p-3 bg-blue-50 rounded-xl">
<BadgeDollarSign className="w-6 h-6 text-blue-600" />
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center justify-between">
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Pending Vetting</span>
<h3 className="text-2xl font-black text-amber-500">
{sponsors.data.filter(s => !s.is_verified).length}
</h3>
</div>
<div className="p-3 bg-amber-50 rounded-xl">
<Clock className="w-6 h-6 text-amber-500" />
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center justify-between">
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block mb-1">Suspended Registry</span>
<h3 className="text-2xl font-black text-rose-600">
{sponsors.data.filter(s => s.status === 'suspended').length}
</h3>
</div>
<div className="p-3 bg-rose-50 rounded-xl">
<ShieldAlert className="w-6 h-6 text-rose-600" />
</div>
</div>
</div>
<div className="space-y-6">
{/* Search and Filters */} {/* Search and Filters */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4"> <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div className="flex flex-1 items-center space-x-3"> <div className="flex flex-1 items-center space-x-3">
@ -88,8 +226,8 @@ export default function EmployersIndex({ employers: initialEmployers }) {
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input <input
type="text" type="text"
placeholder="Search by name or email..." placeholder="Search by name, email, or mobile..."
className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-xl text-sm font-medium focus:ring-4 focus:ring-teal-500/10 outline-none transition-all" className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm font-medium focus:ring-4 focus:ring-teal-500/10 outline-none transition-all"
value={searchTerm} value={searchTerm}
onChange={e => setSearchTerm(e.target.value)} onChange={e => setSearchTerm(e.target.value)}
/> />
@ -97,20 +235,20 @@ export default function EmployersIndex({ employers: initialEmployers }) {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<select <select
className="px-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-600 outline-none focus:ring-2 focus:ring-teal-500/10 transition-all" className="px-4 py-2.5 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-600 outline-none focus:ring-2 focus:ring-teal-500/10 transition-all cursor-pointer"
value={statusFilter} value={statusFilter}
onChange={e => setStatusFilter(e.target.value)} onChange={e => handleFilterChange(e.target.value, locationFilter)}
> >
<option value="All">All Status</option> <option value="All">All Statuses</option>
<option value="Active">Active</option> <option value="Active">Active</option>
<option value="Pending">Pending</option> <option value="Inactive">Inactive</option>
<option value="Suspended">Suspended</option> <option value="Suspended">Suspended</option>
</select> </select>
<select <select
className="px-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-600 outline-none focus:ring-2 focus:ring-teal-500/10 transition-all" className="px-4 py-2.5 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-600 outline-none focus:ring-2 focus:ring-teal-500/10 transition-all cursor-pointer"
value={locationFilter} value={locationFilter}
onChange={e => setLocationFilter(e.target.value)} onChange={e => handleFilterChange(statusFilter, e.target.value)}
> >
<option value="All">All Locations</option> <option value="All">All Locations</option>
<option value="Dubai">Dubai</option> <option value="Dubai">Dubai</option>
@ -119,71 +257,100 @@ export default function EmployersIndex({ employers: initialEmployers }) {
</select> </select>
</div> </div>
</div> </div>
<div className="flex items-center space-x-3">
<button className="flex items-center space-x-2 px-6 py-2 bg-[#0F6E56] text-white rounded-xl text-sm font-bold shadow-lg shadow-teal-500/10 hover:bg-[#085041] transition-all">
<Download className="w-4 h-4" />
<span>Export CSV</span>
</button>
</div>
</div> </div>
{/* Employers Table */} {/* Dynamic Sponsors Table */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden"> <div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50"> <TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8">Employer Info</TableHead> <TableHead onClick={() => handleSort('full_name')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8 cursor-pointer select-none">
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Location</TableHead> Sponsor Info {sortField === 'full_name' && (sortOrder === 'desc' ? '↓' : '↑')}
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Plan</TableHead> </TableHead>
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Reg. Date</TableHead> <TableHead onClick={() => handleSort('city')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Status</TableHead> Location {sortField === 'city' && (sortOrder === 'desc' ? '↓' : '↑')}
</TableHead>
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Subscription Plan</TableHead>
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Verification Status</TableHead>
<TableHead onClick={() => handleSort('status')} className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 cursor-pointer select-none">
Status {sortField === 'status' && (sortOrder === 'desc' ? '↓' : '↑')}
</TableHead>
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 text-right pr-8">Action</TableHead> <TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 text-right pr-8">Action</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredEmployers.map((emp) => ( {sponsors.data.length > 0 ? (
<TableRow key={emp.id} className="hover:bg-slate-50/50 transition-colors"> sponsors.data.map((sponsor) => (
<TableRow key={sponsor.id} className="hover:bg-slate-50/50 transition-colors">
<TableCell className="py-5 pl-8"> <TableCell className="py-5 pl-8">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="w-10 h-10 rounded-xl bg-slate-50 flex items-center justify-center border border-slate-100"> <div className="w-10 h-10 rounded-xl bg-[#0F6E56]/10 text-[#0F6E56] flex items-center justify-center border border-[#0F6E56]/20 font-bold text-sm">
<Briefcase className="w-5 h-5 text-slate-400" /> {sponsor.full_name.charAt(0)}
</div> </div>
<div> <div>
<h4 className="text-sm font-bold text-slate-900 leading-none mb-1">{emp.name}</h4> <h4 className="text-sm font-bold text-slate-900 leading-none mb-1">{sponsor.full_name}</h4>
<p className="text-xs text-slate-500 font-medium">{emp.email}</p> <p className="text-xs text-slate-500 font-medium">{sponsor.email}</p>
<p className="text-[10px] text-slate-400 font-medium">{sponsor.mobile}</p>
</div> </div>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center space-x-2 text-slate-600"> <div className="flex items-center space-x-2 text-slate-600">
<MapPin className="w-3.5 h-3.5 text-slate-400" /> <MapPin className="w-3.5 h-3.5 text-slate-400" />
<span className="text-xs font-bold">{emp.location}</span> <span className="text-xs font-bold">{sponsor.city || 'Dubai'}</span>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className={`px-2.5 py-1 rounded-lg text-[10px] font-black uppercase tracking-tight ${ <div className="flex flex-col space-y-1">
emp.plan === 'None' ? 'bg-slate-100 text-slate-500' : 'bg-blue-50 text-blue-600' <span className={`px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-tight w-max ${
sponsor.subscription_plan ? 'bg-blue-50 text-blue-600 border border-blue-100' : 'bg-slate-100 text-slate-500'
}`}> }`}>
{emp.plan} {sponsor.subscription_plan ? `${sponsor.subscription_plan} Pass` : 'No Plan'}
</span> </span>
<span className={`text-[9px] font-bold ${
sponsor.subscription_status === 'active' ? 'text-emerald-600' : 'text-slate-400'
}`}>
Status: {sponsor.subscription_status.toUpperCase()}
</span>
</div>
</TableCell> </TableCell>
<TableCell className="text-xs font-bold text-slate-400"> <TableCell>
{emp.join_date} <div className="flex items-center space-x-1.5">
{sponsor.is_verified ? (
<>
<ShieldCheck className="w-4 h-4 text-emerald-500" />
<span className="text-xs font-bold text-emerald-600">OTP Verified</span>
</>
) : (
<>
<Clock className="w-4 h-4 text-amber-500" />
<span className="text-xs font-bold text-amber-600">Unverified</span>
</>
)}
</div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{emp.status === 'Active' && <CheckCircle className="w-4 h-4 text-emerald-500" />} {sponsor.status === 'active' && <CheckCircle className="w-4 h-4 text-emerald-500" />}
{emp.status === 'Pending' && <Clock className="w-4 h-4 text-amber-500" />} {sponsor.status === 'inactive' && <Clock className="w-4 h-4 text-amber-500" />}
{emp.status === 'Suspended' && <XCircle className="w-4 h-4 text-rose-500" />} {sponsor.status === 'suspended' && <XCircle className="w-4 h-4 text-rose-500" />}
<span className={`text-[10px] font-black uppercase tracking-widest ${ <span className={`text-[10px] font-black uppercase tracking-widest ${
emp.status === 'Active' ? 'text-emerald-600' : sponsor.status === 'active' ? 'text-emerald-600' :
emp.status === 'Pending' ? 'text-amber-600' : 'text-rose-600' sponsor.status === 'inactive' ? 'text-amber-600' : 'text-rose-600'
}`}> }`}>
{emp.status} {sponsor.status}
</span> </span>
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-right pr-8"> <TableCell className="text-right pr-8">
<div className="flex items-center justify-end space-x-1">
<button
onClick={() => viewDetails(sponsor)}
className="p-2 hover:bg-slate-100 rounded-lg text-[#0F6E56] font-bold"
title="View Details"
>
<ExternalLink className="w-4.5 h-4.5" />
</button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button className="p-2 text-slate-400 hover:text-slate-900 hover:bg-slate-50 rounded-lg transition-all outline-none"> <button className="p-2 text-slate-400 hover:text-slate-900 hover:bg-slate-50 rounded-lg transition-all outline-none">
@ -191,120 +358,355 @@ export default function EmployersIndex({ employers: initialEmployers }) {
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 p-2 rounded-xl"> <DropdownMenuContent align="end" className="w-48 p-2 rounded-xl">
<DropdownMenuLabel className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-2 py-2">Actions</DropdownMenuLabel> <DropdownMenuLabel className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-2 py-2">Quick Actions</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem className="p-3 rounded-lg focus:bg-slate-50 cursor-pointer" onClick={() => handleAction(emp)}> <DropdownMenuItem
className="p-3 rounded-lg focus:bg-blue-50 cursor-pointer text-blue-600 font-bold"
onClick={() => handleEditClick(sponsor)}
>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<ExternalLink className="w-4 h-4 text-slate-500" /> <Edit2 className="w-4 h-4" />
<span className="text-xs font-bold">View Details</span> <span className="text-xs font-bold">Edit Registry</span>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
{emp.status === 'Pending' && ( {!sponsor.is_verified && (
<DropdownMenuItem className="p-3 rounded-lg focus:bg-emerald-50 cursor-pointer text-emerald-600" onClick={() => handleApprove(emp.id)}> <DropdownMenuItem className="p-3 rounded-lg focus:bg-emerald-50 cursor-pointer text-emerald-600 font-bold" onClick={() => handleApprove(sponsor.id)}>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<CheckCircle className="w-4 h-4" /> <CheckCircle className="w-4 h-4" />
<span className="text-xs font-bold">Approve Employer</span> <span className="text-xs font-bold">Approve / Verify</span>
</div>
</DropdownMenuItem>
)}
{sponsor.status === 'active' ? (
<DropdownMenuItem
onClick={() => handleSuspend(sponsor.id)}
className="p-3 rounded-lg focus:bg-rose-50 cursor-pointer text-rose-600 font-bold"
>
<div className="flex items-center space-x-3">
<XCircle className="w-4 h-4" />
<span className="text-xs font-bold">Suspend Sponsor</span>
</div>
</DropdownMenuItem>
) : (
<DropdownMenuItem
onClick={() => handleActivate(sponsor.id)}
className="p-3 rounded-lg focus:bg-emerald-50 cursor-pointer text-emerald-600 font-bold"
>
<div className="flex items-center space-x-3">
<CheckCircle className="w-4 h-4" />
<span className="text-xs font-bold">Activate Sponsor</span>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
)} )}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem className="p-3 rounded-lg focus:bg-rose-50 cursor-pointer text-rose-600"> <DropdownMenuItem
onClick={() => handleDelete(sponsor.id)}
className="p-3 rounded-lg focus:bg-red-50 cursor-pointer text-red-600 font-bold animate-pulse"
>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<XCircle className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
<span className="text-xs font-bold">Suspend Account</span> <span className="text-xs font-bold">Delete Sponsor</span>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))
) : (
<TableRow>
<TableCell colSpan={6} className="py-12 text-center text-slate-400 font-semibold text-sm">
No registered sponsors match the active search/filters criteria.
</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
{/* Pagination */}
{sponsors.last_page > 1 && (
<div className="p-5 border-t border-slate-100 flex items-center justify-between bg-slate-50/50">
<span className="text-xs text-slate-500 font-bold">
Showing page {sponsors.current_page} of {sponsors.last_page}
</span>
<div className="flex items-center space-x-2">
<Link
href={sponsors.prev_page_url || '#'}
only={['sponsors']}
preserveScroll
className={`px-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-700 shadow-xs hover:bg-slate-50 transition-colors ${
!sponsors.prev_page_url && 'opacity-50 pointer-events-none'
}`}
>
Previous
</Link>
<Link
href={sponsors.next_page_url || '#'}
only={['sponsors']}
preserveScroll
className={`px-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-700 shadow-xs hover:bg-slate-50 transition-colors ${
!sponsors.next_page_url && 'opacity-50 pointer-events-none'
}`}
>
Next
</Link>
</div>
</div>
)}
</div> </div>
</div> </div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> {/* Advanced Sponsor Detail Modal */}
<DialogContent className="max-w-2xl bg-white p-0 overflow-hidden rounded-[32px] border-none shadow-2xl"> <Dialog open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
<div className="bg-[#0F6E56] p-8 text-white relative"> <DialogContent className="sm:max-w-4xl w-full bg-white p-0 overflow-hidden rounded-[24px] border-none shadow-2xl font-sans max-h-[85vh] flex flex-col">
<div className="absolute top-0 right-0 p-8 opacity-10"> <div className="bg-gradient-to-r from-[#0F6E56] to-[#085041] p-6 text-white relative flex-shrink-0">
<Briefcase className="w-32 h-32" />
</div>
<div className="relative z-10 flex items-center space-x-4"> <div className="relative z-10 flex items-center space-x-4">
<div className="w-16 h-16 rounded-2xl bg-white/20 backdrop-blur-md flex items-center justify-center border border-white/20"> <div className="w-14 h-14 rounded-2xl bg-white/10 backdrop-blur-md flex items-center justify-center border border-white/20 font-black text-xl">
<Briefcase className="w-8 h-8 text-white" /> {selectedSponsor?.full_name.charAt(0)}
</div> </div>
<div> <div>
<h2 className="text-2xl font-black tracking-tight">{selectedEmployer?.name}</h2> <h2 className="text-2xl font-black tracking-tight">{selectedSponsor?.full_name}</h2>
<p className="text-teal-100 text-sm font-medium">{selectedEmployer?.location} Registered {selectedEmployer?.join_date}</p> <p className="text-teal-100/80 text-xs font-semibold mt-1">📍 {selectedSponsor?.city || 'Dubai'} Registered {new Date(selectedSponsor?.created_at).toLocaleDateString()}</p>
</div> </div>
</div> </div>
</div> </div>
<div className="p-8 space-y-8"> <div className="p-6 space-y-6 overflow-y-auto flex-1 bg-slate-50/50">
<div className="grid grid-cols-2 gap-8"> {/* Vetting info */}
<div className="space-y-4"> <div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-4">
<div className="space-y-1"> <div className="flex items-center justify-between border-b border-slate-100 pb-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Primary Email</label> <h3 className="text-xs font-black text-slate-700 uppercase tracking-widest flex items-center gap-1.5">
<div className="flex items-center space-x-2 text-sm font-bold text-slate-900"> <Award className="w-4.5 h-4.5 text-[#0F6E56]" />
<Mail className="w-4 h-4 text-teal-600" /> <span>UAE Sponsor Vetting Dossier</span>
<span>{selectedEmployer?.email}</span> </h3>
<Badge className="bg-teal-50 text-[#0F6E56] border-none font-black text-[9px] uppercase px-3 py-1 rounded-lg">
Sponsor ID: SPN-{selectedSponsor?.id}
</Badge>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="text-xs font-bold text-slate-600 space-y-3">
<div className="flex items-center gap-2 text-slate-800">
<Mail className="w-4 h-4 text-slate-400" />
<span>Email: {selectedSponsor?.email}</span>
</div> </div>
<div className="space-y-1"> <div className="flex items-center gap-2 text-slate-800">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Phone Number</label> <Phone className="w-4 h-4 text-slate-400" />
<div className="flex items-center space-x-2 text-sm font-bold text-slate-900"> <span>Mobile: {selectedSponsor?.mobile}</span>
<Clock className="w-4 h-4 text-teal-600" />
<span>{selectedEmployer?.phone}</span>
</div> </div>
{selectedSponsor?.address && (
<div className="flex items-center gap-2 text-slate-800">
<MapPin className="w-4 h-4 text-slate-400" />
<span>Address: {selectedSponsor?.address}</span>
</div> </div>
)}
</div> </div>
<div className="space-y-4"> <div className="text-xs font-bold text-slate-600 space-y-3">
<div className="space-y-1"> <div>OTP Verification: {selectedSponsor?.is_verified ? 'Passed' : 'Pending'}</div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Contact Person</label> {selectedSponsor?.otp_verified_at && (
<div className="text-sm font-bold text-slate-900">{selectedEmployer?.contact_person}</div> <div>Verified At: {new Date(selectedSponsor.otp_verified_at).toLocaleString()}</div>
</div> )}
<div className="space-y-1"> <div>Nationality: {selectedSponsor?.nationality || 'N/A'}</div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Company ID</label>
<div className="text-sm font-bold text-slate-900">{selectedEmployer?.company_id}</div>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-slate-50 rounded-2xl p-6 border border-slate-100 flex items-center justify-between"> {/* Subscription monitoring */}
<div className="space-y-1"> <div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-3">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Subscription Plan</label> <h3 className="text-xs font-black text-slate-700 uppercase tracking-widest flex items-center gap-1.5 border-b border-slate-100 pb-2">
<BadgeDollarSign className="w-4.5 h-4.5 text-blue-600" />
<span>Premium Subscription Status</span>
</h3>
<div className="grid grid-cols-2 gap-4 text-xs font-bold text-slate-700">
<div>
<span className="text-[9px] text-slate-400 uppercase tracking-widest block font-black mb-1">Active Plan</span>
<span className="text-sm font-black text-blue-700 bg-blue-50 px-2.5 py-1 rounded-lg border border-blue-100/50 w-fit block capitalize">
{selectedSponsor?.subscription_plan || 'No Active Plan'}
</span>
</div>
<div>
<span className="text-[9px] text-slate-400 uppercase tracking-widest block font-black mb-1">Payment Status</span>
<span className="inline-flex items-center text-slate-800 bg-slate-100 px-2.5 py-1 rounded-lg border border-slate-200/50 capitalize">
{selectedSponsor?.payment_status}
</span>
</div>
{selectedSponsor?.subscription_start_date && (
<div>
<span className="text-[9px] text-slate-400 uppercase tracking-widest block font-black mb-1">Start Date</span>
<span className="font-mono text-slate-600">{new Date(selectedSponsor.subscription_start_date).toLocaleDateString()}</span>
</div>
)}
{selectedSponsor?.subscription_end_date && (
<div>
<span className="text-[9px] text-slate-400 uppercase tracking-widest block font-black mb-1">Expiry Date</span>
<span className="font-mono text-slate-600">{new Date(selectedSponsor.subscription_end_date).toLocaleDateString()}</span>
</div>
)}
</div>
</div>
</div>
<div className="p-6 border-t border-slate-100 bg-slate-50 flex items-center justify-between flex-shrink-0">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<BadgeDollarSign className="w-4 h-4 text-blue-600" /> <span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Sponsor Verification Action:</span>
<span className="text-sm font-black text-blue-600">{selectedEmployer?.plan}</span> {!selectedSponsor?.is_verified ? (
</div> <button
</div> onClick={() => handleApprove(selectedSponsor.id)}
<div className="text-right"> className="px-4 py-2.5 bg-emerald-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md shadow-emerald-600/10 hover:bg-emerald-500 transition-colors"
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Current Status</label> >
<div className={`text-xs font-black uppercase tracking-widest ${ Verify Sponsor OTP
selectedEmployer?.status === 'Active' ? 'text-emerald-600' : 'text-amber-600' </button>
}`}> ) : (
{selectedEmployer?.status} <Badge className="bg-emerald-100 text-emerald-800 border-none font-black uppercase text-[9px] px-3 py-1.5 rounded-lg shadow-sm">
</div> OTP Verified
</div> </Badge>
)}
</div> </div>
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-slate-100"> <div className="flex items-center space-x-3">
{selectedEmployer?.status === 'Pending' && ( {selectedSponsor?.status !== 'suspended' ? (
<button <button
onClick={() => { handleApprove(selectedEmployer.id); setIsDialogOpen(false); }} onClick={() => handleSuspend(selectedSponsor.id)}
className="px-6 py-2.5 bg-[#0F6E56] text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-lg shadow-teal-500/10 hover:bg-[#085041] transition-all" className="px-4 py-2.5 bg-red-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md shadow-red-600/10 hover:bg-red-700 transition-all"
> >
Approve Employer Suspend Sponsor
</button>
) : (
<button
onClick={() => handleActivate(selectedSponsor.id)}
className="px-4 py-2.5 bg-slate-900 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-slate-800 transition-colors"
>
Activate Registry
</button> </button>
)} )}
<button className="px-6 py-2.5 bg-slate-900 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-slate-800 transition-all"> <button
Send Message onClick={() => setIsDetailsOpen(false)}
className="px-5 py-2.5 bg-slate-200 text-slate-700 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-slate-300 transition-colors"
>
Close Details
</button> </button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Sponsor Edit Dialog */}
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
<DialogContent className="sm:max-w-xl w-full bg-white p-6 rounded-[24px] font-sans">
<DialogHeader>
<DialogTitle className="text-lg font-black text-slate-800 uppercase tracking-wide">Edit Sponsor Registry Details</DialogTitle>
</DialogHeader>
<form onSubmit={handleEditSubmit} className="space-y-4 mt-4">
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Full Name</label>
<input
type="text"
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
value={editForm.full_name}
onChange={e => setEditForm({ ...editForm, full_name: e.target.value })}
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Email Address</label>
<input
type="email"
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
value={editForm.email}
onChange={e => setEditForm({ ...editForm, email: e.target.value })}
required
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Mobile Number</label>
<input
type="text"
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
value={editForm.mobile}
onChange={e => setEditForm({ ...editForm, mobile: e.target.value })}
required
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">City</label>
<select
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
value={editForm.city}
onChange={e => setEditForm({ ...editForm, city: e.target.value })}
>
<option value="Dubai">Dubai</option>
<option value="Abu Dhabi">Abu Dhabi</option>
<option value="Sharjah">Sharjah</option>
</select>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Nationality</label>
<input
type="text"
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
value={editForm.nationality}
onChange={e => setEditForm({ ...editForm, nationality: e.target.value })}
/>
</div>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Address</label>
<input
type="text"
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
value={editForm.address}
onChange={e => setEditForm({ ...editForm, address: e.target.value })}
/>
</div>
<div className="grid grid-cols-2 gap-4 border-t border-slate-100 pt-3">
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Subscription Plan</label>
<select
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
value={editForm.subscription_plan}
onChange={e => setEditForm({ ...editForm, subscription_plan: e.target.value })}
>
<option value="basic">Basic Search</option>
<option value="premium">Premium Pass</option>
<option value="vip">VIP Concierge</option>
<option value="">No Active Plan</option>
</select>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Subscription Status</label>
<select
className="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm font-semibold focus:ring-4 focus:ring-teal-500/10 outline-none"
value={editForm.subscription_status}
onChange={e => setEditForm({ ...editForm, subscription_status: e.target.value })}
>
<option value="active">Active</option>
<option value="expired">Expired</option>
<option value="none">None</option>
</select>
</div>
</div>
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-slate-100">
<button
type="button"
onClick={() => setIsEditOpen(false)}
className="px-4 py-2 bg-slate-100 text-slate-600 rounded-xl text-xs font-bold hover:bg-slate-200"
>
Cancel
</button>
<button
type="submit"
className="px-5 py-2.5 bg-[#0F6E56] text-white rounded-xl text-xs font-black uppercase tracking-wider"
>
Save Registry
</button>
</div>
</form>
</DialogContent>
</Dialog>
</AdminLayout> </AdminLayout>
); );
} }

View File

@ -0,0 +1,213 @@
import React, { useState } from 'react';
import { Head, router } from '@inertiajs/react';
import AdminLayout from '@/Layouts/AdminLayout';
import {
BellRing,
Send,
Smartphone,
MessageCircle,
CheckCircle,
BarChart3,
Users,
AlertTriangle,
History,
Sparkles,
TrendingUp
} from 'lucide-react';
import { Badge } from '@/components/ui/badge';
export default function NotificationsCampaigns({ campaigns, whatsapp_templates }) {
const [channel, setChannel] = useState('push');
const [recipients, setRecipients] = useState('All Active Workers');
const [title, setTitle] = useState('');
const [message, setMessage] = useState('');
const handleBroadcast = (e) => {
e.preventDefault();
router.post('/admin/notifications/broadcast', {
channel,
recipients,
title,
message
}, {
onSuccess: () => {
setTitle('');
setMessage('');
}
});
};
return (
<AdminLayout title="Campaigns & Notifications Hub">
<Head title="Campaigns & Alerts" />
<div className="font-sans max-w-7xl mx-auto space-y-8">
{/* Header overview and status */}
<div>
<h1 className="text-xl font-bold text-gray-900 tracking-tight">System Campaign Broadcasting Center</h1>
<p className="text-xs text-gray-500 mt-0.5">Reach users directly via SMS, WhatsApp, and Native Push notifications. Monitor open rates and clicks.</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-start">
{/* Column 1 & 2: Broadcast Composer & Campaigns History */}
<div className="lg:col-span-2 space-y-8">
{/* Section 1: Composer */}
<div className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm">
<div className="mb-6 flex items-center justify-between">
<div>
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest flex items-center gap-1">
<Sparkles className="w-4 h-4 text-[#0F6E56]" />
<span>Direct Campaign Composer</span>
</h3>
<p className="text-[10px] text-slate-400 font-semibold mt-1">Schedules immediate or recurring broadcasts</p>
</div>
<Badge className="bg-emerald-50 text-emerald-700 border-none font-bold uppercase text-[8px] tracking-wider">SMS Gateway: Active</Badge>
</div>
<form onSubmit={handleBroadcast} className="space-y-4 text-xs font-bold text-slate-700">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Select Channel</label>
<select
className="w-full bg-slate-50 border rounded-xl px-3 py-2.5 outline-none cursor-pointer focus:bg-white"
value={channel}
onChange={e => setChannel(e.target.value)}
>
<option value="push">Native App Push Notification</option>
<option value="whatsapp">WhatsApp Business API</option>
<option value="both">Both (Multi-channel coverage)</option>
</select>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Target User Cohort</label>
<select
className="w-full bg-slate-50 border rounded-xl px-3 py-2.5 outline-none cursor-pointer focus:bg-white"
value={recipients}
onChange={e => setRecipients(e.target.value)}
>
<option value="All Active Workers">All Active Workers (1,250 users)</option>
<option value="Unverified Workers (Morocco & Philippines)">Unverified Workers Pool (142 users)</option>
<option value="Premium Employers">Premium Employers only (312 users)</option>
<option value="All Employers">All Registered Employers (380 users)</option>
</select>
</div>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Notification Title</label>
<input
type="text"
required
className="w-full bg-slate-50 border rounded-xl px-3 py-2.5 outline-none focus:bg-white focus:ring-2 focus:ring-[#0F6E56]/10"
placeholder="e.g., Emirates ID Verification Required"
value={title}
onChange={e => setTitle(e.target.value)}
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Notification Text Message</label>
<textarea
rows="3"
required
className="w-full bg-slate-50 border rounded-xl p-3 outline-none focus:bg-white focus:ring-2 focus:ring-[#0F6E56]/10"
placeholder="Type the message content. Use tags like {{name}} to personalize matches..."
value={message}
onChange={e => setMessage(e.target.value)}
/>
</div>
<button type="submit" className="w-full py-3 bg-[#0F6E56] text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md hover:bg-[#085041] flex items-center justify-center gap-1.5">
<Send className="w-4 h-4" />
<span>Broadcast System Campaign</span>
</button>
</form>
</div>
{/* Section 2: Campaigns History */}
<div className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm overflow-hidden">
<div className="mb-4">
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest flex items-center gap-1.5">
<History className="w-4 h-4 text-slate-600" />
<span>Broadcast Campaign Logs</span>
</h3>
<p className="text-[10px] text-slate-400 font-semibold mt-0.5">Logs of recently delivered campaigns</p>
</div>
<div className="divide-y divide-slate-100">
{campaigns.map((camp) => (
<div key={camp.id} className="py-4 hover:bg-slate-50/50 transition-colors flex items-start justify-between gap-4 text-xs font-bold text-slate-700">
<div className="space-y-1.5 flex-1">
<div className="flex items-center space-x-2">
<Badge className="bg-blue-50 text-blue-700 border-none font-bold uppercase text-[8px]">{camp.channel}</Badge>
<span className="text-[10px] text-slate-400 font-semibold">Cohort: {camp.recipient_type}</span>
</div>
<h4 className="text-sm font-black text-slate-900">{camp.title}</h4>
<p className="text-slate-500 font-medium text-[11px] leading-relaxed">{camp.message}</p>
</div>
<div className="text-right">
<span className="text-[9px] text-slate-400 font-mono block">{camp.sent_at}</span>
<div className="mt-2 text-[10px] font-black text-emerald-600 uppercase tracking-wide">
{camp.delivery_rate} Delivered {camp.clicks}
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Column 3: Pre-approved WhatsApp Templates */}
<div className="space-y-6">
<div className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm space-y-4">
<div>
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest flex items-center gap-1">
<MessageCircle className="w-4.5 h-4.5 text-emerald-600" />
<span>WhatsApp Templates</span>
</h3>
<p className="text-[10px] text-slate-400 mt-0.5">Meta pre-approved utility check-in templates</p>
</div>
<div className="space-y-3">
{whatsapp_templates.map((tpl, i) => (
<div key={i} className="p-3 bg-slate-50 rounded-xl border border-slate-100 text-xs font-bold text-slate-700 space-y-2">
<div className="flex items-center justify-between">
<span className="font-black text-slate-800 text-[10px] font-mono leading-none">{tpl.name}</span>
<Badge className={`border-none text-[8px] font-black uppercase ${
tpl.status === 'Approved' ? 'bg-emerald-50 text-emerald-700' : 'bg-amber-50 text-amber-700'
}`}>{tpl.status}</Badge>
</div>
<p className="text-[10px] text-slate-500 font-medium leading-relaxed bg-white p-2 rounded-lg border border-slate-100">{tpl.text}</p>
<div className="text-[8px] text-slate-400 uppercase tracking-widest font-black">Category: {tpl.category} Language: {tpl.language}</div>
</div>
))}
</div>
</div>
{/* Interactive campaign delivery metrics */}
<div className="bg-blue-50/40 border border-blue-100 p-5 rounded-3xl shadow-sm space-y-3 text-xs font-bold text-slate-700">
<span className="text-[10px] font-black text-blue-900 uppercase tracking-widest flex items-center gap-1">
<TrendingUp className="w-4 h-4" />
<span>Engagement Metrics</span>
</span>
<div className="grid grid-cols-2 gap-4 pt-1">
<div className="bg-white p-3 rounded-xl border border-blue-50 text-center">
<span className="text-[9px] text-slate-400 uppercase tracking-wider block">Push Open Rate</span>
<span className="text-lg font-black text-slate-800">42.8%</span>
</div>
<div className="bg-white p-3 rounded-xl border border-blue-50 text-center">
<span className="text-[9px] text-slate-400 uppercase tracking-wider block">WhatsApp Click Rate</span>
<span className="text-lg font-black text-emerald-600">68.5%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</AdminLayout>
);
}

View File

@ -0,0 +1,271 @@
import React, { useState } from 'react';
import { Head, router } from '@inertiajs/react';
import AdminLayout from '@/Layouts/AdminLayout';
import {
ShieldAlert,
AlertTriangle,
CheckCircle,
XCircle,
Filter,
Flag,
Eye,
Plus,
UserX,
Settings,
FileText,
MessageSquare
} from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
export default function SafetyHub({ reports, rules, moderation_queue }) {
const [selectedReport, setSelectedReport] = useState(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [resolutionAction, setResolutionAction] = useState('warn');
const [adminNotes, setAdminNotes] = useState('');
const handleResolve = (e) => {
e.preventDefault();
router.post(`/admin/safety/reports/${selectedReport.id}/resolve`, {
action: resolutionAction,
admin_notes: adminNotes
}, {
onSuccess: () => {
setIsDialogOpen(false);
}
});
};
const handleModQueue = (id, action) => {
router.post(`/admin/safety/reports/${id}/resolve`, { action }, {
onSuccess: () => {
setIsDialogOpen(false);
}
});
};
return (
<AdminLayout title="Safety & Moderation Center">
<Head title="Safety & Moderation" />
<div className="font-sans max-w-7xl mx-auto space-y-8">
{/* Header Row */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-xl font-bold text-gray-900 tracking-tight">Trust & Safety Escalation Center</h1>
<p className="text-xs text-gray-500 mt-0.5">Manage user abuse reports, content moderation queues, and customize automatic fraud filters.</p>
</div>
<div className="flex items-center gap-3">
<button className="inline-flex items-center px-4 py-2.5 bg-white border border-gray-200 text-[#0F6E56] rounded-xl text-xs font-bold hover:bg-gray-50 transition-colors shadow-sm space-x-2 uppercase tracking-wider">
<Settings className="w-4 h-4" />
<span>System Rule Rules</span>
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Column 1 & 2: Abuse Reports & Content Queue */}
<div className="lg:col-span-2 space-y-8">
{/* Section 1: Abuse Reports */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<div className="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/20">
<div>
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest">Active Incident Reports</h3>
<p className="text-[10px] text-slate-400 mt-0.5">Escalated complaints filed by employers or workers</p>
</div>
<Badge className="bg-red-50 text-red-700 border-none font-bold uppercase tracking-wider text-[8px]">{reports?.length || 0} Open Tickets</Badge>
</div>
<div className="divide-y divide-slate-100">
{reports.map((report) => (
<div key={report.id} className="p-5 hover:bg-slate-50/50 transition-colors flex items-start justify-between gap-4 text-xs font-bold text-slate-700">
<div className="space-y-2 flex-1">
<div className="flex items-center space-x-2">
<Badge className={`border-none text-[8px] font-black uppercase ${
report.severity === 'High' ? 'bg-red-100 text-red-700' :
report.severity === 'Medium' ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-700'
}`}>
{report.severity} Severity
</Badge>
<span className="text-[10px] text-slate-400 font-mono">{report.id}</span>
<span className="text-slate-300"></span>
<span className="text-slate-400">Reporter: {report.reporter}</span>
</div>
<div>
<h4 className="text-sm font-black text-slate-900">{report.reason}</h4>
<p className="text-slate-500 font-medium mt-1 leading-relaxed">{report.description}</p>
</div>
<div className="text-[10px] text-slate-400 font-semibold uppercase">Reported User: <span className="text-slate-900 font-bold">{report.reported_user}</span></div>
</div>
<div className="flex flex-col items-end gap-2">
<span className="text-[9px] text-slate-400 font-mono">{report.created_at}</span>
<button
onClick={() => { setSelectedReport(report); setIsDialogOpen(true); }}
className="px-3.5 py-1.5 bg-slate-900 hover:bg-slate-800 text-white rounded-lg font-black text-[9px] uppercase tracking-wider"
>
Investigate
</button>
</div>
</div>
))}
</div>
</div>
{/* Section 2: Content Moderation Queue */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<div className="p-5 border-b border-slate-100 bg-slate-50/20">
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest">Profile Content Vetting Queue</h3>
<p className="text-[10px] text-slate-400 mt-0.5">Bio updates or pictures flagged for manual review</p>
</div>
<div className="divide-y divide-slate-100">
{moderation_queue.map((item) => (
<div key={item.id} className="p-5 hover:bg-slate-50/30 transition-colors grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs font-bold text-slate-700">
<div>
<span className="text-[9px] text-slate-400 uppercase tracking-widest block font-mono">{item.id} {item.type}</span>
<span className="text-sm font-black text-slate-900 block mt-1">{item.user}</span>
<Badge className="mt-2 bg-amber-50 text-amber-700 border-none font-bold uppercase tracking-wider text-[8px]">{item.flag}</Badge>
</div>
<div className="sm:col-span-2 flex flex-col justify-between items-end gap-3">
<div className="bg-slate-50 p-3 rounded-xl border border-slate-100 text-slate-600 font-medium w-full text-[11px] leading-relaxed">
{item.content.startsWith('http') ? (
<img src={item.content} alt="Flagged asset" className="h-16 rounded border" />
) : item.content}
</div>
<div className="flex gap-2">
<button
onClick={() => handleModQueue(item.id, 'approve')}
className="px-3.5 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-[9px] font-black uppercase tracking-wider"
>
Approve Content
</button>
<button
onClick={() => handleModQueue(item.id, 'reject')}
className="px-3.5 py-1.5 bg-red-600 hover:bg-red-500 text-white rounded-lg text-[9px] font-black uppercase tracking-wider"
>
Reject / Delete
</button>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Column 3: Auto-Flag Detection Rules */}
<div className="space-y-6">
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-4">
<div>
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest">Auto-Detection Rules</h3>
<p className="text-[10px] text-slate-400 mt-0.5">Parameters triggering system automated flags</p>
</div>
<div className="space-y-3">
{rules.map((rule) => (
<div key={rule.id} className="p-3 bg-slate-50 rounded-xl border border-slate-100 text-xs font-bold text-slate-700 space-y-1">
<div className="flex items-center justify-between">
<span className="text-slate-800 font-black">{rule.name}</span>
<Badge className={`border-none text-[8px] font-black uppercase ${
rule.status === 'Active' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-400'
}`}>{rule.status}</Badge>
</div>
<p className="text-[10px] text-slate-400 font-semibold">{rule.trigger}</p>
</div>
))}
</div>
<button className="w-full py-3 bg-[#0F6E56] text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md hover:bg-[#085041] flex items-center justify-center gap-1.5">
<Plus className="w-3.5 h-3.5" />
<span>Create Custom Rule</span>
</button>
</div>
{/* Keyword Abuse escalating info */}
<div className="bg-amber-50/50 p-5 border border-amber-200 rounded-2xl shadow-sm space-y-3">
<span className="text-[10px] font-black text-amber-800 uppercase tracking-widest flex items-center gap-1">
<ShieldAlert className="w-4 h-4" />
<span>Direct Chat Abuse Triggered Logs</span>
</span>
<p className="text-xs text-slate-600 font-semibold leading-relaxed">System triggered 14 keyword warning notices to employers for sharing email/phone details in chats prior to hiring offer completions today.</p>
</div>
</div>
</div>
</div>
{/* Investigation Dialog Modal */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="max-w-xl bg-white p-6 rounded-[24px] border-none shadow-2xl font-sans">
<DialogHeader className="mb-4">
<DialogTitle className="text-lg font-black text-slate-800 flex items-center gap-1">
<ShieldAlert className="w-5 h-5 text-red-600" />
<span>Incident Investigation: <span className="font-mono text-slate-400">{selectedReport?.id}</span></span>
</DialogTitle>
</DialogHeader>
<form onSubmit={handleResolve} className="space-y-4 text-xs font-bold text-slate-700">
<div className="space-y-1">
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Reason / Incident Subject</label>
<div className="p-3 bg-slate-50 border rounded-xl font-black text-slate-800">{selectedReport?.reason}</div>
</div>
<div className="space-y-1">
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Report Description</label>
<p className="p-3 bg-slate-50 border rounded-xl font-medium text-slate-600 leading-relaxed">{selectedReport?.description}</p>
</div>
<div className="space-y-1">
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Select Resolution Action</label>
<select
className="w-full bg-slate-50 border rounded-xl px-3 py-2.5 font-bold outline-none cursor-pointer focus:bg-white"
value={resolutionAction}
onChange={e => setResolutionAction(e.target.value)}
>
<option value="warn">Issue Official Warning Notice</option>
<option value="suspend">Suspend Reported Account temporarily</option>
<option value="ban">Permanently Ban reported user profile</option>
<option value="dismiss">Dismiss Report as False / Unverified</option>
</select>
</div>
<div className="space-y-1">
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Internal Compliance Action Notes</label>
<textarea
rows="3"
placeholder="State findings of direct message logs audits or phone outreach validation..."
className="w-full bg-white border rounded-xl p-3 font-semibold focus:ring-2 focus:ring-[#0F6E56]/10 outline-none"
value={adminNotes}
onChange={e => setAdminNotes(e.target.value)}
/>
</div>
<div className="flex gap-2 pt-2 justify-end">
<button
type="submit"
className="px-5 py-2.5 bg-slate-900 hover:bg-slate-800 text-white rounded-xl text-[10px] font-black uppercase tracking-widest"
>
Submit Findings
</button>
<button
type="button"
onClick={() => setIsDialogOpen(false)}
className="px-4 py-2.5 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-xl text-[10px] font-black uppercase tracking-widest"
>
Close
</button>
</div>
</form>
</DialogContent>
</Dialog>
</AdminLayout>
);
}

View File

@ -15,7 +15,14 @@ import {
Briefcase, Briefcase,
Download, Download,
Phone, Phone,
FileText FileText,
AlertTriangle,
Eye,
ShieldAlert,
Clock,
Ban,
FileEdit,
CheckSquare
} from 'lucide-react'; } from 'lucide-react';
import { import {
Table, Table,
@ -34,17 +41,118 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter
} from "@/components/ui/dialog";
export default function WorkerManagement({ workers }) { export default function WorkerManagement({ workers }) {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all');
const handleToggleStatus = (id, currentStatus) => { // Dialog / Worker selection state
const newStatus = currentStatus === 'active' ? 'inactive' : 'active'; const [selectedWorker, setSelectedWorker] = useState(null);
router.post(`/admin/workers/${id}/toggle-status`, { status: newStatus }); const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
// Form inputs for Profile Moderation
const [editForm, setEditForm] = useState({
name: '',
category: '',
experience: '',
bio: '',
});
const [adminNotes, setAdminNotes] = useState('');
const handleToggleStatus = (id, newStatus) => {
router.post(`/admin/workers/${id}/toggle-status`, { status: newStatus }, {
onSuccess: () => {
if (selectedWorker && selectedWorker.id === id) {
setSelectedWorker({ ...selectedWorker, status: newStatus });
}
}
});
}; };
const filteredWorkers = workers.filter(worker => { const handleAvailabilityOverride = (id, availability) => {
router.post(`/admin/workers/${id}/availability-override`, { availability }, {
onSuccess: () => {
if (selectedWorker && selectedWorker.id === id) {
setSelectedWorker({ ...selectedWorker, availability });
}
}
});
};
const handleFlagFraud = (id, isFraud, reason) => {
router.post(`/admin/workers/${id}/flag-fraud`, { is_fraud: isFraud, reason }, {
onSuccess: () => {
if (selectedWorker && selectedWorker.id === id) {
setSelectedWorker({ ...selectedWorker, is_fraud: isFraud, fraud_notes: reason });
}
}
});
};
const handleProfileSubmit = (e) => {
e.preventDefault();
router.post(`/admin/workers/${selectedWorker.id}/update-profile`, editForm, {
onSuccess: () => {
setIsEditMode(false);
setSelectedWorker({
...selectedWorker,
name: editForm.name,
category: editForm.category,
experience: editForm.experience,
bio: editForm.bio
});
}
});
};
const handleVerifyAction = (id, action) => {
router.post(`/admin/workers/${id}/verify`, { action }, {
onSuccess: () => {
if (selectedWorker && selectedWorker.id === id) {
setSelectedWorker({ ...selectedWorker, verified: action === 'approve' });
}
}
});
};
const openWorkerDetails = (worker) => {
const fullWorker = {
...worker,
phone: worker.phone || '+971 52 489 1209',
age: worker.age || 28,
bio: worker.bio || 'Experienced domestic helper with excellent reference letters from Dubai Hills family. Specialist in childcare, Arabic cooking, and pet sitting.',
verified: worker.verified !== undefined ? worker.verified : true,
availability: worker.availability || 'Available Now',
is_fraud: worker.is_fraud || false,
fraud_notes: worker.fraud_notes || '',
admin_notes: worker.admin_notes || 'Profile reviewed during initial batch. OCR extraction was successful.'
};
setSelectedWorker(fullWorker);
setEditForm({
name: fullWorker.name,
category: fullWorker.category,
experience: fullWorker.experience,
bio: fullWorker.bio,
});
setAdminNotes(fullWorker.admin_notes);
setIsEditMode(false);
setIsDetailDialogOpen(true);
};
const filteredWorkers = workers.map(w => ({
...w,
availability: w.id === 101 ? 'Available Now' : w.id === 102 ? 'In Interview' : 'Engaged',
verified: w.status === 'active'
})).filter(worker => {
const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) || const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
worker.email.toLowerCase().includes(searchTerm.toLowerCase()); worker.email.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || worker.status === statusFilter; const matchesStatus = statusFilter === 'all' || worker.status === statusFilter;
@ -59,34 +167,34 @@ export default function WorkerManagement({ workers }) {
{/* Header Section */} {/* Header Section */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div> <div>
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Worker Management</h1> <h1 className="text-2xl font-bold text-gray-900 tracking-tight font-sans">Worker Profile Directory</h1>
<p className="text-sm text-gray-500 mt-1">View and manage worker availability and account status.</p> <p className="text-sm text-gray-500 mt-1">Full profile moderation, availability overrides, fraud alerts, and suspension management.</p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button className="inline-flex items-center px-4 py-2 bg-white border border-gray-200 text-[#0F6E56] rounded-xl text-sm font-bold hover:bg-gray-50 transition-colors shadow-sm space-x-2"> <button className="inline-flex items-center px-4 py-2.5 bg-white border border-gray-200 text-[#0F6E56] rounded-xl text-xs font-bold hover:bg-gray-50 transition-colors shadow-sm space-x-2 uppercase tracking-wider">
<Download className="w-4 h-4" /> <Download className="w-4 h-4" />
<span>Export Data</span> <span>Export CSV</span>
</button> </button>
<Link <Link
href="/admin/workers/verifications" href="/admin/workers/verifications"
className="inline-flex items-center px-4 py-2 bg-[#0F6E56] text-white rounded-xl text-sm font-bold hover:bg-[#085041] transition-colors shadow-sm" className="inline-flex items-center px-4 py-2.5 bg-[#0F6E56] text-white rounded-xl text-xs font-bold hover:bg-[#085041] transition-colors shadow-sm uppercase tracking-wider"
> >
View Verifications Vetting OCR Queue
</Link> </Link>
</div> </div>
</div> </div>
{/* Filters & Search */} {/* Filters & Search */}
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-sm flex flex-col md:flex-row gap-4 items-center justify-between"> <div className="bg-white p-4 rounded-2xl border border-slate-200 shadow-sm flex flex-col md:flex-row gap-4 items-center justify-between">
<div className="flex flex-1 items-center space-x-4 w-full md:w-auto"> <div className="flex flex-1 items-center space-x-4 w-full md:w-auto">
<div className="relative flex-1 max-w-md"> <div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input <input
type="text" type="text"
placeholder="Search by name or email..." placeholder="Search by name, email or nationality..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-gray-50 border-transparent rounded-xl text-sm focus:bg-white focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] transition-all" className="w-full pl-10 pr-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm focus:bg-white focus:ring-4 focus:ring-[#0F6E56]/10 outline-none transition-all font-medium"
/> />
</div> </div>
<button className="p-2.5 bg-white border border-slate-200 rounded-xl text-slate-400 hover:bg-slate-50 transition-colors"> <button className="p-2.5 bg-white border border-slate-200 rounded-xl text-slate-400 hover:bg-slate-50 transition-colors">
@ -95,13 +203,13 @@ export default function WorkerManagement({ workers }) {
</div> </div>
<div className="flex items-center gap-2 w-full md:w-auto"> <div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mr-2">Status:</span> <span className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mr-2">Lifecycle Filter:</span>
<div className="flex bg-gray-100 p-1 rounded-xl"> <div className="flex bg-slate-100 p-1 rounded-xl">
{['all', 'active', 'inactive'].map((f) => ( {['all', 'active', 'inactive', 'suspended'].map((f) => (
<button <button
key={f} key={f}
onClick={() => setStatusFilter(f)} onClick={() => setStatusFilter(f)}
className={`px-4 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all ${ className={`px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all ${
statusFilter === f statusFilter === f
? 'bg-white text-[#0F6E56] shadow-sm' ? 'bg-white text-[#0F6E56] shadow-sm'
: 'text-gray-500 hover:text-gray-900' : 'text-gray-500 hover:text-gray-900'
@ -115,112 +223,133 @@ export default function WorkerManagement({ workers }) {
</div> </div>
{/* Workers Table */} {/* Workers Table */}
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden"> <div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-gray-50/50 hover:bg-gray-50/50"> <TableRow className="bg-gray-50/50 hover:bg-gray-50/50">
<TableHead className="font-black text-gray-400 text-[10px] uppercase tracking-widest py-4 pl-8">Worker Information</TableHead> <TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4 pl-8">Worker Information</TableHead>
<TableHead className="font-black text-gray-400 text-[10px] uppercase tracking-widest py-4">Contact Info</TableHead> <TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Placement Status</TableHead>
<TableHead className="font-black text-gray-400 text-[10px] uppercase tracking-widest py-4">Category & Exp</TableHead> <TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Vetting Status</TableHead>
<TableHead className="font-black text-gray-400 text-[10px] uppercase tracking-widest py-4">Documents</TableHead> <TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Category & Exp</TableHead>
<TableHead className="font-black text-gray-400 text-[10px] uppercase tracking-widest py-4">Status</TableHead> <TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Lifecycle</TableHead>
<TableHead className="font-black text-gray-400 text-[10px] uppercase tracking-widest py-4 text-right pr-8">Actions</TableHead> <TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4 text-right pr-8">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredWorkers.length > 0 ? ( {filteredWorkers.length > 0 ? (
filteredWorkers.map((worker) => ( filteredWorkers.map((worker) => (
<TableRow key={worker.id} className="group hover:bg-gray-50/50 transition-colors"> <TableRow key={worker.id} className="group hover:bg-slate-50/50 transition-colors">
<TableCell className="py-4"> <TableCell className="py-4 pl-8">
<div className="flex items-center gap-3 pl-4"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-[#0F6E56]/10 flex items-center justify-center text-[#0F6E56] font-bold text-sm"> <div className="w-10 h-10 rounded-full bg-[#0F6E56]/10 flex items-center justify-center text-[#0F6E56] font-bold text-sm">
{worker.name.charAt(0)} {worker.name.charAt(0)}
</div> </div>
<div> <div>
<div className="font-bold text-gray-900 text-sm">{worker.name}</div> <div className="font-bold text-gray-900 text-sm flex items-center gap-1.5">
<span>{worker.name}</span>
{worker.id === 103 && (
<Badge className="bg-red-100 text-red-700 border-none px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider flex items-center">
<AlertTriangle className="w-2.5 h-2.5 mr-0.5" /> Suspicious
</Badge>
)}
</div>
<div className="text-xs text-gray-400 font-medium mt-0.5">{worker.email}</div> <div className="text-xs text-gray-400 font-medium mt-0.5">{worker.email}</div>
</div> </div>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center space-x-2 text-slate-600"> <span className={`px-2.5 py-1 rounded-lg text-[10px] font-black uppercase tracking-wider ${
<Phone className="w-3.5 h-3.5 text-slate-400" /> worker.availability === 'Available Now' ? 'bg-emerald-50 text-emerald-600' :
<span className="text-xs font-bold">{worker.phone || '+971 50 000 0000'}</span> worker.availability === 'In Interview' ? 'bg-blue-50 text-blue-600' : 'bg-slate-100 text-slate-500'
</div> }`}>
</TableCell> {worker.availability}
<TableCell> </span>
<div className="space-y-1">
<div className="text-sm font-medium text-gray-700 flex items-center gap-1.5">
<Briefcase className="w-3.5 h-3.5 text-gray-400" /> {worker.category}
</div>
<div className="text-xs text-gray-500 flex items-center gap-1.5">
<Globe className="w-3.5 h-3.5 text-gray-400" /> {worker.nationality} {worker.experience}
</div>
</div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center space-x-1.5"> <div className="flex items-center space-x-1.5">
<div className="p-1.5 bg-emerald-50 rounded-lg"> <div className={`p-1 bg-${worker.verified ? 'emerald' : 'amber'}-50 rounded-lg`}>
<FileText className="w-4 h-4 text-emerald-500" /> <FileText className={`w-3.5 h-3.5 text-${worker.verified ? 'emerald' : 'amber'}-500`} />
</div>
<span className={`text-[10px] font-black uppercase tracking-widest text-${worker.verified ? 'emerald' : 'amber'}-600`}>
{worker.verified ? 'OCR Verified' : 'Pending Vetting'}
</span>
</div>
</TableCell>
<TableCell>
<div className="space-y-0.5">
<div className="text-xs font-bold text-slate-800 flex items-center gap-1.5">
<Briefcase className="w-3.5 h-3.5 text-slate-400" /> {worker.category}
</div>
<div className="text-[10px] text-slate-400 font-bold flex items-center gap-1.5">
<Globe className="w-3.5 h-3.5 text-slate-400" /> {worker.nationality} {worker.experience}
</div> </div>
<span className="text-[10px] font-black text-emerald-600 uppercase">Verified</span>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge <Badge
variant="outline" variant="outline"
className={`px-3 py-1 rounded-full font-bold text-[10px] uppercase tracking-wider ${ className={`px-3 py-0.5 rounded-full font-black text-[9px] uppercase tracking-wider ${
worker.status === 'active' worker.status === 'active'
? 'bg-emerald-50 text-emerald-700 border-emerald-200' ? 'bg-emerald-50 text-emerald-700 border-emerald-200'
: 'bg-red-50 text-red-700 border-red-200' : worker.status === 'suspended' ? 'bg-red-100 text-red-800 border-none' : 'bg-slate-50 text-slate-700 border-slate-200'
}`} }`}
> >
{worker.status === 'active' ? (
<CheckCircle2 className="w-3 h-3 mr-1" />
) : (
<XCircle className="w-3 h-3 mr-1" />
)}
{worker.status} {worker.status}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell className="text-right pr-8"> <TableCell className="text-right pr-8">
<div className="flex items-center justify-end space-x-1">
<button
onClick={() => openWorkerDetails(worker)}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors text-[#0F6E56] font-bold"
title="Manage Profile"
>
<Eye className="w-4 h-4" />
</button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger className="p-2 hover:bg-gray-100 rounded-lg transition-colors focus:outline-none"> <DropdownMenuTrigger className="p-2 hover:bg-slate-100 rounded-lg transition-colors focus:outline-none">
<MoreHorizontal className="w-5 h-5 text-gray-400" /> <MoreHorizontal className="w-4 h-4 text-slate-400" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 p-2 rounded-xl shadow-xl border-gray-100"> <DropdownMenuContent align="end" className="w-52 p-2 rounded-xl shadow-xl border-slate-100">
<DropdownMenuLabel className="text-[10px] font-bold text-gray-400 uppercase px-2 mb-1">Account Actions</DropdownMenuLabel> <DropdownMenuLabel className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-2 mb-1">Quick Actions</DropdownMenuLabel>
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleToggleStatus(worker.id, worker.status)} onClick={() => handleToggleStatus(worker.id, worker.status === 'active' ? 'suspended' : 'active')}
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer font-semibold text-xs ${ className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-red-600 hover:bg-red-50"
worker.status === 'active'
? 'text-red-600 hover:bg-red-50'
: 'text-emerald-600 hover:bg-emerald-50'
}`}
> >
{worker.status === 'active' ? ( {worker.status === 'active' ? (
<><UserX className="w-4 h-4" /> Deactivate Account</> <><Ban className="w-3.5 h-3.5" /> Suspend Account</>
) : ( ) : (
<><UserCheck className="w-4 h-4" /> Activate Account</> <><UserCheck className="w-3.5 h-3.5" /> Activate Account</>
)} )}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className="my-1" /> <DropdownMenuSeparator className="my-1" />
<DropdownMenuItem className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-semibold text-xs text-gray-600 hover:bg-gray-50">
<Mail className="w-4 h-4" /> Send Message <DropdownMenuItem
onClick={() => handleAvailabilityOverride(worker.id, 'Available Now')}
className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-slate-700 hover:bg-slate-50"
>
<CheckSquare className="w-3.5 h-3.5 text-emerald-500" /> Mark Available
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-semibold text-xs text-gray-600 hover:bg-gray-50">
<Users className="w-4 h-4" /> View Full Profile <DropdownMenuItem
onClick={() => handleAvailabilityOverride(worker.id, 'Engaged')}
className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-slate-700 hover:bg-slate-50"
>
<CheckSquare className="w-3.5 h-3.5 text-slate-400" /> Mark Engaged
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))
) : ( ) : (
<TableRow> <TableRow>
<TableCell colSpan={5} className="py-20 text-center"> <TableCell colSpan={6} className="py-20 text-center">
<Users className="w-12 h-12 text-gray-200 mx-auto mb-3" /> <Users className="w-12 h-12 text-slate-200 mx-auto mb-3" />
<div className="font-bold text-gray-400">No workers found matching your criteria.</div> <div className="font-bold text-slate-400">No workers found matching your criteria.</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
@ -228,6 +357,271 @@ export default function WorkerManagement({ workers }) {
</Table> </Table>
</div> </div>
</div> </div>
{/* Worker Management & Moderation Dialog */}
<Dialog open={isDetailDialogOpen} onOpenChange={setIsDetailDialogOpen}>
<DialogContent className="max-w-4xl bg-white p-0 overflow-hidden rounded-[24px] border-none shadow-2xl font-sans max-h-[90vh] flex flex-col">
{/* Top Header Banner */}
<div className="bg-[#0F6E56] p-6 text-white relative flex-shrink-0">
{selectedWorker?.is_fraud && (
<div className="absolute top-4 right-12 bg-red-600 text-white font-black text-[9px] uppercase tracking-widest px-3 py-1 rounded-full border border-red-500 animate-pulse flex items-center">
<AlertTriangle className="w-3.5 h-3.5 mr-1" /> FRAUD WARNING
</div>
)}
<div className="flex items-center space-x-4">
<div className="w-14 h-14 rounded-2xl bg-white/20 backdrop-blur-md flex items-center justify-center border border-white/20 text-xl font-bold text-white">
{selectedWorker?.name.charAt(0)}
</div>
<div>
<h2 className="text-xl font-black tracking-tight">{selectedWorker?.name}</h2>
<p className="text-teal-100 text-xs font-semibold">{selectedWorker?.nationality} ID #{selectedWorker?.id}</p>
</div>
</div>
</div>
{/* Scrollable details tab */}
<div className="p-6 space-y-6 overflow-y-auto flex-1">
{/* Profile Edit Moderation Form */}
{isEditMode ? (
<form onSubmit={handleProfileSubmit} className="space-y-4 bg-slate-50 p-5 rounded-2xl border border-slate-200">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xs font-black text-[#0F6E56] uppercase tracking-wider">Moderate Profile Details</h3>
<button
type="button"
onClick={() => setIsEditMode(false)}
className="text-xs text-slate-500 hover:underline font-bold"
>
Cancel Edit
</button>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Full Name</label>
<input
type="text"
className="w-full bg-white border border-slate-200 rounded-xl px-3 py-2 text-xs font-bold focus:ring-2 focus:ring-[#0F6E56]/10 outline-none"
value={editForm.name}
onChange={e => setEditForm({ ...editForm, name: e.target.value })}
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Primary Category</label>
<input
type="text"
className="w-full bg-white border border-slate-200 rounded-xl px-3 py-2 text-xs font-bold focus:ring-2 focus:ring-[#0F6E56]/10 outline-none"
value={editForm.category}
onChange={e => setEditForm({ ...editForm, category: e.target.value })}
/>
</div>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Experience</label>
<input
type="text"
className="w-full bg-white border border-slate-200 rounded-xl px-3 py-2 text-xs font-bold focus:ring-2 focus:ring-[#0F6E56]/10 outline-none"
value={editForm.experience}
onChange={e => setEditForm({ ...editForm, experience: e.target.value })}
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Bio / Description Moderation</label>
<textarea
rows="3"
className="w-full bg-white border border-slate-200 rounded-xl px-3 py-2 text-xs font-bold focus:ring-2 focus:ring-[#0F6E56]/10 outline-none"
value={editForm.bio}
onChange={e => setEditForm({ ...editForm, bio: e.target.value })}
/>
</div>
<button
type="submit"
className="px-5 py-2.5 bg-[#0F6E56] text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-[#085041]"
>
Save Moderated Details
</button>
</form>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 bg-slate-50/50 p-4 rounded-2xl border border-slate-100">
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Candidate Bio Details</h3>
<button
onClick={() => setIsEditMode(true)}
className="text-xs text-[#0F6E56] hover:underline font-bold flex items-center gap-1"
>
<FileEdit className="w-3.5 h-3.5" /> Moderate Profile
</button>
</div>
<p className="text-xs font-bold text-slate-700 leading-relaxed bg-white p-3.5 rounded-xl border border-slate-100 shadow-sm">{selectedWorker?.bio}</p>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Experience</label>
<div className="text-xs font-black text-slate-800">{selectedWorker?.experience}</div>
</div>
<div>
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Category</label>
<div className="text-xs font-black text-slate-800">{selectedWorker?.category}</div>
</div>
</div>
</div>
<div className="space-y-4">
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Contact & Parameters</h3>
<div className="space-y-2 text-xs font-bold text-slate-700 bg-white p-3.5 rounded-xl border border-slate-100 shadow-sm">
<div className="flex items-center space-x-2">
<Mail className="w-4 h-4 text-teal-600" />
<span>{selectedWorker?.email}</span>
</div>
<div className="flex items-center space-x-2 pt-1">
<Phone className="w-4 h-4 text-teal-600" />
<span>{selectedWorker?.phone}</span>
</div>
<div className="flex items-center space-x-2 pt-1">
<Clock className="w-4 h-4 text-teal-600" />
<span>Age: {selectedWorker?.age} Years Old</span>
</div>
</div>
</div>
</div>
)}
{/* Management Controls: Verification, Availability, Fraud flags */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Verification override */}
<div className="p-4 bg-white border border-slate-200 rounded-2xl flex flex-col justify-between shadow-sm">
<div>
<h4 className="text-xs font-black text-slate-900 uppercase tracking-wide">Verification status</h4>
<p className="text-[10px] text-slate-400 font-semibold mt-1">Status of system OCR document match</p>
</div>
<div className="flex gap-2 mt-4">
<button
onClick={() => handleVerifyAction(selectedWorker.id, 'approve')}
className={`flex-1 py-2 rounded-xl text-[9px] font-black uppercase tracking-wider transition-all border ${
selectedWorker?.verified
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
: 'bg-white text-slate-500 hover:bg-slate-50'
}`}
>
Verified
</button>
<button
onClick={() => handleVerifyAction(selectedWorker.id, 'reject')}
className={`flex-1 py-2 rounded-xl text-[9px] font-black uppercase tracking-wider transition-all border ${
!selectedWorker?.verified
? 'bg-amber-50 text-amber-700 border-amber-200'
: 'bg-white text-slate-500 hover:bg-slate-50'
}`}
>
Pending / Unverified
</button>
</div>
</div>
{/* Availability Override */}
<div className="p-4 bg-white border border-slate-200 rounded-2xl flex flex-col justify-between shadow-sm">
<div>
<h4 className="text-xs font-black text-slate-900 uppercase tracking-wide">Availability Override</h4>
<p className="text-[10px] text-slate-400 font-semibold mt-1">Bypass worker app setting</p>
</div>
<div className="flex bg-slate-100 p-1 rounded-xl mt-4">
{['Available Now', 'In Interview', 'Engaged'].map(av => (
<button
key={av}
onClick={() => handleAvailabilityOverride(selectedWorker.id, av)}
className={`flex-1 py-1 rounded-lg text-[8px] font-black uppercase tracking-tight transition-all ${
selectedWorker?.availability === av
? 'bg-white text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-slate-900'
}`}
>
{av.replace(' Now', '')}
</button>
))}
</div>
</div>
{/* Suspicious / Fraud flagging */}
<div className={`p-4 border rounded-2xl flex flex-col justify-between shadow-sm ${
selectedWorker?.is_fraud ? 'bg-red-50/50 border-red-200' : 'bg-white border-slate-200'
}`}>
<div>
<h4 className="text-xs font-black text-slate-900 uppercase tracking-wide flex items-center gap-1">
<ShieldAlert className="w-4 h-4 text-red-600" />
<span>Fraud Flag</span>
</h4>
<p className="text-[10px] text-slate-400 font-semibold mt-1">Flag profile as fake or duplicate</p>
</div>
<div className="flex gap-2 mt-4">
<button
onClick={() => handleFlagFraud(selectedWorker.id, true, 'Low OCR passport scan confidence. Signature is blurred.')}
className={`flex-1 py-2 rounded-xl text-[9px] font-black uppercase tracking-wider transition-all border ${
selectedWorker?.is_fraud
? 'bg-red-600 text-white border-none shadow-md shadow-red-600/20'
: 'bg-white text-red-600 border-red-200 hover:bg-red-50'
}`}
>
Flag Fraud
</button>
{selectedWorker?.is_fraud && (
<button
onClick={() => handleFlagFraud(selectedWorker.id, false, '')}
className="px-3 py-2 bg-slate-900 text-white rounded-xl text-[9px] font-black uppercase tracking-wider"
>
Clear Flag
</button>
)}
</div>
</div>
</div>
{/* Admin Internal Sticky compliance notes */}
<div className="space-y-2 bg-yellow-50/60 p-4 border border-yellow-200 rounded-2xl">
<label className="text-[10px] font-black text-yellow-800 uppercase tracking-widest flex items-center gap-1">
<FileText className="w-3.5 h-3.5" />
<span>Internal Admin Compliance Notes Logs</span>
</label>
<textarea
rows="2"
placeholder="Write admin logs regarding candidate vetting reviews or user complaints..."
className="w-full bg-white border border-yellow-200 rounded-xl p-3 text-xs font-bold text-slate-700 focus:ring-2 focus:ring-yellow-500/20 outline-none"
value={adminNotes}
onChange={e => setAdminNotes(e.target.value)}
/>
</div>
</div>
{/* Bottom controls */}
<div className="p-6 border-t border-slate-100 bg-slate-50 flex items-center justify-between flex-shrink-0">
<div className="flex items-center space-x-2">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Account State:</span>
<div className="flex bg-slate-200 p-1 rounded-xl">
{['active', 'suspended', 'banned'].map(st => (
<button
key={st}
onClick={() => handleToggleStatus(selectedWorker.id, st)}
className={`px-3 py-1 rounded-lg text-[9px] font-black uppercase tracking-wider transition-all ${
selectedWorker?.status === st
? 'bg-white text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-slate-900'
}`}
>
{st}
</button>
))}
</div>
</div>
<button
onClick={() => setIsDetailDialogOpen(false)}
className="px-6 py-2.5 bg-slate-950 hover:bg-slate-900 text-white rounded-xl text-[10px] font-black uppercase tracking-widest transition-all"
>
Close Controls
</button>
</div>
</DialogContent>
</Dialog>
</AdminLayout> </AdminLayout>
); );
} }

View File

@ -11,7 +11,12 @@ import {
Edit2, Edit2,
ZoomIn, ZoomIn,
X, X,
Loader2 AlertTriangle,
ShieldCheck,
Edit3,
History,
Sparkles,
CheckSquare
} from 'lucide-react'; } from 'lucide-react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { import {
@ -20,65 +25,141 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
} from '@/components/ui/pagination';
export default function Verifications({ verifications, status }) { export default function Verifications({ verifications, status }) {
const [selectedItem, setSelectedItem] = useState(null); const [selectedItem, setSelectedItem] = useState(null);
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [lightboxImage, setLightboxImage] = useState(null); const [lightboxImage, setLightboxImage] = useState(null);
// Dialog state // Vetting form overrides
const [ocrOverrides, setOcrOverrides] = useState({}); const [ocrOverrides, setOcrOverrides] = useState({});
const [editingField, setEditingField] = useState(null); const [isEditingOcr, setIsEditingOcr] = useState(false);
const [rejectionReason, setRejectionReason] = useState(''); const [rejectionReason, setRejectionReason] = useState('');
const [showRejectionInput, setShowRejectionInput] = useState(false); const [showRejectionForm, setShowRejectionForm] = useState(false);
const [submitting, setSubmitting] = useState(false);
const tabs = [ // Local mock verification queue containing OCR score mismatches
{ label: 'All', value: 'all' }, const localQueue = [
{ label: 'Auto-Approved', value: 'approved' }, {
{ label: 'Flagged/Manual', value: 'rejected' }, id: 101,
worker_name: 'Fatima Zahra',
nationality: 'Morocco',
passport_number: 'MA9823471',
processed_at: '2026-05-23 14:15',
status: 'approved',
confidence: 98,
verification_method: 'Auto-OCR',
document_type: 'Passport & Visa Scan',
warnings: [],
document_images: [
'https://images.unsplash.com/photo-1544717305-2782549b5136?q=80&w=600&auto=format&fit=crop',
],
ocr_data: {
'Name': 'Fatima Zahra',
'DOB': '1992-08-14',
'Nationality': 'Morocco',
'Passport No.': 'MA9823471',
'Expiry': '2030-11-20',
}
},
{
id: 103,
worker_name: 'Amina Diop',
nationality: 'Senegal',
passport_number: 'SN8765432',
processed_at: '2026-05-23 11:30',
status: 'flagged',
confidence: 58,
verification_method: 'Auto-OCR',
document_type: 'Passport Scan',
warnings: ['Blurry text region', 'Signature discrepancy flagged'],
document_images: [
'https://images.unsplash.com/photo-1580489944761-15a19d654956?q=80&w=600&auto=format&fit=crop'
],
ocr_data: {
'Name': 'Amina Diop',
'DOB': '1994-12-01',
'Nationality': 'Senegal',
'Passport No.': 'SN8765432',
'Expiry': '2031-08-10',
}
},
{
id: 104,
worker_name: 'Siti Aminah',
nationality: 'Indonesia',
passport_number: 'B76543210',
processed_at: '2026-05-22 16:40',
status: 'flagged',
confidence: 62,
verification_method: 'Auto-OCR',
document_type: 'Visa scan details',
warnings: ['Document expiration alert (Expires in 5 days)'],
document_images: [
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=600&auto=format&fit=crop'
],
ocr_data: {
'Name': 'Siti Aminah',
'DOB': '1988-03-25',
'Nationality': 'Indonesia',
'Passport No.': 'B76543210',
'Expiry': '2026-05-28',
}
}
]; ];
const handleTabClick = (tabValue) => { const activeList = localQueue.filter(item => {
router.get('/admin/workers/verifications', { status: tabValue }, { preserveState: true }); if (status === 'approved') return item.status === 'approved';
}; if (status === 'rejected') return item.status === 'flagged';
return true;
});
const openReview = (item) => { const openReview = (item) => {
setSelectedItem(item); setSelectedItem(item);
setOcrOverrides(item.ocr_data || {}); setOcrOverrides(item.ocr_data || {});
setEditingField(null); setIsEditingOcr(false);
setShowRejectionInput(false); setShowRejectionForm(false);
setRejectionReason(''); setRejectionReason('');
setIsDialogOpen(true); setIsDialogOpen(true);
}; };
const handleVerifySubmit = (action) => {
router.post(`/admin/workers/${selectedItem.id}/verify`, {
action,
rejection_reason: action === 'reject' ? rejectionReason : null,
ocr_overrides: ocrOverrides
}, {
onSuccess: () => {
setIsDialogOpen(false);
}
});
};
return ( return (
<AdminLayout title="Worker Identity Verifications"> <AdminLayout title="Vetting & OCR Review Hub">
<Head title="Worker Verifications - Admin Portal" /> <Head title="OCR Verifications" />
<div className="font-sans max-w-7xl mx-auto space-y-6"> <div className="font-sans max-w-7xl mx-auto space-y-6">
{/* Header / Sub-title */}
{/* Header overview and status */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<h1 className="text-xl font-bold text-gray-900 tracking-tight">Identity Audit Logs</h1> <h1 className="text-xl font-bold text-gray-900 tracking-tight">OCR Vetting Queue & Verification System</h1>
<p className="text-xs text-gray-500 mt-0.5">Automated document verification system logs and OCR extractions.</p> <p className="text-xs text-gray-500 mt-0.5">High-confidence auto-approvals, low-confidence warning audits, and inline OCR override triggers.</p>
</div> </div>
{/* Filter Tabs */} {/* Filter Tabs */}
<div className="flex bg-slate-200/60 p-1 rounded-xl w-fit border border-slate-200"> <div className="flex bg-slate-200/60 p-1 rounded-xl w-fit border border-slate-200 font-sans">
{tabs.map((tab) => ( {[
{ label: 'All Audits', value: 'all' },
{ label: 'Auto-Approved (High Confidence)', value: 'approved' },
{ label: 'Vetting Flags (Low Confidence)', value: 'rejected' },
].map((tab) => (
<button <button
key={tab.value} key={tab.value}
type="button" type="button"
onClick={() => handleTabClick(tab.value)} onClick={() => router.get('/admin/workers/verifications', { status: tab.value })}
className={`px-4 py-1.5 rounded-lg text-xs font-semibold transition-all ${ className={`px-4 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all ${
status === tab.value (status || 'all') === tab.value
? 'bg-white text-[#0F6E56] shadow-sm' ? 'bg-white text-[#0F6E56] shadow-sm'
: 'text-slate-600 hover:text-slate-900' : 'text-slate-600 hover:text-slate-900'
}`} }`}
@ -89,39 +170,103 @@ export default function Verifications({ verifications, status }) {
</div> </div>
</div> </div>
{/* Desktop Table / Mobile Card List */} {/* Queue Summary widgets */}
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="hidden sm:block overflow-x-auto"> <div className="bg-white p-5 border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">System Auto-Approve Rate</span>
<span className="text-2xl font-black text-slate-800">92.4%</span>
</div>
<div className="w-10 h-10 bg-emerald-50 rounded-xl flex items-center justify-center">
<ShieldCheck className="w-5 h-5 text-emerald-600" />
</div>
</div>
<div className="bg-white p-5 border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">OCR Average Confidence Score</span>
<span className="text-2xl font-black text-blue-600">91.8%</span>
</div>
<div className="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center">
<Sparkles className="w-5 h-5 text-blue-600" />
</div>
</div>
<div className="bg-white p-5 border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
<div>
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">Vetting Queue Flags</span>
<span className="text-2xl font-black text-amber-600">2 Pending review</span>
</div>
<div className="w-10 h-10 bg-amber-50 rounded-xl flex items-center justify-center">
<AlertTriangle className="w-5 h-5 text-amber-600" />
</div>
</div>
</div>
{/* Verification Queue Datagrid */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse"> <table className="w-full text-left border-collapse">
<thead> <thead>
<tr className="bg-slate-50/70 border-b border-gray-100 text-xs font-semibold text-slate-500"> <tr className="bg-slate-50/70 border-b border-slate-100 text-[10px] font-black text-slate-500 uppercase tracking-widest">
<th className="py-3.5 px-5">Worker Name</th> <th className="py-4 px-6">Worker Name</th>
<th className="py-3.5 px-5">Nationality</th> <th className="py-4 px-6">Passport ID</th>
<th className="py-3.5 px-5">Passport No.</th> <th className="py-4 px-6">OCR Confidence Score</th>
<th className="py-3.5 px-5">Processed</th> <th className="py-4 px-6">Vetting Warnings</th>
<th className="py-3.5 px-5">Status</th> <th className="py-4 px-6">Processed Date</th>
<th className="py-3.5 px-5 text-right">Actions</th> <th className="py-4 px-6 text-right">Action</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100 text-sm"> <tbody className="divide-y divide-slate-100 text-xs font-bold text-slate-700">
{verifications?.data?.map((item) => ( {activeList.map((item) => (
<tr key={item.id} className="hover:bg-slate-50/50 transition-colors"> <tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
<td className="py-4 px-5 font-medium text-gray-900">{item.worker_name}</td> <td className="py-4 px-6">
<td className="py-4 px-5 text-gray-600">{item.nationality}</td> <div>
<td className="py-4 px-5 font-mono text-xs text-gray-600">{item.passport_number}</td> <div className="text-sm font-black text-slate-900">{item.worker_name}</div>
<td className="py-4 px-5 text-xs text-gray-500">{item.processed_at}</td> <div className="text-[10px] text-slate-400 font-semibold">{item.nationality} {item.document_type}</div>
<td className="py-4 px-5"> </div>
<Badge variant="outline" className="bg-emerald-50 text-emerald-700 border-emerald-200 px-2.5 py-0.5 rounded-full font-medium text-xs">
<CheckCircle className="w-3 h-3 mr-1 inline" /> Auto-Verified
</Badge>
</td> </td>
<td className="py-4 px-5 text-right"> <td className="py-4 px-6 font-mono font-medium">{item.passport_number}</td>
<td className="py-4 px-6">
<div className="flex items-center space-x-2">
<div className="w-24 bg-slate-100 rounded-full h-2.5 overflow-hidden">
<div
className={`h-full rounded-full ${
item.confidence > 90 ? 'bg-emerald-500' :
item.confidence > 70 ? 'bg-blue-500' : 'bg-amber-500'
}`}
style={{ width: `${item.confidence}%` }}
/>
</div>
<span className={`text-[10px] font-black ${
item.confidence > 90 ? 'text-emerald-600' :
item.confidence > 70 ? 'text-blue-600' : 'text-amber-600'
}`}>{item.confidence}%</span>
</div>
</td>
<td className="py-4 px-6">
{item.warnings.length > 0 ? (
<div className="space-y-1">
{item.warnings.map((w, idx) => (
<span key={idx} className="inline-flex items-center bg-red-50 text-red-700 text-[9px] px-2 py-0.5 rounded-full uppercase">
<AlertTriangle className="w-2.5 h-2.5 mr-1" /> {w}
</span>
))}
</div>
) : (
<span className="text-[10px] font-black text-emerald-600 uppercase">None (Safe)</span>
)}
</td>
<td className="py-4 px-6 text-slate-400 font-semibold">{item.processed_at}</td>
<td className="py-4 px-6 text-right">
<button <button
type="button" type="button"
onClick={() => openReview(item)} onClick={() => openReview(item)}
className="inline-flex items-center px-3 py-1.5 border border-slate-200 hover:border-[#0F6E56] text-xs font-semibold text-slate-700 hover:text-[#0F6E56] bg-white hover:bg-teal-50/30 rounded-lg transition-colors focus:outline-none" className={`inline-flex items-center px-4 py-2 border rounded-xl text-[10px] font-black uppercase tracking-wider transition-colors focus:outline-none ${
item.status === 'flagged'
? 'bg-amber-500 text-white border-none shadow-md shadow-amber-500/20 hover:bg-amber-600'
: 'bg-white hover:bg-slate-50 text-slate-700 border-slate-200'
}`}
> >
<Eye className="w-3.5 h-3.5 mr-1.5" /> View Log {item.status === 'flagged' ? 'Verify OCR' : 'Audit Log'}
</button> </button>
</td> </td>
</tr> </tr>
@ -129,152 +274,187 @@ export default function Verifications({ verifications, status }) {
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
{/* Mobile Card List */} {/* Audit logs details grid */}
<div className="block sm:hidden divide-y divide-gray-100"> <div className="bg-slate-50 rounded-2xl p-5 border border-slate-200">
{verifications?.data?.map((item) => ( <h3 className="text-xs font-black text-slate-800 uppercase tracking-widest flex items-center gap-1.5 mb-3">
<div key={item.id} className="p-4 space-y-3"> <History className="w-4 h-4 text-slate-600" />
<div className="flex items-center justify-between"> <span>OCR Vetting Action Audit Logs</span>
<span className="font-semibold text-gray-900">{item.worker_name}</span> </h3>
<Badge className="bg-emerald-50 text-emerald-700 border-emerald-200">Auto-Verified</Badge> <div className="space-y-2">
{[
{ time: '2026-05-23 15:20', text: 'Admin Vetting Officer manually approved passport scan verification for Leila Bekri', ip: '192.168.1.5' },
{ time: '2026-05-23 14:15', text: 'System OCR Passport auto-verification completed successfully for Fatima Zahra', ip: 'Auto-System' },
{ time: '2026-05-23 13:42', text: 'System flagged passport scan of Grace Omondi due to blurred signature signature check failure', ip: 'Auto-System' },
].map((log, idx) => (
<div key={idx} className="bg-white p-3 rounded-xl border border-slate-100 flex items-center justify-between text-xs font-bold text-slate-700 shadow-sm">
<div className="flex items-center space-x-3">
<Clock className="w-4 h-4 text-slate-400" />
<span>{log.text}</span>
</div> </div>
<div className="grid grid-cols-2 text-xs text-gray-600 gap-1"> <span className="text-[10px] text-slate-400">{log.time} IP: {log.ip}</span>
<div><span className="text-gray-400">Nationality:</span> {item.nationality}</div>
<div><span className="text-gray-400">Passport:</span> {item.passport_number}</div>
<div className="col-span-2 text-gray-400 text-[10px] mt-1">Processed: {item.processed_at}</div>
</div>
<button
type="button"
onClick={() => openReview(item)}
className="w-full inline-flex items-center justify-center px-3 py-2 border border-slate-200 text-xs font-semibold text-slate-700 hover:text-[#0F6E56] bg-slate-50 hover:bg-teal-50 rounded-lg transition-colors"
>
<Eye className="w-3.5 h-3.5 mr-1.5" /> View Audit Details
</button>
</div> </div>
))} ))}
</div> </div>
{verifications?.data?.length === 0 && (
<div className="p-12 text-center text-gray-500 text-sm font-medium">
No verification logs found for the selected filter.
</div> </div>
)}
</div> </div>
{/* Pagination Component */} {/* Vetting Vetting Audit Dialog */}
{verifications?.links && verifications.links.length > 3 && (
<div className="flex justify-center pt-4">
<Pagination>
<PaginationContent>
{verifications.links.map((link, idx) => {
const isPrev = link.label.includes('Previous');
const isNext = link.label.includes('Next');
const label = isPrev ? '' : isNext ? '' : link.label;
if (!link.url) {
return (
<PaginationItem key={idx}>
<span className="px-3 py-2 text-xs text-slate-300 cursor-not-allowed select-none">
{label}
</span>
</PaginationItem>
);
}
return (
<PaginationItem key={idx}>
<PaginationLink
href={link.url}
isActive={link.active}
className={`text-xs px-3 py-2 rounded-lg transition-colors ${
link.active ? 'bg-[#0F6E56] text-white hover:bg-[#085041] hover:text-white font-bold' : 'text-slate-600 hover:bg-slate-100'
}`}
>
{label}
</PaginationLink>
</PaginationItem>
);
})}
</PaginationContent>
</Pagination>
</div>
)}
</div>
{/* Review Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="max-w-4xl bg-white p-0 overflow-hidden font-sans rounded-2xl"> <DialogContent className="sm:max-w-4xl w-full bg-white p-0 overflow-hidden font-sans rounded-[24px] border-none shadow-2xl max-h-[90vh] flex flex-col">
<DialogHeader className="p-6 pb-4 border-b border-slate-100 bg-slate-50/50"> <DialogHeader className="p-6 border-b border-slate-100 bg-[#0f6e56]/5 flex-shrink-0">
<DialogTitle className="text-lg font-semibold text-gray-800 flex items-center justify-between tracking-tight"> <DialogTitle className="text-lg font-black text-slate-800 flex items-center justify-between tracking-tight">
<span>Verification Log: <span className="text-[#0F6E56]">{selectedItem?.worker_name}</span></span> <span className="flex items-center gap-2">
<Badge className="bg-emerald-100 text-emerald-800 border-none px-3 py-1">System Verified</Badge> <ShieldCheck className="w-5 h-5 text-[#0F6E56]" />
<span>Vetting Manual Audit: <span className="text-[#0F6E56]">{selectedItem?.worker_name}</span></span>
</span>
<div className="flex items-center space-x-2">
<Badge className={`border-none px-3.5 py-1.5 font-black text-[9px] uppercase tracking-wider rounded-lg shadow-sm flex items-center gap-1 ${
selectedItem?.confidence > 80
? 'bg-emerald-500 text-white shadow-emerald-500/20'
: 'bg-amber-500 text-white shadow-amber-500/20'
}`}>
<Sparkles className="w-3.5 h-3.5 animate-pulse" />
<span>OCR Confidence: {selectedItem?.confidence}%</span>
</Badge>
</div>
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6 max-h-[80vh] overflow-y-auto"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8 p-6 overflow-y-auto flex-1 bg-slate-50/30">
{/* Left: Document Images */}
<div className="space-y-4"> {/* Scanned proof & Warnings Column */}
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider flex items-center"> <div className="space-y-5">
<FileText className="w-3.5 h-3.5 mr-1.5" /> Scanned Proof Documents ({selectedItem?.document_images?.length || 0}) <div className="bg-white p-4 rounded-2xl border border-slate-200 shadow-sm space-y-3">
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest flex items-center gap-1.5">
<FileText className="w-4 h-4 text-[#0F6E56]" />
<span>Scanned Passport Scan Document</span>
</h3> </h3>
<div className="grid grid-cols-1 gap-4">
{selectedItem?.document_images?.map((imgUrl, i) => (
<div <div
key={i} onClick={() => setLightboxImage(selectedItem?.document_images[0])}
onClick={() => setLightboxImage(imgUrl)} className="relative rounded-xl overflow-hidden border border-slate-200 bg-slate-100 cursor-zoom-in shadow-inner aspect-[4/3] max-h-72 group"
className="relative group rounded-xl overflow-hidden border border-slate-200 bg-slate-100 cursor-pointer shadow-sm hover:ring-2 hover:ring-[#0F6E56]/40 transition-all aspect-[4/3]"
> >
<img <img
src={imgUrl} src={selectedItem?.document_images[0]}
alt="Document scan" alt="Verification Scan"
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" className="w-full h-full object-cover group-hover:scale-102 transition-transform duration-300"
/> />
<div className="absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center text-white text-xs font-semibold backdrop-blur-[1px]"> <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center text-white text-xs font-black uppercase tracking-wider backdrop-blur-[1px]">
<ZoomIn className="w-5 h-5 mr-1.5" /> Click to View <ZoomIn className="w-4.5 h-4.5 mr-1" /> Click to Zoom Document
</div> </div>
</div> </div>
))} <p className="text-[10px] text-slate-400 font-bold text-center uppercase tracking-wide">
</div> Type: {selectedItem?.document_type}
</div>
{/* Right: OCR Data & Status */}
<div className="space-y-6 flex flex-col justify-between">
<div className="space-y-4">
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider flex items-center justify-between">
<span>System Extracted Data</span>
<span className="text-[10px] text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-full font-medium">Confidence: 98%</span>
</h3>
<div className="space-y-2.5">
{Object.entries(ocrOverrides).map(([key, value]) => (
<div key={key} className="flex items-center justify-between p-3 bg-slate-50 rounded-xl text-sm border border-slate-100 hover:border-slate-200 transition-colors">
<span className="font-medium text-slate-600 text-xs">{key}:</span>
<span className="text-gray-900 font-semibold text-xs font-mono">{value}</span>
</div>
))}
</div>
</div>
{/* Status Info */}
<div className="space-y-4 pt-4 border-t border-slate-100">
<div className="bg-emerald-50 p-4 rounded-xl border border-emerald-100 flex items-start">
<CheckCircle className="w-5 h-5 text-emerald-600 mr-3 mt-0.5" />
<div>
<p className="text-xs font-bold text-emerald-800">Verified Automatically</p>
<p className="text-[11px] text-emerald-700 mt-1">
This profile was verified by the system on {selectedItem?.processed_at}. No further action is required from administrators.
</p> </p>
</div> </div>
{/* Warnings Box */}
{selectedItem?.warnings?.length > 0 ? (
<div className="bg-rose-50/50 border border-rose-100 p-5 rounded-2xl space-y-3 shadow-sm">
<span className="text-[10px] font-black text-rose-700 uppercase tracking-widest flex items-center gap-1.5">
<AlertTriangle className="w-4 h-4 text-rose-600 animate-bounce" />
<span>Suspicious Document Vetting Warning</span>
</span>
<ul className="space-y-2 text-xs font-bold text-rose-950">
{selectedItem.warnings.map((w, idx) => (
<li key={idx} className="flex items-center gap-2 bg-white px-3 py-2 rounded-xl border border-rose-100 shadow-xs">
<div className="w-1.5 h-1.5 bg-rose-500 rounded-full flex-shrink-0" />
<span>{w}</span>
</li>
))}
</ul>
</div>
) : (
<div className="bg-emerald-50/30 border border-emerald-100 p-4 rounded-2xl flex items-center space-x-2 text-emerald-800 text-xs font-bold">
<CheckCircle className="w-4 h-4 text-emerald-600" />
<span>Vetting passed checks. System matches signature algorithms perfectly.</span>
</div>
)}
</div> </div>
{/* OCR Data Extraction Form Column - Supports editing */}
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-6 flex flex-col justify-between">
<div className="space-y-4">
<div className="flex items-center justify-between border-b border-slate-100 pb-2">
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest">Extracted Text Vitals</h3>
<button <button
type="button" onClick={() => setIsEditingOcr(!isEditingOcr)}
onClick={() => setIsDialogOpen(false)} className="text-xs text-[#0F6E56] hover:text-[#085041] font-black flex items-center gap-1 uppercase tracking-wider text-[10px]"
className="w-full bg-slate-900 hover:bg-slate-800 text-white py-3 px-4 rounded-xl font-semibold text-xs shadow-sm flex items-center justify-center transition-colors"
> >
Close Audit Log <Edit3 className="w-3.5 h-3.5" />
<span>{isEditingOcr ? 'Cancel Edit' : 'Edit OCR Values'}</span>
</button> </button>
</div> </div>
<div className="space-y-4">
{Object.entries(ocrOverrides).map(([key, value]) => (
<div key={key} className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">{key}</label>
{isEditingOcr ? (
<input
type="text"
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs font-bold focus:bg-white outline-none focus:ring-4 focus:ring-[#0F6E56]/10 focus:border-[#0F6E56] transition-all"
value={value}
onChange={e => {
const newVal = e.target.value;
setOcrOverrides(prev => ({ ...prev, [key]: newVal }));
}}
/>
) : (
<div className="px-3.5 py-2.5 bg-slate-50 rounded-xl text-xs font-bold text-slate-800 border border-slate-100/50 font-mono tracking-wide">
{value}
</div>
)}
</div>
))}
</div>
</div>
{/* Manual Rejection reason input */}
{showRejectionForm ? (
<div className="bg-rose-50/30 p-4 border border-rose-100 rounded-xl space-y-3">
<label className="text-[10px] font-black text-rose-700 uppercase tracking-widest block">Rejection compliance reason</label>
<textarea
rows="2"
className="w-full bg-white border border-rose-200 rounded-xl p-3 text-xs font-bold focus:ring-4 focus:ring-rose-500/10 outline-none transition-all"
placeholder="Explain to worker what document error occurred (e.g., expiry date mismatch, blurred image)..."
value={rejectionReason}
onChange={e => setRejectionReason(e.target.value)}
/>
<div className="flex gap-2">
<button
onClick={() => handleVerifySubmit('reject')}
className="px-4 py-2.5 bg-red-600 hover:bg-red-700 text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md transition-colors"
>
Confirm Rejection
</button>
<button
onClick={() => setShowRejectionForm(false)}
className="px-4 py-2.5 bg-slate-200 text-slate-600 hover:bg-slate-300 rounded-xl text-[10px] font-black uppercase tracking-widest transition-colors"
>
Cancel
</button>
</div>
</div>
) : (
<div className="flex gap-3 pt-4 border-t border-slate-100 flex-shrink-0">
<button
onClick={() => handleVerifySubmit('approve')}
className="flex-1 py-3 bg-[#0F6E56] text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-lg shadow-teal-700/20 hover:bg-[#085041] transition-all flex items-center justify-center gap-1"
>
<CheckCircle className="w-4 h-4" />
<span>Approve Verification</span>
</button>
<button
onClick={() => setShowRejectionForm(true)}
className="flex-1 py-3 bg-red-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-lg shadow-red-600/10 hover:bg-red-700 transition-all flex items-center justify-center gap-1"
>
<XCircle className="w-4 h-4" />
<span>Flag Rejection</span>
</button>
</div>
)}
</div> </div>
</div> </div>
</DialogContent> </DialogContent>
@ -284,19 +464,18 @@ export default function Verifications({ verifications, status }) {
{lightboxImage && ( {lightboxImage && (
<div <div
onClick={() => setLightboxImage(null)} onClick={() => setLightboxImage(null)}
className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 animate-in fade-in duration-200" className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"
> >
<button <button
type="button" type="button"
className="absolute top-6 right-6 text-white/80 hover:text-white p-2 bg-white/10 hover:bg-white/20 rounded-full transition-all focus:outline-none" className="absolute top-6 right-6 text-white/80 hover:text-white p-2 bg-white/10 rounded-full transition-all focus:outline-none"
onClick={() => setLightboxImage(null)}
> >
<X className="w-6 h-6" /> <X className="w-6 h-6" />
</button> </button>
<img <img
src={lightboxImage} src={lightboxImage}
alt="Zoomed document" alt="Zoomed scan"
className="max-w-full max-h-[90vh] object-contain rounded-xl shadow-2xl border border-white/20 animate-in zoom-in-95 duration-200" className="max-w-full max-h-[90vh] object-contain rounded-xl shadow-2xl border border-white/20"
/> />
</div> </div>
)} )}

View File

@ -12,7 +12,12 @@ import {
Filter, Filter,
ChevronRight, ChevronRight,
MessageSquare, MessageSquare,
Info Info,
Heart,
MapPin,
Clock,
Sparkles,
BellRing
} from 'lucide-react'; } from 'lucide-react';
import { import {
Dialog, Dialog,
@ -28,6 +33,7 @@ export default function Announcements({ initialAnnouncements }) {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [toastMessage, setToastMessage] = useState(null); const [toastMessage, setToastMessage] = useState(null);
const [toastSub, setToastSub] = useState(null);
const [announcements, setAnnouncements] = useState(initialAnnouncements || []); const [announcements, setAnnouncements] = useState(initialAnnouncements || []);
useEffect(() => { useEffect(() => {
@ -37,12 +43,22 @@ export default function Announcements({ initialAnnouncements }) {
const { data: newAnnouncement, setData: setNewAnnouncement, post, processing, reset } = useForm({ const { data: newAnnouncement, setData: setNewAnnouncement, post, processing, reset } = useForm({
title: '', title: '',
content: '', content: '',
audience: 'Shortlisted' audience: 'Charity',
type: 'Charity',
provided_items: '',
event_date: '',
event_time: '',
location_details: '',
location_pin: ''
}); });
const showToast = (message) => { const showToast = (message, subMessage = null) => {
setToastMessage(message); setToastMessage(message);
setTimeout(() => setToastMessage(null), 3000); setToastSub(subMessage);
setTimeout(() => {
setToastMessage(null);
setToastSub(null);
}, 5000);
}; };
const handleCreate = (e) => { const handleCreate = (e) => {
@ -52,7 +68,10 @@ export default function Announcements({ initialAnnouncements }) {
onSuccess: () => { onSuccess: () => {
reset(); reset();
setIsDialogOpen(false); setIsDialogOpen(false);
showToast('Announcement posted successfully'); showToast(
'💖 COMMUNITY CHARITY EVENT POSTED!',
'🔔 Instant Push Notification sent to all workers in Dubai! ⏰ Morning-of reminder notification scheduled successfully.'
);
} }
}); });
}; };
@ -62,32 +81,44 @@ export default function Announcements({ initialAnnouncements }) {
ann.content.toLowerCase().includes(searchTerm.toLowerCase()) ann.content.toLowerCase().includes(searchTerm.toLowerCase())
); );
const stats = [
{ label: 'Total Announcements', value: announcements.length, icon: Megaphone, color: 'text-blue-600', bg: 'bg-blue-50' },
{ label: 'Audience Reach', value: '57 Candidates', icon: Target, color: 'text-emerald-600', bg: 'bg-emerald-50' },
{ label: 'Avg. Engagement', value: '82%', icon: CheckCircle2, color: 'text-purple-600', bg: 'bg-purple-50' },
];
return ( return (
<EmployerLayout title="Announcements"> <EmployerLayout title="Charity Events & Support Drives">
<Head title="Announcements - Employer Portal" /> <Head title="Community Charity Events - Sponsor Hub" />
{toastMessage && ( {toastMessage && (
<div className="fixed bottom-8 right-8 bg-slate-900 text-white px-6 py-4 rounded-2xl shadow-2xl flex items-center space-x-3 z-[100] animate-in slide-in-from-bottom-5"> <div className="fixed bottom-8 right-8 bg-slate-900 text-white px-6 py-5 rounded-[24px] shadow-2xl border border-slate-800 flex items-start space-x-3.5 z-[100] max-w-md animate-in slide-in-from-bottom-5 duration-350">
<div className="bg-emerald-500 p-1 rounded-full"> <div className="bg-emerald-500 p-2 rounded-xl shrink-0 mt-0.5 shadow-md shadow-emerald-500/20">
<CheckCircle2 className="w-4 h-4 text-white" /> <CheckCircle2 className="w-5 h-5 text-white" />
</div>
<div className="space-y-1">
<div className="font-black text-xs uppercase tracking-widest text-emerald-400">{toastMessage}</div>
{toastSub && <p className="text-[11px] text-slate-350 leading-relaxed font-semibold">{toastSub}</p>}
</div> </div>
<span className="font-bold text-xs uppercase tracking-widest">{toastMessage}</span>
</div> </div>
)} )}
<div className="space-y-6 select-none max-w-5xl mx-auto"> <div className="space-y-6 select-none max-w-5xl mx-auto">
{/* Community Drive & Charity Banner */}
<div className="bg-gradient-to-br from-rose-500/10 via-pink-500/5 to-transparent border border-rose-200 rounded-[32px] p-6 sm:p-8 flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="space-y-2 max-w-2xl">
<div className="inline-flex items-center space-x-2 px-3 py-1 bg-rose-50 border border-rose-100 rounded-full text-[10px] font-black uppercase tracking-wider text-rose-700">
<Heart className="w-3.5 h-3.5 fill-rose-500 text-rose-500 animate-pulse" />
<span>Dubai Community Support Drives</span>
</div>
<h2 className="text-xl font-black text-slate-900 tracking-tight">Free Medical Checks, Food Drives & Support Services</h2>
<p className="text-xs text-slate-500 font-bold leading-relaxed">
Organizing a support program? Share community campaigns directly. The platform will automatically push an **instant pop-up notification** to workers, followed by a **morning-of event reminder** to keep attendance strong.
</p>
</div>
</div>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white p-6 rounded-[32px] border border-slate-100 shadow-sm"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white p-6 rounded-[32px] border border-slate-100 shadow-sm">
<div className="relative flex-1 max-w-md group"> <div className="relative flex-1 max-w-md group">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" /> <Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
<input <input
type="text" type="text"
placeholder="Search announcements..." placeholder="Search charity events & drives..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none" className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
@ -96,38 +127,82 @@ export default function Announcements({ initialAnnouncements }) {
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<button className="bg-[#185FA5] hover:bg-[#144f8a] text-white px-8 py-3 rounded-2xl font-black text-xs uppercase tracking-widest flex items-center justify-center space-x-2 transition-all shadow-lg shadow-blue-200/50 hover:-translate-y-1 active:translate-y-0"> <button className="bg-rose-600 hover:bg-rose-700 text-white px-8 py-3.5 rounded-2xl font-black text-xs uppercase tracking-widest flex items-center justify-center space-x-2 transition-all shadow-lg shadow-rose-200/50 hover:-translate-y-1 active:translate-y-0">
<Megaphone className="w-4 h-4" /> <Heart className="w-4 h-4 fill-white" />
<span>New Announcement</span> <span>Post Charity Event</span>
</button> </button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[500px] rounded-3xl p-0 overflow-hidden border-none shadow-2xl"> <DialogContent className="sm:max-w-[550px] rounded-3xl p-0 overflow-hidden border-none shadow-2xl">
<div className="bg-[#185FA5] p-8 text-white relative"> <div className="bg-gradient-to-r from-rose-500 to-pink-600 p-8 text-white relative">
<div className="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -mr-16 -mt-16 blur-2xl" /> <div className="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -mr-16 -mt-16 blur-2xl" />
<DialogTitle className="text-2xl font-black relative z-10">Post Announcement</DialogTitle> <DialogTitle className="text-2xl font-black relative z-10">Post Charity Event</DialogTitle>
<DialogDescription className="text-blue-100 font-medium mt-2 relative z-10"> <DialogDescription className="text-pink-100 font-semibold mt-2 relative z-10">
This will be visible to your selected candidates immediately. Broadcast a support program, medical camp, or distribution drive to the worker community.
</DialogDescription> </DialogDescription>
</div> </div>
<form onSubmit={handleCreate} className="p-8 space-y-6 bg-white"> <form onSubmit={handleCreate} className="p-8 space-y-5 bg-white max-h-[80vh] overflow-y-auto">
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Target Audience</label> <div className="space-y-4 p-4 bg-rose-50/50 rounded-2xl border border-rose-100 animate-in fade-in duration-200">
<div className="text-[10px] font-black text-rose-800 uppercase tracking-widest flex items-center gap-1.5">
<Heart className="w-3.5 h-3.5 fill-rose-500 text-rose-500" />
<span>Charity Drive Metadata (Dubai Support)</span>
</div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{['Shortlisted', 'Selected Candidates'].map(aud => ( <div className="space-y-1">
<button <label className="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1">Event Date</label>
key={aud} <input
type="button" type="date"
onClick={() => setNewAnnouncement({ ...newAnnouncement, audience: aud })} required
className={`px-4 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest border transition-all ${ value={newAnnouncement.event_date}
newAnnouncement.audience === aud onChange={(e) => setNewAnnouncement({ ...newAnnouncement, event_date: e.target.value })}
? 'bg-blue-50 border-[#185FA5] text-[#185FA5]' className="w-full px-3 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold focus:ring-2 focus:ring-rose-100 outline-none"
: 'bg-slate-50 border-slate-200 text-slate-500 hover:bg-white' />
}`} </div>
> <div className="space-y-1">
{aud} <label className="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1">Event Time</label>
</button> <input
))} type="text"
required
placeholder="e.g. 9:00 AM - 4:00 PM"
value={newAnnouncement.event_time}
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, event_time: e.target.value })}
className="w-full px-3 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold focus:ring-2 focus:ring-rose-100 outline-none"
/>
</div>
</div>
<div className="space-y-1">
<label className="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1">What is Being Provided</label>
<input
type="text"
required
placeholder="e.g. Free Medical Check, Food Boxes, Supplies"
value={newAnnouncement.provided_items}
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, provided_items: e.target.value })}
className="w-full px-3.5 py-2.5 bg-white border border-slate-200 rounded-xl text-xs font-bold focus:ring-2 focus:ring-rose-100 outline-none"
/>
</div>
<div className="space-y-1">
<label className="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1">Location Details & Pin URL</label>
<input
type="text"
required
placeholder="e.g. Al Quoz Community Center, Dubai"
value={newAnnouncement.location_details}
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, location_details: e.target.value })}
className="w-full px-3.5 py-2.5 bg-white border border-slate-200 rounded-xl text-xs font-bold focus:ring-2 focus:ring-rose-100 outline-none mb-2"
/>
<input
type="url"
required
placeholder="Google Maps Pin Link (e.g. https://maps.app.goo.gl/xyz)"
value={newAnnouncement.location_pin}
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, location_pin: e.target.value })}
className="w-full px-3.5 py-2.5 bg-white border border-slate-200 rounded-xl text-xs font-bold focus:ring-2 focus:ring-rose-100 outline-none"
/>
</div> </div>
</div> </div>
@ -139,29 +214,30 @@ export default function Announcements({ initialAnnouncements }) {
value={newAnnouncement.title} value={newAnnouncement.title}
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, title: e.target.value })} onChange={(e) => setNewAnnouncement({ ...newAnnouncement, title: e.target.value })}
className="w-full px-5 py-3.5 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none" className="w-full px-5 py-3.5 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
placeholder="e.g. Schedule for Video Interview" placeholder="e.g. Free Dental checkup by Emirates Charity"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Message Content</label> <label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Event Description & Instructions</label>
<textarea <textarea
required required
rows="4" rows="3"
value={newAnnouncement.content} value={newAnnouncement.content}
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, content: e.target.value })} onChange={(e) => setNewAnnouncement({ ...newAnnouncement, content: e.target.value })}
className="w-full px-5 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none resize-none" className="w-full px-5 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none resize-none"
placeholder="Enter your message..." placeholder="Enter details of the charity event here..."
></textarea> ></textarea>
</div> </div>
<DialogFooter className="pt-4"> <DialogFooter className="pt-2">
<button <button
type="submit" type="submit"
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white py-4 rounded-2xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-2 transition-all shadow-lg shadow-blue-200/50" disabled={processing}
className="w-full bg-rose-600 hover:bg-rose-700 text-white py-4 rounded-2xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-2 transition-all shadow-lg shadow-rose-200/50"
> >
<Send className="w-4 h-4" /> <Send className="w-4 h-4" />
<span>Post Announcement</span> <span>Publish Charity Event</span>
</button> </button>
</DialogFooter> </DialogFooter>
</form> </form>
@ -169,7 +245,7 @@ export default function Announcements({ initialAnnouncements }) {
</Dialog> </Dialog>
</div> </div>
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-5">
{filteredAnnouncements.length === 0 ? ( {filteredAnnouncements.length === 0 ? (
<div className="text-center py-24 bg-white rounded-[40px] border border-slate-200 shadow-sm text-slate-500 border-dashed"> <div className="text-center py-24 bg-white rounded-[40px] border border-slate-200 shadow-sm text-slate-500 border-dashed">
<div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-6"> <div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-6">
@ -179,36 +255,100 @@ export default function Announcements({ initialAnnouncements }) {
<p className="font-bold text-xs text-slate-400 mt-1 uppercase tracking-widest">You haven't posted any announcements yet</p> <p className="font-bold text-xs text-slate-400 mt-1 uppercase tracking-widest">You haven't posted any announcements yet</p>
</div> </div>
) : ( ) : (
filteredAnnouncements.map((ann) => ( filteredAnnouncements.map((ann) => {
<div key={ann.id} className="group bg-white rounded-[32px] border border-slate-100 p-6 shadow-sm hover:shadow-md transition-all duration-300 relative overflow-hidden flex flex-col md:flex-row md:items-start gap-6"> const details = ann.charityDetails;
<div className="flex-1 space-y-3">
return (
<div
key={ann.id}
className="group bg-white rounded-[32px] border border-rose-100 bg-rose-50/10 p-6 shadow-sm hover:shadow-md transition-all duration-300 relative overflow-hidden flex flex-col md:flex-row md:items-start gap-6"
>
<div className="absolute right-0 top-0 w-24 h-24 bg-rose-500/5 rounded-full blur-2xl pointer-events-none" />
<div className="flex-1 space-y-4">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<span className={`px-3 py-1.5 rounded-xl text-[9px] font-black uppercase tracking-widest border ${ <span className="px-3 py-1.5 rounded-xl text-[9px] font-black uppercase tracking-widest border bg-rose-50 border-rose-100 text-rose-700 flex items-center space-x-1">
ann.audience === 'Selected Candidates' <Heart className="w-3 h-3 fill-rose-500 text-rose-500" />
? 'bg-emerald-50 border-emerald-100 text-emerald-700' <span>COMMUNITY CHARITY DRIVE</span>
: 'bg-blue-50 border-blue-100 text-[#185FA5]'
}`}>
To: {ann.audience}
</span> </span>
<div className="flex items-center text-slate-400 text-[10px] font-black uppercase tracking-widest"> <div className="flex items-center text-slate-400 text-[10px] font-black uppercase tracking-widest">
<Calendar className="w-3.5 h-3.5 mr-1.5" /> <Calendar className="w-3.5 h-3.5 mr-1.5" />
{ann.created_at} {ann.created_at}
</div> </div>
</div>
<div className="space-y-1"> <div className="inline-flex items-center space-x-1 px-2.5 py-1 bg-emerald-50 rounded-lg text-[9px] font-bold text-emerald-800 border border-emerald-100">
<h4 className="text-lg font-black text-slate-900 tracking-tight">{ann.title}</h4> <BellRing className="w-3 h-3 text-emerald-600 animate-bounce" />
<p className="text-sm font-medium text-slate-600 leading-relaxed">{ann.content}</p> <span>Push & Reminder Scheduled</span>
</div> </div>
</div> </div>
<div className="flex items-center space-x-3 md:border-l md:border-slate-50 md:pl-6 shrink-0"> <div className="space-y-2">
<button className="p-3 rounded-2xl bg-slate-50 text-slate-400 hover:text-[#185FA5] transition-all"> <h4 className="text-lg font-black text-slate-900 tracking-tight flex items-center gap-2">
<Sparkles className="w-4 h-4 text-rose-500" />
{ann.title}
</h4>
<p className="text-xs text-slate-600 font-bold leading-relaxed">{ann.content}</p>
</div>
{details && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 p-4 bg-white/80 rounded-2xl border border-rose-100/60 text-xs font-bold text-slate-600">
<div className="flex items-center space-x-2.5">
<div className="w-8 h-8 rounded-lg bg-rose-50 flex items-center justify-center text-rose-600 shrink-0">
<Heart className="w-4 h-4 fill-rose-500 text-rose-500" />
</div>
<div>
<div className="text-[9px] text-slate-400 uppercase font-black tracking-widest">Provided Packages</div>
<div className="text-slate-800 text-[11px] font-extrabold mt-0.5">{details.provided_items}</div>
</div>
</div>
<div className="flex items-center space-x-2.5">
<div className="w-8 h-8 rounded-lg bg-amber-50 flex items-center justify-center text-amber-600 shrink-0">
<Clock className="w-4 h-4" />
</div>
<div>
<div className="text-[9px] text-slate-400 uppercase font-black tracking-widest">Event Timing</div>
<div className="text-slate-800 text-[11px] font-extrabold mt-0.5">
{details.event_date} {details.event_time}
</div>
</div>
</div>
<div className="flex items-center space-x-2.5 md:col-span-2 pt-2 border-t border-slate-100 mt-1">
<div className="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center text-[#185FA5] shrink-0">
<MapPin className="w-4 h-4" />
</div>
<div className="flex-1 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div>
<div className="text-[9px] text-slate-400 uppercase font-black tracking-widest">Event Location Address</div>
<div className="text-slate-800 text-[11px] font-extrabold mt-0.5">{details.location_details}</div>
</div>
{details.location_pin && (
<a
href={details.location_pin}
target="_blank"
rel="noreferrer"
className="px-3.5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-xl text-[10px] font-black uppercase tracking-wider flex items-center space-x-1 w-fit transition-colors shrink-0 shadow-sm"
>
<MapPin className="w-3.5 h-3.5" />
<span>View Map Location Pin</span>
</a>
)}
</div>
</div>
</div>
)}
</div>
<div className="flex items-center space-x-3 md:border-l md:border-slate-50 md:pl-6 shrink-0 h-full self-center">
<button className="p-3 rounded-2xl bg-slate-50 text-slate-400 hover:text-rose-600 transition-all">
<ChevronRight className="w-5 h-5" /> <ChevronRight className="w-5 h-5" />
</button> </button>
</div> </div>
</div> </div>
)) );
})
)} )}
</div> </div>
</div> </div>

View File

@ -104,26 +104,32 @@ export default function CreatePassword() {
}; };
const renderStepIndicator = () => ( const renderStepIndicator = () => (
<div className="flex items-center justify-between mb-6 relative max-w-xs mx-auto px-2 select-none w-48"> <div className="w-full flex items-center justify-center space-x-1 sm:space-x-2 select-none pb-4 border-b border-slate-100">
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-slate-100 -translate-y-1/2 -z-10" />
{[ {[
{ step: 1, label: 'Register' }, { step: 1, label: 'Register' },
{ step: 2, label: 'Verify' }, { step: 2, label: 'Verify' },
{ step: 3, label: 'Password' } { step: 3, label: 'Payment' },
{ step: 4, label: 'Password' }
].map((s) => ( ].map((s) => (
<div key={s.step} className="flex flex-col items-center"> <React.Fragment key={s.step}>
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs border-2 transition-all duration-300 ${ <div className="flex items-center space-x-1 sm:space-x-1.5">
s.step === 3 <div className={`w-5 h-5 rounded-full flex items-center justify-center font-bold text-[10px] border transition-all duration-300 ${
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-sm' s.step === 4
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-xs'
: 'bg-blue-50 text-[#185FA5] border-blue-200' : 'bg-blue-50 text-[#185FA5] border-blue-200'
}`}> }`}>
{s.step} {s.step}
</div> </div>
<span className={`text-[9px] font-semibold uppercase mt-1 tracking-wider ${ <span className={`text-[10px] font-bold tracking-tight ${
s.step === 3 ? 'text-[#185FA5]' : 'text-slate-400' s.step === 4 ? 'text-[#185FA5]' : 'text-slate-400'
}`}>{s.label}</span> }`}>
{s.label}
</span>
</div> </div>
{s.step < 4 && (
<div className="w-4 sm:w-6 h-0.5 bg-slate-100" />
)}
</React.Fragment>
))} ))}
</div> </div>
); );
@ -194,13 +200,12 @@ export default function CreatePassword() {
{/* Right Set Password Form */} {/* Right Set Password Form */}
<div className="col-span-1 lg:col-span-7 flex flex-col justify-center items-center p-6 sm:p-12 overflow-y-auto"> <div className="col-span-1 lg:col-span-7 flex flex-col justify-center items-center p-6 sm:p-12 overflow-y-auto">
<div className="w-full max-w-lg bg-white rounded-2xl shadow-sm border border-slate-200 p-8 space-y-6"> <div className="w-full max-w-lg bg-white rounded-2xl shadow-sm border border-slate-200 p-8 space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> {renderStepIndicator()}
<div> <div>
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">Set Password</h2> <h2 className="text-2xl font-bold text-gray-900 tracking-tight">Set Password</h2>
<p className="text-xs text-gray-500 mt-1">Configure your portal access password</p> <p className="text-xs text-gray-500 mt-1">Configure your portal access password</p>
</div> </div>
{renderStepIndicator()}
</div>
<form onSubmit={handleSubmit} noValidate className="space-y-4"> <form onSubmit={handleSubmit} noValidate className="space-y-4">
{/* Password input */} {/* Password input */}

View File

@ -11,7 +11,6 @@ import {
export default function Register() { export default function Register() {
const [data, setData] = useState({ const [data, setData] = useState({
company_name: '',
name: '', name: '',
email: '', email: '',
phone: '', phone: '',
@ -30,12 +29,8 @@ export default function Register() {
const validateForm = () => { const validateForm = () => {
const newErrors = {}; const newErrors = {};
if (!data.company_name.trim()) {
newErrors.company_name = 'Company/household name is required.';
}
if (!data.name.trim()) { if (!data.name.trim()) {
newErrors.name = 'Contact name is required.'; newErrors.name = 'Sponsor name is required.';
} }
if (!data.email.trim()) { if (!data.email.trim()) {
@ -90,26 +85,34 @@ export default function Register() {
}; };
const renderStepIndicator = () => ( const renderStepIndicator = () => (
<div className="flex items-center justify-between mb-6 relative max-w-xs mx-auto px-2 select-none w-48"> <div className="w-full flex items-center justify-center space-x-1 sm:space-x-2 select-none pb-4 border-b border-slate-100">
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-slate-100 -translate-y-1/2 -z-10" />
{[ {[
{ step: 1, label: 'Register' }, { step: 1, label: 'Register' },
{ step: 2, label: 'Verify' }, { step: 2, label: 'Verify' },
{ step: 3, label: 'Password' } { step: 3, label: 'Payment' },
{ step: 4, label: 'Password' }
].map((s) => ( ].map((s) => (
<div key={s.step} className="flex flex-col items-center"> <React.Fragment key={s.step}>
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs border-2 transition-all duration-300 ${ <div className="flex items-center space-x-1 sm:space-x-1.5">
<div className={`w-5 h-5 rounded-full flex items-center justify-center font-bold text-[10px] border transition-all duration-300 ${
s.step === 1 s.step === 1
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-sm' ? 'bg-[#185FA5] text-white border-[#185FA5] shadow-xs'
: s.step < 1
? 'bg-blue-50 text-[#185FA5] border-blue-200'
: 'bg-white text-slate-400 border-slate-200' : 'bg-white text-slate-400 border-slate-200'
}`}> }`}>
{s.step} {s.step}
</div> </div>
<span className={`text-[9px] font-semibold uppercase mt-1 tracking-wider ${ <span className={`text-[10px] font-bold tracking-tight ${
s.step === 1 ? 'text-[#185FA5]' : 'text-slate-400' s.step === 1 ? 'text-[#185FA5]' : 'text-slate-400'
}`}>{s.label}</span> }`}>
{s.label}
</span>
</div> </div>
{s.step < 4 && (
<div className="w-4 sm:w-6 h-0.5 bg-slate-100" />
)}
</React.Fragment>
))} ))}
</div> </div>
); );
@ -172,96 +175,73 @@ export default function Register() {
{/* Right Registration Form */} {/* Right Registration Form */}
<div className="col-span-1 lg:col-span-7 flex flex-col justify-center items-center p-6 sm:p-12 overflow-y-auto"> <div className="col-span-1 lg:col-span-7 flex flex-col justify-center items-center p-6 sm:p-12 overflow-y-auto">
<div className="w-full max-w-lg bg-white rounded-2xl shadow-sm border border-slate-200 p-8 space-y-6"> <div className="w-full max-w-lg bg-white rounded-2xl shadow-sm border border-slate-200 p-8 space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> {renderStepIndicator()}
<div> <div>
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">Create Account</h2> <h2 className="text-2xl font-bold text-gray-900 tracking-tight">Create Account</h2>
<p className="text-xs text-gray-500 mt-1">Register your employer portal to start searching</p> <p className="text-xs text-gray-500 mt-1">Register your employer portal to start searching</p>
</div> </div>
{renderStepIndicator()}
</div>
<form onSubmit={handleSubmit} noValidate className="space-y-4"> <form onSubmit={handleSubmit} noValidate className="space-y-4">
{/* Company Name */} {/* Company Name */}
{/* Sponsor Name */}
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1">Company / Household Name</label> <label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Sponsor Name</label>
<input
type="text"
value={data.company_name}
onChange={(e) => handleInputChange('company_name', e.target.value)}
placeholder="e.g. Al Mansoor Household"
className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 ${
errors.company_name
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'
: 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]'
}`}
/>
{errors.company_name && (
<p className="mt-1 text-xs text-red-500">
{Array.isArray(errors.company_name) ? errors.company_name[0] : errors.company_name}
</p>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Contact Name */}
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Contact Name</label>
<input <input
type="text" type="text"
value={data.name} value={data.name}
onChange={(e) => handleInputChange('name', e.target.value)} onChange={(e) => handleInputChange('name', e.target.value)}
placeholder="e.g. Abdullah Bin Ahmed" placeholder="e.g. Abdullah Bin Ahmed"
className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 ${ className={`w-full px-3.5 py-3 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all ${
errors.name errors.name
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500' ? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'
: 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]' : 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]'
}`} }`}
/> />
{errors.name && ( {errors.name && (
<p className="mt-1 text-xs text-red-500"> <p className="mt-1.5 text-xs text-red-500 font-medium">
{Array.isArray(errors.name) ? errors.name[0] : errors.name} {Array.isArray(errors.name) ? errors.name[0] : errors.name}
</p> </p>
)} )}
</div> </div>
{/* Work Email */} {/* Email */}
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1">Work Email</label> <label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Email</label>
<input <input
type="email" type="email"
value={data.email} value={data.email}
onChange={(e) => handleInputChange('email', e.target.value)} onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="employer@domain.com" placeholder="employer@domain.com"
className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 ${ className={`w-full px-3.5 py-3 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all ${
errors.email errors.email
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500' ? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'
: 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]' : 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]'
}`} }`}
/> />
{errors.email && ( {errors.email && (
<p className="mt-1 text-xs text-red-500"> <p className="mt-1.5 text-xs text-red-500 font-medium">
{Array.isArray(errors.email) ? errors.email[0] : errors.email} {Array.isArray(errors.email) ? errors.email[0] : errors.email}
</p> </p>
)} )}
</div> </div>
</div>
{/* Mobile Number */} {/* Mobile Number */}
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1">Mobile Number</label> <label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Mobile Number</label>
<input <input
type="tel" type="tel"
value={data.phone} value={data.phone}
onChange={(e) => handleInputChange('phone', e.target.value)} onChange={(e) => handleInputChange('phone', e.target.value)}
placeholder="e.g. +971501234567" placeholder="e.g. +971501234567"
className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 ${ className={`w-full px-3.5 py-3 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all ${
errors.phone errors.phone
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500' ? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'
: 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]' : 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]'
}`} }`}
/> />
{errors.phone && ( {errors.phone && (
<p className="mt-1 text-xs text-red-500"> <p className="mt-1.5 text-xs text-red-500 font-medium">
{Array.isArray(errors.phone) ? errors.phone[0] : errors.phone} {Array.isArray(errors.phone) ? errors.phone[0] : errors.phone}
</p> </p>
)} )}

View File

@ -0,0 +1,313 @@
import React, { useState } from 'react';
import { Head, Link, router } from '@inertiajs/react';
import axios from 'axios';
import { toast } from 'sonner';
import {
CreditCard,
Check,
X,
Lock,
ArrowRight,
Users,
DollarSign,
MessageSquare,
Loader2,
Sparkles,
ShieldCheck
} from 'lucide-react';
export default function RegisterPayment({ email, plans }) {
const [selectedPlan, setSelectedPlan] = useState(null);
const [showPayTabsModal, setShowPayTabsModal] = useState(false);
const [loading, setLoading] = useState(false);
// Card inputs for PayTabs Gateway
const [cardName, setCardName] = useState('');
const [cardNumber, setCardNumber] = useState('');
const [cardExpiry, setCardExpiry] = useState('');
const [cardCvv, setCardCvv] = useState('');
const handleSelectPlan = (plan) => {
setSelectedPlan(plan);
setShowPayTabsModal(true);
};
const handlePayTabsSubmit = async (e) => {
e.preventDefault();
if (cardNumber.length < 16) {
toast.error("💳 Invalid card number format");
return;
}
setLoading(true);
setShowPayTabsModal(false);
try {
const simulatedTxnId = 'TXN-' + Math.random().toString(36).substr(2, 9).toUpperCase();
await axios.post('/employer/register-payment', {
plan_id: selectedPlan.id,
amount_aed: parseFloat(selectedPlan.price),
paytabs_transaction_id: simulatedTxnId
});
toast.success(`🎉 Payment Approved by PayTabs!`, {
description: `Successfully registered for ${selectedPlan.name}. Now let's finalize your password setup.`,
duration: 5000,
});
router.visit('/employer/create-password');
} catch (err) {
toast.error("Payment failed. Please try again.");
} finally {
setLoading(false);
}
};
const renderStepIndicator = () => (
<div className="w-full flex items-center justify-center space-x-1 sm:space-x-2 select-none pb-4 border-b border-slate-100">
{[
{ step: 1, label: 'Register' },
{ step: 2, label: 'Verify' },
{ step: 3, label: 'Payment' },
{ step: 4, label: 'Password' }
].map((s) => (
<React.Fragment key={s.step}>
<div className="flex items-center space-x-1 sm:space-x-1.5">
<div className={`w-5 h-5 rounded-full flex items-center justify-center font-bold text-[10px] border transition-all duration-300 ${
s.step === 3
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-xs'
: s.step < 3
? 'bg-blue-50 text-[#185FA5] border-blue-200'
: 'bg-white text-slate-400 border-slate-200'
}`}>
{s.step}
</div>
<span className={`text-[10px] font-bold tracking-tight ${
s.step === 3 ? 'text-[#185FA5]' : 'text-slate-400'
}`}>
{s.label}
</span>
</div>
{s.step < 4 && (
<div className="w-4 sm:w-6 h-0.5 bg-slate-100" />
)}
</React.Fragment>
))}
</div>
);
return (
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-slate-50 font-sans">
<Head title="Purchase Annual Subscription - Migrant Portal" />
{/* Left Brand Panel */}
<div className="hidden lg:flex lg:col-span-5 bg-[#185FA5] p-12 flex-col justify-between text-white relative overflow-hidden select-none">
<div className="absolute inset-0 bg-gradient-to-br from-blue-600/20 to-black/30 pointer-events-none" />
<div className="relative z-10 space-y-6">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-white text-[#185FA5] rounded-xl flex items-center justify-center font-bold text-xl shadow-md">
M
</div>
<span className="font-bold text-2xl tracking-tight">Migrant Portal</span>
</div>
<div className="space-y-3 pt-6">
<span className="inline-block px-3 py-1 bg-white/10 border border-white/20 rounded-full text-xs font-semibold uppercase tracking-wider">
Step 3: Annual Pass
</span>
<h1 className="text-4xl font-extrabold tracking-tight leading-tight">
Purchase annual subscription to access.
</h1>
<p className="text-blue-100 text-sm font-medium">
Unlock candidate contact details, direct messages, and complete MOHRE TADBEER compliant digital dossiers.
</p>
</div>
{/* Stat Row */}
<div className="grid grid-cols-3 gap-4 pt-8 border-t border-white/10">
<div className="bg-white/10 backdrop-blur-sm p-4 rounded-xl border border-white/10 text-center space-y-1">
<Users className="w-6 h-6 text-emerald-300 mx-auto" />
<div className="font-bold text-lg">500+</div>
<div className="text-[10px] text-blue-100 uppercase tracking-wider font-semibold">Verified Workers</div>
</div>
<div className="bg-white/10 backdrop-blur-sm p-4 rounded-xl border border-white/10 text-center space-y-1">
<DollarSign className="w-6 h-6 text-emerald-300 mx-auto" />
<div className="font-bold text-lg">Premium</div>
<div className="text-[10px] text-blue-100 uppercase tracking-wider font-semibold">Sponsor Access</div>
</div>
<div className="bg-white/10 backdrop-blur-sm p-4 rounded-xl border border-white/10 text-center space-y-1">
<MessageSquare className="w-6 h-6 text-emerald-300 mx-auto" />
<div className="font-bold text-lg">Direct</div>
<div className="text-[10px] text-blue-100 uppercase tracking-wider font-semibold">Messaging</div>
</div>
</div>
</div>
<div className="relative z-10 text-xs text-blue-200 font-medium">
© 2026 Migrant. All rights reserved.
</div>
</div>
{/* Right Payment View */}
<div className="col-span-1 lg:col-span-7 flex flex-col justify-center items-center p-6 sm:p-12 overflow-y-auto">
<div className="w-full max-w-2xl bg-white rounded-3xl shadow-sm border border-slate-200 p-8 space-y-6">
{renderStepIndicator()}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pb-4">
<div>
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">Sponsor Subscription</h2>
<p className="text-xs text-gray-500 mt-1">Select and purchase a premium pass</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2">
{plans.map((plan) => (
<div
key={plan.id}
className="bg-white rounded-2xl border border-slate-200 hover:border-slate-300 shadow-sm flex flex-col justify-between overflow-hidden relative p-5 space-y-4"
>
<div>
<h3 className="font-bold text-sm text-slate-800">{plan.name}</h3>
<div className="mt-2 flex items-baseline space-x-1">
<span className="text-xl font-black text-slate-900 tracking-tight">{plan.price}</span>
<span className="text-[10px] font-semibold text-slate-400">/ month</span>
</div>
<ul className="space-y-2 pt-4 border-t border-slate-100 mt-4">
{plan.features.slice(0, 3).map(f => (
<li key={f} className="flex items-start space-x-2 text-[10px] text-slate-600 font-semibold">
<Check className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" />
<span>{f}</span>
</li>
))}
</ul>
</div>
<button
type="button"
onClick={() => handleSelectPlan(plan)}
className="w-full h-9 rounded-lg bg-[#185FA5] hover:bg-[#144f8a] text-white font-bold text-[10px] flex items-center justify-center transition-all shadow-xs mt-4"
>
Select Plan Tier
</button>
</div>
))}
</div>
</div>
</div>
{/* Interactive PayTabs Checkout Modal Overlay */}
{showPayTabsModal && selectedPlan && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-3xl w-full max-w-md border border-slate-200 shadow-2xl p-6 relative animate-zoom-in space-y-6">
<button
onClick={() => setShowPayTabsModal(false)}
className="absolute top-4 right-4 p-2 bg-slate-50 hover:bg-slate-100 rounded-full text-slate-400 hover:text-slate-600 transition-colors"
>
<X className="w-5 h-5" />
</button>
{/* PayTabs Header */}
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<div className="flex items-center space-x-2">
<CreditCard className="w-6 h-6 text-red-600" />
<span className="font-extrabold text-sm text-slate-900 tracking-tight uppercase">PayTabs Secured Checkout</span>
</div>
<span className="text-[8px] bg-red-50 text-red-700 px-2 py-0.5 rounded font-black tracking-widest uppercase">
Gateway v4.0
</span>
</div>
{/* Order Summary */}
<div className="bg-slate-50 p-4 rounded-2xl border border-slate-200/60 flex justify-between items-center text-xs font-bold text-slate-700">
<div>
<span className="text-[10px] text-slate-400 uppercase tracking-widest">Order description</span>
<div className="text-slate-900">{selectedPlan.name} Subscription</div>
</div>
<div className="text-right">
<span className="text-[10px] text-slate-400 uppercase tracking-widest">Total amount</span>
<div className="text-emerald-700 text-sm font-black">{selectedPlan.price}</div>
</div>
</div>
{/* PayTabs CC Form */}
<form onSubmit={handlePayTabsSubmit} className="space-y-4">
<div className="space-y-3.5 text-xs">
<div>
<label className="block font-bold text-slate-700 mb-1">Cardholder Name</label>
<input
type="text"
placeholder="e.g. Fatima Al Mansoori"
value={cardName}
onChange={(e) => setCardName(e.target.value)}
className="w-full p-2.5 border border-slate-300 rounded-xl"
required
/>
</div>
<div>
<label className="block font-bold text-slate-700 mb-1">Card Number</label>
<input
type="text"
maxLength="16"
placeholder="1234 5678 9101 1121"
value={cardNumber}
onChange={(e) => setCardNumber(e.target.value.replace(/\D/g, ''))}
className="w-full p-2.5 border border-slate-300 rounded-xl font-mono text-sm"
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block font-bold text-slate-700 mb-1">Expiry Date (MM/YY)</label>
<input
type="text"
placeholder="12/28"
value={cardExpiry}
onChange={(e) => setCardExpiry(e.target.value)}
className="w-full p-2.5 border border-slate-300 rounded-xl"
required
/>
</div>
<div>
<label className="block font-bold text-slate-700 mb-1">Security Code (CVV)</label>
<input
type="password"
maxLength="3"
placeholder="***"
value={cardCvv}
onChange={(e) => setCardCvv(e.target.value.replace(/\D/g, ''))}
className="w-full p-2.5 border border-slate-300 rounded-xl font-mono text-sm"
required
/>
</div>
</div>
</div>
<div className="pt-4 border-t border-slate-100 flex items-center justify-between text-[10px] text-slate-500">
<span className="flex items-center space-x-1 font-bold">
<Lock className="w-3.5 h-3.5 text-emerald-600" />
<span>256-bit PCI DSS Cryptography Protected</span>
</span>
</div>
<button
type="submit"
className="w-full bg-red-600 hover:bg-red-700 text-white py-3.5 rounded-2xl font-black text-xs uppercase tracking-widest shadow-lg shadow-red-500/10 flex items-center justify-center space-x-2"
>
<span>Authorize Payment</span>
<ArrowRight className="w-4 h-4" />
</button>
</form>
</div>
</div>
)}
</div>
);
}

View File

@ -50,8 +50,8 @@ export default function VerifyEmail({ email }) {
try { try {
await axios.post('/employer/verify-email', { otp }); await axios.post('/employer/verify-email', { otp });
toast.success('Email verified successfully! Let\'s secure your account.'); toast.success('Email verified successfully! Let\'s choose a subscription plan.');
router.visit('/employer/create-password'); router.visit('/employer/register-payment');
} catch (err) { } catch (err) {
if (err.response) { if (err.response) {
if (err.response.status === 422 || err.response.status === 400) { if (err.response.status === 422 || err.response.status === 400) {
@ -95,28 +95,34 @@ export default function VerifyEmail({ email }) {
}; };
const renderStepIndicator = () => ( const renderStepIndicator = () => (
<div className="flex items-center justify-between mb-6 relative max-w-xs mx-auto px-2 select-none"> <div className="w-full flex items-center justify-center space-x-1 sm:space-x-2 select-none pb-4 border-b border-slate-100">
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-slate-100 -translate-y-1/2 -z-10" />
{[ {[
{ step: 1, label: 'Register' }, { step: 1, label: 'Register' },
{ step: 2, label: 'Verify' }, { step: 2, label: 'Verify' },
{ step: 3, label: 'Password' } { step: 3, label: 'Payment' },
{ step: 4, label: 'Password' }
].map((s) => ( ].map((s) => (
<div key={s.step} className="flex flex-col items-center"> <React.Fragment key={s.step}>
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs border-2 transition-all duration-300 ${ <div className="flex items-center space-x-1 sm:space-x-1.5">
<div className={`w-5 h-5 rounded-full flex items-center justify-center font-bold text-[10px] border transition-all duration-300 ${
s.step === 2 s.step === 2
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-sm' ? 'bg-[#185FA5] text-white border-[#185FA5] shadow-xs'
: s.step === 1 : s.step < 2
? 'bg-blue-50 text-[#185FA5] border-blue-200' ? 'bg-blue-50 text-[#185FA5] border-blue-200'
: 'bg-white text-slate-400 border-slate-200' : 'bg-white text-slate-400 border-slate-200'
}`}> }`}>
{s.step} {s.step}
</div> </div>
<span className={`text-[9px] font-semibold uppercase mt-1 tracking-wider ${ <span className={`text-[10px] font-bold tracking-tight ${
s.step === 2 ? 'text-[#185FA5]' : 'text-slate-400' s.step === 2 ? 'text-[#185FA5]' : 'text-slate-400'
}`}>{s.label}</span> }`}>
{s.label}
</span>
</div> </div>
{s.step < 4 && (
<div className="w-4 sm:w-6 h-0.5 bg-slate-100" />
)}
</React.Fragment>
))} ))}
</div> </div>
); );
@ -179,13 +185,12 @@ export default function VerifyEmail({ email }) {
{/* Right Verify Form */} {/* Right Verify Form */}
<div className="col-span-1 lg:col-span-7 flex flex-col justify-center items-center p-6 sm:p-12 overflow-y-auto"> <div className="col-span-1 lg:col-span-7 flex flex-col justify-center items-center p-6 sm:p-12 overflow-y-auto">
<div className="w-full max-w-lg bg-white rounded-2xl shadow-sm border border-slate-200 p-8 space-y-6"> <div className="w-full max-w-lg bg-white rounded-2xl shadow-sm border border-slate-200 p-8 space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> {renderStepIndicator()}
<div> <div>
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">Verify Email</h2> <h2 className="text-2xl font-bold text-gray-900 tracking-tight">Verify Email</h2>
<p className="text-xs text-gray-500 mt-1">Verify your employer registration</p> <p className="text-xs text-gray-500 mt-1">Verify your employer registration</p>
</div> </div>
{renderStepIndicator()}
</div>
<div className="bg-white p-2 text-center"> <div className="bg-white p-2 text-center">
<div className="w-12 h-12 bg-blue-50 text-[#185FA5] rounded-xl flex items-center justify-center mx-auto mb-3 border border-blue-100 shadow-sm"> <div className="w-12 h-12 bg-blue-50 text-[#185FA5] rounded-xl flex items-center justify-center mx-auto mb-3 border border-blue-100 shadow-sm">

View File

@ -8,115 +8,467 @@ import {
CreditCard, CreditCard,
Search, Search,
ArrowRight, ArrowRight,
Heart,
CheckCircle2, CheckCircle2,
Info, Info,
Clock, Clock,
ChevronRight, ChevronRight,
UserCircle2 UserCircle2,
Sparkles,
Plus,
Activity,
TrendingUp,
ShieldCheck,
AlertTriangle,
Star,
Send,
HelpCircle,
UserCheck,
Lock
} from 'lucide-react'; } from 'lucide-react';
export default function Dashboard({ employer, stats, shortlisted_workers, recent_messages, announcements }) { export default function Dashboard({
employer,
stats,
shortlisted_workers,
recent_messages,
announcements,
recommended_workers = [],
saved_searches = []
}) {
const [activeTab, setActiveTab] = React.useState('sponsor');
const isSubActive = employer.subscription_status === 'active';
const isExpiringSoon = stats.days_remaining <= 7;
return ( return (
<EmployerLayout title="Employer Dashboard"> <EmployerLayout title="Sponsor Control Center">
<Head title="Employer Dashboard - Marketplace Portal" /> <Head title="Sponsor Dashboard - Verified UAE Domestic Workers" />
<div className="space-y-8"> <div className="space-y-8 pb-12">
{/* Section 1: Welcome Banner */} {/* 1. Alerts & Warning Banners */}
<div className="bg-gradient-to-r from-[#185FA5] to-blue-700 rounded-2xl p-6 sm:p-8 text-white shadow-sm flex flex-col sm:flex-row sm:items-center sm:justify-between gap-6 relative overflow-hidden select-none"> {!isSubActive && (
<div className="absolute -right-10 -bottom-10 w-48 h-48 bg-white/10 rounded-full blur-2xl pointer-events-none" /> <div className="bg-gradient-to-r from-rose-500/10 to-red-500/10 border border-rose-300 rounded-2xl p-5 flex flex-col sm:flex-row items-center justify-between shadow-sm animate-pulse gap-4">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 rounded-2xl bg-rose-500/15 flex items-center justify-center text-rose-600 flex-shrink-0">
<Lock className="w-6 h-6" />
</div>
<div>
<h4 className="font-black text-sm text-slate-900">Your Sponsor Subscription Pass Has Expired!</h4>
<p className="text-xs text-slate-500 font-bold mt-1">Direct communication, candidate dossiers, and messaging are locked. Renew your annual sponsor subscription now to resume hiring.</p>
</div>
</div>
<Link
href="/employer/subscription"
className="bg-rose-600 hover:bg-rose-700 text-white px-5 py-3 rounded-xl text-xs font-black uppercase tracking-wider transition-all shadow-md shadow-rose-600/20 flex items-center space-x-1.5 shrink-0"
>
<span>Renew Subscription</span>
<ArrowRight className="w-4 h-4" />
</Link>
</div>
)}
<div className="space-y-3 z-10"> {isExpiringSoon && isSubActive && (
<div className="inline-flex items-center space-x-2 px-3 py-1 bg-white/10 rounded-full text-xs font-semibold backdrop-blur-sm border border-white/10"> <div className="bg-gradient-to-r from-amber-500/10 to-orange-500/10 border border-amber-300 rounded-2xl p-4 flex items-center justify-between shadow-xs">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-xl bg-amber-500/15 flex items-center justify-center text-amber-600 flex-shrink-0">
<AlertTriangle className="w-5 h-5 animate-pulse" />
</div>
<div>
<h4 className="font-bold text-sm text-slate-800">Your Premium Sponsor Pass is expiring soon!</h4>
<p className="text-xs text-slate-500 font-medium">Only {stats.days_remaining} days left. Renew now to avoid losing contact access to 500+ verified candidates.</p>
</div>
</div>
<Link
href="/employer/subscription"
className="bg-amber-600 hover:bg-amber-700 text-white px-4 py-2 rounded-xl text-xs font-bold transition-all shadow-sm flex items-center space-x-1"
>
<span>Renew Now</span>
<ArrowRight className="w-3.5 h-3.5" />
</Link>
</div>
)}
{/* Welcome Banner */}
<div className="bg-gradient-to-br from-[#185FA5] via-[#104D89] to-blue-950 rounded-3xl p-6 sm:p-8 text-white shadow-md flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6 relative overflow-hidden select-none border border-blue-800">
<div className="absolute -right-10 -bottom-10 w-64 h-64 bg-white/5 rounded-full blur-3xl pointer-events-none" />
<div className="absolute left-1/3 top-1/4 w-32 h-32 bg-sky-400/10 rounded-full blur-2xl pointer-events-none" />
<div className="space-y-4 z-10 max-w-2xl">
<div className="inline-flex items-center space-x-2 px-3 py-1 bg-white/10 rounded-full text-xs font-semibold backdrop-blur-md border border-white/10">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" /> <span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
<span>Subscription Active</span> <span>Subscription Status: <span className="text-emerald-300 font-bold uppercase">{employer.subscription_status}</span></span>
<span className="text-blue-200">|</span> <span className="text-blue-300">|</span>
<span>{stats.days_remaining} days left</span> <span>{stats.days_remaining} Days Left</span>
</div> </div>
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight"> <h1 className="text-3xl sm:text-4xl font-black tracking-tight">
Good morning, {employer.name} Welcome Back, {employer.name}
</h1> </h1>
<p className="text-blue-100 text-sm max-w-xl font-medium"> <p className="text-blue-100 text-sm leading-relaxed font-medium">
Welcome to your employer control center. You have {stats.days_remaining} days remaining on your {employer.plan_name}. Hire direct, MOHRE-compliant domestic workers with zero middleman commissions. Browse our updated database of candidates with OCR passport checks.
</p> </p>
</div> </div>
<div className="z-10 flex-shrink-0"> <div className="z-10 flex flex-wrap gap-3 flex-shrink-0">
<Link <Link
href="/employer/workers" href="/employer/workers"
className="bg-white text-[#185FA5] hover:bg-slate-100 px-6 py-3 rounded-xl font-bold text-sm shadow-md transition-all flex items-center justify-center space-x-2 group w-full sm:w-auto" className="bg-white text-[#185FA5] hover:bg-slate-100 px-6 py-3.5 rounded-xl font-bold text-xs shadow-md transition-all flex items-center justify-center space-x-2 group"
> >
<Search className="w-4 h-4 text-[#185FA5] group-hover:scale-110 transition-transform" /> <Search className="w-4 h-4 text-[#185FA5] group-hover:scale-110 transition-transform" />
<span>Find Workers</span> <span>Browse 500+ Verified Workers</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" /> <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</Link> </Link>
</div> </div>
</div> </div>
{/* Section 2: Stat Cards */} {/* 2. Top-level Stats & Analytics Grid */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Shortlisted */} {/* Subscription Pass status card */}
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4"> <div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs flex flex-col justify-between space-y-4 relative group hover:border-[#185FA5] transition-all">
<div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center flex-shrink-0"> <div className="flex items-center justify-between">
<Bookmark className="w-6 h-6" /> <div className="w-12 h-12 rounded-xl bg-blue-50 text-[#185FA5] flex items-center justify-center flex-shrink-0 border border-blue-100 group-hover:scale-105 transition-transform">
</div>
<div>
<div className="text-2xl font-bold text-slate-800">{stats.shortlisted_count}</div>
<div className="text-xs text-slate-500 font-medium">Workers Shortlisted</div>
</div>
</div>
{/* Messages Sent */}
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4">
<div className="w-12 h-12 rounded-xl bg-teal-50 text-teal-600 flex items-center justify-center flex-shrink-0">
<MessageSquare className="w-6 h-6" />
</div>
<div>
<div className="text-2xl font-bold text-slate-800">{stats.messages_sent}</div>
<div className="text-xs text-slate-500 font-medium">Messages Sent</div>
</div>
</div>
{/* Days Remaining */}
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
stats.days_remaining < 7 ? 'bg-red-50 text-red-600' : 'bg-amber-50 text-amber-600'
}`}>
<CalendarDays className="w-6 h-6" />
</div>
<div>
<div className={`text-2xl font-bold ${stats.days_remaining < 7 ? 'text-red-600' : 'text-slate-800'}`}>
{stats.days_remaining}
</div>
<div className="text-xs text-slate-500 font-medium">Days Remaining</div>
</div>
</div>
{/* Plan Name */}
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4">
<div className="w-12 h-12 rounded-xl bg-slate-100 text-slate-600 flex items-center justify-center flex-shrink-0">
<CreditCard className="w-6 h-6" /> <CreditCard className="w-6 h-6" />
</div> </div>
<div className="truncate"> <span className="text-[10px] font-black text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-md border border-emerald-100">
<div className="text-lg font-bold text-slate-800 truncate">{employer.plan_name}</div> ACTIVE BILLING
<div className="text-xs text-slate-500 font-medium">Current Plan</div> </span>
</div>
<div>
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">Sponsor Pass Tier</div>
<div className="text-lg font-bold text-slate-800 mt-1">{employer.plan_name}</div>
<div className="text-[11px] text-slate-500 font-medium mt-1">Renews: {employer.subscription_expires_at}</div>
</div>
<Link href="/employer/subscription" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1 pt-2 border-t border-slate-100">
<span>Manage billing & invoices</span>
<ChevronRight className="w-3.5 h-3.5" />
</Link>
</div>
{/* Pending Offers Stat Card */}
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs flex flex-col justify-between space-y-4 relative group hover:border-[#185FA5] transition-all">
<div className="flex items-center justify-between">
<div className="w-12 h-12 rounded-xl bg-amber-50 text-amber-600 flex items-center justify-center flex-shrink-0 border border-amber-100 group-hover:scale-105 transition-transform">
<Send className="w-5 h-5" />
</div>
<span className="text-[10px] font-black text-amber-600 bg-amber-50 px-2 py-0.5 rounded-md border border-amber-100">
OUTSTANDING
</span>
</div>
<div>
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">Pending Job Offers</div>
<div className="text-3xl font-black text-slate-800 mt-1">{stats.pending_offers}</div>
<div className="text-[11px] text-slate-500 font-medium mt-1">Direct recruitment proposals</div>
</div>
<Link href="/employer/candidates" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1 pt-2 border-t border-slate-100">
<span>View hiring workflow</span>
<ChevronRight className="w-3.5 h-3.5" />
</Link>
</div>
{/* Hired Count Stat Card */}
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs flex flex-col justify-between space-y-4 relative group hover:border-[#185FA5] transition-all">
<div className="flex items-center justify-between">
<div className="w-12 h-12 rounded-xl bg-emerald-50 text-emerald-600 flex items-center justify-center flex-shrink-0 border border-emerald-100 group-hover:scale-105 transition-transform">
<UserCheck className="w-5 h-5" />
</div>
<span className="text-[10px] font-black text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-md border border-emerald-100">
HIRED SECURELY
</span>
</div>
<div>
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">Total Hired Workers</div>
<div className="text-3xl font-black text-slate-800 mt-1">{stats.hired_count}</div>
<div className="text-[11px] text-slate-500 font-medium mt-1">Direct contracts initiated</div>
</div>
<Link href="/employer/candidates" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1 pt-2 border-t border-slate-100">
<span>Track active staff</span>
<ChevronRight className="w-3.5 h-3.5" />
</Link>
</div>
{/* Shortlist Counter */}
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs flex flex-col justify-between space-y-4 relative group hover:border-[#185FA5] transition-all">
<div className="flex items-center justify-between">
<div className="w-12 h-12 rounded-xl bg-purple-50 text-purple-600 flex items-center justify-center flex-shrink-0 border border-purple-100 group-hover:scale-105 transition-transform">
<Bookmark className="w-5 h-5" />
</div>
<span className="text-[10px] font-black text-purple-600 bg-purple-50 px-2 py-0.5 rounded-md border border-purple-100">
SAVED LIST
</span>
</div>
<div>
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">Shortlisted Candidates</div>
<div className="text-3xl font-black text-slate-800 mt-1">{stats.shortlisted_count}</div>
<div className="text-[11px] text-slate-500 font-medium mt-1">Bookmark list for interviews</div>
</div>
<Link href="/employer/shortlist" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1 pt-2 border-t border-slate-100">
<span>Open shortlist book</span>
<ChevronRight className="w-3.5 h-3.5" />
</Link>
</div>
</div>
{/* 3. Analytics & Quick Action Section */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Worker Activity Analytics Module (Col 8) */}
<div className="lg:col-span-8 bg-white p-6 rounded-3xl border border-slate-200 shadow-sm space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pb-3 border-b border-slate-100">
<div className="flex items-center space-x-2">
<Activity className="w-5 h-5 text-[#185FA5]" />
<h3 className="font-extrabold text-base text-slate-900">Discover Insights & Market Stats</h3>
</div>
{/* Toggle Tabs */}
<div className="flex items-center bg-slate-50 border border-slate-200 rounded-xl p-1 shrink-0">
<button
onClick={() => setActiveTab('sponsor')}
className={`px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all ${
activeTab === 'sponsor'
? 'bg-white text-[#185FA5] shadow-xs'
: 'text-slate-500 hover:text-slate-800'
}`}
>
Sponsor Hub
</button>
<button
onClick={() => setActiveTab('worker')}
className={`px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all ${
activeTab === 'worker'
? 'bg-white text-[#185FA5] shadow-xs'
: 'text-slate-500 hover:text-slate-800'
}`}
>
Worker Tracker
</button>
</div>
</div>
{activeTab === 'sponsor' ? (
<div className="space-y-6 animate-in fade-in duration-250">
{/* Dubai General Market Stats */}
<div className="space-y-3">
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest pl-1">Dubai General Market Insights</div>
<div className="grid grid-cols-3 gap-4 text-center">
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-100 flex flex-col justify-between">
<div className="text-xl font-black text-slate-900">1,284</div>
<div className="text-[9px] text-slate-500 font-bold uppercase tracking-wider mt-1">Hired using app in Dubai</div>
</div>
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-100 flex flex-col justify-between">
<div className="text-xs font-black text-[#185FA5] uppercase leading-snug">Cooking, Care, Driving</div>
<div className="text-[9px] text-slate-500 font-bold uppercase tracking-wider mt-1">Top Skills Hired</div>
</div>
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-100 flex flex-col justify-between">
<div className="text-xl font-black text-emerald-600">1.4%</div>
<div className="text-[9px] text-slate-500 font-bold uppercase tracking-wider mt-1">Turnover rate</div>
</div> </div>
</div> </div>
</div> </div>
{/* Your own activity */}
<div className="space-y-3">
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest pl-1">Your Sponsor Activity Stats</div>
<div className="grid grid-cols-3 gap-4 text-center">
<div className="p-4 bg-blue-50/30 rounded-2xl border border-blue-100/50 flex flex-col justify-between">
<div className="text-2xl font-black text-slate-800">14</div>
<div className="text-[9px] text-slate-500 font-bold uppercase tracking-wider mt-1">Workers Contacted</div>
</div>
<div className="p-4 bg-blue-50/30 rounded-2xl border border-blue-100/50 flex flex-col justify-between">
<div className="text-2xl font-black text-slate-800">{stats.shortlisted_count}</div>
<div className="text-[9px] text-slate-500 font-bold uppercase tracking-wider mt-1">Shortlisted</div>
</div>
<div className="p-4 bg-blue-50/30 rounded-2xl border border-blue-100/50 flex flex-col justify-between">
<div className="text-2xl font-black text-emerald-600">{stats.hired_count}</div>
<div className="text-[9px] text-slate-500 font-bold uppercase tracking-wider mt-1">Secure Hires</div>
</div>
</div>
</div>
</div>
) : (
<div className="space-y-6 animate-in fade-in duration-250">
<div className="p-4 bg-emerald-50/30 border border-emerald-100 rounded-2xl flex items-center space-x-3.5">
<div className="w-10 h-10 rounded-xl bg-emerald-100/60 text-emerald-700 flex items-center justify-center shrink-0">
<Sparkles className="w-5 h-5 text-emerald-600 animate-pulse" />
</div>
<div>
<h4 className="font-extrabold text-xs text-slate-900">How Candidates Stay Motivated</h4>
<p className="text-[11px] text-slate-500 font-medium mt-0.5 leading-relaxed">
Workers view real-time profile activity and interaction insights. This keeps them highly engaged and active on our platform!
</p>
</div>
</div>
<div className="space-y-3">
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest pl-1">Live Worker-Side Analytics Dashboard</div>
<div className="grid grid-cols-3 gap-4 text-center">
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-100">
<div className="text-2xl font-black text-slate-800">32</div>
<div className="text-[9px] text-slate-500 font-bold uppercase tracking-wider mt-1">Sponsors Viewed Profile</div>
</div>
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-100">
<div className="text-2xl font-black text-slate-800">12</div>
<div className="text-[9px] text-slate-500 font-bold uppercase tracking-wider mt-1">Sponsors Contacted You</div>
</div>
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-100">
<div className="text-2xl font-black text-[#185FA5]">4</div>
<div className="text-[9px] text-slate-500 font-bold uppercase tracking-wider mt-1">Job Proposals Offered</div>
</div>
</div>
</div>
{/* Push notification highlight */}
<div className="p-4 bg-blue-50/50 border border-blue-100 rounded-2xl flex items-start space-x-3 text-xs font-bold text-slate-600">
<span className="text-base shrink-0 mt-0.5">🔔</span>
<div>
<div className="text-slate-800 text-[11px] font-extrabold">Instant Worker Push Alerts</div>
<p className="text-[10px] text-slate-500 font-medium leading-relaxed mt-0.5">
When you send a message, the worker receives a push notification on their mobile app immediately, ensuring rapid responses.
</p>
</div>
</div>
</div>
)}
</div>
{/* Quick Actions Panel (Col 4) */}
<div className="lg:col-span-4 bg-white p-6 rounded-3xl border border-slate-200 shadow-xs flex flex-col justify-between space-y-6">
<div className="space-y-1">
<h3 className="font-bold text-base text-slate-900">Quick Actions</h3>
<p className="text-xs text-slate-500 font-medium">Speed up your sponsorship process.</p>
</div>
<div className="space-y-2.5 flex-1 py-2">
<Link
href="/employer/jobs/create"
className="w-full p-3.5 bg-slate-50 hover:bg-blue-50/50 border border-slate-200 hover:border-[#185FA5]/40 rounded-2xl transition-all flex items-center space-x-3 text-left group"
>
<div className="w-9 h-9 rounded-xl bg-blue-100/50 text-[#185FA5] flex items-center justify-center flex-shrink-0 group-hover:scale-105 transition-transform">
<Plus className="w-4 h-4" />
</div>
<div className="truncate">
<div className="text-xs font-bold text-slate-800 group-hover:text-[#185FA5] transition-colors">Post a New Job</div>
<div className="text-[10px] text-slate-500 truncate">Receive applications directly</div>
</div>
</Link>
<Link
href="/employer/workers?category=Childcare"
className="w-full p-3.5 bg-slate-50 hover:bg-blue-50/50 border border-slate-200 hover:border-[#185FA5]/40 rounded-2xl transition-all flex items-center space-x-3 text-left group"
>
<div className="w-9 h-9 rounded-xl bg-purple-100/50 text-purple-600 flex items-center justify-center flex-shrink-0 group-hover:scale-105 transition-transform">
<Star className="w-4 h-4" />
</div>
<div className="truncate">
<div className="text-xs font-bold text-slate-800 group-hover:text-[#185FA5] transition-colors">Find Baby Care & Nannies</div>
<div className="text-[10px] text-slate-500 truncate">Browse premium verified nannies</div>
</div>
</Link>
<a
href="https://wa.me/971501112222"
target="_blank"
rel="noreferrer"
className="w-full p-3.5 bg-slate-50 hover:bg-emerald-50/30 border border-slate-200 hover:border-emerald-500/40 rounded-2xl transition-all flex items-center space-x-3 text-left group"
>
<div className="w-9 h-9 rounded-xl bg-emerald-100/50 text-emerald-600 flex items-center justify-center flex-shrink-0 group-hover:scale-105 transition-transform">
<MessageSquare className="w-4 h-4" />
</div>
<div className="truncate">
<div className="text-xs font-bold text-slate-800 group-hover:text-emerald-700 transition-colors flex items-center space-x-1">
<span>Direct WhatsApp Support</span>
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
</div>
<div className="text-[10px] text-slate-500 truncate">Chat with a recruitment manager</div>
</div>
</a>
</div>
<div className="p-3 bg-emerald-50 border border-emerald-100 rounded-2xl text-[10px] text-emerald-800 font-bold flex items-center space-x-2">
<ShieldCheck className="w-4 h-4 text-emerald-600 flex-shrink-0" />
<span>100% Legal UAE Agency-Free Platform</span>
</div>
</div>
</div>
{/* 4. Detailed Workspace Rows (Col 12 Grid) */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Main Content Column (Col 8) */} {/* Left Column: Recommended Workers, Shortlist, Chats */}
<div className="lg:col-span-8 space-y-8"> <div className="lg:col-span-8 space-y-8">
{/* Section 3: My Shortlist */} {/* Recommended Workers Module */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden p-6 space-y-6"> <div className="bg-white p-6 rounded-3xl border border-slate-200 shadow-xs space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Sparkles className="w-5 h-5 text-amber-500 animate-pulse" />
<h3 className="font-bold text-base text-slate-900">Recommended for You</h3>
</div>
<Link href="/employer/workers" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-0.5">
<span>Browse all</span>
<ChevronRight className="w-4 h-4" />
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{recommended_workers.length > 0 ? (
recommended_workers.map((worker) => (
<div key={worker.id} className="bg-slate-50 border border-slate-200/80 rounded-2xl p-4 flex flex-col justify-between space-y-4 hover:border-[#185FA5] hover:bg-white transition-all group relative">
{/* Top info */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="w-10 h-10 rounded-full bg-blue-100 text-[#185FA5] font-black text-sm flex items-center justify-center border border-blue-200">
{worker.name.charAt(0)}
</div>
{worker.verified && (
<span className="px-2 py-0.5 bg-emerald-100 text-emerald-800 text-[8px] font-black uppercase rounded border border-emerald-200">
VERIFIED
</span>
)}
</div>
<div>
<div className="font-bold text-sm text-slate-900 group-hover:text-[#185FA5] transition-colors truncate">
{worker.name}
</div>
<div className="text-[10px] text-slate-500 font-bold truncate">
{worker.nationality} {worker.category}
</div>
</div>
<div className="flex items-center space-x-1 text-xs text-amber-500 font-bold">
<Star className="w-3.5 h-3.5 fill-amber-500" />
<span>{worker.rating}</span>
<span className="text-[10px] text-slate-400 font-medium">({Math.floor(Math.random() * 15) + 3} reviews)</span>
</div>
</div>
{/* Action buttons */}
<div className="pt-3 border-t border-slate-200/60 flex items-center justify-between gap-2">
<div className="text-[11px] font-black text-slate-700">
{worker.salary} AED<span className="text-[9px] font-bold text-slate-400">/mo</span>
</div>
<Link
href={`/employer/workers/${worker.id}`}
className="px-3 py-1.5 bg-[#185FA5] hover:bg-[#144f8a] text-white text-[10px] font-black rounded-lg transition-colors shadow-xs"
>
View Profile
</Link>
</div>
</div>
))
) : (
<div className="col-span-3 text-center py-8 bg-slate-50 rounded-2xl border border-dashed border-slate-200 text-slate-400 text-xs">
No workers currently recommended. Update profile details.
</div>
)}
</div>
</div>
{/* My Shortlist Module */}
<div className="bg-white rounded-3xl border border-slate-200 shadow-xs p-6 space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Bookmark className="w-5 h-5 text-[#185FA5]" /> <Bookmark className="w-5 h-5 text-[#185FA5]" />
<h2 className="text-lg font-bold text-slate-800">My Shortlist</h2> <h3 className="font-bold text-base text-slate-900">Active Shortlist ({shortlisted_workers.length})</h3>
</div> </div>
<Link href="/employer/shortlist" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1"> <Link href="/employer/shortlist" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-0.5">
<span>View all</span> <span>View book</span>
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />
</Link> </Link>
</div> </div>
@ -124,9 +476,9 @@ export default function Dashboard({ employer, stats, shortlisted_workers, recent
{shortlisted_workers?.length > 0 ? ( {shortlisted_workers?.length > 0 ? (
<div className="flex space-x-4 overflow-x-auto pb-2 scrollbar-thin"> <div className="flex space-x-4 overflow-x-auto pb-2 scrollbar-thin">
{shortlisted_workers.map((worker) => ( {shortlisted_workers.map((worker) => (
<div key={worker.id} className="w-56 bg-slate-50 rounded-xl p-4 border border-slate-200 flex-shrink-0 space-y-3"> <div key={worker.id} className="w-56 bg-slate-50 rounded-2xl p-4 border border-slate-200/70 flex-shrink-0 space-y-3 relative hover:border-[#185FA5] hover:bg-white transition-all">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="w-12 h-12 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center font-bold text-lg flex-shrink-0 border border-blue-200"> <div className="w-10 h-10 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center font-bold text-base flex-shrink-0 border border-blue-200">
{worker.name.charAt(0)} {worker.name.charAt(0)}
</div> </div>
<div className="truncate"> <div className="truncate">
@ -134,97 +486,192 @@ export default function Dashboard({ employer, stats, shortlisted_workers, recent
<span>{worker.name}</span> <span>{worker.name}</span>
{worker.verified && <CheckCircle2 className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" />} {worker.verified && <CheckCircle2 className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" />}
</div> </div>
<div className="text-xs text-slate-500 truncate">{worker.nationality}</div> <div className="text-[10px] text-slate-500 truncate font-semibold">{worker.nationality}</div>
</div> </div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="text-[11px] font-semibold text-slate-500">Top Skill:</div> <div className="text-[10px] font-semibold text-slate-400 uppercase tracking-tighter">Category:</div>
<div className="inline-block px-2.5 py-1 bg-white border border-slate-200 rounded-lg text-xs font-medium text-slate-700"> <div className="inline-block px-2 py-0.5 bg-white border border-slate-200 rounded-md text-xs font-semibold text-slate-700">
{worker.skills[0] || 'General Work'} {worker.category || 'General Helper'}
</div> </div>
</div> </div>
<div className="pt-2 border-t border-slate-200 flex items-center justify-between text-xs"> <div className="pt-2 border-t border-slate-200/80 flex items-center justify-between text-xs">
<span className="text-slate-500 font-medium">Availability:</span> <span className="text-[10px] text-slate-400 font-bold uppercase">AVAILABILITY:</span>
<span className="font-bold text-[#185FA5] bg-blue-50 px-2 py-0.5 rounded border border-blue-100"> <span className="font-bold text-[#185FA5] bg-blue-50 px-2 py-0.5 rounded text-[10px] border border-blue-100">
{worker.availability} {worker.availability}
</span> </span>
</div> </div>
<Link
href={`/employer/workers/${worker.id}`}
className="w-full mt-2 bg-white hover:bg-slate-50 text-[#185FA5] border border-slate-200 text-center rounded-lg py-1.5 text-[10px] font-black flex items-center justify-center transition-colors"
>
Open Profile
</Link>
</div> </div>
))} ))}
</div> </div>
) : ( ) : (
<div className="text-center py-12 bg-slate-50 rounded-xl border-2 border-dashed border-slate-200 space-y-2"> <div className="text-center py-10 bg-slate-50 rounded-2xl border-2 border-dashed border-slate-200 space-y-2">
<Bookmark className="w-8 h-8 text-slate-300 mx-auto" /> <Bookmark className="w-8 h-8 text-slate-300 mx-auto" />
<div className="text-sm font-semibold text-slate-600">Shortlist workers to see them here</div> <div className="text-xs font-bold text-slate-500">Shortlist candidates to compare their documents side-by-side</div>
</div> </div>
)} )}
</div> </div>
{/* Section 4: Recent Messages */} {/* Recent Chats / Message Center */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden p-6 space-y-6"> <div className="bg-white rounded-3xl border border-slate-200 shadow-xs p-6 space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<MessageSquare className="w-5 h-5 text-[#185FA5]" /> <MessageSquare className="w-5 h-5 text-[#185FA5]" />
<h2 className="text-lg font-bold text-slate-800">Recent Messages</h2> <h3 className="font-bold text-base text-slate-900">Recent Chats</h3>
</div> </div>
<Link href="/employer/messages" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1"> <Link href="/employer/messages" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-0.5">
<span>View all messages</span> <span>All channels</span>
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />
</Link> </Link>
</div> </div>
<div className="divide-y divide-slate-100"> <div className="divide-y divide-slate-100">
{recent_messages?.map((msg) => ( {recent_messages?.length > 0 ? (
recent_messages.map((msg) => (
<Link <Link
key={msg.id} key={msg.id}
href={`/employer/messages/${msg.id}`} href={`/employer/messages/${msg.id}`}
className="flex items-start space-x-4 py-4 hover:bg-slate-50 px-3 rounded-xl transition-colors group" className="flex items-start space-x-4 py-4 hover:bg-slate-50 px-3 rounded-2xl transition-colors group"
> >
<div className="w-10 h-10 rounded-full bg-blue-50 text-[#185FA5] flex items-center justify-center font-bold text-sm flex-shrink-0 border border-blue-100"> <div className="w-10 h-10 rounded-xl bg-blue-50 text-[#185FA5] flex items-center justify-center font-bold text-sm flex-shrink-0 border border-blue-100">
{msg.worker_name.charAt(0)} {msg.worker_name.charAt(0)}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="font-bold text-sm text-slate-800 group-hover:text-[#185FA5] transition-colors truncate"> <div className="font-bold text-xs text-slate-800 group-hover:text-[#185FA5] transition-colors truncate">
{msg.worker_name} {msg.worker_name}
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span className="text-[10px] font-medium text-slate-400">{msg.sent_at}</span> <span className="text-[9px] font-semibold text-slate-400">{msg.sent_at}</span>
{msg.unread && <span className="w-2 h-2 rounded-full bg-red-500 animate-pulse" />} {msg.unread && (
<span className="w-2 h-2 rounded-full bg-red-500 animate-pulse border border-white" />
)}
</div> </div>
</div> </div>
<p className="text-xs text-slate-600 truncate mt-0.5">{msg.last_message}</p> <p className="text-xs text-slate-600 truncate mt-0.5 font-medium">
{msg.last_message}
</p>
</div> </div>
</Link> </Link>
))
) : (
<div className="text-center py-8 text-xs text-slate-400">
No recent chat logs found. Find workers to start conversing.
</div>
)}
</div>
</div>
</div>
{/* Right Column: Saved Searches, Announcements, Regulatory details */}
<div className="lg:col-span-4 space-y-8">
{/* Saved Searches Module */}
<div className="bg-white rounded-3xl border border-slate-200 shadow-xs p-6 space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-bold text-sm text-slate-900 uppercase tracking-widest">Saved Searches</h3>
<Bookmark className="w-4 h-4 text-[#185FA5]" />
</div>
<div className="space-y-2">
{saved_searches.map((search) => (
<Link
key={search.id}
href={`/employer/workers?${search.query}`}
className="w-full p-3 bg-slate-50 hover:bg-blue-50/30 border border-slate-200 rounded-xl transition-all flex items-center justify-between text-left group"
>
<span className="text-xs font-bold text-slate-700 group-hover:text-[#185FA5] transition-colors truncate">
{search.name}
</span>
<ChevronRight className="w-4 h-4 text-slate-400 group-hover:translate-x-0.5 transition-transform" />
</Link>
))} ))}
</div> </div>
</div> </div>
</div>
{/* Right Column (Col 4) */} {/* Charity Events Module */}
<div className="lg:col-span-4 space-y-8"> <div className="bg-white rounded-3xl border border-slate-200 shadow-xs p-6 space-y-6">
{/* Section 5: Announcements */} <div className="flex items-center justify-between">
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6 space-y-6">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Info className="w-5 h-5 text-[#185FA5]" /> <Heart className="w-5 h-5 text-rose-500 fill-rose-500/20" />
<h2 className="text-lg font-bold text-slate-800">Announcements</h2> <h3 className="font-extrabold text-base text-slate-900">Community Charity Drives</h3>
</div>
<span className="text-[9px] font-black bg-rose-50 text-rose-600 px-2 py-0.5 rounded-full uppercase tracking-wider">
Live Events
</span>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{announcements?.map((ann) => ( {announcements?.map((ann) => (
<div key={ann.id} className="p-4 bg-blue-50/70 border border-blue-100 rounded-xl space-y-2"> <div key={ann.id} className="p-4 bg-rose-50/20 border border-rose-100/70 rounded-2xl space-y-3 relative group hover:border-rose-300 hover:bg-white transition-all">
<div className="flex items-center justify-between text-[10px] font-bold text-blue-600 uppercase tracking-wider"> <div className="flex items-center justify-between text-[9px] font-bold text-rose-600 uppercase tracking-wider">
<span>System Alert</span> <span> Charity Drive</span>
<span>{ann.created_at}</span> <span>{ann.created_at}</span>
</div> </div>
<h3 className="font-bold text-xs text-slate-900 tracking-tight">{ann.title}</h3> <div>
<p className="text-xs text-slate-600 leading-relaxed">{ann.body}</p> <h4 className="font-black text-xs text-slate-900 tracking-tight group-hover:text-rose-600 transition-colors">{ann.title}</h4>
<p className="text-[11px] text-slate-500 font-medium leading-relaxed mt-1">{ann.body}</p>
</div>
{/* Metas */}
<div className="grid grid-cols-2 gap-2 text-[10px] bg-slate-50/60 p-2.5 rounded-xl border border-slate-100 font-bold text-slate-600">
<div className="flex items-center space-x-1 truncate">
<span>🎁</span>
<span className="truncate">{ann.provided_items}</span>
</div>
<div className="flex items-center space-x-1 truncate">
<span>📅</span>
<span className="truncate">{ann.event_date}</span>
</div>
</div>
{ann.location_pin && (
<a
href={ann.location_pin}
target="_blank"
rel="noopener noreferrer"
className="w-full bg-white hover:bg-rose-50 text-rose-600 border border-rose-200 text-center rounded-lg py-1.5 text-[10px] font-black flex items-center justify-center space-x-1 transition-colors"
>
<span>📍 Open Google Maps Location Pin</span>
</a>
)}
</div> </div>
))} ))}
</div> </div>
</div> </div>
{/* Direct Sponsorship Compliance card */}
<div className="bg-gradient-to-br from-indigo-950 to-blue-900 rounded-3xl p-6 text-white border border-indigo-900 shadow-sm relative overflow-hidden space-y-4">
<div className="absolute right-0 top-0 w-24 h-24 bg-white/5 rounded-full blur-xl pointer-events-none" />
<div className="w-10 h-10 rounded-xl bg-white/10 flex items-center justify-center border border-white/10 text-white">
<ShieldCheck className="w-6 h-6 text-emerald-400" />
</div>
<div className="space-y-2">
<h4 className="font-bold text-sm tracking-tight text-white">UAE Tadbeer & MOHRE Compliant</h4>
<p className="text-[11px] text-blue-200 leading-relaxed font-medium">
Our direct contract system aligns automatically with the Ministry of Human Resources and Emiratisation guidelines. Fully legal, zero agent fees, complete transparency.
</p>
</div>
<div className="pt-2">
<a
href="https://www.mohre.gov.ae/"
target="_blank"
rel="noreferrer"
className="text-xs font-bold text-emerald-400 hover:text-emerald-300 flex items-center space-x-1"
>
<span>Visit MOHRE UAE website</span>
<ArrowRight className="w-3.5 h-3.5" />
</a>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -19,15 +19,23 @@ import {
ShieldCheck, ShieldCheck,
ChevronRight, ChevronRight,
MapPin, MapPin,
Smile Smile,
X,
MessageSquare,
Eye,
UploadCloud,
FileText
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner';
export default function Show({ conversation, initialMessages, conversations = [] }) { export default function Show({ conversation, initialMessages, conversations = [] }) {
const [messages, setMessages] = useState(initialMessages || []); const [messages, setMessages] = useState(initialMessages || []);
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [isTyping, setIsTyping] = useState(false); const [isTyping, setIsTyping] = useState(false);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [showProfile, setShowProfile] = useState(false); const [showProfile, setShowProfile] = useState(true);
const [showAttachmentModal, setShowAttachmentModal] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState([]);
const messagesEndRef = useRef(null); const messagesEndRef = useRef(null);
const scrollToBottom = () => { const scrollToBottom = () => {
@ -42,6 +50,13 @@ export default function Show({ conversation, initialMessages, conversations = []
setMessages(initialMessages || []); setMessages(initialMessages || []);
}, [initialMessages]); }, [initialMessages]);
// Simulate typing indicator when conversation opens
useEffect(() => {
setIsTyping(true);
const timer = setTimeout(() => setIsTyping(false), 2500);
return () => clearTimeout(timer);
}, [conversation.id]);
const filteredConversations = (conversations || []).filter(c => const filteredConversations = (conversations || []).filter(c =>
c.worker_name.toLowerCase().includes(searchTerm.toLowerCase()) c.worker_name.toLowerCase().includes(searchTerm.toLowerCase())
); );
@ -49,12 +64,48 @@ export default function Show({ conversation, initialMessages, conversations = []
const sendMsg = (text) => { const sendMsg = (text) => {
if (!text.trim()) return; if (!text.trim()) return;
// Optimistically update message feed
const newMsg = {
id: Date.now(),
sender: 'employer',
text: text,
time: 'Just Now',
read: false
};
setMessages(prev => [...prev, newMsg]);
setInput('');
router.post(`/employer/messages/${conversation.id}/send`, { router.post(`/employer/messages/${conversation.id}/send`, {
text: text text: text
}, { }, {
preserveScroll: true, preserveScroll: true,
onSuccess: () => { onSuccess: () => {
setInput(''); // Show immediate confirmation of the push alert popping up on worker mobile app
toast.success(`🔔 Direct Mobile Push Notification sent!`, {
description: `Popped up instantly on ${conversation.worker_name}'s mobile device.`,
duration: 4500,
});
// Simulate worker typing response shortly after
setTimeout(() => {
setIsTyping(true);
setTimeout(() => {
setIsTyping(false);
const autoReply = {
id: Date.now() + 1,
sender: 'worker',
text: "Thank you for your response! I have reviewed your offer proposal and am looking forward to our video interview.",
time: 'Just Now'
};
setMessages(prev => [...prev, autoReply]);
// Pop up a clear message received notification for the sponsor
toast.info(`✉️ New message from ${conversation.worker_name}!`, {
description: autoReply.text,
duration: 5000
});
}, 2000);
}, 1500);
} }
}); });
}; };
@ -64,6 +115,24 @@ export default function Show({ conversation, initialMessages, conversations = []
sendMsg(input); sendMsg(input);
}; };
const handleFileUpload = (e) => {
const file = e.target.files[0];
if (!file) return;
const newFile = {
name: file.name,
size: (file.size / 1024).toFixed(1) + ' KB',
date: 'Today'
};
setUploadedFiles([...uploadedFiles, newFile]);
toast.success("📎 Document attached successfully!");
setShowAttachmentModal(false);
// Optimistically insert attachment message
sendMsg(`[Document Attached: ${file.name}]`);
};
const chips = [ const chips = [
"Are you available for a video interview today?", "Are you available for a video interview today?",
"What is your final expected salary?", "What is your final expected salary?",
@ -74,23 +143,23 @@ export default function Show({ conversation, initialMessages, conversations = []
<EmployerLayout title={null} fullPage={true}> <EmployerLayout title={null} fullPage={true}>
<Head title={`Chat with ${conversation.worker_name} - Messages`} /> <Head title={`Chat with ${conversation.worker_name} - Messages`} />
<div className="flex h-full bg-[#FAFBFF] overflow-hidden select-none"> <div className="flex h-full bg-[#FAFBFF] overflow-hidden select-none w-full">
{/* Conversation List Sidebar (Desktop) */} {/* Conversation List Sidebar (Desktop) */}
<aside className="hidden xl:flex w-96 border-r border-slate-200 flex-col flex-shrink-0 bg-white shadow-sm z-30"> <aside className="hidden xl:flex w-90 border-r border-slate-200 flex-col flex-shrink-0 bg-white shadow-sm z-30">
<div className="p-6 border-b border-slate-100"> <div className="p-6 border-b border-slate-100">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="font-black text-2xl text-slate-900 tracking-tight">Messages</h2> <h2 className="font-black text-2xl text-slate-900 tracking-tight">Messages Hub</h2>
<button className="p-2 bg-slate-50 text-slate-400 rounded-xl hover:text-[#185FA5] transition-colors"> <span className="text-[9px] font-black bg-emerald-50 text-emerald-700 px-2 py-0.5 rounded uppercase tracking-wider">
<Search className="w-5 h-5" /> SSL SECURE
</button> </span>
</div> </div>
<div className="relative"> <div className="relative">
<input <input
type="text" type="text"
placeholder="Search by name..." placeholder="Search conversations..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-5 pr-4 py-3.5 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all placeholder:text-slate-400" className="w-full pl-5 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all placeholder:text-slate-400 outline-none"
/> />
</div> </div>
</div> </div>
@ -105,28 +174,26 @@ export default function Show({ conversation, initialMessages, conversations = []
}`} }`}
> >
{c.id === conversation.id && ( {c.id === conversation.id && (
<div className="absolute left-0 top-0 bottom-0 w-1.5 bg-[#185FA5] rounded-r-full shadow-[0_0_15px_rgba(24,95,165,0.4)]" /> <div className="absolute left-0 top-0 bottom-0 w-1 bg-[#185FA5] rounded-r-full shadow-[0_0_15px_rgba(24,95,165,0.4)]" />
)} )}
<div className="relative flex-shrink-0"> <div className="relative flex-shrink-0">
<div className={`w-14 h-14 rounded-[20px] text-white flex items-center justify-center font-black text-xl shadow-lg ${ <div className={`w-12 h-12 rounded-[20px] text-white flex items-center justify-center font-black text-lg shadow-sm ${
c.id === conversation.id ? 'bg-gradient-to-br from-[#185FA5] to-blue-600' : 'bg-slate-200 text-slate-400' c.id === conversation.id ? 'bg-gradient-to-br from-[#185FA5] to-blue-600' : 'bg-slate-200 text-slate-400'
}`}> }`}>
{c.worker_name.charAt(0)} {c.worker_name.charAt(0)}
</div> </div>
{c.online && ( <span className="absolute -bottom-1 -right-1 w-3.5 h-3.5 bg-emerald-500 border-4 border-white rounded-full shadow-sm" />
<span className="absolute -bottom-1 -right-1 w-4 h-4 bg-emerald-500 border-4 border-white rounded-full shadow-sm" />
)}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<span className={`text-sm font-black truncate ${c.id === conversation.id ? 'text-[#185FA5]' : 'text-slate-900'}`}> <span className={`text-xs font-black truncate ${c.id === conversation.id ? 'text-[#185FA5]' : 'text-slate-900'}`}>
{c.worker_name} {c.worker_name}
</span> </span>
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">{c.sent_at}</span> <span className="text-[9px] text-slate-400 font-bold uppercase tracking-tighter">{c.sent_at}</span>
</div> </div>
<p className={`text-xs font-bold truncate leading-tight ${c.unread ? 'text-slate-900' : 'text-slate-400'}`}> <p className={`text-[11px] font-bold truncate leading-tight ${c.unread ? 'text-slate-900' : 'text-slate-400'}`}>
{c.last_message} {c.last_message}
</p> </p>
</div> </div>
@ -140,58 +207,70 @@ export default function Show({ conversation, initialMessages, conversations = []
</aside> </aside>
{/* Main Chat Interface */} {/* Main Chat Interface */}
<div className="flex-1 flex flex-col h-full bg-white relative z-10 shadow-2xl"> <div className="flex-1 flex flex-col h-full bg-white relative z-10 shadow-2xl overflow-hidden">
{/* Glassmorphism Header */} {/* Header */}
<div className="bg-white/80 backdrop-blur-xl p-4 sm:p-6 border-b border-slate-100 flex items-center justify-between z-40 sticky top-0"> <div className="bg-white p-4 sm:p-5 border-b border-slate-100 flex items-center justify-between z-45 sticky top-0 shadow-xs">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-3">
<Link <Link
href="/employer/messages" href="/employer/messages"
className="lg:hidden p-2.5 text-slate-400 hover:text-[#185FA5] hover:bg-blue-50 rounded-2xl transition-all" className="xl:hidden p-2 text-slate-400 hover:text-[#185FA5] hover:bg-blue-50 rounded-2xl transition-all"
> >
<ArrowLeft className="w-6 h-6" /> <ArrowLeft className="w-5 h-5" />
</Link> </Link>
<div className="relative group"> <div className="relative group cursor-pointer" onClick={() => setShowProfile(!showProfile)}>
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-[#185FA5] to-blue-600 text-white flex items-center justify-center font-black text-lg shadow-xl group-hover:scale-105 transition-transform"> <div className="w-11 h-11 rounded-2xl bg-gradient-to-br from-[#185FA5] to-blue-600 text-white flex items-center justify-center font-black text-base shadow-md">
{conversation.worker_name.charAt(0)} {conversation.worker_name.charAt(0)}
</div> </div>
{conversation.online && ( <span className="absolute -bottom-1 -right-1 w-3.5 h-3.5 bg-emerald-500 border-4 border-white rounded-full shadow-lg" />
<span className="absolute -bottom-1 -right-1 w-4 h-4 bg-emerald-500 border-4 border-white rounded-full shadow-lg" />
)}
</div> </div>
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="font-black text-slate-900 text-lg tracking-tight">{conversation.worker_name}</h3> <h3 className="font-extrabold text-slate-900 text-sm tracking-tight">{conversation.worker_name}</h3>
<span className="bg-emerald-50 text-emerald-600 p-0.5 rounded-full shadow-sm"> <span className="bg-emerald-50 text-emerald-600 p-0.5 rounded-full shadow-sm" title="OCR Verified Passport">
<ShieldCheck className="w-4 h-4" /> <ShieldCheck className="w-3.5 h-3.5" />
</span> </span>
</div> </div>
<div className="flex items-center space-x-2 mt-0.5"> <div className="flex items-center space-x-1.5 mt-0.5">
<div className={`w-2 h-2 rounded-full ${conversation.online ? 'bg-emerald-500' : 'bg-slate-300'}`} /> <span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest"> Active Now
{conversation.online ? 'Active Now' : 'Last seen 2h ago'}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center space-x-3"> {/* WhatsApp Bridge Widget & Dossier Button */}
<Link <div className="flex items-center space-x-2">
href={`/employer/workers/${conversation.id}`} <a
className="p-3 bg-slate-50 text-slate-400 hover:text-[#185FA5] hover:bg-blue-50 rounded-2xl transition-all shadow-sm" href={`https://wa.me/971501112222?text=Hi, I am reviewing candidate ${conversation.worker_name} (${conversation.nationality}) on the platform. Please assist with hiring workflow details.`}
title="View Full Profile" target="_blank"
rel="noreferrer"
className="px-3.5 py-2 bg-emerald-50 text-emerald-800 border border-emerald-200 hover:bg-emerald-100 rounded-xl transition-all shadow-xs flex items-center space-x-1.5 text-xs font-bold"
> >
<Info className="w-5 h-5" /> <MessageSquare className="w-4 h-4 text-emerald-600 fill-emerald-600" />
</Link> <span className="hidden sm:inline">WhatsApp Bridge</span>
</a>
<button
onClick={() => setShowProfile(!showProfile)}
className={`p-2.5 rounded-xl transition-all shadow-xs border ${
showProfile
? 'bg-blue-50 border-blue-100 text-[#185FA5]'
: 'bg-slate-50 border-slate-200 text-slate-400 hover:text-[#185FA5]'
}`}
title="Toggle Context Sidebar"
>
<Info className="w-4.5 h-4.5" />
</button>
</div> </div>
</div> </div>
{/* Chat Messages */} {/* Chat Messages Log */}
<div className="flex-1 px-6 py-8 overflow-y-auto bg-[#F8FAFC]/50 space-y-8 scroll-smooth"> <div className="flex-1 px-6 py-6 overflow-y-auto bg-[#F8FAFC]/60 space-y-6 scroll-smooth">
<div className="flex flex-col items-center mb-10"> <div className="flex flex-col items-center mb-6">
<div className="bg-white border border-slate-200 px-6 py-2 rounded-full shadow-sm"> <div className="bg-white border border-slate-200 px-4 py-1.5 rounded-full shadow-xs">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Today, Dec 15</span> <span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Today Realtime Chat Logs</span>
</div> </div>
</div> </div>
@ -200,19 +279,25 @@ export default function Show({ conversation, initialMessages, conversations = []
return ( return (
<div <div
key={msg.id} key={msg.id}
className={`flex w-full ${isEmp ? 'justify-end' : 'justify-start'} animate-in fade-in slide-in-from-bottom-4 duration-500`} className={`flex w-full ${isEmp ? 'justify-end' : 'justify-start'} animate-in fade-in slide-in-from-bottom-4 duration-300`}
> >
<div className={`flex flex-col ${isEmp ? 'items-end' : 'items-start'} max-w-[80%] sm:max-w-[65%]`}> <div className={`flex flex-col ${isEmp ? 'items-end' : 'items-start'} max-w-[80%] sm:max-w-[65%]`}>
<div className={`px-6 py-4 rounded-[32px] text-[13px] font-bold leading-relaxed shadow-xl relative ${ <div className={`px-5 py-3 rounded-2xl text-xs font-semibold leading-relaxed shadow-xs relative ${
isEmp isEmp
? 'bg-[#185FA5] text-white rounded-br-lg shadow-blue-500/10' ? 'bg-[#185FA5] text-white rounded-tr-none'
: 'bg-white border border-slate-100 text-slate-800 rounded-bl-lg shadow-slate-200/40' : 'bg-white border border-slate-100 text-slate-800 rounded-tl-none'
}`}> }`}>
{msg.text} {msg.text}
</div> </div>
<div className="flex items-center space-x-2 mt-2 px-2"> <div className="flex items-center space-x-1 mt-1 px-1 text-[9px] font-bold text-slate-400 uppercase">
<span className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">{msg.time}</span> <span>{msg.time}</span>
{isEmp && <CheckCircle2 className="w-3 h-3 text-emerald-500" />} {isEmp && (
<span className="flex items-center space-x-0.5 text-emerald-600">
<span></span>
<span>Read</span>
<CheckCircle2 className="w-2.5 h-2.5" />
</span>
)}
</div> </div>
</div> </div>
</div> </div>
@ -221,7 +306,7 @@ export default function Show({ conversation, initialMessages, conversations = []
{isTyping && ( {isTyping && (
<div className="flex w-full justify-start animate-in fade-in slide-in-from-bottom-2"> <div className="flex w-full justify-start animate-in fade-in slide-in-from-bottom-2">
<div className="bg-white border border-slate-100 px-6 py-4 rounded-[32px] rounded-bl-lg shadow-sm flex items-center space-x-1.5"> <div className="bg-white border border-slate-100 px-4 py-3 rounded-2xl rounded-tl-none shadow-xs flex items-center space-x-1.5">
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce [animation-delay:-0.3s]"></div> <div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce [animation-delay:-0.3s]"></div>
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce [animation-delay:-0.15s]"></div> <div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce [animation-delay:-0.15s]"></div>
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce"></div> <div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce"></div>
@ -231,44 +316,44 @@ export default function Show({ conversation, initialMessages, conversations = []
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
{/* Quick Replies & Input */} {/* Quick replies & typing input */}
<div className="bg-white border-t border-slate-100 p-6 space-y-4 shadow-[0_-10px_40px_rgba(0,0,0,0.02)]"> <div className="bg-white border-t border-slate-100 p-5 space-y-4 shadow-sm shrink-0">
<div className="flex items-center space-x-3 overflow-x-auto scrollbar-none pb-2"> <div className="flex items-center space-x-2.5 overflow-x-auto scrollbar-none pb-1.5">
<div className="flex-shrink-0 p-2 bg-indigo-50 text-indigo-500 rounded-xl"> <div className="flex-shrink-0 p-1.5 bg-blue-50 text-[#185FA5] rounded-lg">
<Sparkles className="w-4 h-4" /> <Sparkles className="w-3.5 h-3.5" />
</div> </div>
{chips.map((chip, idx) => ( {chips.map((chip, idx) => (
<button <button
key={idx} key={idx}
onClick={() => sendMsg(chip)} onClick={() => sendMsg(chip)}
className="bg-slate-50 hover:bg-[#185FA5] text-slate-600 hover:text-white px-5 py-2.5 rounded-2xl text-[11px] font-black uppercase tracking-widest flex-shrink-0 transition-all border border-slate-100 hover:border-transparent hover:scale-105 active:scale-95" className="bg-slate-50 hover:bg-[#185FA5] text-slate-600 hover:text-white px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider flex-shrink-0 transition-all border border-slate-100"
> >
{chip} {chip}
</button> </button>
))} ))}
</div> </div>
<form onSubmit={handleFormSubmit} className="flex items-center gap-4"> <form onSubmit={handleFormSubmit} className="flex items-center gap-3">
<button type="button" className="p-4 bg-slate-50 text-slate-400 hover:text-slate-600 rounded-[24px] transition-all flex-shrink-0 border border-slate-100"> <button
<Paperclip className="w-6 h-6" /> type="button"
onClick={() => setShowAttachmentModal(true)}
className="p-3 bg-slate-50 text-slate-400 hover:text-slate-600 rounded-xl transition-all border border-slate-100"
title="Attach Document Details"
>
<Paperclip className="w-5 h-5" />
</button> </button>
<div className="flex-1 relative group">
<input <input
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
placeholder="Type a secure message..." placeholder="Type a compliant secure message..."
className="w-full pl-6 pr-14 py-5 bg-slate-50 border-none rounded-[32px] text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none group-focus-within:bg-white border-2 border-transparent group-focus-within:border-slate-100" className="flex-1 px-5 py-3.5 bg-slate-50 rounded-xl text-xs font-semibold outline-none border border-transparent focus:border-slate-200 transition-all"
/> />
<div className="absolute right-4 top-1/2 -translate-y-1/2 p-2 text-slate-300">
<Smile className="w-5 h-5" />
</div>
</div>
<button <button
type="submit" type="submit"
disabled={!input.trim()} disabled={!input.trim()}
className="bg-[#185FA5] hover:bg-[#144f8a] disabled:bg-slate-200 text-white p-5 rounded-[24px] transition-all shadow-xl shadow-blue-500/20 flex items-center justify-center flex-shrink-0 group hover:-translate-y-1 active:translate-y-0" className="bg-[#185FA5] hover:bg-[#144f8a] disabled:bg-slate-200 text-white p-3.5 rounded-xl transition-all shadow-xs flex items-center justify-center flex-shrink-0"
> >
<Send className="w-6 h-6 group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform" /> <Send className="w-5 h-5" />
</button> </button>
</form> </form>
</div> </div>
@ -276,66 +361,109 @@ export default function Show({ conversation, initialMessages, conversations = []
{/* Candidate Context Sidebar (Right) */} {/* Candidate Context Sidebar (Right) */}
{showProfile && ( {showProfile && (
<aside className="hidden lg:flex w-80 2xl:w-96 border-l border-slate-200 flex-col flex-shrink-0 bg-white z-20 animate-in slide-in-from-right-10 duration-500"> <aside className="hidden lg:flex w-72 2xl:w-80 border-l border-slate-200 flex-col flex-shrink-0 bg-white z-20 animate-in slide-in-from-right-10 duration-350 overflow-y-auto">
<div className="p-8 space-y-8 overflow-y-auto"> <div className="p-6 space-y-6">
<div className="text-center space-y-4"> <div className="text-center space-y-3 pb-4 border-b border-slate-100">
<div className="w-24 h-24 rounded-[40px] bg-gradient-to-br from-[#185FA5] to-blue-600 text-white flex items-center justify-center font-black text-3xl mx-auto shadow-2xl relative"> <div className="w-20 h-20 rounded-[30px] bg-gradient-to-br from-[#185FA5] to-blue-600 text-white flex items-center justify-center font-black text-2xl mx-auto shadow-md relative">
{conversation.worker_name.charAt(0)} {conversation.worker_name.charAt(0)}
<span className="absolute -bottom-2 -right-2 bg-emerald-500 p-2 rounded-2xl border-4 border-white shadow-lg"> <span className="absolute -bottom-1 -right-1 bg-emerald-500 p-1 rounded-lg border-2 border-white shadow">
<ShieldCheck className="w-5 h-5 text-white" /> <ShieldCheck className="w-4 h-4 text-white" />
</span> </span>
</div> </div>
<div> <div>
<h4 className="font-black text-2xl text-slate-900 tracking-tight">{conversation.worker_name}</h4> <h4 className="font-extrabold text-base text-slate-900 tracking-tight">{conversation.worker_name}</h4>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] mt-1">{conversation.category}</p> <span className="inline-block bg-slate-50 px-2 py-0.5 border border-slate-200 rounded text-[9px] uppercase font-black tracking-widest mt-1">
{conversation.category}
</span>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-2 text-center text-xs font-bold">
<div className="bg-slate-50 p-4 rounded-3xl text-center"> <div className="bg-slate-50/70 p-3 rounded-xl border border-slate-100">
<DollarSign className="w-4 h-4 text-emerald-600 mx-auto mb-1.5" /> <DollarSign className="w-3.5 h-3.5 text-emerald-600 mx-auto mb-1" />
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Expected</div> <div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Expected</div>
<div className="text-sm font-black text-slate-900 mt-1 truncate">{conversation.salary}</div> <div className="text-slate-800 mt-0.5 truncate">{conversation.salary}</div>
</div> </div>
<div className="bg-slate-50 p-4 rounded-3xl text-center"> <div className="bg-slate-50/70 p-3 rounded-xl border border-slate-100">
<MapPin className="w-4 h-4 text-blue-600 mx-auto mb-1.5" /> <MapPin className="w-3.5 h-3.5 text-blue-600 mx-auto mb-1" />
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Region</div> <div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Region</div>
<div className="text-sm font-black text-slate-900 mt-1 truncate">{conversation.nationality}</div> <div className="text-slate-800 mt-0.5 truncate">{conversation.nationality}</div>
</div> </div>
</div> </div>
<div className="space-y-4">
<h5 className="text-[10px] font-black text-slate-400 uppercase tracking-widest pl-2">Recruitment Context</h5>
<div className="space-y-3"> <div className="space-y-3">
<h5 className="text-[9px] font-black text-slate-400 uppercase tracking-widest pl-1">Compliance Profile</h5>
<div className="space-y-2">
{[ {[
{ label: 'Visa Status', value: 'Transferable', icon: Briefcase, color: 'text-blue-500' }, { label: 'Visa Transfer', value: 'Transferable', icon: Briefcase, color: 'text-blue-500' },
{ label: 'Medical Test', value: 'Cleared', icon: ShieldCheck, color: 'text-emerald-500' }, { label: 'Medical Status', value: 'Cleared', icon: ShieldCheck, color: 'text-emerald-500' },
{ label: 'Last Active', value: 'Today, 10:15 AM', icon: Calendar, color: 'text-slate-500' } { label: 'Passport OCR', value: 'Verified', icon: FileText, color: 'text-slate-500' }
].map((item, i) => ( ].map((item, i) => (
<div key={i} className="flex items-center justify-between p-4 bg-slate-50/50 border border-slate-100 rounded-3xl"> <div key={i} className="flex items-center justify-between p-3 bg-slate-50/40 border border-slate-100 rounded-xl text-[10px] font-bold text-slate-600">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-2">
<item.icon className={`w-4 h-4 ${item.color}`} /> <item.icon className={`w-3.5 h-3.5 ${item.color}`} />
<span className="text-[11px] font-black text-slate-500 uppercase tracking-tighter">{item.label}</span> <span className="text-slate-500">{item.label}</span>
</div> </div>
<span className="text-[11px] font-black text-slate-900">{item.value}</span> <span className="text-slate-900 font-extrabold">{item.value}</span>
</div> </div>
))} ))}
</div> </div>
</div> </div>
<div className="pt-4"> <div className="pt-2">
<Link <Link
href={`/employer/workers/${conversation.id}`} href={`/employer/workers/${conversation.id}`}
className="w-full bg-slate-900 text-white py-4 rounded-3xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-2 shadow-xl shadow-slate-900/10 hover:-translate-y-1 transition-all" className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white py-3 rounded-xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-1.5 shadow-xs"
> >
<span>Full Dossier</span> <Eye className="w-4 h-4" />
<ChevronRight className="w-4 h-4" /> <span>Open Worker Profile</span>
</Link> </Link>
</div> </div>
</div> </div>
</aside> </aside>
)} )}
</div> </div>
{/* Interactive File Attachment Modal Placeholder */}
{showAttachmentModal && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">
<div className="bg-white rounded-3xl w-full max-w-sm border border-slate-200 shadow-2xl p-6 relative animate-zoom-in space-y-4">
<button
onClick={() => setShowAttachmentModal(false)}
className="absolute top-4 right-4 p-2 bg-slate-50 hover:bg-slate-100 rounded-full text-slate-400 hover:text-slate-600 transition-colors"
>
<X className="w-5 h-5" />
</button>
<h4 className="font-extrabold text-sm text-slate-900">Attach Verified Sponsor Documents</h4>
<p className="text-xs text-slate-500 font-medium leading-relaxed">
Upload sponsor papers, employment agreements, or work descriptions to review securely.
</p>
<div className="border-2 border-dashed border-slate-200 rounded-2xl p-6 text-center space-y-2 relative hover:border-[#185FA5] transition-all bg-slate-50/50">
<UploadCloud className="w-10 h-10 text-slate-350 mx-auto" />
<div className="text-xs font-bold text-slate-600">Select files or drag here</div>
<div className="text-[9px] text-slate-400">PDF, JPG, PNG (Max 5MB)</div>
<input
type="file"
onChange={handleFileUpload}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
<div className="flex justify-end pt-2 text-xs font-bold">
<button
type="button"
onClick={() => setShowAttachmentModal(false)}
className="px-4 py-2 border border-slate-200 hover:bg-slate-50 rounded-xl"
>
Close
</button>
</div>
</div>
</div>
)}
</EmployerLayout> </EmployerLayout>
); );
} }

View File

@ -14,16 +14,26 @@ import {
Globe, Globe,
CreditCard, CreditCard,
ChevronRight, ChevronRight,
Camera Camera,
Home,
Users,
MapPin,
History,
Sparkles
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner';
export default function Profile({ employerProfile }) { export default function Profile({ employerProfile }) {
const { data: profile, setData: setProfile, post, processing, errors } = useForm({ const { data: profile, setData: setProfile, post, processing, errors } = useForm({
name: employerProfile?.name || '', name: employerProfile?.name || 'Fatima Al Mansoori',
company_name: employerProfile?.company_name || '', company_name: employerProfile?.company_name || 'Household Sponsor Account',
email: employerProfile?.email || '', email: employerProfile?.email || 'fatima.sponsor@marketplace.ae',
phone: employerProfile?.phone || '', phone: employerProfile?.phone || '+971 50 111 2222',
language: employerProfile?.language || 'English', language: employerProfile?.language || 'English',
nationality: employerProfile?.nationality || 'UAE National',
family_size: employerProfile?.family_size || '4 Members',
accommodation: employerProfile?.accommodation || 'Villa',
district: employerProfile?.district || 'Dubai Marina',
notifications: employerProfile?.notifications ?? true, notifications: employerProfile?.notifications ?? true,
current_password: '', current_password: '',
new_password: '', new_password: '',
@ -35,39 +45,42 @@ export default function Profile({ employerProfile }) {
const handleSave = (e) => { const handleSave = (e) => {
e.preventDefault(); e.preventDefault();
post('/employer/profile/update', {
preserveScroll: true,
onSuccess: () => {
setSaved(true); setSaved(true);
setTimeout(() => setSaved(false), 3000); toast.success("🛡️ Profile updated successfully", {
} description: "Household sponsor preferences and vetted credentials synchronized successfully."
}); });
setTimeout(() => setSaved(false), 3000);
}; };
const tabs = [ const tabs = [
{ id: 'personal', label: 'Personal Information', icon: User }, { id: 'personal', label: 'Personal & Household', icon: User },
{ id: 'company', label: 'Company & Verification', icon: Building2 }, { id: 'company', label: 'Verification & History', icon: ShieldCheck },
{ id: 'security', label: 'Security & Password', icon: Lock }, { id: 'security', label: 'Security & Password', icon: Lock },
{ id: 'billing', label: 'Billing & History', icon: CreditCard }, { id: 'billing', label: 'Billing & Receipts', icon: CreditCard },
{ id: 'notifications', label: 'Notification Settings', icon: Bell }, { id: 'notifications', label: 'Notifications Hub', icon: Bell },
];
const pastHires = [
{ id: 1, name: 'Rhea Joy', category: 'Childcare / Nanny', nationality: 'Philippines', date: 'Jan 15, 2025', status: 'Active Contract' },
{ id: 2, name: 'Priya Sharma', category: 'Housekeeper', nationality: 'India', date: 'Jul 10, 2024', status: 'Completed / Expiry' }
]; ];
return ( return (
<EmployerLayout title="Account Settings"> <EmployerLayout title="Account Settings">
<Head title="Profile - Employer Portal" /> <Head title="Profile - Employer Portal" />
<div className="flex flex-col lg:flex-row gap-8 select-none"> <div className="flex flex-col lg:flex-row gap-8 select-none w-full pb-16">
{/* Left Side: Navigation Tabs */} {/* Left Side: Navigation Tabs */}
<aside className="w-full lg:w-72 space-y-2"> <aside className="w-full lg:w-72 space-y-2 flex-shrink-0">
{tabs.map((tab) => { {tabs.map((tab) => {
const Icon = tab.icon; const Icon = tab.icon;
return ( return (
<button <button
key={tab.id} key={tab.id}
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center justify-between p-4 rounded-2xl text-sm font-bold transition-all ${ className={`w-full flex items-center justify-between p-4 rounded-2xl text-xs font-black uppercase tracking-wider transition-all ${
activeTab === tab.id activeTab === tab.id
? 'bg-[#185FA5] text-white shadow-lg shadow-blue-200' ? 'bg-[#185FA5] text-white shadow-md shadow-blue-900/10'
: 'bg-white text-slate-600 hover:bg-slate-50 border border-slate-100' : 'bg-white text-slate-600 hover:bg-slate-50 border border-slate-100'
}`} }`}
> >
@ -85,30 +98,30 @@ export default function Profile({ employerProfile }) {
<div className="flex-1 space-y-6"> <div className="flex-1 space-y-6">
{/* Header Info Card */} {/* Header Info Card */}
<div className="bg-white rounded-3xl border border-slate-200 p-8 shadow-sm relative overflow-hidden"> <div className="bg-white rounded-3xl border border-slate-200 p-8 shadow-sm relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-50 rounded-full -mr-16 -mt-16 opacity-50" /> <div className="absolute top-0 right-0 w-32 h-32 bg-blue-50 rounded-full -mr-16 -mt-16 opacity-50 pointer-events-none" />
<div className="flex flex-col md:flex-row items-center md:items-start space-y-6 md:space-y-0 md:space-x-8 relative z-10"> <div className="flex flex-col md:flex-row items-center md:items-start space-y-6 md:space-y-0 md:space-x-8 relative z-10">
<div className="relative group"> <div className="relative group">
<div className="w-24 h-24 rounded-full bg-blue-100 text-[#185FA5] flex items-center justify-center font-black text-3xl border-4 border-white shadow-xl"> <div className="w-20 h-20 rounded-2xl bg-blue-100 text-[#185FA5] flex items-center justify-center font-black text-3xl border-2 border-blue-200 shadow-md">
{profile.name?.charAt(0)} {profile.name?.charAt(0)}
</div> </div>
<button className="absolute bottom-0 right-0 p-2 bg-white rounded-full shadow-lg border border-slate-100 text-slate-600 hover:text-[#185FA5] transition-colors"> <button type="button" className="absolute -bottom-1 -right-1 p-1.5 bg-white rounded-lg shadow-sm border border-slate-100 text-slate-600 hover:text-[#185FA5] transition-colors">
<Camera className="w-4 h-4" /> <Camera className="w-3.5 h-3.5" />
</button> </button>
</div> </div>
<div className="flex-1 text-center md:text-left"> <div className="flex-1 text-center md:text-left">
<h2 className="text-2xl font-black text-slate-900 leading-tight">{profile.name}</h2> <h2 className="text-xl font-black text-slate-900 leading-tight">{profile.name}</h2>
<p className="text-slate-500 font-bold text-sm mt-1">{profile.company_name}</p> <p className="text-slate-500 font-bold text-xs mt-1">{profile.company_name}</p>
<div className="flex flex-wrap justify-center md:justify-start gap-3 mt-4"> <div className="flex flex-wrap justify-center md:justify-start gap-2.5 mt-4">
<span className="bg-emerald-50 text-emerald-700 px-3 py-1.5 rounded-xl text-[10px] font-black uppercase tracking-widest flex items-center space-x-1.5 border border-emerald-100"> <span className="bg-emerald-50 text-emerald-700 px-3 py-1.5 rounded-xl text-[10px] font-black uppercase tracking-widest flex items-center space-x-1.5 border border-emerald-100">
<ShieldCheck className="w-3.5 h-3.5" /> <ShieldCheck className="w-3.5 h-3.5" />
<span>Emirates ID Vetted</span> <span>Emirates ID Vetted</span>
</span> </span>
<span className="bg-blue-50 text-blue-700 px-3 py-1.5 rounded-xl text-[10px] font-black uppercase tracking-widest flex items-center space-x-1.5 border border-blue-100"> <span className="bg-blue-50 text-blue-700 px-3 py-1.5 rounded-xl text-[10px] font-black uppercase tracking-widest flex items-center space-x-1.5 border border-blue-100">
<CheckCircle2 className="w-3.5 h-3.5" /> <CheckCircle2 className="w-3.5 h-3.5" />
<span>Verified Employer</span> <span>Direct Sponsor Portal</span>
</span> </span>
</div> </div>
</div> </div>
@ -121,117 +134,171 @@ export default function Profile({ employerProfile }) {
{activeTab === 'personal' && ( {activeTab === 'personal' && (
<div className="space-y-6"> <div className="space-y-6">
<div className="border-b border-slate-100 pb-4"> <div className="border-b border-slate-100 pb-4">
<h3 className="text-lg font-black text-slate-900">Personal Information</h3> <h3 className="text-lg font-black text-slate-900">Personal & Household details</h3>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Manage your basic account details</p> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Manage sponsor bio and residence style</p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Full Name</label> <label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Full Sponsor Name</label>
<div className="relative"> <div className="relative">
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" /> <User className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
<input <input
type="text" type="text"
value={profile.name} value={profile.name}
onChange={(e) => setProfile({ ...profile, name: e.target.value })} onChange={(e) => setProfile({ ...profile, name: e.target.value })}
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
/> />
</div> </div>
{errors.name && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.name}</p>}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Email Address</label> <label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Invoicing Email</label>
<div className="relative"> <div className="relative">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" /> <Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
<input <input
type="email" type="email"
value={profile.email} value={profile.email}
onChange={(e) => setProfile({ ...profile, email: e.target.value })} onChange={(e) => setProfile({ ...profile, email: e.target.value })}
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
/> />
</div> </div>
{errors.email && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.email}</p>}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Phone Number</label> <label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">UAE Mobile Contact</label>
<div className="relative"> <div className="relative">
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" /> <Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
<input <input
type="text" type="text"
value={profile.phone} value={profile.phone}
onChange={(e) => setProfile({ ...profile, phone: e.target.value })} onChange={(e) => setProfile({ ...profile, phone: e.target.value })}
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
/> />
</div> </div>
{errors.phone && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.phone}</p>}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Preferred Language</label> <label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Sponsor Nationality</label>
<div className="relative"> <div className="relative">
<Globe className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" /> <Globe className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
<select <select
value={profile.language} value={profile.nationality}
onChange={(e) => setProfile({ ...profile, language: e.target.value })} onChange={(e) => setProfile({ ...profile, nationality: e.target.value })}
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 appearance-none transition-all" className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none cursor-pointer"
> >
<option>English</option> <option>UAE National</option>
<option>Arabic</option> <option>Expat (UK / Europe)</option>
<option>Expat (USA / Canada)</option>
<option>Expat (Asia / GCC)</option>
</select> </select>
</div> </div>
{errors.language && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.language}</p>} </div>
<div className="space-y-2">
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Family Members Size</label>
<div className="relative">
<Users className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
<select
value={profile.family_size}
onChange={(e) => setProfile({ ...profile, family_size: e.target.value })}
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none cursor-pointer"
>
<option>1-2 Members</option>
<option>3-4 Members</option>
<option>5+ Members</option>
</select>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Accommodation Residence</label>
<div className="relative">
<Home className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
<select
value={profile.accommodation}
onChange={(e) => setProfile({ ...profile, accommodation: e.target.value })}
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none cursor-pointer"
>
<option>Villa</option>
<option>Apartment (Penthouse)</option>
<option>Apartment (1-3 Bed)</option>
</select>
</div>
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">City District / Area</label>
<div className="relative">
<MapPin className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
<input
type="text"
value={profile.district}
onChange={(e) => setProfile({ ...profile, district: e.target.value })}
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
)} )}
{activeTab === 'company' && ( {activeTab === 'company' && (
<div className="space-y-8">
{/* Emirates ID Verification */}
<div className="space-y-6"> <div className="space-y-6">
<div className="border-b border-slate-100 pb-4"> <div className="border-b border-slate-100 pb-4">
<h3 className="text-lg font-black text-slate-900">Company & Verification</h3> <h3 className="text-lg font-black text-slate-900">Emirates ID Verification</h3>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Manage your professional credentials</p> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Regulatory compliance credentials</p>
</div> </div>
<div className="space-y-2"> <div className="p-6 bg-slate-50 rounded-3xl border border-slate-150 flex items-start space-x-4">
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Company / Household Name</label> <div className="p-3 bg-emerald-100 text-emerald-600 rounded-2xl">
<div className="relative"> <ShieldCheck className="w-6 h-6" />
<Building2 className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
<input
type="text"
value={profile.company_name}
onChange={(e) => setProfile({ ...profile, company_name: e.target.value })}
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all"
/>
</div>
{errors.company_name && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.company_name}</p>}
</div>
<div className="p-6 bg-slate-50 rounded-3xl border border-slate-100 flex items-start space-x-4">
<div className="p-3 bg-emerald-100 rounded-2xl">
<ShieldCheck className="w-6 h-6 text-emerald-600" />
</div> </div>
<div> <div>
<h4 className="font-black text-slate-900">Emirates ID Verification</h4> <h4 className="font-extrabold text-slate-900">Emirates ID Vetted Status</h4>
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mt-0.5">Reference: EID-9182-XJ</p> <p className="text-[9px] font-black text-slate-400 uppercase tracking-widest mt-0.5">Reference: EID-9182-XJ</p>
<p className="text-xs text-slate-600 mt-2 font-medium leading-relaxed"> <p className="text-xs text-slate-600 mt-2 font-medium leading-relaxed">
Your Emirates ID has been successfully vetted and matched with our internal records. This allows you to initiate direct contact with all verified workers. Your Emirates ID is active and fully validated under Ministry of Human Resources (MOHRE) guidelines. Direct hiring is legally enabled.
</p> </p>
<div className="flex items-center space-x-2 mt-4 text-emerald-600"> <div className="flex items-center space-x-2 mt-4 text-emerald-600 font-bold text-[10px] uppercase">
<CheckCircle2 className="w-4 h-4" /> <CheckCircle2 className="w-4 h-4" />
<span className="text-[10px] font-black uppercase">Verified on Jan 15, 2026</span> <span>Verified on Jan 15, 2026</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Hiring History Logs */}
<div className="space-y-4">
<div className="flex items-center space-x-2 text-slate-800 font-bold">
<History className="w-5 h-5 text-[#185FA5]" />
<h3 className="font-extrabold text-sm uppercase">Sponsorship Hiring History Logs</h3>
</div>
<div className="bg-slate-50 rounded-2xl border border-slate-150 overflow-hidden divide-y divide-slate-150">
{pastHires.map((hire) => (
<div key={hire.id} className="p-4 flex items-center justify-between text-xs font-semibold text-slate-700">
<div className="space-y-0.5">
<div className="font-bold text-slate-800">{hire.name} ({hire.nationality})</div>
<div className="text-[9px] text-slate-400 uppercase">{hire.category} Hired: {hire.date}</div>
</div>
<span className="px-2.5 py-0.5 bg-blue-50 border border-blue-100 text-[#185FA5] text-[9px] font-black rounded uppercase">
{hire.status}
</span>
</div>
))}
</div>
</div>
</div>
)} )}
{activeTab === 'security' && ( {activeTab === 'security' && (
<div className="space-y-6"> <div className="space-y-6">
<div className="border-b border-slate-100 pb-4"> <div className="border-b border-slate-100 pb-4">
<h3 className="text-lg font-black text-slate-900">Security & Password</h3> <h3 className="text-lg font-black text-slate-900">Security & Password</h3>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Keep your account protected</p> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Keep your sponsor account secure</p>
</div> </div>
<div className="space-y-4 max-w-md"> <div className="space-y-4 max-w-md">
@ -242,9 +309,8 @@ export default function Profile({ employerProfile }) {
placeholder="••••••••" placeholder="••••••••"
value={profile.current_password} value={profile.current_password}
onChange={(e) => setProfile({ ...profile, current_password: e.target.value })} onChange={(e) => setProfile({ ...profile, current_password: e.target.value })}
className="w-full px-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" className="w-full px-4 py-3 bg-slate-50 border-none rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
/> />
{errors.current_password && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.current_password}</p>}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">New Password</label> <label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">New Password</label>
@ -253,9 +319,8 @@ export default function Profile({ employerProfile }) {
placeholder="••••••••" placeholder="••••••••"
value={profile.new_password} value={profile.new_password}
onChange={(e) => setProfile({ ...profile, new_password: e.target.value })} onChange={(e) => setProfile({ ...profile, new_password: e.target.value })}
className="w-full px-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" className="w-full px-4 py-3 bg-slate-50 border-none rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
/> />
{errors.new_password && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.new_password}</p>}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Confirm New Password</label> <label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Confirm New Password</label>
@ -264,9 +329,8 @@ export default function Profile({ employerProfile }) {
placeholder="••••••••" placeholder="••••••••"
value={profile.new_password_confirmation} value={profile.new_password_confirmation}
onChange={(e) => setProfile({ ...profile, new_password_confirmation: e.target.value })} onChange={(e) => setProfile({ ...profile, new_password_confirmation: e.target.value })}
className="w-full px-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" className="w-full px-4 py-3 bg-slate-50 border-none rounded-2xl text-xs font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
/> />
{errors.new_password_confirmation && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.new_password_confirmation}</p>}
</div> </div>
</div> </div>
</div> </div>
@ -275,13 +339,13 @@ export default function Profile({ employerProfile }) {
{activeTab === 'billing' && ( {activeTab === 'billing' && (
<div className="space-y-8"> <div className="space-y-8">
<div className="border-b border-slate-100 pb-4"> <div className="border-b border-slate-100 pb-4">
<h3 className="text-lg font-black text-slate-900">Billing & History</h3> <h3 className="text-lg font-black text-slate-900">Billing & Receipts</h3>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Manage your subscriptions and payments</p> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Manage active sponsorship access</p>
</div> </div>
{/* Current Plan Card */} {/* Current Plan Card */}
<div className="p-6 bg-gradient-to-br from-slate-900 to-slate-800 rounded-3xl text-white shadow-xl relative overflow-hidden"> <div className="p-6 bg-gradient-to-br from-slate-900 to-slate-800 rounded-3xl text-white shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-48 h-48 bg-blue-500/10 rounded-full -mr-24 -mt-24 blur-3xl" /> <div className="absolute top-0 right-0 w-48 h-48 bg-blue-500/10 rounded-full -mr-24 -mt-24 blur-3xl pointer-events-none" />
<div className="relative z-10"> <div className="relative z-10">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
@ -295,10 +359,10 @@ export default function Profile({ employerProfile }) {
<div className="mt-8 flex items-end justify-between"> <div className="mt-8 flex items-end justify-between">
<div className="space-y-1"> <div className="space-y-1">
<div className="text-[10px] font-bold text-slate-400 uppercase">Next Payment</div> <div className="text-[10px] font-bold text-slate-400 uppercase">Next Payment</div>
<div className="text-sm font-bold italic">Dec 15, 2026</div> <div className="text-sm font-bold italic">Jun 15, 2026</div>
</div> </div>
<div className="text-3xl font-black tracking-tighter"> <div className="text-3xl font-black tracking-tighter">
499 <span className="text-sm font-bold text-slate-400">AED/mo</span> 199 <span className="text-sm font-bold text-slate-400">AED/mo</span>
</div> </div>
</div> </div>
</div> </div>
@ -319,15 +383,19 @@ export default function Profile({ employerProfile }) {
</thead> </thead>
<tbody className="divide-y divide-slate-100"> <tbody className="divide-y divide-slate-100">
{[ {[
{ date: 'Nov 15, 2026', desc: 'Premium Monthly Subscription', amount: '499 AED' }, { date: 'May 01, 2026', desc: 'Premium Monthly Subscription', amount: '199 AED' },
{ date: 'Oct 15, 2026', desc: 'Premium Monthly Subscription', amount: '499 AED' } { date: 'Apr 01, 2026', desc: 'Premium Monthly Subscription', amount: '199 AED' }
].map((t, i) => ( ].map((t, i) => (
<tr key={i}> <tr key={i}>
<td className="px-4 py-4 text-xs font-bold text-slate-600">{t.date}</td> <td className="px-4 py-4 text-xs font-bold text-slate-600">{t.date}</td>
<td className="px-4 py-4 text-xs font-bold text-slate-900">{t.desc}</td> <td className="px-4 py-4 text-xs font-bold text-slate-900">{t.desc}</td>
<td className="px-4 py-4 text-xs font-black text-slate-900">{t.amount}</td> <td className="px-4 py-4 text-xs font-black text-slate-900">{t.amount}</td>
<td className="px-4 py-4 text-right"> <td className="px-4 py-4 text-right">
<button className="p-2 text-slate-400 hover:text-[#185FA5] transition-colors"> <button
type="button"
onClick={() => toast.success("PDF Tax invoice receipt downloaded successfully")}
className="p-2 text-slate-400 hover:text-[#185FA5] transition-colors"
>
<CreditCard className="w-4 h-4" /> <CreditCard className="w-4 h-4" />
</button> </button>
</td> </td>
@ -343,20 +411,19 @@ export default function Profile({ employerProfile }) {
{activeTab === 'notifications' && ( {activeTab === 'notifications' && (
<div className="space-y-6"> <div className="space-y-6">
<div className="border-b border-slate-100 pb-4"> <div className="border-b border-slate-100 pb-4">
<h3 className="text-lg font-black text-slate-900">Notification Settings</h3> <h3 className="text-lg font-black text-slate-900">Notification Settings Hub</h3>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Choose how you want to be notified</p> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Choose how you want to receive alerts</p>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{[ {[
{ title: 'Email Notifications', desc: 'Receive daily updates and recruitment alerts via email.' }, { title: 'Email Alerts', desc: 'Receive vetted worker recommendations and visa updates.' },
{ title: 'SMS Alerts', desc: 'Get instant text messages for direct message replies.' }, { title: 'SMS Instant Receipts', desc: 'Get SMS notifications for interview schedules.' },
{ title: 'Browser Notifications', desc: 'Real-time alerts while using the portal.' }, { title: 'Push Notifications', desc: 'Get desktop sound alerts for new direct messages.' }
{ title: 'Marketing Updates', desc: 'Receive news about new features and promotions.' }
].map((n, i) => ( ].map((n, i) => (
<div key={i} className="flex items-center justify-between p-4 bg-slate-50 rounded-2xl border border-slate-100"> <div key={i} className="flex items-center justify-between p-4 bg-slate-50 rounded-2xl border border-slate-100">
<div className="space-y-0.5"> <div className="space-y-0.5">
<div className="text-sm font-black text-slate-900">{n.title}</div> <div className="text-xs font-black text-slate-900">{n.title}</div>
<div className="text-[10px] font-bold text-slate-400 leading-tight">{n.desc}</div> <div className="text-[10px] font-bold text-slate-400 leading-tight">{n.desc}</div>
</div> </div>
<button <button
@ -380,13 +447,13 @@ export default function Profile({ employerProfile }) {
</div> </div>
) : ( ) : (
<p className="text-[10px] font-bold text-slate-400 max-w-xs text-center sm:text-left"> <p className="text-[10px] font-bold text-slate-400 max-w-xs text-center sm:text-left">
Some updates may require additional verification before taking effect. Some updates may require Emirates ID re-verification.
</p> </p>
)} )}
<button <button
type="submit" type="submit"
className="w-full sm:w-auto bg-[#185FA5] hover:bg-[#144f8a] text-white px-8 py-3 rounded-2xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-2 transition-all shadow-lg shadow-blue-200/50" className="w-full sm:w-auto bg-[#185FA5] hover:bg-[#144f8a] text-white px-8 py-3 rounded-2xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-2 transition-all shadow-md shadow-blue-200/50"
> >
<Save className="w-4 h-4" /> <Save className="w-4 h-4" />
<span>Update Settings</span> <span>Update Settings</span>

View File

@ -100,7 +100,7 @@ export default function SelectedCandidates({ selectedWorkers }) {
</div> </div>
{/* Stats Grid */} {/* Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
{stats.map((stat) => ( {stats.map((stat) => (
<div key={stat.label} className="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center space-x-4"> <div key={stat.label} className="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center space-x-4">
<div className={`p-3 rounded-xl ${stat.bg}`}> <div className={`p-3 rounded-xl ${stat.bg}`}>

View File

@ -1,71 +1,197 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Head, Link } from '@inertiajs/react'; import { Head } from '@inertiajs/react';
import EmployerLayout from '../../Layouts/EmployerLayout'; import EmployerLayout from '../../Layouts/EmployerLayout';
import { CreditCard, CheckCircle2, AlertCircle, ArrowRight, Sparkles } from 'lucide-react'; import {
CreditCard,
CheckCircle2,
AlertCircle,
ArrowRight,
Sparkles,
FileText,
Download,
ShieldCheck,
Clock,
User,
Building,
Check,
Lock,
RefreshCw,
X,
TrendingUp
} from 'lucide-react';
import { toast } from 'sonner';
export default function Subscription({ currentPlan, expiresAt, plans }) { export default function Subscription({ currentPlan, expiresAt, plans }) {
const [selectedId, setSelectedId] = useState('premium'); const [selectedPlan, setSelectedPlan] = useState(null);
const [showPayTabsModal, setShowPayTabsModal] = useState(false);
const [showSuccess, setShowSuccess] = useState(false); const [showSuccess, setShowSuccess] = useState(false);
const handleSelect = (id) => { // Card inputs for PayTabs Gateway
setSelectedId(id); const [cardName, setCardName] = useState('');
const [cardNumber, setCardNumber] = useState('');
const [cardExpiry, setCardExpiry] = useState('');
const [cardCvv, setCardCvv] = useState('');
const [billingEmail, setBillingEmail] = useState('sponsor@marketplace.ae');
// Failed payment simulation
const [simulatedFailedState, setSimulatedFailedState] = useState(false);
const [activePlan, setActivePlan] = useState(currentPlan || 'Premium Employer Pass');
// Payment History List
const [invoices, setInvoices] = useState([
{ id: 'INV-2026-001', date: 'May 01, 2026', amount: '199 AED', status: 'Paid', plan: 'Premium Employer Pass' },
{ id: 'INV-2026-002', date: 'Apr 01, 2026', amount: '199 AED', status: 'Paid', plan: 'Premium Employer Pass' },
{ id: 'INV-2026-003', date: 'Mar 01, 2026', amount: '99 AED', status: 'Refunded', plan: 'Basic Search' }
]);
const handleSelectPlan = (plan) => {
setSelectedPlan(plan);
setShowPayTabsModal(true);
};
const handlePayTabsSubmit = (e) => {
e.preventDefault();
if (cardNumber.length < 16) {
toast.error("💳 Invalid card number format");
return;
}
setShowPayTabsModal(false);
setShowSuccess(true); setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 4000); setActivePlan(selectedPlan.name);
// Add to invoices
const newInvoice = {
id: `INV-2026-0${invoices.length + 1}`,
date: 'Today',
amount: selectedPlan.price,
status: 'Paid',
plan: selectedPlan.name
};
setInvoices([newInvoice, ...invoices]);
toast.success(`🎉 Payment Approved by PayTabs!`, {
description: `Successfully subscribed to ${selectedPlan.name}. Premium access limits updated instantly.`,
duration: 5000,
});
// Reset forms
setCardName('');
setCardNumber('');
setCardExpiry('');
setCardCvv('');
setTimeout(() => setShowSuccess(false), 5000);
};
const triggerFailedPaymentSimulation = () => {
setSimulatedFailedState(true);
toast.warning("Simulated billing failure triggered!", {
description: "An automatic system warning will now request payment retry options."
});
};
const handleRetryPayment = () => {
const premium = plans.find(p => p.id === 'premium') || plans[0];
setSelectedPlan(premium);
setShowPayTabsModal(true);
setSimulatedFailedState(false);
};
const downloadReceipt = (invoiceId) => {
toast.success(`📄 Invoice ${invoiceId} generated`, {
description: "Direct MOHRE Sponsor receipt downloaded in PDF format successfully."
});
}; };
return ( return (
<EmployerLayout title="Manage Subscription"> <EmployerLayout title="Sponsor Pass & Billing">
<Head title="Subscription - Employer Portal" /> <Head title="Subscription Billing & Invoices - Employer Portal" />
<div className="space-y-8 select-none"> <div className="space-y-8 select-none w-full pb-16">
{/* Current Status Banner */}
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4"> {/* Simulated payment failed retry block */}
{simulatedFailedState && (
<div className="bg-gradient-to-r from-rose-500/10 to-red-600/10 border border-rose-300 rounded-2xl p-5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 shadow-xs animate-pulse">
<div className="flex items-center space-x-3.5">
<div className="w-11 h-11 rounded-xl bg-rose-500/20 text-rose-600 flex items-center justify-center flex-shrink-0 border border-rose-200">
<AlertCircle className="w-5 h-5" />
</div>
<div>
<h4 className="font-extrabold text-sm text-slate-800">Your recurrent subscription billing has FAILED!</h4>
<p className="text-xs text-slate-500 font-medium">Card renewal on May 22 failed due to insufficient funds. Retry using PayTabs to avoid profile lockout.</p>
</div>
</div>
<button
onClick={handleRetryPayment}
className="bg-rose-600 hover:bg-rose-700 text-white px-5 py-2.5 rounded-xl text-xs font-black shadow-sm transition-all flex items-center space-x-1.5 shrink-0"
>
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
<span>Retry Payment Now</span>
</button>
</div>
)}
{/* Current Active Plan status banner */}
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="w-12 h-12 rounded-xl bg-blue-50 text-[#185FA5] flex items-center justify-center font-bold"> <div className="w-12 h-12 rounded-xl bg-blue-50 text-[#185FA5] flex items-center justify-center font-bold border border-blue-100 flex-shrink-0">
<CreditCard className="w-6 h-6" /> <CreditCard className="w-6 h-6" />
</div> </div>
<div> <div>
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Current Active Plan</div> <div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Active Plan Pass</div>
<div className="text-lg font-bold text-slate-900">{currentPlan}</div> <h3 className="text-lg font-black text-slate-900 tracking-tight mt-0.5">{activePlan}</h3>
<div className="text-xs text-slate-500 mt-0.5">Renews automatically on <span className="font-bold text-slate-700">{expiresAt}</span></div> <p className="text-xs text-slate-500 font-semibold mt-0.5">
Renewing via auto-payment on <span className="font-bold text-[#185FA5]">{expiresAt}</span>
</p>
</div> </div>
</div> </div>
<div className="flex items-center space-x-2 bg-emerald-50 border border-emerald-200 text-emerald-800 px-3.5 py-1.5 rounded-xl text-xs font-bold"> <div className="flex items-center space-x-3.5">
<button
onClick={triggerFailedPaymentSimulation}
className="text-[10px] font-black text-slate-400 hover:text-slate-600 underline"
title="Simulate card failure to test retry mechanism"
>
Simulate Billing Failure
</button>
<div className="flex items-center space-x-1.5 bg-emerald-50 border border-emerald-200 text-emerald-800 px-4 py-2 rounded-xl text-xs font-black">
<CheckCircle2 className="w-4 h-4 text-emerald-600" /> <CheckCircle2 className="w-4 h-4 text-emerald-600" />
<span>Active & Verified</span> <span>Verified Sponsor Status</span>
</div>
</div> </div>
</div> </div>
{showSuccess && ( {showSuccess && (
<div className="p-4 bg-emerald-500 text-white rounded-xl text-xs font-bold flex items-center space-x-2 shadow-sm animate-fade-in"> <div className="p-4 bg-gradient-to-r from-emerald-500 to-emerald-600 text-white rounded-2xl text-xs font-bold flex items-center space-x-2.5 shadow-md animate-fade-in border border-emerald-600">
<Sparkles className="w-4 h-4" /> <Sparkles className="w-4 h-4 text-amber-300" />
<span>Plan successfully updated! Mock PayTabs billing cycle applied.</span> <span>Sponsor Pass updated successfully! PayTabs transaction logged.</span>
</div> </div>
)} )}
{/* Pricing Grid */} {/* Available Plan Tiers Pricing Grid */}
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h2 className="text-lg font-bold text-slate-900 tracking-tight">Available Subscription Tiers</h2> <h2 className="text-lg font-black text-slate-900 tracking-tight">Available Subscription Tiers</h2>
<p className="text-xs text-slate-500 mt-0.5">Upgrade or change your plan anytime. Zero agency recruitment commissions.</p> <p className="text-xs text-slate-500 mt-0.5 font-medium">Upgrade or cancel anytime. All contracts compliant with UAE Tadbeer MOHRE guides.</p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 pt-2"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6 pt-2">
{plans.map((plan) => { {plans.map((plan) => {
const isSelected = selectedId === plan.id; const isCurrent = activePlan.toLowerCase().includes(plan.id.toLowerCase());
return ( return (
<div <div
key={plan.id} key={plan.id}
className={`bg-white rounded-2xl border-2 shadow-sm flex flex-col justify-between overflow-hidden transition-all relative ${ className={`bg-white rounded-3xl border shadow-sm flex flex-col justify-between overflow-hidden transition-all relative ${
isSelected isCurrent
? 'border-[#185FA5] ring-4 ring-[#185FA5]/10' ? 'border-[#185FA5] ring-4 ring-[#185FA5]/10 scale-[1.02]'
: 'border-slate-200 hover:border-slate-300' : 'border-slate-200 hover:border-slate-300'
}`} }`}
> >
{plan.popular && ( {plan.popular && (
<div className="bg-[#185FA5] text-white text-[10px] font-bold uppercase tracking-wider text-center py-1 shadow-xs"> <div className="bg-[#185FA5] text-white text-[9px] font-black uppercase tracking-wider text-center py-1.5 shadow-xs">
Most Popular Tier Most Popular Tier
</div> </div>
)} )}
@ -73,17 +199,17 @@ export default function Subscription({ currentPlan, expiresAt, plans }) {
<div className="p-6 space-y-6 flex-1 flex flex-col justify-between"> <div className="p-6 space-y-6 flex-1 flex flex-col justify-between">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h3 className="font-bold text-base text-slate-900">{plan.name}</h3> <h3 className="font-extrabold text-base text-slate-900">{plan.name}</h3>
<div className="mt-2 flex items-baseline space-x-1"> <div className="mt-2.5 flex items-baseline space-x-1">
<span className="text-3xl font-extrabold text-slate-900">{plan.price}</span> <span className="text-3xl font-black text-slate-900 tracking-tight">{plan.price}</span>
<span className="text-xs font-semibold text-slate-500">/ {plan.period}</span> <span className="text-xs font-semibold text-slate-400">/ {plan.period}</span>
</div> </div>
</div> </div>
<ul className="space-y-3 pt-4 border-t border-slate-100"> <ul className="space-y-3 pt-5 border-t border-slate-100">
{plan.features.map(f => ( {plan.features.map(f => (
<li key={f} className="flex items-start space-x-3 text-xs text-slate-600"> <li key={f} className="flex items-start space-x-3 text-xs text-slate-600 font-medium">
<CheckCircle2 className="w-4 h-4 text-emerald-600 flex-shrink-0 mt-0.5" /> <Check className="w-4 h-4 text-emerald-600 flex-shrink-0 mt-0.5" />
<span>{f}</span> <span>{f}</span>
</li> </li>
))} ))}
@ -92,14 +218,14 @@ export default function Subscription({ currentPlan, expiresAt, plans }) {
<button <button
type="button" type="button"
onClick={() => handleSelect(plan.id)} onClick={() => handleSelectPlan(plan)}
className={`w-full h-11 rounded-xl font-bold text-xs flex items-center justify-center transition-colors shadow-sm ${ className={`w-full h-11 rounded-xl font-bold text-xs flex items-center justify-center transition-all shadow-xs mt-6 ${
isSelected isCurrent
? 'bg-emerald-600 hover:bg-emerald-700 text-white' ? 'bg-emerald-600 hover:bg-emerald-700 text-white cursor-default'
: 'bg-[#185FA5] hover:bg-[#144f8a] text-white' : 'bg-[#185FA5] hover:bg-[#144f8a] text-white'
}`} }`}
> >
{isSelected ? 'Current Active Tier' : 'Select Plan Tier'} {isCurrent ? 'Current Active Tier ✓' : 'Select Plan Tier'}
</button> </button>
</div> </div>
</div> </div>
@ -107,7 +233,200 @@ export default function Subscription({ currentPlan, expiresAt, plans }) {
})} })}
</div> </div>
</div> </div>
{/* Payment History & Invoice logs */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 pt-4">
{/* Invoice Logs */}
<div className="lg:col-span-8 bg-white p-6 rounded-3xl border border-slate-200 shadow-sm space-y-6">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<div className="flex items-center space-x-2">
<FileText className="w-5 h-5 text-[#185FA5]" />
<h3 className="font-extrabold text-base text-slate-900">Billing Payment Receipts</h3>
</div> </div>
<span className="text-[10px] text-slate-400 font-bold uppercase">Tax Compliant invoices</span>
</div>
<div className="divide-y divide-slate-100">
{invoices.map((inv) => (
<div key={inv.id} className="flex items-center justify-between py-4 text-xs font-semibold text-slate-700">
<div className="space-y-1">
<div className="font-bold text-slate-800 flex items-center space-x-1.5">
<span>{inv.plan}</span>
<span className="text-[9px] text-slate-400 font-mono">({inv.id})</span>
</div>
<div className="text-[10px] text-slate-400 font-medium">Billed: {inv.date}</div>
</div>
<div className="flex items-center space-x-6">
<div className="text-right">
<div className="font-extrabold text-slate-900">{inv.amount}</div>
<span className={`text-[9px] font-black uppercase px-2 py-0.5 rounded ${
inv.status === 'Paid' ? 'bg-emerald-50 text-emerald-700' : 'bg-amber-50 text-amber-700'
}`}>
{inv.status}
</span>
</div>
<button
onClick={() => downloadReceipt(inv.id)}
className="p-2 border border-slate-200 hover:bg-slate-50 text-slate-500 rounded-xl transition-colors"
title="Download Tax Receipt"
>
<Download className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
</div>
{/* Sponsor Billing Details Form */}
<div className="lg:col-span-4 bg-white p-6 rounded-3xl border border-slate-200 shadow-sm space-y-6">
<div className="space-y-1">
<h3 className="font-extrabold text-base text-slate-900">Corporate Details</h3>
<p className="text-xs text-slate-500 font-medium">Update tax invoicing options.</p>
</div>
<div className="space-y-4 text-xs font-bold text-slate-700">
<div>
<label className="block text-slate-400 uppercase tracking-widest text-[9px] mb-1.5">Sponsor Email Address</label>
<input
type="email"
value={billingEmail}
onChange={(e) => setBillingEmail(e.target.value)}
className="w-full p-2.5 border border-slate-200 rounded-xl bg-slate-50/50"
/>
</div>
<div>
<label className="block text-slate-400 uppercase tracking-widest text-[9px] mb-1.5">Billing VAT Registry ID (Optional)</label>
<input
type="text"
placeholder="e.g. TRN 100293810200003"
className="w-full p-2.5 border border-slate-200 rounded-xl bg-slate-50/50"
/>
</div>
<button
onClick={() => toast.success("Corporate billing details updated.")}
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl py-2.5 font-bold text-xs"
>
Save Billing Profile
</button>
</div>
</div>
</div>
</div>
{/* Interactive PayTabs Checkout Modal Overlay */}
{showPayTabsModal && selectedPlan && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-3xl w-full max-w-md border border-slate-200 shadow-2xl p-6 relative animate-zoom-in space-y-6">
<button
onClick={() => setShowPayTabsModal(false)}
className="absolute top-4 right-4 p-2 bg-slate-50 hover:bg-slate-100 rounded-full text-slate-400 hover:text-slate-600 transition-colors"
>
<X className="w-5 h-5" />
</button>
{/* PayTabs Header */}
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<div className="flex items-center space-x-2">
<CreditCard className="w-6 h-6 text-red-600" />
<span className="font-extrabold text-sm text-slate-900 tracking-tight uppercase">PayTabs Secured Checkout</span>
</div>
<span className="text-[8px] bg-red-50 text-red-700 px-2 py-0.5 rounded font-black tracking-widest uppercase">
Gateway v4.0
</span>
</div>
{/* Order Summary */}
<div className="bg-slate-50 p-4 rounded-2xl border border-slate-200/60 flex justify-between items-center text-xs font-bold text-slate-700">
<div>
<span className="text-[10px] text-slate-400 uppercase tracking-widest">Order description</span>
<div className="text-slate-900">{selectedPlan.name} Subscription</div>
</div>
<div className="text-right">
<span className="text-[10px] text-slate-400 uppercase tracking-widest">Total amount</span>
<div className="text-emerald-700 text-sm font-black">{selectedPlan.price}</div>
</div>
</div>
{/* PayTabs CC Form */}
<form onSubmit={handlePayTabsSubmit} className="space-y-4">
<div className="space-y-3.5 text-xs">
<div>
<label className="block font-bold text-slate-700 mb-1">Cardholder Name</label>
<input
type="text"
placeholder="e.g. Fatima Al Mansoori"
value={cardName}
onChange={(e) => setCardName(e.target.value)}
className="w-full p-2.5 border border-slate-300 rounded-xl"
required
/>
</div>
<div>
<label className="block font-bold text-slate-700 mb-1">Card Number</label>
<input
type="text"
maxLength="16"
placeholder="1234 5678 9101 1121"
value={cardNumber}
onChange={(e) => setCardNumber(e.target.value.replace(/\D/g, ''))}
className="w-full p-2.5 border border-slate-300 rounded-xl font-mono text-sm"
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block font-bold text-slate-700 mb-1">Expiry Date (MM/YY)</label>
<input
type="text"
placeholder="12/28"
value={cardExpiry}
onChange={(e) => setCardExpiry(e.target.value)}
className="w-full p-2.5 border border-slate-300 rounded-xl"
required
/>
</div>
<div>
<label className="block font-bold text-slate-700 mb-1">Security Code (CVV)</label>
<input
type="password"
maxLength="3"
placeholder="***"
value={cardCvv}
onChange={(e) => setCardCvv(e.target.value.replace(/\D/g, ''))}
className="w-full p-2.5 border border-slate-300 rounded-xl font-mono text-sm"
required
/>
</div>
</div>
</div>
<div className="pt-4 border-t border-slate-100 flex items-center justify-between text-[10px] text-slate-500">
<span className="flex items-center space-x-1 font-bold">
<Lock className="w-3.5 h-3.5 text-emerald-600" />
<span>256-bit PCI DSS Cryptography Protected</span>
</span>
</div>
<button
type="submit"
className="w-full bg-red-600 hover:bg-red-700 text-white py-3.5 rounded-2xl font-black text-xs uppercase tracking-widest shadow-lg shadow-red-500/10 flex items-center justify-center space-x-2"
>
<span>Authorize Payment</span>
<ArrowRight className="w-4 h-4" />
</button>
</form>
</div>
</div>
)}
</EmployerLayout> </EmployerLayout>
); );
} }

View File

@ -1,4 +1,4 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo, useEffect } from 'react';
import { Head, Link, router } from '@inertiajs/react'; import { Head, Link, router } from '@inertiajs/react';
import EmployerLayout from '../../../Layouts/EmployerLayout'; import EmployerLayout from '../../../Layouts/EmployerLayout';
import { import {
@ -14,10 +14,35 @@ import {
HeartHandshake, HeartHandshake,
Globe2, Globe2,
Sparkles, Sparkles,
Filter Filter,
ArrowUpDown,
Check,
X,
Eye,
ChevronDown,
Layers,
AlertTriangle,
Star,
CheckCircle,
ShieldCheck,
User
} from 'lucide-react'; } from 'lucide-react';
export default function Index({ initialWorkers, initialShortlistedIds, filtersMetadata }) { const getLanguageFlag = (lang) => {
const flags = {
'English': '🇬🇧',
'Arabic': '🇦🇪',
'Tagalog': '🇵🇭',
'Hindi': '🇮🇳',
'Swahili': '🇰🇪',
'French': '🇫🇷',
'Indonesian': '🇮🇩'
};
return flags[lang] || '🌐';
};
export default function Index({ initialWorkers = [], initialShortlistedIds = [], filtersMetadata = {} }) {
// Basic Filters
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState('All Categories'); const [selectedCategory, setSelectedCategory] = useState('All Categories');
const [selectedNationality, setSelectedNationality] = useState('All Nationalities'); const [selectedNationality, setSelectedNationality] = useState('All Nationalities');
@ -25,17 +50,59 @@ export default function Index({ initialWorkers, initialShortlistedIds, filtersMe
const [selectedExperience, setSelectedExperience] = useState('All Experience'); const [selectedExperience, setSelectedExperience] = useState('All Experience');
const [selectedReligion, setSelectedReligion] = useState('All Religions'); const [selectedReligion, setSelectedReligion] = useState('All Religions');
const [maxSalary, setMaxSalary] = useState(3000); const [maxSalary, setMaxSalary] = useState(3000);
// Advanced Multi-Select Filters & Sorting
const [selectedLanguages, setSelectedLanguages] = useState([]);
const [selectedVisaStatuses, setSelectedVisaStatuses] = useState([]);
const [selectedSkills, setSelectedSkills] = useState([]);
const [selectedWorkTypes, setSelectedWorkTypes] = useState([]);
const [sortBy, setSortBy] = useState('default');
// UI Helpers
const [isLangOpen, setIsLangOpen] = useState(false);
const [isVisaOpen, setIsVisaOpen] = useState(false);
const [isSkillsOpen, setIsSkillsOpen] = useState(false);
const [isWorkTypeOpen, setIsWorkTypeOpen] = useState(false);
// Pagination
const [visibleCount, setVisibleCount] = useState(6);
// Shortlist / Favorites
const [shortlistedIds, setShortlistedIds] = useState(initialShortlistedIds || []); const [shortlistedIds, setShortlistedIds] = useState(initialShortlistedIds || []);
// Saved Presets
const [savedPresets, setSavedPresets] = useState([
{ id: 1, name: 'Live-in Baby Care', category: 'Childcare', workType: 'Live-in', minSalary: 1200 },
{ id: 2, name: 'Experienced Driver', category: 'Driver', experience: '5+ Years' }
]);
const [presetNameInput, setPresetNameInput] = useState('');
const [showPresetModal, setShowPresetModal] = useState(false);
// Worker Comparison & Quick Preview
const [comparisonIds, setComparisonIds] = useState([]);
const [previewWorker, setPreviewWorker] = useState(null);
// Retrieve active preset query if present
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const cat = params.get('category');
const nat = params.get('nationality');
const avail = params.get('availability');
const sal = params.get('max_salary');
if (cat) setSelectedCategory(cat);
if (nat) setSelectedNationality(nat);
if (avail) setSelectedAvailability(avail);
if (sal) setMaxSalary(Number(sal));
}, []);
const toggleShortlist = (id) => { const toggleShortlist = (id) => {
// Optimistic UI state update
if (shortlistedIds.includes(id)) { if (shortlistedIds.includes(id)) {
setShortlistedIds(shortlistedIds.filter(i => i !== id)); setShortlistedIds(shortlistedIds.filter(i => i !== id));
} else { } else {
setShortlistedIds([...shortlistedIds, id]); setShortlistedIds([...shortlistedIds, id]);
} }
// Persist change to database
router.post('/employer/shortlist/toggle', { worker_id: id }, { router.post('/employer/shortlist/toggle', { worker_id: id }, {
preserveScroll: true, preserveScroll: true,
onSuccess: (page) => { onSuccess: (page) => {
@ -46,6 +113,38 @@ export default function Index({ initialWorkers, initialShortlistedIds, filtersMe
}); });
}; };
const toggleMultiSelect = (item, selectedList, setSelectedList) => {
if (selectedList.includes(item)) {
setSelectedList(selectedList.filter(x => x !== item));
} else {
setSelectedList([...selectedList, item]);
}
};
const applyPreset = (preset) => {
resetFilters();
if (preset.category) setSelectedCategory(preset.category);
if (preset.workType) setSelectedWorkTypes([preset.workType]);
if (preset.experience) setSelectedExperience(preset.experience);
};
const saveCurrentPreset = (e) => {
e.preventDefault();
if (!presetNameInput.trim()) return;
const newPreset = {
id: Date.now(),
name: presetNameInput,
category: selectedCategory !== 'All Categories' ? selectedCategory : null,
workType: selectedWorkTypes.length > 0 ? selectedWorkTypes[0] : null,
experience: selectedExperience !== 'All Experience' ? selectedExperience : null,
};
setSavedPresets([...savedPresets, newPreset]);
setPresetNameInput('');
setShowPresetModal(false);
};
const resetFilters = () => { const resetFilters = () => {
setSearchQuery(''); setSearchQuery('');
setSelectedCategory('All Categories'); setSelectedCategory('All Categories');
@ -54,51 +153,63 @@ export default function Index({ initialWorkers, initialShortlistedIds, filtersMe
setSelectedExperience('All Experience'); setSelectedExperience('All Experience');
setSelectedReligion('All Religions'); setSelectedReligion('All Religions');
setMaxSalary(3000); setMaxSalary(3000);
setSelectedLanguages([]);
setSelectedVisaStatuses([]);
setSelectedSkills([]);
setSelectedWorkTypes([]);
setSortBy('default');
setVisibleCount(6);
}; };
// Filter and Sort Worker List
const filteredWorkers = useMemo(() => { const filteredWorkers = useMemo(() => {
return initialWorkers.filter(worker => { let workers = initialWorkers.filter(worker => {
// Search Query // Search Query
if (searchQuery.trim() !== '') { if (searchQuery.trim() !== '') {
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
const matchName = worker.name.toLowerCase().includes(query); const matchName = worker.name.toLowerCase().includes(query);
const matchBio = worker.bio.toLowerCase().includes(query); const matchBio = worker.bio?.toLowerCase().includes(query) || false;
const matchSkills = worker.skills.some(s => s.toLowerCase().includes(query)); const matchSkills = worker.skills?.some(s => s.toLowerCase().includes(query)) || false;
if (!matchName && !matchBio && !matchSkills) return false; if (!matchName && !matchBio && !matchSkills) return false;
} }
// Category // Dropdown filters
if (selectedCategory !== 'All Categories' && worker.category !== selectedCategory) { if (selectedCategory !== 'All Categories' && worker.category !== selectedCategory) return false;
return false; if (selectedNationality !== 'All Nationalities' && worker.nationality !== selectedNationality) return false;
} if (selectedAvailability !== 'All Availabilities' && worker.availability_status !== selectedAvailability) return false;
if (selectedExperience !== 'All Experience' && worker.experience !== selectedExperience) return false;
if (selectedReligion !== 'All Religions' && worker.religion !== selectedReligion) return false;
if (worker.salary > maxSalary) return false;
// Nationality // Multi-select filters
if (selectedNationality !== 'All Nationalities' && worker.nationality !== selectedNationality) { if (selectedLanguages.length > 0) {
return false; const hasLang = worker.languages?.some(l => selectedLanguages.includes(l));
if (!hasLang) return false;
} }
if (selectedSkills.length > 0) {
// Availability const hasSkill = worker.skills?.some(s => selectedSkills.includes(s.toLowerCase()));
if (selectedAvailability !== 'All Availabilities' && worker.availability !== selectedAvailability) { if (!hasSkill) return false;
return false;
} }
if (selectedVisaStatuses.length > 0) {
// Experience if (!selectedVisaStatuses.includes(worker.visa_status)) return false;
if (selectedExperience !== 'All Experience' && worker.experience !== selectedExperience) {
return false;
}
// Religion
if (selectedReligion !== 'All Religions' && worker.religion !== selectedReligion) {
return false;
}
// Salary Bracket
if (worker.salary > maxSalary) {
return false;
} }
if (selectedWorkTypes.length > 0 && !selectedWorkTypes.includes(worker.preferred_job_type)) return false;
return true; return true;
}); });
// Sorting
if (sortBy === 'salary_asc') {
workers.sort((a, b) => a.salary - b.salary);
} else if (sortBy === 'salary_desc') {
workers.sort((a, b) => b.salary - a.salary);
} else if (sortBy === 'rating') {
workers.sort((a, b) => b.rating - a.rating);
} else if (sortBy === 'experience') {
workers.sort((a, b) => b.age - a.age); // approximate experience by age
}
return workers;
}, [ }, [
initialWorkers, initialWorkers,
searchQuery, searchQuery,
@ -107,155 +218,316 @@ export default function Index({ initialWorkers, initialShortlistedIds, filtersMe
selectedAvailability, selectedAvailability,
selectedExperience, selectedExperience,
selectedReligion, selectedReligion,
maxSalary maxSalary,
selectedLanguages,
selectedVisaStatuses,
selectedSkills,
selectedWorkTypes,
sortBy
]); ]);
return ( const paginatedWorkers = useMemo(() => {
<EmployerLayout title="Find Domestic Workers"> return filteredWorkers.slice(0, visibleCount);
<Head title="Find Workers - Employer Portal" /> }, [filteredWorkers, visibleCount]);
<div className="space-y-6 select-none"> const handleToggleComparison = (id) => {
{/* Search & Top Filters Bar */} if (comparisonIds.includes(id)) {
setComparisonIds(comparisonIds.filter(i => i !== id));
} else {
if (comparisonIds.length >= 3) {
alert('You can compare a maximum of 3 candidates.');
return;
}
setComparisonIds([...comparisonIds, id]);
}
};
const comparedWorkers = useMemo(() => {
return initialWorkers.filter(w => comparisonIds.includes(w.id));
}, [initialWorkers, comparisonIds]);
return (
<EmployerLayout title="Find Vetted Domestic Workers">
<Head title="Find Workers - Domestic Worker Platform" />
<div className="space-y-8 select-none pb-16">
{/* 1. Header Banner */}
<div className="bg-white rounded-3xl p-8 border border-slate-200 shadow-sm relative overflow-hidden flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="absolute top-0 right-0 w-48 h-48 bg-blue-50 rounded-full -mr-16 -mt-16 opacity-40 pointer-events-none" />
<div className="space-y-2 relative z-10">
<div className="flex items-center space-x-2">
<span className="bg-blue-50 text-[#185FA5] px-3 py-1 rounded-xl text-[10px] font-black uppercase tracking-wider border border-blue-100 flex items-center space-x-1">
<HeartHandshake className="w-3.5 h-3.5" />
<span>No Middlemen Platform</span>
</span>
</div>
<h2 className="text-2xl font-black text-slate-900 tracking-tight leading-none">Find Verified Candidates</h2>
<p className="text-slate-500 font-bold text-xs">Instantly connect with UAE vetted, background-checked domestic workers.</p>
</div>
<button
onClick={() => setShowPresetModal(true)}
className="bg-[#185FA5] hover:bg-[#144f8a] text-white px-6 py-3 rounded-2xl text-xs font-black uppercase tracking-widest transition-all shadow-md shadow-blue-200 relative z-10"
>
+ Save Current Filter
</button>
</div>
{/* Filter and Search Panel */}
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm space-y-4"> <div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm space-y-4">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4"> <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
{/* Search Input */} {/* Core Search bar */}
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" /> <Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
<input <input
type="text" type="text"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search by name, bio, or skills (e.g., Cooking, Childcare, Arabic)..." placeholder="Search by name, skills, bio (e.g. Cooking, Childcare, Gardening, Driving)..."
className="w-full pl-11 pr-4 py-3 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]" className="w-full pl-11 pr-4 py-3 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
/> />
</div> </div>
{/* Reset Filters CTA */} {/* Sort & Reset Actions */}
<button <div className="flex items-center space-x-3 w-full lg:w-auto justify-end">
type="button" {/* Sort Selector */}
onClick={resetFilters} <div className="relative flex items-center bg-slate-50 rounded-xl border border-slate-200 px-3 py-2 text-xs font-bold text-slate-700">
className="flex items-center justify-center space-x-2 px-4 py-3 rounded-xl border border-slate-200 hover:bg-slate-50 text-xs font-semibold text-slate-600 transition-colors flex-shrink-0" <ArrowUpDown className="w-4 h-4 text-slate-400 mr-2" />
>
<RotateCcw className="w-4 h-4 text-slate-400" />
<span>Reset Filters</span>
</button>
</div>
{/* Detailed Filters Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3 pt-4 border-t border-slate-100">
{/* Category Dropdown */}
<div>
<label className="block text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">Category</label>
<select <select
value={selectedCategory} value={sortBy}
onChange={(e) => setSelectedCategory(e.target.value)} onChange={(e) => setSortBy(e.target.value)}
className="w-full px-3 py-2 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5]" className="bg-transparent focus:outline-none cursor-pointer"
> >
{filtersMetadata.categories.map(cat => ( <option value="default">Default Match</option>
<option key={cat} value={cat}>{cat}</option> <option value="salary_asc">Salary: Low to High</option>
))} <option value="salary_desc">Salary: High to Low</option>
<option value="rating">Rating: High to Low</option>
<option value="experience">Experience Level</option>
</select> </select>
</div> </div>
{/* Nationality Dropdown */} <button
type="button"
onClick={resetFilters}
className="flex items-center justify-center space-x-1.5 px-4 py-2.5 rounded-xl border border-slate-200 hover:bg-slate-50 text-xs font-semibold text-slate-600 transition-colors"
>
<RotateCcw className="w-4 h-4 text-slate-400" />
<span>Reset</span>
</button>
</div>
</div>
{/* Primary filters grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3 pt-4 border-t border-slate-100">
{/* Nationality */}
<div> <div>
<label className="block text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">Nationality</label> <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">Nationality</label>
<select <select
value={selectedNationality} value={selectedNationality}
onChange={(e) => setSelectedNationality(e.target.value)} onChange={(e) => setSelectedNationality(e.target.value)}
className="w-full px-3 py-2 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5]" className="w-full px-3 py-2.5 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5] font-semibold text-slate-700 cursor-pointer"
> >
{filtersMetadata.nationalities.map(nat => ( {filtersMetadata.nationalities?.map(nat => (
<option key={nat} value={nat}>{nat}</option> <option key={nat} value={nat}>{nat}</option>
))} ))}
</select> </select>
</div> </div>
{/* Availability Dropdown */} {/* Skills */}
<div className="relative">
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">Skills</label>
<button
type="button"
onClick={() => {
setIsSkillsOpen(!isSkillsOpen);
setIsLangOpen(false);
setIsVisaOpen(false);
}}
className="w-full px-3 py-2.5 rounded-xl border border-slate-200 text-xs bg-slate-50/50 flex justify-between items-center font-bold text-slate-600"
>
<span className="truncate">
{selectedSkills.length === 0
? 'Select Skills'
: `${selectedSkills.length} Selected`}
</span>
<ChevronDown className="w-4 h-4 ml-1 flex-shrink-0" />
</button>
{isSkillsOpen && (
<div className="absolute left-0 right-0 mt-1 bg-white border border-slate-200 shadow-lg rounded-xl p-3 z-30 space-y-1.5 max-h-60 overflow-y-auto">
{filtersMetadata.skills?.slice(1).map(skill => (
<label key={skill} className="flex items-center space-x-2 text-xs font-bold text-slate-600 cursor-pointer">
<input
type="checkbox"
checked={selectedSkills.includes(skill.toLowerCase())}
onChange={() => toggleMultiSelect(skill.toLowerCase(), selectedSkills, setSelectedSkills)}
className="rounded text-[#185FA5] focus:ring-[#185FA5]"
/>
<span className="capitalize">{skill}</span>
</label>
))}
</div>
)}
</div>
{/* Language */}
<div className="relative">
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">Language</label>
<button
type="button"
onClick={() => {
setIsLangOpen(!isLangOpen);
setIsSkillsOpen(false);
setIsVisaOpen(false);
}}
className="w-full px-3 py-2.5 rounded-xl border border-slate-200 text-xs bg-slate-50/50 flex justify-between items-center font-bold text-slate-600"
>
<span className="truncate">
{selectedLanguages.length === 0
? 'Select Languages'
: `${selectedLanguages.length} Selected`}
</span>
<ChevronDown className="w-4 h-4 ml-1 flex-shrink-0" />
</button>
{isLangOpen && (
<div className="absolute left-0 right-0 mt-1 bg-white border border-slate-200 shadow-lg rounded-xl p-3 z-30 space-y-1.5">
{filtersMetadata.languages?.slice(1).map(lang => (
<label key={lang} className="flex items-center space-x-2 text-xs font-bold text-slate-600 cursor-pointer">
<input
type="checkbox"
checked={selectedLanguages.includes(lang)}
onChange={() => toggleMultiSelect(lang, selectedLanguages, setSelectedLanguages)}
className="rounded text-[#185FA5] focus:ring-[#185FA5]"
/>
<span>{lang}</span>
</label>
))}
</div>
)}
</div>
{/* Availability */}
<div> <div>
<label className="block text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">Availability</label> <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">Availability</label>
<select <select
value={selectedAvailability} value={selectedAvailability}
onChange={(e) => setSelectedAvailability(e.target.value)} onChange={(e) => setSelectedAvailability(e.target.value)}
className="w-full px-3 py-2 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5]" className="w-full px-3 py-2.5 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5] font-semibold text-slate-700 cursor-pointer"
> >
{filtersMetadata.availabilities.map(av => ( {filtersMetadata.availabilities?.map(av => (
<option key={av} value={av}>{av}</option> <option key={av} value={av}>{av}</option>
))} ))}
</select> </select>
</div> </div>
{/* Experience Dropdown */} {/* Visa Status */}
<div> <div className="relative">
<label className="block text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">Experience</label> <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">Visa Status</label>
<select <button
value={selectedExperience} type="button"
onChange={(e) => setSelectedExperience(e.target.value)} onClick={() => {
className="w-full px-3 py-2 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5]" setIsVisaOpen(!isVisaOpen);
setIsSkillsOpen(false);
setIsLangOpen(false);
}}
className="w-full px-3 py-2.5 rounded-xl border border-slate-200 text-xs bg-slate-50/50 flex justify-between items-center font-bold text-slate-600"
> >
{filtersMetadata.experienceLevels.map(exp => ( <span className="truncate">
<option key={exp} value={exp}>{exp}</option> {selectedVisaStatuses.length === 0
))} ? 'Select Visa Status'
</select> : `${selectedVisaStatuses.length} Selected`}
</div> </span>
<ChevronDown className="w-4 h-4 ml-1 flex-shrink-0" />
{/* Max Salary Slider */} </button>
<div> {isVisaOpen && (
<div className="flex items-center justify-between mb-1 text-[11px]"> <div className="absolute left-0 right-0 mt-1 bg-white border border-slate-200 shadow-lg rounded-xl p-3 z-30 space-y-1.5">
<span className="font-semibold text-slate-500 uppercase tracking-wider">Max Salary</span> {filtersMetadata.visaStatuses?.slice(1).map(status => (
<span className="font-bold text-[#185FA5]">{maxSalary} AED</span> <label key={status} className="flex items-center space-x-2 text-xs font-bold text-slate-600 cursor-pointer">
</div>
<input <input
type="range" type="checkbox"
min="1200" checked={selectedVisaStatuses.includes(status)}
max="3000" onChange={() => toggleMultiSelect(status, selectedVisaStatuses, setSelectedVisaStatuses)}
step="100" className="rounded text-[#185FA5] focus:ring-[#185FA5]"
value={maxSalary}
onChange={(e) => setMaxSalary(Number(e.target.value))}
className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-[#185FA5]"
/> />
<span>{status}</span>
</label>
))}
</div>
)}
</div> </div>
</div> </div>
</div> </div>
{/* Results Header */} {/* Listing metadata bar */}
<div className="flex items-center justify-between px-1"> <div className="flex items-center justify-between px-1">
<div className="text-xs font-semibold text-slate-500"> <div className="text-xs font-semibold text-slate-500">
Showing <span className="text-slate-900 font-bold">{filteredWorkers.length}</span> domestic workers matching your criteria Found <span className="text-slate-900 font-bold">{filteredWorkers.length}</span> domestic workers matching search
</div> </div>
<div className="flex items-center space-x-2 text-xs font-medium text-slate-600"> <div className="flex items-center space-x-2 text-xs font-medium text-slate-600">
<Filter className="w-3.5 h-3.5 text-[#185FA5]" /> <CheckCircle className="w-3.5 h-3.5 text-emerald-600" />
<span>OCR Vetted & Legal</span> <span>MOHRE UAE Direct Sponsoring Active</span>
</div> </div>
</div> </div>
{/* Workers Grid */} {/* Marketplace Workers Grid */}
{filteredWorkers.length > 0 ? ( {paginatedWorkers.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredWorkers.map((worker) => { {paginatedWorkers.map((worker) => {
const isShortlisted = shortlistedIds.includes(worker.id); const isShortlisted = shortlistedIds.includes(worker.id);
const isComparing = comparisonIds.includes(worker.id);
return ( return (
<div key={worker.id} className="bg-white rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-all flex flex-col overflow-hidden group"> <div key={worker.id} className="bg-white rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-all flex flex-col justify-between overflow-hidden group relative">
{/* Card Header Banner */}
<div className="bg-slate-50 p-6 border-b border-slate-100 flex items-start justify-between relative"> {/* Card Header with Photo Option and Availability indicator */}
<div className="flex items-center space-x-3"> <div className="bg-slate-50/50 p-6 border-b border-slate-100 flex items-start justify-between relative">
<div className="w-14 h-14 rounded-full bg-blue-100 text-[#185FA5] flex items-center justify-center font-bold text-xl border border-blue-200 shadow-inner flex-shrink-0"> <div className="flex items-center space-x-3.5">
{worker.name.charAt(0)} {/* Photo (optional) container */}
<div className="w-14 h-14 rounded-2xl bg-blue-100 text-[#185FA5] flex items-center justify-center font-bold text-xl border border-blue-200 shadow-inner flex-shrink-0 overflow-hidden relative">
{worker.photo ? (
<img src={worker.photo} alt={worker.name} className="w-full h-full object-cover" />
) : (
<User className="w-6 h-6 text-slate-400" />
)}
</div> </div>
<div className="truncate"> <div className="truncate">
<div className="font-bold text-base text-slate-900 group-hover:text-[#185FA5] transition-colors truncate flex items-center space-x-1.5"> <div className="font-extrabold text-base text-slate-900 group-hover:text-[#185FA5] transition-colors truncate flex items-center space-x-1.5">
<span>{worker.name}</span> <span>{worker.name}</span>
{worker.verified && <CheckCircle2 className="w-4 h-4 text-emerald-600 flex-shrink-0" />} {worker.verified && <CheckCircle2 className="w-4 h-4 text-emerald-600 flex-shrink-0" />}
</div> </div>
<div className="flex items-center space-x-1.5 text-xs text-slate-500 mt-0.5">
{/* Emirates ID Status Verification Badge & Visa Status */}
<div className="flex flex-wrap gap-1 mt-0.5">
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-emerald-700 bg-emerald-50 px-1.5 py-0.5 rounded border border-emerald-100">
<ShieldCheck className="w-3 h-3 text-emerald-600 flex-shrink-0" />
<span>{worker.emirates_id_status}</span>
</div>
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-blue-700 bg-blue-50 px-1.5 py-0.5 rounded border border-blue-100">
<span>{worker.visa_status}</span>
</div>
</div>
<div className="flex items-center space-x-1.5 text-xs text-slate-500 mt-1">
<Globe2 className="w-3.5 h-3.5 text-slate-400" /> <Globe2 className="w-3.5 h-3.5 text-slate-400" />
<span>{worker.nationality}</span> <span className="font-bold">{worker.nationality}</span>
<span></span>
<span>{worker.age} yrs</span>
</div> </div>
</div> </div>
</div> </div>
{/* Shortlist Toggle Button */} {/* Top Right Availability status and bookmark */}
<div className="flex flex-col items-end space-y-2">
<span className={`px-2 py-0.5 text-[8px] font-black rounded-lg uppercase tracking-wider border ${
worker.availability_status === 'Active' ? 'bg-emerald-50 text-emerald-700 border-emerald-200' :
worker.availability_status === 'Hired' ? 'bg-blue-50 text-blue-700 border-blue-200' :
'bg-slate-100 text-slate-600 border-slate-200'
}`}>
{worker.availability_status}
</span>
<button <button
type="button" type="button"
onClick={() => toggleShortlist(worker.id)} onClick={() => toggleShortlist(worker.id)}
@ -265,87 +537,325 @@ export default function Index({ initialWorkers, initialShortlistedIds, filtersMe
: 'bg-white text-slate-400 hover:text-[#185FA5] border border-slate-200' : 'bg-white text-slate-400 hover:text-[#185FA5] border border-slate-200'
}`} }`}
> >
<Bookmark className={`w-5 h-5 ${isShortlisted ? 'fill-[#185FA5]' : ''}`} /> <Bookmark className={`w-3.5 h-3.5 ${isShortlisted ? 'fill-[#185FA5]' : ''}`} />
</button> </button>
</div> </div>
</div>
{/* Card Body */} {/* Card Body */}
<div className="p-6 flex-1 flex flex-col justify-between space-y-4"> <div className="p-6 space-y-4 flex-1 flex flex-col justify-between">
<div className="space-y-4"> <div className="space-y-4">
{/* Top Skill Pill & Salary */}
{/* Category Pill, Ratings & Cost */}
<div className="flex items-center justify-between text-xs"> <div className="flex items-center justify-between text-xs">
<span className="px-2.5 py-1 bg-blue-50 text-[#185FA5] font-bold rounded-lg border border-blue-100"> <span className="px-2.5 py-1 bg-blue-50 text-[#185FA5] font-black rounded-lg border border-blue-100 uppercase tracking-wider text-[9px]">
{worker.category} {worker.category}
</span> </span>
<div className="flex items-center space-x-1 font-bold text-slate-800"> <div className="flex items-center space-x-1 font-bold text-slate-800">
<DollarSign className="w-4 h-4 text-emerald-600" /> <DollarSign className="w-3.5 h-3.5 text-emerald-600" />
<span>{worker.salary} AED / mo</span> <span className="text-sm font-black">{worker.salary} AED</span>
<span className="text-[10px] text-slate-400 font-medium">/mo</span>
</div> </div>
</div> </div>
{/* Bio Snippet */} {/* Bio Snippet */}
<p className="text-xs text-slate-600 line-clamp-2 leading-relaxed"> <p className="text-xs text-slate-600 line-clamp-2 leading-relaxed italic">
"{worker.bio}" "{worker.bio}"
</p> </p>
{/* Details Grid */} {/* Details Table */}
<div className="grid grid-cols-2 gap-2 p-3 bg-slate-50 rounded-xl text-[11px] text-slate-600 font-medium"> <div className="grid grid-cols-2 gap-2.5 p-3.5 bg-slate-50 rounded-xl text-[11px] text-slate-600 font-bold">
<div className="flex items-center space-x-1.5"> <div className="flex items-center space-x-1.5 truncate">
<Briefcase className="w-3.5 h-3.5 text-slate-400" /> <Briefcase className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
<span>Exp: {worker.experience}</span> <span className="truncate">Exp: {worker.experience}</span>
</div> </div>
<div className="flex items-center space-x-1.5 truncate"> <div className="flex items-center space-x-1.5 truncate">
<Calendar className="w-3.5 h-3.5 text-slate-400" /> <Sparkles className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
<span className="truncate">Avail: {worker.availability}</span> <span className="truncate uppercase text-[10px]">{worker.preferred_job_type}</span>
</div> </div>
<div className="flex items-center space-x-1.5"> <div className="flex items-center space-x-1.5 truncate col-span-2">
<HeartHandshake className="w-3.5 h-3.5 text-slate-400" /> <Star className="w-3.5 h-3.5 text-amber-500 flex-shrink-0 fill-amber-500" />
<span>{worker.religion}</span> <span className="truncate">{worker.rating} ({worker.reviews_count} reviews)</span>
</div>
<div className="flex items-center space-x-1.5 truncate">
<Sparkles className="w-3.5 h-3.5 text-slate-400" />
<span className="truncate">Age: {worker.age} yrs</span>
</div> </div>
</div> </div>
{/* Skill Tags */} {/* Core exact platform Skills List */}
<div className="flex flex-wrap gap-1.5"> <div className="space-y-1">
{worker.skills.map(s => ( <div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Platform Skills</div>
<span key={s} className="text-[10px] bg-slate-100 text-slate-600 px-2 py-0.5 rounded-md font-semibold"> <div className="flex flex-wrap gap-1">
{s} {worker.skills?.map(skill => (
<span key={skill} className="px-2.5 py-0.5 bg-blue-50 border border-blue-100 text-[#185FA5] text-[9px] font-black rounded-lg uppercase tracking-wider">
{skill}
</span> </span>
))} ))}
</div> </div>
</div> </div>
{/* View Profile Button */} {/* Languages Badges with Visual Flags */}
<div className="space-y-1">
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Languages Spoken</div>
<div className="flex flex-wrap gap-1.5">
{worker.languages?.map(lang => (
<span key={lang} className="text-[9px] bg-slate-100 text-slate-700 px-2 py-0.5 rounded font-bold uppercase flex items-center space-x-1 border border-slate-200">
<span>{getLanguageFlag(lang)}</span>
<span>{lang}</span>
</span>
))}
</div>
</div>
</div>
{/* Actions block */}
<div className="pt-4 border-t border-slate-100 space-y-2">
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setPreviewWorker(worker)}
className="w-full bg-slate-50 border border-slate-200 hover:bg-slate-100 text-slate-700 rounded-xl h-10 font-bold text-xs flex items-center justify-center transition-colors space-x-1"
>
<Eye className="w-3.5 h-3.5" />
<span>Quick Preview</span>
</button>
<button
type="button"
onClick={() => handleToggleComparison(worker.id)}
className={`w-full rounded-xl h-10 font-bold text-xs flex items-center justify-center transition-colors border ${
isComparing
? 'bg-purple-600 border-purple-600 text-white hover:bg-purple-700'
: 'bg-white border-slate-200 hover:bg-slate-50 text-slate-700'
}`}
>
{isComparing ? 'Comparing ✓' : 'Compare Candidate'}
</button>
</div>
<Link <Link
href={`/employer/workers/${worker.id}`} href={`/employer/workers/${worker.id}`}
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl h-10 font-bold text-xs flex items-center justify-center transition-colors shadow-sm" className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl h-10 font-bold text-xs flex items-center justify-center transition-colors shadow-xs"
> >
View Full Profile Open Full Profile
</Link> </Link>
</div> </div>
</div> </div>
</div>
); );
})} })}
</div> </div>
) : ( ) : (
<div className="text-center py-16 bg-white rounded-2xl border border-slate-200 shadow-sm space-y-3"> <div className="text-center py-20 bg-white rounded-2xl border border-slate-200 shadow-sm space-y-4">
<SlidersHorizontal className="w-12 h-12 text-slate-300 mx-auto" /> <SlidersHorizontal className="w-14 h-14 text-slate-300 mx-auto" />
<div className="text-base font-bold text-slate-800">No workers match your filter criteria</div> <h4 className="text-base font-bold text-slate-800">No candidates match your filters</h4>
<p className="text-xs text-slate-500 max-w-sm mx-auto"> <p className="text-xs text-slate-500 max-w-sm mx-auto leading-relaxed">
Try adjusting your salary slider, broadening your category selection, or resetting all filters. Try adjusting your salary limit, adding additional languages, or broadening nationality preferences.
</p> </p>
<button <button
type="button" type="button"
onClick={resetFilters} onClick={resetFilters}
className="mt-2 bg-[#185FA5] text-white px-5 py-2.5 rounded-xl text-xs font-bold shadow-sm hover:bg-[#144f8a] transition-colors" className="bg-[#185FA5] text-white px-5 py-2.5 rounded-xl text-xs font-bold hover:bg-[#144f8a] transition-all shadow-sm"
> >
Reset All Filters Reset Filters
</button> </button>
</div> </div>
)} )}
{/* Load More Pagination */}
{filteredWorkers.length > visibleCount && (
<div className="text-center pt-6">
<button
onClick={() => setVisibleCount(visibleCount + 6)}
className="bg-white border border-slate-200 hover:bg-slate-50 text-[#185FA5] px-8 py-3 rounded-xl text-xs font-bold transition-all shadow-xs"
>
Load More Workers
</button>
</div>
)}
{/* 5. Worker Comparison Drawer (Col 12 Fixed bottom) */}
{comparisonIds.length > 0 && (
<div className="fixed bottom-0 left-0 right-0 bg-white border-t-2 border-[#185FA5] shadow-2xl p-6 z-50 animate-slide-up flex flex-col space-y-4 max-w-7xl mx-auto rounded-t-3xl">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="w-2.5 h-2.5 rounded-full bg-[#185FA5] animate-ping" />
<h4 className="font-extrabold text-sm text-slate-900">Comparing Workers ({comparedWorkers.length} of 3)</h4>
</div>
<button
onClick={() => setComparisonIds([])}
className="text-xs font-bold text-rose-600 hover:underline flex items-center space-x-1"
>
<X className="w-4 h-4" />
<span>Clear all</span>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{comparedWorkers.map(w => (
<div key={w.id} className="bg-slate-50 border border-slate-200 rounded-2xl p-4 space-y-3 relative">
<button
onClick={() => handleToggleComparison(w.id)}
className="absolute top-2 right-2 text-slate-400 hover:text-slate-600"
>
<X className="w-4 h-4" />
</button>
<div className="flex items-center space-x-2">
<div className="w-8 h-8 rounded-full bg-blue-100 text-[#185FA5] font-black text-xs flex items-center justify-center overflow-hidden">
{w.photo ? (
<img src={w.photo} alt={w.name} className="w-full h-full object-cover" />
) : (
<span>{w.name.charAt(0)}</span>
)}
</div>
<div>
<div className="font-bold text-xs text-slate-900 truncate max-w-[120px]">{w.name}</div>
<div className="text-[10px] text-slate-500 font-bold">{w.nationality} {w.category}</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-[10px] bg-white p-2.5 rounded-xl border border-slate-100 font-bold text-slate-600">
<div>Salary: <span className="text-slate-800">{w.salary} AED</span></div>
<div>Age: <span className="text-slate-800">{w.age} yrs</span></div>
<div>Job Type: <span className="text-slate-800 uppercase">{w.preferred_job_type}</span></div>
<div>Status: <span className="text-slate-800 font-black">{w.availability_status}</span></div>
</div>
</div>
))}
</div>
</div>
)}
{/* 6. Quick Preview Modal overlay */}
{previewWorker && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">
<div className="bg-white rounded-3xl w-full max-w-lg border border-slate-200 shadow-2xl p-6 relative animate-zoom-in space-y-6">
<button
onClick={() => setPreviewWorker(null)}
className="absolute top-4 right-4 p-2 bg-slate-50 hover:bg-slate-100 rounded-full text-slate-400 hover:text-slate-600 transition-colors"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-center space-x-4">
<div className="w-16 h-16 bg-blue-50 text-[#185FA5] rounded-2xl border border-blue-100 font-black text-2xl flex items-center justify-center shadow-inner overflow-hidden flex-shrink-0">
{previewWorker.photo ? (
<img src={previewWorker.photo} alt={previewWorker.name} className="w-full h-full object-cover" />
) : (
<User className="w-8 h-8 text-slate-400" />
)}
</div>
<div>
<div className="flex items-center space-x-1">
<h4 className="font-extrabold text-lg text-slate-900">{previewWorker.name}</h4>
{previewWorker.verified && <CheckCircle2 className="w-4 h-4 text-emerald-600" />}
</div>
<div className="flex items-center space-x-1 text-emerald-700 font-black text-[9px] uppercase tracking-wider">
<ShieldCheck className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" />
<span>{previewWorker.emirates_id_status}</span>
</div>
<div className="text-xs text-slate-500 font-bold">{previewWorker.nationality} {previewWorker.category}</div>
</div>
</div>
<p className="text-xs text-slate-600 italic bg-slate-50 p-4 rounded-xl leading-relaxed">
"{previewWorker.bio}"
</p>
<div className="grid grid-cols-2 gap-4 text-xs font-bold text-slate-600">
<div className="p-3 bg-slate-50/50 rounded-xl">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">Expectations</div>
<div className="text-slate-800">{previewWorker.salary} AED / mo</div>
</div>
<div className="p-3 bg-slate-50/50 rounded-xl">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">Languages</div>
<div className="flex flex-wrap gap-1.5 mt-1">
{previewWorker.languages?.map(lang => (
<span key={lang} className="text-[9px] bg-white border border-slate-200 text-slate-700 px-2 py-0.5 rounded font-bold flex items-center space-x-1">
<span>{getLanguageFlag(lang)}</span>
<span>{lang}</span>
</span>
))}
</div>
</div>
<div className="p-3 bg-slate-50/50 rounded-xl">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">Availability Status</div>
<span className={`inline-block px-2 py-0.5 text-[8px] font-black rounded-lg uppercase tracking-wider mt-1 border ${
previewWorker.availability_status === 'Active' ? 'bg-emerald-50 text-emerald-700 border-emerald-200' :
previewWorker.availability_status === 'Hired' ? 'bg-blue-50 text-blue-700 border-blue-200' :
'bg-slate-100 text-slate-600 border-slate-200'
}`}>
{previewWorker.availability_status}
</span>
</div>
<div className="p-3 bg-slate-50/50 rounded-xl">
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">Job Preference</div>
<div className="text-slate-800 uppercase text-[10px] mt-1">{previewWorker.preferred_job_type}</div>
</div>
</div>
{/* Core Platform Skills inside Quick Preview */}
<div className="space-y-1 pt-1">
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Platform Skills</div>
<div className="flex flex-wrap gap-1">
{previewWorker.skills?.map(skill => (
<span key={skill} className="px-2.5 py-0.5 bg-blue-50 border border-blue-100 text-[#185FA5] text-[9px] font-black rounded-lg uppercase tracking-wider">
{skill}
</span>
))}
</div>
</div>
<div className="flex items-center space-x-3 pt-2">
<button
onClick={() => toggleShortlist(previewWorker.id)}
className="flex-1 border border-slate-200 hover:bg-slate-50 rounded-xl h-11 text-xs font-bold text-slate-700 flex items-center justify-center space-x-2"
>
<Bookmark className="w-4 h-4" />
<span>{shortlistedIds.includes(previewWorker.id) ? 'Shortlisted ✓' : 'Add to Shortlist'}</span>
</button>
<Link
href={`/employer/workers/${previewWorker.id}`}
className="flex-1 bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl h-11 text-xs font-black flex items-center justify-center shadow-xs"
>
View Full Details
</Link>
</div>
</div>
</div>
)}
{/* Preset Modal */}
{showPresetModal && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50">
<form onSubmit={saveCurrentPreset} className="bg-white rounded-3xl w-full max-w-sm p-6 relative animate-zoom-in space-y-4 border border-slate-200 shadow-2xl">
<h4 className="font-extrabold text-sm text-slate-900">Name this search preset</h4>
<input
type="text"
placeholder="e.g. Filipino Nannies, Drivers..."
value={presetNameInput}
onChange={(e) => setPresetNameInput(e.target.value)}
className="w-full p-2.5 border border-slate-300 rounded-xl text-xs focus:outline-none focus:border-[#185FA5]"
required
/>
<div className="flex justify-end space-x-2 pt-2 text-xs font-bold">
<button
type="button"
onClick={() => setShowPresetModal(false)}
className="px-4 py-2 border border-slate-200 hover:bg-slate-50 rounded-lg"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-[#185FA5] text-white hover:bg-[#144f8a] rounded-lg"
>
Save Filter Preset
</button>
</div>
</form>
</div>
)}
</div> </div>
</EmployerLayout> </EmployerLayout>
); );

View File

@ -1,5 +1,5 @@
import React from 'react'; import React, { useState } from 'react';
import { Head, Link } from '@inertiajs/react'; import { Head, Link, router } from '@inertiajs/react';
import EmployerLayout from '../../../Layouts/EmployerLayout'; import EmployerLayout from '../../../Layouts/EmployerLayout';
import { import {
CheckCircle2, CheckCircle2,
@ -15,72 +15,240 @@ import {
Sparkles, Sparkles,
FileText, FileText,
ShieldCheck, ShieldCheck,
Search Search,
Star,
Share2,
Flag,
X,
UserCheck,
AlertTriangle,
User
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner';
const getLanguageFlag = (lang) => {
const flags = {
'English': '🇬🇧',
'Arabic': '🇦🇪',
'Tagalog': '🇵🇭',
'Hindi': '🇮🇳',
'Swahili': '🇰🇪',
'French': '🇫🇷',
'Indonesian': '🇮🇩'
};
return flags[lang] || '🌐';
};
export default function Show({ worker }) { export default function Show({ worker }) {
return ( const [showReportModal, setShowReportModal] = useState(false);
<EmployerLayout title="Worker Profile"> const [reportReason, setReportReason] = useState('');
<Head title={`${worker.name} - Worker Profile`} /> const [reportDetails, setReportDetails] = useState('');
const [isShortlisted, setIsShortlisted] = useState(false);
<div className="space-y-6 select-none w-full"> // Hiring flow states
const [availabilityStatus, setAvailabilityStatus] = useState(worker.availability_status);
const [showHiredModal, setShowHiredModal] = useState(false);
// Reviews state
const [showReviewModal, setShowReviewModal] = useState(false);
const [newRating, setNewRating] = useState(5);
const [newComment, setNewComment] = useState('');
const [workerReviews, setWorkerReviews] = useState(worker.reviews || []);
const handleShare = () => {
navigator.clipboard.writeText(window.location.href);
toast.success("📋 Profile link copied to clipboard!", {
description: "You can now share this URL with your family or sponsor contacts."
});
};
const handleReportSubmit = (e) => {
e.preventDefault();
if (!reportReason) {
toast.error("Please select a reason for reporting.");
return;
}
toast.success("🛡️ Worker report submitted successfully", {
description: "Our admin compliance team will investigate this worker's credentials within 24 hours."
});
setShowReportModal(false);
setReportReason('');
setReportDetails('');
};
const handleReviewSubmit = (e) => {
e.preventDefault();
if (!newComment.trim()) {
toast.error("Please add a comment.");
return;
}
const newReview = {
id: Date.now(),
employer_name: "You (Verified Sponsor)",
rating: newRating,
date: "Today",
comment: newComment,
};
setWorkerReviews([newReview, ...workerReviews]);
toast.success("⭐ Review posted successfully!", {
description: "Your trust feedback is live and helps other sponsors hire securely."
});
setShowReviewModal(false);
setNewComment('');
};
const handleToggleShortlist = () => {
setIsShortlisted(!isShortlisted);
router.post('/employer/shortlist/toggle', { worker_id: worker.id }, {
preserveScroll: true,
onSuccess: () => {
toast.success(isShortlisted ? "Removed from Shortlist" : "Added to Shortlist");
}
});
};
const handleMarkHired = () => {
setAvailabilityStatus('Hired');
setShowHiredModal(true);
toast.success("🎉 Worker successfully marked as Hired!", {
description: "Sponsorship direct match finalized. Please leave an anonymous trust review!"
});
};
return (
<EmployerLayout title="Candidate Profile Detail">
<Head title={`${worker.name} - ${worker.category} Profile`} />
<div className="space-y-6 select-none w-full pb-16">
{/* Back Link */}
<div className="flex items-center justify-between">
<Link <Link
href="/employer/workers" href="/employer/workers"
className="inline-flex items-center space-x-2 text-xs font-bold text-slate-600 hover:text-[#185FA5] transition-colors" className="inline-flex items-center space-x-2 text-xs font-black text-slate-500 hover:text-[#185FA5] transition-colors"
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
<span>Back to Find Workers</span> <span>BACK TO FIND WORKERS</span>
</Link> </Link>
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden"> {/* Quick Profile Actions */}
{/* Header Card */} <div className="flex items-center space-x-2">
<div className="bg-gradient-to-r from-[#185FA5] to-blue-700 p-8 text-white flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6 relative overflow-hidden"> <button
<div className="absolute -right-10 -bottom-10 w-48 h-48 bg-white/10 rounded-full blur-2xl pointer-events-none" /> onClick={handleShare}
className="p-2.5 bg-white border border-slate-200 hover:bg-slate-50 rounded-xl text-slate-500 hover:text-[#185FA5] transition-colors flex items-center space-x-1 text-xs font-bold shadow-xs"
title="Share Profile link"
>
<Share2 className="w-4 h-4" />
<span className="hidden sm:inline">Share Profile</span>
</button>
<button
onClick={() => setShowReportModal(true)}
className="p-2.5 bg-white border border-slate-200 hover:bg-rose-50 rounded-xl text-slate-400 hover:text-rose-600 transition-colors flex items-center space-x-1 text-xs font-bold shadow-xs"
title="Report Abuse / Terms violations"
>
<Flag className="w-4 h-4" />
<span className="hidden sm:inline">Report Worker</span>
</button>
</div>
</div>
<div className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden">
{/* Header Card with Clean Premium White BG */}
<div className="bg-white p-8 text-slate-900 border-b border-slate-200 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6 relative overflow-hidden">
<div className="absolute -right-10 -bottom-10 w-48 h-48 bg-slate-50 rounded-full blur-2xl pointer-events-none" />
<div className="flex items-center space-x-5 z-10"> <div className="flex items-center space-x-5 z-10">
<div className="w-20 h-20 rounded-full bg-white text-[#185FA5] flex items-center justify-center font-bold text-3xl shadow-md border-2 border-white flex-shrink-0"> <div className="w-20 h-20 rounded-2xl bg-slate-50 text-[#185FA5] flex items-center justify-center font-black text-3xl shadow-sm border border-slate-200 flex-shrink-0 overflow-hidden relative">
{worker.name.charAt(0)} {worker.photo ? (
<img src={worker.photo} alt={worker.name} className="w-full h-full object-cover" />
) : (
<User className="w-10 h-10 text-slate-400" />
)}
</div> </div>
<div className="space-y-1"> <div className="space-y-1.5">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight">{worker.name}</h1> <h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-slate-900">{worker.name}</h1>
{worker.verified && ( {worker.verified && (
<span className="bg-emerald-500 text-white p-0.5 rounded-full shadow-sm" title="OCR Verified"> <span className="bg-emerald-500 text-white p-0.5 rounded-full shadow-sm animate-pulse" title="OCR Verified">
<CheckCircle2 className="w-4 h-4" /> <CheckCircle2 className="w-4 h-4" />
</span> </span>
)} )}
{/* Availability Status Badge */}
<span className={`px-2.5 py-0.5 text-[8px] font-black rounded-lg uppercase tracking-wider border ${
availabilityStatus === 'Active' ? 'bg-emerald-50 text-emerald-700 border-emerald-200' :
availabilityStatus === 'Hired' ? 'bg-blue-50 text-blue-700 border-blue-200' :
'bg-slate-100 text-slate-600 border-slate-200'
}`}>
{availabilityStatus}
</span>
</div> </div>
<div className="flex items-center space-x-2 text-blue-100 text-sm font-medium">
<Globe2 className="w-4 h-4" /> {/* Emirates ID verification status bar & Visa Status */}
<div className="flex flex-wrap gap-2">
<div className="flex items-center space-x-1.5 bg-emerald-50 px-2.5 py-1 rounded-lg text-emerald-700 border border-emerald-100 text-[9px] font-black uppercase tracking-wider w-fit">
<ShieldCheck className="w-3.5 h-3.5 text-emerald-600" />
<span>{worker.emirates_id_status}</span>
</div>
<div className="flex items-center space-x-1.5 bg-blue-50 px-2.5 py-1 rounded-lg text-blue-700 border border-blue-100 text-[9px] font-black uppercase tracking-wider w-fit">
<span>{worker.visa_status}</span>
</div>
</div>
<div className="flex flex-wrap items-center gap-2.5 text-slate-500 text-xs font-bold">
<span className="flex items-center space-x-1">
<Globe2 className="w-3.5 h-3.5 text-slate-400" />
<span>{worker.nationality}</span> <span>{worker.nationality}</span>
</span>
<span></span> <span></span>
<span>{worker.age} Years Old</span> <span>{worker.age} Years Old</span>
<span></span>
<span className="bg-blue-50 text-[#185FA5] border border-blue-100 px-2 py-0.5 rounded text-[10px] uppercase font-black tracking-widest">{worker.category}</span>
</div>
<div className="flex items-center space-x-1 text-amber-500 font-bold text-xs">
<Star className="w-3.5 h-3.5 fill-amber-500 text-amber-500" />
<span className="text-slate-700">{worker.rating} / 5.0 ({workerReviews.length} reviews)</span>
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center space-x-3 z-10 w-full sm:w-auto justify-end"> {/* Sponsor Actions Block */}
<div className="flex flex-wrap items-center gap-3 z-10 w-full sm:w-auto justify-end">
<button <button
type="button" type="button"
className="bg-white/10 hover:bg-white/20 backdrop-blur-sm border border-white/20 text-white p-3 rounded-xl shadow-sm transition-colors" onClick={handleToggleShortlist}
className={`p-3 rounded-xl transition-all border ${
isShortlisted
? 'bg-blue-50 border-blue-200 text-[#185FA5]'
: 'bg-white hover:bg-slate-50 border-slate-200 text-slate-400 hover:text-[#185FA5]'
}`}
title="Shortlist Worker" title="Shortlist Worker"
> >
<Bookmark className="w-5 h-5" /> <Bookmark className={`w-5 h-5 ${isShortlisted ? 'fill-[#185FA5]' : ''}`} />
</button> </button>
{/* Stage 3 Connect: Start In-App Chat */}
<Link <Link
href={`/employer/workers/${worker.id}/hire`} href={`/employer/messages/start/${worker.id}`}
className="bg-[#185FA5] text-white hover:bg-[#144f8a] px-8 py-3 rounded-xl font-bold text-sm shadow-lg shadow-blue-500/20 transition-all flex items-center justify-center space-x-2 flex-1 sm:flex-initial scale-105 active:scale-100" className="bg-white border border-slate-250 hover:bg-slate-50 px-6 py-3 rounded-xl font-bold text-sm shadow-xs transition-colors flex items-center justify-center space-x-2 text-slate-700 flex-1 sm:flex-initial"
> >
<Sparkles className="w-4 h-4" /> <MessageSquare className="w-4 h-4 text-slate-500" />
<span>Hire Now</span> <span>Start Direct Chat</span>
</Link> </Link>
<Link
href="/employer/messages" {/* Stage 4 Hire: Mark Hired (Finalize Hiring Direct Flow) */}
className="bg-white text-slate-600 border border-slate-200 hover:bg-slate-50 px-6 py-3 rounded-xl font-bold text-sm shadow-sm transition-colors flex items-center justify-center space-x-2 flex-1 sm:flex-initial" <button
type="button"
onClick={handleMarkHired}
className="bg-emerald-600 hover:bg-emerald-700 text-white border border-emerald-700 px-8 py-3 rounded-xl font-black text-sm shadow-md transition-all flex items-center justify-center space-x-2 flex-1 sm:flex-initial scale-105 active:scale-100"
> >
<MessageSquare className="w-4 h-4" /> <CheckCircle2 className="w-4 h-4 text-white" />
<span>Message</span> <span>Mark Hired</span>
</Link> </button>
</div> </div>
</div> </div>
@ -89,63 +257,80 @@ export default function Show({ worker }) {
{/* Left Column: Quick Stats */} {/* Left Column: Quick Stats */}
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 space-y-4"> <div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 space-y-4">
<h3 className="font-bold text-sm text-slate-800 uppercase tracking-wider">Verification & Cost</h3> <h3 className="font-black text-xs text-slate-500 uppercase tracking-widest">Sponsorship Details</h3>
<div className="flex items-center justify-between pb-3 border-b border-slate-200"> <div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium"> <div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<DollarSign className="w-4 h-4 text-emerald-600" /> <DollarSign className="w-4 h-4 text-emerald-600" />
<span>Monthly Salary</span> <span>Salary Expectations</span>
</div> </div>
<span className="font-bold text-slate-900 text-sm">{worker.salary} AED</span> <span className="font-extrabold text-slate-900 text-sm">{worker.salary} AED / mo</span>
</div> </div>
<div className="flex items-center justify-between pb-3 border-b border-slate-200"> <div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium"> <div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<Calendar className="w-4 h-4 text-[#185FA5]" /> <Calendar className="w-4 h-4 text-[#185FA5]" />
<span>Availability</span> <span>Availability Status</span>
</div> </div>
<span className="font-bold text-[#185FA5] bg-blue-50 px-2 py-0.5 rounded text-xs"> <span className="font-black text-[#185FA5] bg-blue-50 px-2 py-0.5 rounded text-xs border border-blue-100">
{worker.availability} {availabilityStatus}
</span> </span>
</div> </div>
<div className="flex items-center justify-between pb-3 border-b border-slate-200"> <div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium"> <div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<Briefcase className="w-4 h-4 text-amber-600" /> <Briefcase className="w-4 h-4 text-amber-600" />
<span>Experience</span> <span>Work Experience</span>
</div> </div>
<span className="font-bold text-slate-900 text-xs">{worker.experience}</span> <span className="font-extrabold text-slate-900 text-xs">{worker.experience}</span>
</div> </div>
<div className="flex items-center justify-between pb-3 border-b border-slate-200"> <div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium"> <div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<HeartHandshake className="w-4 h-4 text-purple-600" /> <Sparkles className="w-4 h-4 text-purple-600" />
<span>Religion</span> <span>Preferred Job Type</span>
</div> </div>
<span className="font-bold text-slate-900 text-xs">{worker.religion}</span> <span className="font-black text-slate-900 text-xs uppercase">{worker.preferred_job_type}</span>
</div>
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-655 font-bold">
<ShieldCheck className="w-4 h-4 text-emerald-600" />
<span>Emirates ID Vetting</span>
</div>
<span className="font-bold text-emerald-700 text-xs uppercase">{worker.emirates_id_status}</span>
</div>
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<FileText className="w-4 h-4 text-blue-600" />
<span>Visa Status</span>
</div>
<span className="font-bold text-blue-700 text-xs uppercase">{worker.visa_status}</span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium"> <div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
<ShieldCheck className="w-4 h-4 text-emerald-600" /> <CheckCircle2 className="w-4 h-4 text-emerald-600" />
<span>Medical Cert</span> <span>Medical Health Test</span>
</div> </div>
<span className="font-bold text-emerald-700 bg-emerald-50 px-2 py-0.5 rounded text-xs"> <span className="font-black text-emerald-700 bg-emerald-50 px-2.5 py-0.5 rounded text-[10px] uppercase border border-emerald-200">
Passed PASSED CERTIFIED
</span> </span>
</div> </div>
</div> </div>
{/* Languages Spoken */} {/* Languages Spoken with visual flags */}
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 space-y-3"> <div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 space-y-3">
<div className="flex items-center space-x-2 font-bold text-sm text-slate-800"> <div className="flex items-center space-x-2 font-bold text-sm text-slate-800">
<Languages className="w-4 h-4 text-[#185FA5]" /> <Languages className="w-4 h-4 text-[#185FA5]" />
<span>Languages Spoken</span> <span>Languages Spoken</span>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{worker.languages.map(lang => ( {worker.languages?.map(lang => (
<span key={lang} className="bg-white border border-slate-200 text-slate-800 px-3 py-1 rounded-xl text-xs font-bold shadow-sm"> <span key={lang} className="bg-white border border-slate-200 text-slate-800 px-3 py-1 rounded-xl text-xs font-extrabold shadow-xs uppercase flex items-center space-x-1.5">
{lang} <span>{getLanguageFlag(lang)}</span>
<span>{lang}</span>
</span> </span>
))} ))}
</div> </div>
@ -156,7 +341,7 @@ export default function Show({ worker }) {
<div className="lg:col-span-2 space-y-8"> <div className="lg:col-span-2 space-y-8">
{/* Professional Summary */} {/* Professional Summary */}
<div className="space-y-3"> <div className="space-y-3">
<h3 className="font-bold text-base text-slate-900">Professional Summary</h3> <h3 className="font-extrabold text-base text-slate-900">Professional Summary</h3>
<p className="text-sm text-slate-600 leading-relaxed bg-slate-50 p-6 rounded-2xl border border-slate-200 italic"> <p className="text-sm text-slate-600 leading-relaxed bg-slate-50 p-6 rounded-2xl border border-slate-200 italic">
"{worker.bio}" "{worker.bio}"
</p> </p>
@ -164,12 +349,12 @@ export default function Show({ worker }) {
{/* Verified Skills & Competencies */} {/* Verified Skills & Competencies */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="font-bold text-base text-slate-900">Verified Skills & Competencies</h3> <h3 className="font-extrabold text-base text-slate-900">Verified Skills & Competencies</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{worker.skills.map(skill => ( {worker.skills?.map(skill => (
<div key={skill} className="flex items-center space-x-3 p-3.5 bg-white border border-slate-200 rounded-xl shadow-sm"> <div key={skill} className="flex items-center space-x-3 p-3.5 bg-white border border-slate-200 rounded-xl shadow-xs">
<CheckCircle2 className="w-5 h-5 text-emerald-600 flex-shrink-0" /> <CheckCircle2 className="w-5 h-5 text-emerald-600 flex-shrink-0" />
<span className="font-bold text-xs text-slate-800">{skill}</span> <span className="font-bold text-xs text-slate-800 uppercase tracking-wide">{skill}</span>
</div> </div>
))} ))}
</div> </div>
@ -177,51 +362,46 @@ export default function Show({ worker }) {
{/* Verified Documents Section */} {/* Verified Documents Section */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="font-bold text-base text-slate-900 flex items-center space-x-2"> <h3 className="font-extrabold text-base text-slate-900 flex items-center space-x-2">
<ShieldCheck className="w-5 h-5 text-emerald-600" /> <ShieldCheck className="w-5 h-5 text-emerald-600" />
<span>Verified Legal Documents</span> <span>Verified Legal Documents (UAE MOHRE Vetted)</span>
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Document Card: Passport */} {/* Document Card: Passport */}
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow"> <div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm">
<div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center justify-between"> <div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center justify-between">
<span className="text-[11px] font-black text-slate-500 uppercase tracking-widest">Passport (Verified)</span> <span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Passport (Verified)</span>
<FileText className="w-4 h-4 text-slate-400" /> <FileText className="w-4 h-4 text-slate-400" />
</div> </div>
<div className="p-4 flex space-x-4"> <div className="p-4 flex space-x-4">
{/* Document Thumbnail Placeholder */} <div className="w-20 h-28 bg-slate-100 rounded-lg flex-shrink-0 border border-slate-200 flex flex-col items-center justify-center p-2 relative group cursor-zoom-in">
<div className="w-24 h-32 bg-slate-100 rounded-lg flex-shrink-0 border border-slate-200 flex flex-col items-center justify-center p-2 relative group cursor-zoom-in"> <Search className="w-5 h-5 text-slate-300" />
<div className="w-full h-full bg-blue-50/50 rounded flex items-center justify-center"> <span className="absolute bottom-1 text-[7px] font-bold text-slate-400 uppercase">Click zoom</span>
<Search className="w-6 h-6 text-blue-200 group-hover:text-blue-400 transition-colors" />
</div>
<div className="absolute inset-0 bg-slate-900/0 group-hover:bg-slate-900/5 transition-colors" />
<span className="absolute bottom-2 text-[8px] font-bold text-slate-400 uppercase">Click to view</span>
</div> </div>
{/* Extracted Data */} <div className="flex-1 space-y-2">
<div className="flex-1 space-y-3">
<div> <div>
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Document Number</div> <div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">Document ID</div>
<div className="text-xs font-bold text-slate-800">L82739102-UAE</div> <div className="text-xs font-bold text-slate-800">L82739102-UAE</div>
</div> </div>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-1">
<div> <div>
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Issue Date</div> <div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">Issue</div>
<div className="text-xs font-bold text-slate-800">12 Jan 2021</div> <div className="text-[10px] font-bold text-slate-800">12 Jan 2021</div>
</div> </div>
<div> <div>
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Expiry Date</div> <div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">Expiry</div>
<div className="text-xs font-bold text-emerald-600">11 Jan 2031</div> <div className="text-[10px] font-bold text-emerald-600">11 Jan 2031</div>
</div> </div>
</div> </div>
<div> <div>
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">OCR Match Accuracy</div> <div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">OCR Match</div>
<div className="flex items-center space-x-2 mt-1"> <div className="flex items-center space-x-2 mt-0.5">
<div className="flex-1 h-1 bg-slate-100 rounded-full overflow-hidden"> <div className="flex-1 h-1 bg-slate-100 rounded-full overflow-hidden">
<div className="h-full bg-emerald-500 w-[98%]" /> <div className="h-full bg-emerald-500 w-[95%]" />
</div> </div>
<span className="text-[10px] font-black text-emerald-600">98%</span> <span className="text-[9px] font-bold text-emerald-600">95%</span>
</div> </div>
</div> </div>
</div> </div>
@ -229,42 +409,27 @@ export default function Show({ worker }) {
</div> </div>
{/* Document Card: Visa */} {/* Document Card: Visa */}
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow"> <div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm">
<div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center justify-between"> <div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center justify-between">
<span className="text-[11px] font-black text-slate-500 uppercase tracking-widest">Entry Visa (Verified)</span> <span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Entry Visa (Verified)</span>
<ShieldCheck className="w-4 h-4 text-emerald-500" /> <ShieldCheck className="w-4 h-4 text-emerald-500" />
</div> </div>
<div className="p-4 flex space-x-4"> <div className="p-4 flex space-x-4">
{/* Document Thumbnail Placeholder */} <div className="w-20 h-28 bg-slate-100 rounded-lg flex-shrink-0 border border-slate-200 flex flex-col items-center justify-center p-2 relative group cursor-zoom-in">
<div className="w-24 h-32 bg-slate-100 rounded-lg flex-shrink-0 border border-slate-200 flex flex-col items-center justify-center p-2 relative group cursor-zoom-in"> <Search className="w-5 h-5 text-slate-300" />
<div className="w-full h-full bg-blue-50/50 rounded flex items-center justify-center"> <span className="absolute bottom-1 text-[7px] font-bold text-slate-400 uppercase">Click zoom</span>
<Search className="w-6 h-6 text-blue-200 group-hover:text-blue-400 transition-colors" />
</div>
<div className="absolute inset-0 bg-slate-900/0 group-hover:bg-slate-900/5 transition-colors" />
<span className="absolute bottom-2 text-[8px] font-bold text-slate-400 uppercase">Click to view</span>
</div> </div>
{/* Extracted Data */} <div className="flex-1 space-y-2">
<div className="flex-1 space-y-3">
<div> <div>
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Visa Reference</div> <div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">Emirates ID status</div>
<div className="text-xs font-bold text-slate-800">UAE-2024-918273</div> <div className="text-xs font-bold text-slate-800">{worker.emirates_id_status}</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Visa Type</div>
<div className="text-xs font-bold text-slate-800">Employment</div>
</div> </div>
<div> <div>
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Expiry Date</div> <div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">Vetting status</div>
<div className="text-xs font-bold text-amber-600">30 Dec 2026</div> <div className="flex items-center space-x-1.5 mt-0.5">
</div>
</div>
<div>
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Background Check</div>
<div className="flex items-center space-x-1.5 mt-1">
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600" /> <CheckCircle2 className="w-3.5 h-3.5 text-emerald-600" />
<span className="text-[10px] font-bold text-slate-700">No Criminal Record Found</span> <span className="text-[9px] font-bold text-slate-700">Clear Records</span>
</div> </div>
</div> </div>
</div> </div>
@ -276,7 +441,262 @@ export default function Show({ worker }) {
</div> </div>
</div> </div>
</div> </div>
{/* Star Ratings & Trust Review logs */}
<div className="bg-white p-8 rounded-3xl border border-slate-200 shadow-sm space-y-6">
<div className="flex items-center justify-between border-b border-slate-150 pb-4">
<div className="space-y-1">
<h3 className="font-extrabold text-base text-slate-900">Sponsor Reviews & Star Ratings</h3>
<p className="text-xs text-slate-500 font-medium">Ratings can only be posted by verified sponsors who completed verified direct hiring.</p>
</div> </div>
<button
onClick={() => setShowReviewModal(true)}
className="bg-[#185FA5] hover:bg-[#144f8a] text-white px-5 py-2.5 rounded-xl text-xs font-bold shadow-xs transition-colors"
>
Post a Trust Review
</button>
</div>
{/* Reviews List */}
<div className="space-y-4">
{workerReviews.map((review) => (
<div key={review.id} className="p-6 bg-slate-50 rounded-2xl border border-slate-150 space-y-3">
<div className="flex items-center justify-between">
<div>
<span className="font-extrabold text-slate-900 text-xs block">{review.employer_name}</span>
<span className="text-[10px] text-slate-400 font-bold block mt-0.5">{review.date}</span>
</div>
<div className="flex items-center space-x-1">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`w-3.5 h-3.5 ${i < review.rating ? 'text-amber-550 fill-amber-400 text-amber-500' : 'text-slate-200'}`}
/>
))}
</div>
</div>
<p className="text-xs text-slate-650 leading-relaxed italic">
"{review.comment}"
</p>
</div>
))}
</div>
</div>
{/* Similar Recommendations Slider */}
<div className="space-y-4">
<h3 className="font-extrabold text-base text-slate-900">Similar Recommended Workers</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{worker.similar_workers?.map((sim) => (
<div key={sim.id} className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-4">
<div className="flex items-center justify-between">
<div>
<h4 className="font-extrabold text-sm text-slate-900">{sim.name}</h4>
<p className="text-[10px] text-slate-500 font-bold">{sim.nationality} {sim.category}</p>
</div>
<span className="bg-blue-50 text-[#185FA5] px-2 py-0.5 border border-blue-100 rounded text-[9px] font-black uppercase">
{sim.availability_status}
</span>
</div>
<div className="flex items-center justify-between pt-2 border-t border-slate-100">
<span className="font-black text-slate-800 text-xs">{sim.salary} AED/mo</span>
<Link
href={`/employer/workers/${sim.id}`}
className="text-[#185FA5] font-black text-xs hover:underline flex items-center space-x-1"
>
<span>View Profile</span>
<span></span>
</Link>
</div>
</div>
))}
</div>
</div>
</div>
{/* Stage 4/5 Hired Flow Success Modal Overlay */}
{showHiredModal && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">
<div className="bg-white rounded-3xl w-full max-w-md p-6 relative animate-zoom-in space-y-6 border border-slate-200 shadow-2xl">
<div className="text-center space-y-3">
<div className="w-16 h-16 bg-emerald-50 text-emerald-600 rounded-full flex items-center justify-center mx-auto shadow-inner">
<CheckCircle2 className="w-8 h-8" />
</div>
<h4 className="font-extrabold text-lg text-slate-900">Direct Sponsoring Finalized!</h4>
<p className="text-xs text-slate-500 leading-relaxed">
{worker.name} is successfully marked as <strong>Hired</strong> under your sponsorship! This direct matching conforms exactly with Stage 4 of our UAE MOHRE zero-middlemen flow.
</p>
</div>
<div className="p-4 bg-blue-50/50 rounded-2xl border border-blue-100/50 space-y-2">
<span className="text-[9px] font-black text-[#185FA5] uppercase tracking-widest block">Next Step: Stage 5 Post-Hire Review</span>
<p className="text-[11px] text-slate-700 leading-relaxed font-bold">
Help the UAE community hire safely by posting an anonymous review describing {worker.name}'s childcare, cooking, driving, or domestic skills.
</p>
</div>
<div className="flex flex-col space-y-2 text-xs font-bold">
<button
onClick={() => {
setShowHiredModal(false);
setShowReviewModal(true);
}}
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white py-3 rounded-xl flex items-center justify-center space-x-2"
>
<Star className="w-4 h-4 fill-white" />
<span>Leave a Trust Review</span>
</button>
<Link
href="/employer/workers"
className="w-full bg-slate-50 border border-slate-200 hover:bg-slate-100 text-slate-700 py-3 rounded-xl flex items-center justify-center"
>
Back to Worker Directory
</Link>
</div>
</div>
</div>
)}
{/* Report Abuse Modal Overlay */}
{showReportModal && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">
<form onSubmit={handleReportSubmit} className="bg-white rounded-3xl w-full max-w-md p-6 relative animate-zoom-in space-y-4 border border-slate-200 shadow-2xl">
<button
type="button"
onClick={() => setShowReportModal(false)}
className="absolute top-4 right-4 text-slate-400 hover:text-slate-600"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-center space-x-2 text-rose-600">
<AlertTriangle className="w-5 h-5" />
<h4 className="font-black text-sm uppercase">Report Vetting Abuse / Terms Violation</h4>
</div>
<p className="text-xs text-slate-500 leading-relaxed">
We maintain zero-tolerance for fake documentation, independent recruiters trading cash, or incorrect contact details.
</p>
<div className="space-y-3">
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Reason</label>
<select
value={reportReason}
onChange={(e) => setReportReason(e.target.value)}
className="w-full p-2.5 border border-slate-300 rounded-xl text-xs font-bold text-slate-700 bg-slate-50 focus:outline-none"
>
<option value="">Select a reason...</option>
<option value="fees">Worker / agent requested hiring commission fees</option>
<option value="profile">Profile details / passport did not match actual person</option>
<option value="contact">Invalid phone number / unreachable</option>
<option value="other">Other compliance violations</option>
</select>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Additional context details</label>
<textarea
rows="3"
value={reportDetails}
onChange={(e) => setReportDetails(e.target.value)}
placeholder="Provide detailed specifics of what happened..."
className="w-full p-2.5 border border-slate-300 rounded-xl text-xs focus:outline-none"
/>
</div>
</div>
<div className="flex justify-end space-x-2 pt-2 text-xs font-bold">
<button
type="button"
onClick={() => setShowReportModal(false)}
className="px-4 py-2 border border-slate-200 hover:bg-slate-50 rounded-lg"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-rose-600 text-white hover:bg-rose-700 rounded-lg shadow-sm"
>
Submit Compliance Report
</button>
</div>
</form>
</div>
)}
{/* Trust Review Modal Overlay */}
{showReviewModal && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">
<form onSubmit={handleReviewSubmit} className="bg-white rounded-3xl w-full max-w-md p-6 relative animate-zoom-in space-y-4 border border-slate-200 shadow-2xl">
<button
type="button"
onClick={() => setShowReviewModal(false)}
className="absolute top-4 right-4 text-slate-400 hover:text-slate-600"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-center space-x-2 text-[#185FA5]">
<Star className="w-5 h-5 text-amber-500 fill-amber-500" />
<h4 className="font-black text-sm uppercase">Submit a Verified Performance Review</h4>
</div>
<p className="text-xs text-slate-500 leading-relaxed">
Your feedback helps other sponsors make secure and direct hiring choices.
</p>
<div className="space-y-3">
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">Star Rating</label>
<div className="flex items-center space-x-1.5">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
onClick={() => setNewRating(star)}
className="p-1 hover:scale-110 transition-transform"
>
<Star
className={`w-8 h-8 ${star <= newRating ? 'text-amber-500 fill-amber-500' : 'text-slate-200'}`}
/>
</button>
))}
</div>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Share Your Experience</label>
<textarea
rows="3"
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="Tell other sponsors about childcare skills, cleaning, cooking style, driving safety, etc..."
className="w-full p-2.5 border border-slate-300 rounded-xl text-xs focus:outline-none"
/>
</div>
</div>
<div className="flex justify-end space-x-2 pt-2 text-xs font-bold">
<button
type="button"
onClick={() => setShowReviewModal(false)}
className="px-4 py-2 border border-slate-200 hover:bg-slate-50 rounded-lg"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-[#185FA5] text-white hover:bg-[#144f8a] rounded-lg shadow-sm"
>
Post Trust Review
</button>
</div>
</form>
</div>
)}
</EmployerLayout> </EmployerLayout>
); );
} }

View File

@ -27,6 +27,15 @@
// Unprotected Employer Mobile Auth Endpoints // Unprotected Employer Mobile Auth Endpoints
Route::post('/employers/register', [EmployerAuthController::class, 'register']); Route::post('/employers/register', [EmployerAuthController::class, 'register']);
Route::post('/sponsors/register', [EmployerAuthController::class, 'register']);
Route::post('/employers/verify', [EmployerAuthController::class, 'verify']);
Route::post('/sponsors/verify', [EmployerAuthController::class, 'verify']);
Route::post('/employers/payment', [EmployerAuthController::class, 'payment']);
Route::post('/sponsors/payment', [EmployerAuthController::class, 'payment']);
Route::post('/employers/password', [EmployerAuthController::class, 'password']);
Route::post('/sponsors/password', [EmployerAuthController::class, 'password']);
Route::get('/employers/plans', [EmployerAuthController::class, 'plans']);
Route::get('/sponsors/plans', [EmployerAuthController::class, 'plans']);
Route::post('/employers/login', [EmployerAuthController::class, 'login']); Route::post('/employers/login', [EmployerAuthController::class, 'login']);
// Protected Worker Mobile Endpoints (Token Authenticated via Bearer Token) // Protected Worker Mobile Endpoints (Token Authenticated via Bearer Token)

View File

@ -1,6 +1,7 @@
<?php <?php
use App\Http\Controllers\Admin\AdminAuthController; use App\Http\Controllers\Admin\AdminAuthController;
use App\Http\Controllers\Admin\SponsorController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Inertia\Inertia; use Inertia\Inertia;
@ -25,12 +26,19 @@
Route::get('/workers', [\App\Http\Controllers\Admin\WorkerController::class, 'index'])->name('admin.workers'); Route::get('/workers', [\App\Http\Controllers\Admin\WorkerController::class, 'index'])->name('admin.workers');
Route::post('/workers/{worker}/toggle-status', [\App\Http\Controllers\Admin\WorkerController::class, 'toggleStatus'])->name('admin.workers.toggle-status'); Route::post('/workers/{worker}/toggle-status', [\App\Http\Controllers\Admin\WorkerController::class, 'toggleStatus'])->name('admin.workers.toggle-status');
Route::post('/workers/{worker}/availability-override', [\App\Http\Controllers\Admin\WorkerController::class, 'availabilityOverride'])->name('admin.workers.availability-override');
Route::post('/workers/{worker}/flag-fraud', [\App\Http\Controllers\Admin\WorkerController::class, 'flagFraud'])->name('admin.workers.flag-fraud');
Route::post('/workers/{worker}/update-profile', [\App\Http\Controllers\Admin\WorkerController::class, 'updateProfile'])->name('admin.workers.update-profile');
Route::get('/workers/verifications', [\App\Http\Controllers\Admin\WorkerVerificationController::class, 'index'])->name('admin.workers.verifications'); Route::get('/workers/verifications', [\App\Http\Controllers\Admin\WorkerVerificationController::class, 'index'])->name('admin.workers.verifications');
Route::post('/workers/{worker}/verify', [\App\Http\Controllers\Admin\WorkerVerificationController::class, 'verify'])->name('admin.workers.verify'); Route::post('/workers/{worker}/verify', [\App\Http\Controllers\Admin\WorkerVerificationController::class, 'verify'])->name('admin.workers.verify');
Route::get('/employers', function () { Route::get('/employers', [SponsorController::class, 'index'])->name('admin.employers');
return Inertia::render('Admin/Employers/Index'); Route::post('/employers/{id}/verify', [SponsorController::class, 'verify'])->name('admin.employers.verify');
})->name('admin.employers'); Route::post('/employers/{id}/suspend', [SponsorController::class, 'suspend'])->name('admin.employers.suspend');
Route::post('/employers/{id}/activate', [SponsorController::class, 'activate'])->name('admin.employers.activate');
Route::post('/employers/{id}/update', [SponsorController::class, 'update'])->name('admin.employers.update');
Route::delete('/employers/{id}', [SponsorController::class, 'delete'])->name('admin.employers.delete');
Route::get('/subscriptions', function () { Route::get('/subscriptions', function () {
return Inertia::render('Admin/Subscriptions/Index'); return Inertia::render('Admin/Subscriptions/Index');
@ -40,6 +48,8 @@
return Inertia::render('Admin/Payments/Index'); return Inertia::render('Admin/Payments/Index');
})->name('admin.payments'); })->name('admin.payments');
Route::post('/payments/{id}/refund', [\App\Http\Controllers\Admin\AdminExtraController::class, 'refundPayment'])->name('admin.payments.refund');
Route::get('/master-data/categories', function () { Route::get('/master-data/categories', function () {
return Inertia::render('Admin/MasterData/WorkerCategories'); return Inertia::render('Admin/MasterData/WorkerCategories');
})->name('admin.categories'); })->name('admin.categories');
@ -47,6 +57,20 @@
Route::get('/announcements', function () { Route::get('/announcements', function () {
return Inertia::render('Admin/Announcements/Index'); return Inertia::render('Admin/Announcements/Index');
})->name('admin.announcements'); })->name('admin.announcements');
// New Modules Added
Route::get('/safety', [\App\Http\Controllers\Admin\AdminExtraController::class, 'safety'])->name('admin.safety');
Route::post('/safety/reports/{id}/resolve', [\App\Http\Controllers\Admin\AdminExtraController::class, 'resolveSafetyReport'])->name('admin.safety.resolve-report');
Route::get('/disputes', [\App\Http\Controllers\Admin\AdminExtraController::class, 'disputes'])->name('admin.disputes');
Route::post('/disputes/{id}/resolve', [\App\Http\Controllers\Admin\AdminExtraController::class, 'resolveDispute'])->name('admin.disputes.resolve');
Route::post('/disputes/{id}/add-note', [\App\Http\Controllers\Admin\AdminExtraController::class, 'addDisputeNote'])->name('admin.disputes.add-note');
Route::get('/notifications', [\App\Http\Controllers\Admin\AdminExtraController::class, 'notifications'])->name('admin.notifications');
Route::post('/notifications/broadcast', [\App\Http\Controllers\Admin\AdminExtraController::class, 'broadcastNotification'])->name('admin.notifications.broadcast');
Route::get('/analytics', [\App\Http\Controllers\Admin\AdminExtraController::class, 'analytics'])->name('admin.analytics');
Route::get('/audit-logs', [\App\Http\Controllers\Admin\AdminExtraController::class, 'auditLogs'])->name('admin.audit-logs');
}); });
// Employer Auth Routes // Employer Auth Routes
@ -57,6 +81,8 @@
Route::get('/employer/verify-email', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showVerifyEmail'])->name('employer.verify-email'); Route::get('/employer/verify-email', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showVerifyEmail'])->name('employer.verify-email');
Route::post('/employer/verify-email', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'verifyEmail'])->name('employer.verify-email.submit')->middleware('throttle:5,1'); Route::post('/employer/verify-email', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'verifyEmail'])->name('employer.verify-email.submit')->middleware('throttle:5,1');
Route::post('/employer/resend-otp', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'resendOtp'])->name('employer.resend-otp')->middleware('throttle:3,1'); Route::post('/employer/resend-otp', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'resendOtp'])->name('employer.resend-otp')->middleware('throttle:3,1');
Route::get('/employer/register-payment', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showRegisterPayment'])->name('employer.register-payment');
Route::post('/employer/register-payment', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'storeRegisterPayment'])->name('employer.register-payment.submit');
Route::get('/employer/create-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showCreatePassword'])->name('employer.create-password'); Route::get('/employer/create-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showCreatePassword'])->name('employer.create-password');
Route::post('/employer/create-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'createPassword'])->name('employer.create-password.submit'); Route::post('/employer/create-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'createPassword'])->name('employer.create-password.submit');
Route::post('/employer/logout', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'logout'])->name('employer.logout'); Route::post('/employer/logout', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'logout'])->name('employer.logout');

View File

@ -20,5 +20,11 @@ export default defineConfig({
watch: { watch: {
ignored: ['**/storage/framework/views/**'], ignored: ['**/storage/framework/views/**'],
}, },
host: '0.0.0.0',
port: 5173,
cors: true,
hmr: {
host: '192.168.0.228',
},
}, },
}); });