Compare commits
No commits in common. "4a0753652f8bb180008ad95df5b0f6a24ffc3845" and "7aecb53065eeb1a6881ee2f6eec517b89ef630b5" have entirely different histories.
4a0753652f
...
7aecb53065
@ -1,446 +0,0 @@
|
|||||||
<?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,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -13,45 +13,15 @@ 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' => $totalWorkers,
|
'total_workers' => 1420,
|
||||||
'active_workers' => $activeWorkers,
|
'pending_verifications' => 0, // No longer pending as they are auto-approved
|
||||||
'inactive_workers' => $inactiveWorkers,
|
'active_subscriptions' => 312,
|
||||||
'total_employers' => $totalSponsors, // Kept key name to avoid breaking frontend destructuring
|
'revenue_this_month_aed' => 48500,
|
||||||
'active_subscriptions' => $activeSubs,
|
'new_employers_this_week' => 28,
|
||||||
'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
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Process verifications automatically or fetched dynamically
|
// This section now tracks recently auto-processed verifications instead of pending ones
|
||||||
$recentVerifications = [
|
$recentVerifications = [
|
||||||
[
|
[
|
||||||
'id' => 101,
|
'id' => 101,
|
||||||
@ -64,41 +34,50 @@ public function index()
|
|||||||
[
|
[
|
||||||
'id' => 102,
|
'id' => 102,
|
||||||
'name' => 'Ahmed Mansoor',
|
'name' => 'Ahmed Mansoor',
|
||||||
'type' => 'Sponsor',
|
'type' => 'Employer',
|
||||||
'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
|
$recentSubscriptions = [
|
||||||
$recentSponsorsWithPlans = \App\Models\Sponsor::whereNotNull('subscription_plan')
|
[
|
||||||
->latest('created_at')
|
'id' => 501,
|
||||||
->take(5)
|
'employer_name' => 'Hassan Al Hosani',
|
||||||
->get();
|
'plan_name' => 'Premium Employer Plan',
|
||||||
|
'amount_aed' => 499,
|
||||||
$recentSubscriptions = [];
|
'subscribed_at' => 'Today, 10:30 AM',
|
||||||
foreach ($recentSponsorsWithPlans as $sp) {
|
],
|
||||||
$recentSubscriptions[] = [
|
[
|
||||||
'id' => $sp->id,
|
'id' => 502,
|
||||||
'employer_name' => $sp->full_name,
|
'employer_name' => 'Sarah Sterling',
|
||||||
'plan_name' => ucfirst($sp->subscription_plan) . ' Pass',
|
'plan_name' => 'Standard Search Plan',
|
||||||
'amount_aed' => $sp->subscription_plan === 'vip' ? 499 : ($sp->subscription_plan === 'premium' ? 199 : 99),
|
'amount_aed' => 199,
|
||||||
'subscribed_at' => $sp->created_at->diffForHumans(),
|
'subscribed_at' => 'Yesterday',
|
||||||
];
|
],
|
||||||
}
|
[
|
||||||
|
'id' => 503,
|
||||||
if (count($recentSubscriptions) === 0) {
|
'employer_name' => 'Rashid Al Nuaimi',
|
||||||
$recentSubscriptions = [
|
'plan_name' => 'Enterprise VIP',
|
||||||
[
|
'amount_aed' => 1299,
|
||||||
'id' => 501,
|
'subscribed_at' => 'May 12, 2026',
|
||||||
'employer_name' => 'Ahmed Malik',
|
],
|
||||||
'plan_name' => 'Premium Pass',
|
[
|
||||||
'amount_aed' => 499,
|
'id' => 504,
|
||||||
'subscribed_at' => 'Today, 10:30 AM',
|
'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,
|
||||||
|
|||||||
@ -1,132 +0,0 @@
|
|||||||
<?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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -78,71 +78,12 @@ 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,suspended,banned',
|
'status' => 'required|in:active,inactive',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 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'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,9 +94,11 @@ 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()) {
|
||||||
@ -108,188 +110,38 @@ public function register(Request $request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$otp = '111111'; // default development OTP
|
// Generate API token
|
||||||
if (app()->environment('production')) {
|
$apiToken = Str::random(80);
|
||||||
$otp = strval(rand(100000, 999999));
|
|
||||||
}
|
|
||||||
|
|
||||||
\Illuminate\Support\Facades\Cache::put('employer_otp_' . $request->email, [
|
$user = null;
|
||||||
'code' => $otp,
|
\Illuminate\Support\Facades\DB::transaction(function () use ($request, $apiToken, &$user) {
|
||||||
'expires_at' => now()->addMinutes(10)
|
$user = User::create([
|
||||||
], 600);
|
'name' => $request->name,
|
||||||
|
'email' => $request->email,
|
||||||
// Create inactive User
|
'password' => Hash::make($request->password),
|
||||||
$user = User::create([
|
'role' => 'employer',
|
||||||
'name' => $request->name,
|
'subscription_status' => 'active', // Auto-active
|
||||||
'email' => $request->email,
|
|
||||||
'password' => Hash::make(Str::random(16)), // temporary password
|
|
||||||
'role' => 'employer',
|
|
||||||
'subscription_status' => 'none',
|
|
||||||
'subscription_expires_at' => null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create pending Profile
|
|
||||||
EmployerProfile::create([
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'company_name' => $request->name . ' Household',
|
|
||||||
'phone' => $request->phone,
|
|
||||||
'country' => 'United Arab Emirates',
|
|
||||||
'verification_status' => 'pending',
|
|
||||||
'language' => 'English',
|
|
||||||
'notifications' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create unverified Sponsor
|
|
||||||
\App\Models\Sponsor::create([
|
|
||||||
'full_name' => $request->name,
|
|
||||||
'email' => $request->email,
|
|
||||||
'mobile' => $request->phone,
|
|
||||||
'password' => $user->password,
|
|
||||||
'country_code' => 'AE',
|
|
||||||
'subscription_status' => 'none',
|
|
||||||
'subscription_plan' => null,
|
|
||||||
'subscription_start_date' => null,
|
|
||||||
'subscription_end_date' => null,
|
|
||||||
'payment_status' => 'unpaid',
|
|
||||||
'is_verified' => false,
|
|
||||||
'otp_verified_at' => null,
|
|
||||||
'status' => 'inactive',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Try sending email
|
|
||||||
try {
|
|
||||||
\Illuminate\Support\Facades\Mail::to($request->email)->send(new \App\Mail\EmployerOtpMail(
|
|
||||||
$otp,
|
|
||||||
$request->name . ' Household',
|
|
||||||
$request->name
|
|
||||||
));
|
|
||||||
} catch (\Exception $mailEx) {
|
|
||||||
logger()->error('API Sponsor Registration Mail Failure: ' . $mailEx->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'message' => 'Verification code sent to email successfully.',
|
|
||||||
'email' => $request->email
|
|
||||||
], 201);
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
logger()->error('API Sponsor Registration Failure: ' . $e->getMessage());
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Registration failed. Please try again.',
|
|
||||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function verify(Request $request)
|
|
||||||
{
|
|
||||||
$validator = Validator::make($request->all(), [
|
|
||||||
'email' => 'required|email',
|
|
||||||
'otp' => 'required|string|size:6',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($validator->fails()) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'errors' => $validator->errors()
|
|
||||||
], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
$cachedData = \Illuminate\Support\Facades\Cache::get('employer_otp_' . $request->email);
|
|
||||||
|
|
||||||
if (!$cachedData || $cachedData['code'] !== $request->otp || now()->gt($cachedData['expires_at'])) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Invalid or expired verification code.'
|
|
||||||
], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Update Sponsor verification status
|
|
||||||
$sponsor = \App\Models\Sponsor::where('email', $request->email)->first();
|
|
||||||
if ($sponsor) {
|
|
||||||
$sponsor->update([
|
|
||||||
'is_verified' => true,
|
|
||||||
'otp_verified_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear Cache
|
|
||||||
\Illuminate\Support\Facades\Cache::forget('employer_otp_' . $request->email);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'message' => 'Email verified successfully. Proceed to payment selection.',
|
|
||||||
'email' => $request->email
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Verification failed. Please try again.'
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function payment(Request $request)
|
|
||||||
{
|
|
||||||
$validator = Validator::make($request->all(), [
|
|
||||||
'email' => 'required|email',
|
|
||||||
'plan_id' => 'required|string',
|
|
||||||
'amount_aed' => 'required|numeric',
|
|
||||||
'paytabs_transaction_id' => 'required|string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($validator->fails()) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'errors' => $validator->errors()
|
|
||||||
], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$user = User::where('email', $request->email)->first();
|
|
||||||
$sponsor = \App\Models\Sponsor::where('email', $request->email)->first();
|
|
||||||
|
|
||||||
if (!$user || !$sponsor) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'User account not found.'
|
|
||||||
], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$sponsor->is_verified) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Please verify your email before initiating payment.'
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
\Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor) {
|
|
||||||
// Update User subscription
|
|
||||||
$user->update([
|
|
||||||
'subscription_status' => 'active',
|
|
||||||
'subscription_expires_at' => now()->addDays(30),
|
'subscription_expires_at' => now()->addDays(30),
|
||||||
|
'api_token' => $apiToken,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Update Sponsor status
|
EmployerProfile::create([
|
||||||
$sponsor->update([
|
'user_id' => $user->id,
|
||||||
'subscription_status' => 'active',
|
'company_name' => $request->company_name,
|
||||||
'subscription_plan' => $request->plan_id,
|
'phone' => $request->phone,
|
||||||
'subscription_start_date' => now(),
|
'country' => 'United Arab Emirates',
|
||||||
'subscription_end_date' => now()->addDays(30),
|
'verification_status' => 'approved', // Auto-approved
|
||||||
'payment_status' => 'paid',
|
'language' => 'English',
|
||||||
|
'notifications' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Insert into subscriptions table
|
// Create active premium subscription
|
||||||
\Illuminate\Support\Facades\DB::table('subscriptions')->insert([
|
\Illuminate\Support\Facades\DB::table('subscriptions')->insert([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'plan_id' => $request->plan_id,
|
'plan_id' => 'premium',
|
||||||
'amount_aed' => $request->amount_aed,
|
'amount_aed' => 199.00,
|
||||||
'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(),
|
||||||
@ -298,126 +150,21 @@ public function payment(Request $request)
|
|||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Subscription payment processed and activated successfully.',
|
'message' => 'Employer registered successfully.',
|
||||||
'email' => $request->email
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Payment processing failed. Please try again.'
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function password(Request $request)
|
|
||||||
{
|
|
||||||
$validator = Validator::make($request->all(), [
|
|
||||||
'email' => 'required|email',
|
|
||||||
'password' => 'required|string|min:8|confirmed',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($validator->fails()) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'errors' => $validator->errors()
|
|
||||||
], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$user = User::where('email', $request->email)->first();
|
|
||||||
$sponsor = \App\Models\Sponsor::where('email', $request->email)->first();
|
|
||||||
|
|
||||||
if (!$user || !$sponsor) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'User account not found.'
|
|
||||||
], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($sponsor->payment_status !== 'paid') {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Please complete subscription payment first.'
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate API Bearer token
|
|
||||||
$apiToken = Str::random(80);
|
|
||||||
|
|
||||||
\Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor, $apiToken) {
|
|
||||||
$hashedPassword = Hash::make($request->password);
|
|
||||||
|
|
||||||
$user->update([
|
|
||||||
'password' => $hashedPassword,
|
|
||||||
'api_token' => $apiToken,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$sponsor->update([
|
|
||||||
'password' => $hashedPassword,
|
|
||||||
'status' => 'active',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Approve profile verification
|
|
||||||
$profile = EmployerProfile::where('user_id', $user->id)->first();
|
|
||||||
if ($profile) {
|
|
||||||
$profile->update([
|
|
||||||
'verification_status' => 'approved'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'message' => 'Password created successfully. Registration finalized.',
|
|
||||||
'data' => [
|
'data' => [
|
||||||
'sponsor' => $user->load('employerProfile'),
|
'employer' => $user->load('employerProfile'),
|
||||||
'token' => $apiToken
|
'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' => 'Failed to set password. Please try again.'
|
'message' => 'An error occurred during registration. Please try again.',
|
||||||
|
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||||
], 500);
|
], 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,
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,406 +5,13 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -14,37 +14,11 @@ 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' => $content,
|
'content' => $ann->body,
|
||||||
'audience' => 'Charity',
|
'audience' => in_array($ann->type, ['Shortlisted', 'Selected Candidates']) ? $ann->type : 'Selected Candidates',
|
||||||
'isCharity' => $isCharity,
|
|
||||||
'charityDetails' => $charityDetails,
|
|
||||||
'created_at' => $ann->created_at->diffForHumans(),
|
'created_at' => $ann->created_at->diffForHumans(),
|
||||||
];
|
];
|
||||||
})->toArray();
|
})->toArray();
|
||||||
@ -59,33 +33,15 @@ 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',
|
||||||
'provided_items' => 'required|string',
|
'audience' => 'required|string|in:Shortlisted,Selected Candidates',
|
||||||
'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' => $body,
|
'body' => $request->content,
|
||||||
'type' => 'Charity',
|
'type' => $request->audience,
|
||||||
'employer_id' => $sessId,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return back()->with('success', 'Charity Event posted successfully.');
|
return back()->with('success', 'Announcement posted successfully.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,40 +46,19 @@ public function index(Request $request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get subscription expires at date
|
// Get subscription expires at date
|
||||||
$sub = \Illuminate\Support\Facades\DB::table('subscriptions')->where('user_id', $user->id)->where('status', 'active')->latest('id')->first();
|
$sub = $user->subscription()->where('status', 'active')->latest()->first();
|
||||||
$expiresAt = $sub ? Carbon::parse($sub->expires_at) : now()->addDays(24);
|
$expiresAt = $sub ? $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
|
||||||
@ -124,85 +103,17 @@ public function index(Request $request)
|
|||||||
];
|
];
|
||||||
})->filter()->sortByDesc('timestamp')->values()->toArray();
|
})->filter()->sortByDesc('timestamp')->values()->toArray();
|
||||||
|
|
||||||
// 4. Charity Events
|
// 4. Announcements
|
||||||
$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' => $body,
|
'body' => $ann->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,
|
||||||
@ -214,8 +125,6 @@ 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,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,7 +81,7 @@ public function register(Request $request)
|
|||||||
{
|
{
|
||||||
// 1. Validation
|
// 1. Validation
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'company_name' => 'nullable|string|max:255',
|
'company_name' => 'required|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 ?: ($request->name . ' Household'),
|
'company_name' => $request->company_name,
|
||||||
'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 or 111111 in local environment for automated testing)
|
// Hash comparison (allow 000000 in local environment for automated testing)
|
||||||
$isLocalDebug = app()->environment('local') && ($request->otp === '000000' || $request->otp === '111111');
|
$isLocalDebug = app()->environment('local') && $request->otp === '000000';
|
||||||
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.register-payment')
|
return redirect()->route('employer.create-password')
|
||||||
->with('success', 'Email verified successfully! Please choose a subscription plan to continue.');
|
->with('success', 'Email verified successfully. Please set your password.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function resendOtp(Request $request)
|
public function resendOtp(Request $request)
|
||||||
@ -243,72 +243,13 @@ public function resendOtp(Request $request)
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function showRegisterPayment()
|
public function showCreatePassword()
|
||||||
{
|
{
|
||||||
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');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,13 +270,12 @@ 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') || !session()->has('pending_employer_payment')) {
|
if (!session()->has('pending_employer_registration') || !session('employer_email_verified')) {
|
||||||
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([
|
||||||
@ -343,7 +283,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',
|
'subscription_status' => 'active', // Auto-active for direct entry
|
||||||
'subscription_expires_at' => now()->addDays(30),
|
'subscription_expires_at' => now()->addDays(30),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -358,32 +298,14 @@ public function createPassword(Request $request)
|
|||||||
'notifications' => true,
|
'notifications' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Create Sponsor
|
// Create a default active premium subscription record
|
||||||
\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' => $payment['plan_id'],
|
'plan_id' => 'premium',
|
||||||
'amount_aed' => $payment['amount_aed'],
|
'amount_aed' => 499.00,
|
||||||
'starts_at' => now(),
|
'starts_at' => now(),
|
||||||
'expires_at' => now()->addDays(30),
|
'expires_at' => now()->addDays(30),
|
||||||
'paytabs_transaction_id' => $payment['paytabs_transaction_id'],
|
'paytabs_transaction_id' => 'TXN-' . strtoupper(bin2hex(random_bytes(6))),
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
@ -408,8 +330,7 @@ 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')
|
||||||
@ -423,7 +344,6 @@ 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.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,84 +42,41 @@ 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,
|
||||||
'photo' => $photo,
|
'category' => $w->category ? $w->category->name : 'General Helper',
|
||||||
'emirates_id_status' => $emiratesIdStatus,
|
'skills' => $w->skills->pluck('name')->toArray(),
|
||||||
'category' => $w->category ? $w->category->name : 'Domestic Worker',
|
'availability' => $w->availability,
|
||||||
'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' => $langs,
|
'languages' => ['English', 'Arabic'],
|
||||||
'age' => $w->age,
|
'age' => $w->age,
|
||||||
'verified' => (bool)$w->verified,
|
'verified' => (bool)$w->verified,
|
||||||
'preferred_job_type' => $preferredJobType,
|
'status' => $w->status, // Map worker status dynamically (e.g. active, Hired)
|
||||||
'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', 'Active', 'Hidden', 'Hired'],
|
'availabilities' => ['All Availabilities', 'Immediate', '1 Week', '2 Weeks', '1 Month'],
|
||||||
'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', [
|
||||||
@ -133,96 +90,24 @@ 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,
|
||||||
'photo' => $photo,
|
'category' => $w->category ? $w->category->name : 'General Helper',
|
||||||
'emirates_id_status' => $emiratesIdStatus,
|
'skills' => $w->skills->pluck('name')->toArray(),
|
||||||
'category' => $w->category ? $w->category->name : 'Domestic Worker',
|
'availability' => $w->availability,
|
||||||
'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' => $langs,
|
'languages' => ['English', 'Arabic'],
|
||||||
'age' => $w->age,
|
'age' => $w->age,
|
||||||
'verified' => (bool)$w->verified,
|
'verified' => (bool)$w->verified,
|
||||||
'preferred_job_type' => $preferredJobType,
|
'status' => $w->status,
|
||||||
'bio' => $w->bio,
|
'bio' => $w->bio,
|
||||||
'rating' => $rating,
|
'passport_status' => 'OCR Verified',
|
||||||
'reviews_count' => $reviewsCount,
|
'visa_status' => 'Transferable',
|
||||||
'reviews' => $reviews,
|
|
||||||
'similar_workers' => $similarWorkers,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return Inertia::render('Employer/Workers/Show', [
|
return Inertia::render('Employer/Workers/Show', [
|
||||||
@ -230,6 +115,13 @@ 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([
|
||||||
@ -242,8 +134,10 @@ 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,
|
||||||
|
|||||||
@ -21,8 +21,8 @@ public function handle(Request $request, Closure $next): Response
|
|||||||
return redirect()->route('employer.login');
|
return redirect()->route('employer.login');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($subStatus !== 'active' && !$request->routeIs('employer.subscription')) {
|
if ($subStatus === 'expired' && !$request->routeIs('employer.subscription')) {
|
||||||
return redirect()->route('employer.subscription')->with('error', 'Please purchase a subscription to access the portal.');
|
return redirect()->route('employer.subscription')->with('error', 'Your subscription has expired.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
|
|||||||
@ -1,51 +0,0 @@
|
|||||||
<?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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
<?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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -84,120 +84,6 @@ 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([
|
||||||
[
|
[
|
||||||
|
|||||||
@ -13,7 +13,7 @@ class SkillSeeder extends Seeder
|
|||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$skills = [
|
$skills = [
|
||||||
'cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'
|
'Wiring', 'Pipe Fitting', 'Tiling', 'Driving', 'Supervision', 'Cleaning', 'Masonry', 'Plumbing'
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($skills as $skill) {
|
foreach ($skills as $skill) {
|
||||||
|
|||||||
@ -574,8 +574,8 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Employer/Auth"
|
"Employer/Auth"
|
||||||
],
|
],
|
||||||
"summary": "Register Sponsor (Alias)",
|
"summary": "Register Employer",
|
||||||
"description": "Legacy route matching the simplified sponsor registration (Name, Email, Phone).",
|
"description": "Registers a new employer profile and triggers premium access instantly.",
|
||||||
"security": [],
|
"security": [],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@ -584,25 +584,37 @@
|
|||||||
"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 Bin Ahmed",
|
"example": "Ahmad"
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -611,354 +623,7 @@
|
|||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"201": {
|
"201": {
|
||||||
"description": "Sponsor registered successfully."
|
"description": "Employer 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."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1112,18 +777,17 @@
|
|||||||
"description": "Conversation started successfully."
|
"description": "Conversation started successfully."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"/workers/announcements": {
|
"/workers/announcements": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Worker/CharityEvents"
|
"Worker/Announcements"
|
||||||
],
|
],
|
||||||
"summary": "Get Charity Events (Worker)",
|
"summary": "Get Announcements (Worker)",
|
||||||
"description": "Allows workers to retrieve the list of active charity events, food drives, and medical support programs scheduled in Dubai.",
|
"description": "Allows workers to retrieve the list of active announcements posted by employers or the system admin.",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "List of charity events retrieved successfully."
|
"description": "List of announcements retrieved successfully."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1131,22 +795,22 @@
|
|||||||
"/employers/announcements": {
|
"/employers/announcements": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Employer/CharityEvents"
|
"Employer/Announcements"
|
||||||
],
|
],
|
||||||
"summary": "Get Posted Charity Events (Employer)",
|
"summary": "Get Posted Announcements (Employer)",
|
||||||
"description": "Allows employers/sponsors to retrieve the list of charity events they have published.",
|
"description": "Allows employers to retrieve the list of announcements they have created.",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "List of posted charity events retrieved successfully."
|
"description": "List of posted announcements retrieved successfully."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Employer/CharityEvents"
|
"Employer/Announcements"
|
||||||
],
|
],
|
||||||
"summary": "Publish Charity Event (Employer)",
|
"summary": "Create Announcement (Employer)",
|
||||||
"description": "Allows employers/sponsors to publish a new community charity event or drive to all workers in Dubai, triggering instant push notifications and morning-of reminders.",
|
"description": "Allows employers to broadcast a new announcement to all workers.",
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
"content": {
|
"content": {
|
||||||
@ -1155,43 +819,25 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"title",
|
"title",
|
||||||
"content",
|
"body"
|
||||||
"provided_items",
|
|
||||||
"event_date",
|
|
||||||
"event_time",
|
|
||||||
"location_details",
|
|
||||||
"location_pin"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"title": {
|
"title": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Free Dental Checkup Camp"
|
"example": "Weekend Maintenance"
|
||||||
},
|
},
|
||||||
"content": {
|
"body": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Emirates Charity is providing free professional dental screening, cleanings, and educational packages for domestic helpers."
|
"example": "Our services will experience brief downtime this Sunday."
|
||||||
},
|
},
|
||||||
"provided_items": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Free Dental screening, cleanings, and wellness kits"
|
"enum": [
|
||||||
},
|
"info",
|
||||||
"event_date": {
|
"warning",
|
||||||
"type": "string",
|
"success"
|
||||||
"format": "date",
|
],
|
||||||
"example": "2026-06-15"
|
"example": "warning"
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1200,7 +846,7 @@
|
|||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"201": {
|
"201": {
|
||||||
"description": "Charity event created and notifications scheduled successfully."
|
"description": "Announcement created successfully."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1208,10 +854,10 @@
|
|||||||
"/employers/announcements/{id}": {
|
"/employers/announcements/{id}": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Employer/CharityEvents"
|
"Employer/Announcements"
|
||||||
],
|
],
|
||||||
"summary": "Delete Charity Event (Employer)",
|
"summary": "Delete Announcement (Employer)",
|
||||||
"description": "Allows employers/sponsors to delete a previously published charity event.",
|
"description": "Allows employers to delete a previously created announcement.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
@ -1224,10 +870,9 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Charity event deleted successfully."
|
"description": "Announcement deleted successfully."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"/employers/profile": {
|
"/employers/profile": {
|
||||||
"get": {
|
"get": {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
@ -11,13 +11,7 @@ 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';
|
||||||
|
|
||||||
@ -28,18 +22,13 @@ export default function AdminLayout({ children, title = 'Dashboard' }) {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ name: 'Dashboard Overview', href: '/admin/dashboard', icon: LayoutDashboard },
|
{ name: 'Dashboard', href: '/admin/dashboard', icon: LayoutDashboard },
|
||||||
{ name: 'Worker Profiles', href: '/admin/workers', icon: Users },
|
{ name: 'Workers', href: '/admin/workers', icon: Users },
|
||||||
{ name: 'Sponsor Registry', href: '/admin/employers', icon: Briefcase },
|
{ name: 'Employers', href: '/admin/employers', icon: Briefcase },
|
||||||
{ name: 'OCR Verification', href: '/admin/workers/verifications', icon: ShieldCheck },
|
{ name: 'Subscriptions', href: '/admin/subscriptions', icon: CreditCard },
|
||||||
{ name: 'Safety & Moderation', href: '/admin/safety', icon: ShieldAlert },
|
{ name: 'Payments', href: '/admin/payments', icon: BadgeDollarSign },
|
||||||
{ 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: 'Master Categories', href: '/admin/master-data/categories', icon: Settings },
|
{ name: 'Worker Categories', href: '/admin/master-data/categories', icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
const getInitials = (name) => {
|
const getInitials = (name) => {
|
||||||
|
|||||||
@ -15,8 +15,7 @@ import {
|
|||||||
ArrowRight,
|
ArrowRight,
|
||||||
UserCheck,
|
UserCheck,
|
||||||
Megaphone,
|
Megaphone,
|
||||||
Briefcase,
|
Briefcase
|
||||||
Heart
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -60,10 +59,11 @@ 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: 'Charity Events', href: '/employer/announcements', icon: Heart },
|
{ name: 'Announcements', href: '/employer/announcements', icon: Megaphone },
|
||||||
{ 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 },
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,323 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,19 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import React 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,
|
||||||
ShieldCheck,
|
ShieldAlert,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
BadgeDollarSign,
|
BadgeDollarSign,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
ArrowRight,
|
ArrowRight
|
||||||
ArrowUpRight,
|
|
||||||
MessageSquare,
|
|
||||||
TrendingUp,
|
|
||||||
Percent,
|
|
||||||
PieChart,
|
|
||||||
Sparkles
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@ -25,380 +19,89 @@ 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 [activeTrendTab, setActiveTrendTab] = useState('All');
|
const handleApprove = (id) => {
|
||||||
const [selectedSlice, setSelectedSlice] = useState(null);
|
router.post('/admin/verifications/approve', { id });
|
||||||
|
};
|
||||||
|
|
||||||
// Mock trend monthly data for custom SVG Line chart
|
const handleReject = (id) => {
|
||||||
const trendData = [
|
router.post('/admin/verifications/reject', { id });
|
||||||
{ 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-6 mb-8 font-sans">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 mb-8 font-sans">
|
||||||
{/* Total Workers & active/inactive */}
|
{/* Total Workers */}
|
||||||
<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="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">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-black text-slate-400 uppercase tracking-wider">Workers Registered</span>
|
<span className="text-sm font-medium text-gray-500">Total Workers</span>
|
||||||
<div className="w-10 h-10 bg-teal-50 rounded-xl flex items-center justify-center shadow-inner">
|
<div className="w-10 h-10 bg-[#0F6E56]/10 rounded-lg 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-3xl font-black text-slate-900 tracking-tight">
|
<h3 className="text-2xl font-bold text-gray-900 tracking-tight">
|
||||||
{stats?.total_workers?.toLocaleString() || 1420}
|
{stats?.total_workers?.toLocaleString() || 0}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center space-x-2 mt-2 text-xs font-bold">
|
<p className="text-xs text-emerald-600 font-medium mt-1">+12% from last month</p>
|
||||||
<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>
|
||||||
|
|
||||||
{/* Verification Stats */}
|
{/* Verified Today */}
|
||||||
<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="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">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-black text-slate-400 uppercase tracking-wider">Vetting & OCR Rate</span>
|
<span className="text-sm font-medium text-gray-500">Auto-Verified Today</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 relative shadow-inner">
|
||||||
<ShieldCheck className="w-5 h-5 text-emerald-600" />
|
<CheckCircle 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-3xl font-black text-slate-900 tracking-tight">
|
<h3 className="text-2xl font-bold text-gray-900 tracking-tight">
|
||||||
{stats?.verification_rate || 88.5}%
|
{stats?.active_subscriptions?.toLocaleString() || 0}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-slate-500 font-medium mt-2 flex items-center">
|
<p className="text-xs text-blue-600 font-medium mt-1">
|
||||||
<CheckCircle className="w-3.5 h-3.5 text-emerald-500 mr-1 inline" />
|
+{stats?.new_employers_this_week || 0} new this week
|
||||||
<span>1,256 Profiles Auto-OCR Verified</span>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chat-To-Hire and Hiring Conversion */}
|
{/* Monthly 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="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">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-black text-slate-400 uppercase tracking-wider">Hiring Conversion</span>
|
<span className="text-sm font-medium text-gray-500">Revenue This Month</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-3xl font-black text-slate-900 tracking-tight">
|
<h3 className="text-2xl font-bold text-gray-900 tracking-tight">
|
||||||
AED {stats?.revenue_this_month_aed?.toLocaleString() || '48,500'}
|
AED {stats?.revenue_this_month_aed?.toLocaleString() || 0}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-emerald-600 font-bold mt-2 flex items-center">
|
<p className="text-xs text-emerald-600 font-medium mt-1">Updated real-time</p>
|
||||||
<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">
|
||||||
@ -410,7 +113,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 OCR Vetting queue <ArrowRight className="w-3.5 h-3.5 ml-1" />
|
View audit logs <ArrowRight className="w-3.5 h-3.5 ml-1" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -462,13 +165,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 sponsor subscription activations and renewals</p>
|
<p className="text-xs text-gray-500 mt-0.5">Latest employer subscription activations and renewals</p>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/payments"
|
href="/admin/subscriptions"
|
||||||
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 payments <ArrowRight className="w-3.5 h-3.5 ml-1" />
|
View all <ArrowRight className="w-3.5 h-3.5 ml-1" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -476,7 +179,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">Sponsor</TableHead>
|
<TableHead className="font-semibold text-slate-600 text-xs h-10">Employer</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>
|
||||||
|
|||||||
@ -1,235 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,11 +1,11 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import { Head, router, Link } from '@inertiajs/react';
|
import { Head } from '@inertiajs/react';
|
||||||
import AdminLayout from '@/Layouts/AdminLayout';
|
import AdminLayout from '@/Layouts/AdminLayout';
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Filter,
|
Filter,
|
||||||
Download,
|
Download,
|
||||||
Users,
|
Briefcase,
|
||||||
Mail,
|
Mail,
|
||||||
MapPin,
|
MapPin,
|
||||||
Calendar,
|
Calendar,
|
||||||
@ -14,16 +14,7 @@ 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,
|
||||||
@ -46,179 +37,50 @@ 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 SponsorsIndex({ sponsors, filters = {} }) {
|
export default function EmployersIndex({ employers: initialEmployers }) {
|
||||||
const [searchTerm, setSearchTerm] = useState(filters.search || '');
|
const [searchTerm, setSearchTerm] = React.useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState(filters.status || 'All');
|
const [statusFilter, setStatusFilter] = React.useState('All');
|
||||||
const [locationFilter, setLocationFilter] = useState(filters.location || 'All');
|
const [locationFilter, setLocationFilter] = React.useState('All');
|
||||||
const [sortField, setSortField] = useState(filters.sort_field || 'created_at');
|
const [selectedEmployer, setSelectedEmployer] = React.useState(null);
|
||||||
const [sortOrder, setSortOrder] = useState(filters.sort_order || 'desc');
|
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
||||||
|
|
||||||
const [selectedSponsor, setSelectedSponsor] = useState(null);
|
const [employers, setEmployers] = React.useState(initialEmployers || [
|
||||||
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
{ 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 [isEditOpen, setIsEditOpen] = 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 [editForm, setEditForm] = useState({
|
{ 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' },
|
||||||
id: '',
|
{ 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' },
|
||||||
full_name: '',
|
{ 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' },
|
||||||
email: '',
|
]);
|
||||||
mobile: '',
|
|
||||||
city: '',
|
|
||||||
nationality: '',
|
|
||||||
address: '',
|
|
||||||
subscription_plan: 'premium',
|
|
||||||
subscription_status: 'active'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle search/filter queries
|
const filteredEmployers = React.useMemo(() => {
|
||||||
const triggerSearch = (searchVal, statusVal, locationVal, sortF, sortO) => {
|
return employers.filter(emp => {
|
||||||
router.get('/admin/employers', {
|
const matchesSearch = emp.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
search: searchVal,
|
emp.email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
status: statusVal,
|
const matchesStatus = statusFilter === 'All' || emp.status === statusFilter;
|
||||||
location: locationVal,
|
const matchesLocation = locationFilter === 'All' || emp.location === locationFilter;
|
||||||
sort_field: sortF,
|
return matchesSearch && matchesStatus && matchesLocation;
|
||||||
sort_order: sortO
|
|
||||||
}, {
|
|
||||||
preserveState: true,
|
|
||||||
preserveScroll: true,
|
|
||||||
replace: true
|
|
||||||
});
|
});
|
||||||
};
|
}, [employers, searchTerm, statusFilter, locationFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const delayDebounceFn = setTimeout(() => {
|
|
||||||
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) => {
|
const handleApprove = (id) => {
|
||||||
router.post(`/admin/employers/${id}/verify`, {}, {
|
setEmployers(employers.map(emp =>
|
||||||
onSuccess: () => {
|
emp.id === id ? { ...emp, status: 'Active' } : emp
|
||||||
setIsDetailsOpen(false);
|
));
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSuspend = (id) => {
|
const handleAction = (emp) => {
|
||||||
router.post(`/admin/employers/${id}/suspend`, {}, {
|
setSelectedEmployer(emp);
|
||||||
onSuccess: () => {
|
setIsDialogOpen(true);
|
||||||
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="Sponsor Registry & Moderation">
|
<AdminLayout title="Employer Management">
|
||||||
<Head title="Sponsor Management" />
|
<Head title="Employers" />
|
||||||
|
|
||||||
<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">
|
||||||
@ -226,8 +88,8 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
<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, email, or mobile..."
|
placeholder="Search by name or email..."
|
||||||
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"
|
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"
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -235,20 +97,20 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<select
|
<select
|
||||||
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"
|
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"
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onChange={e => handleFilterChange(e.target.value, locationFilter)}
|
onChange={e => setStatusFilter(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="All">All Statuses</option>
|
<option value="All">All Status</option>
|
||||||
<option value="Active">Active</option>
|
<option value="Active">Active</option>
|
||||||
<option value="Inactive">Inactive</option>
|
<option value="Pending">Pending</option>
|
||||||
<option value="Suspended">Suspended</option>
|
<option value="Suspended">Suspended</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
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"
|
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"
|
||||||
value={locationFilter}
|
value={locationFilter}
|
||||||
onChange={e => handleFilterChange(statusFilter, e.target.value)}
|
onChange={e => setLocationFilter(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>
|
||||||
@ -257,454 +119,190 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
</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>
|
||||||
|
|
||||||
{/* Dynamic Sponsors Table */}
|
{/* Employers 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 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 pl-8">Employer Info</TableHead>
|
||||||
Sponsor Info {sortField === 'full_name' && (sortOrder === 'desc' ? '↓' : '↑')}
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Location</TableHead>
|
||||||
</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Plan</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">Reg. Date</TableHead>
|
||||||
Location {sortField === 'city' && (sortOrder === 'desc' ? '↓' : '↑')}
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Status</TableHead>
|
||||||
</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>
|
||||||
{sponsors.data.length > 0 ? (
|
{filteredEmployers.map((emp) => (
|
||||||
sponsors.data.map((sponsor) => (
|
<TableRow key={emp.id} className="hover:bg-slate-50/50 transition-colors">
|
||||||
<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>
|
|
||||||
<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">{sponsor.email}</p>
|
|
||||||
<p className="text-[10px] text-slate-400 font-medium">{sponsor.mobile}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
<div>
|
||||||
<TableCell>
|
<h4 className="text-sm font-bold text-slate-900 leading-none mb-1">{emp.name}</h4>
|
||||||
<div className="flex items-center space-x-2 text-slate-600">
|
<p className="text-xs text-slate-500 font-medium">{emp.email}</p>
|
||||||
<MapPin className="w-3.5 h-3.5 text-slate-400" />
|
|
||||||
<span className="text-xs font-bold">{sponsor.city || 'Dubai'}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</div>
|
||||||
<TableCell>
|
</TableCell>
|
||||||
<div className="flex flex-col space-y-1">
|
<TableCell>
|
||||||
<span className={`px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-tight w-max ${
|
<div className="flex items-center space-x-2 text-slate-600">
|
||||||
sponsor.subscription_plan ? 'bg-blue-50 text-blue-600 border border-blue-100' : 'bg-slate-100 text-slate-500'
|
<MapPin className="w-3.5 h-3.5 text-slate-400" />
|
||||||
}`}>
|
<span className="text-xs font-bold">{emp.location}</span>
|
||||||
{sponsor.subscription_plan ? `${sponsor.subscription_plan} Pass` : 'No Plan'}
|
</div>
|
||||||
</span>
|
</TableCell>
|
||||||
<span className={`text-[9px] font-bold ${
|
<TableCell>
|
||||||
sponsor.subscription_status === 'active' ? 'text-emerald-600' : 'text-slate-400'
|
<span className={`px-2.5 py-1 rounded-lg text-[10px] font-black uppercase tracking-tight ${
|
||||||
}`}>
|
emp.plan === 'None' ? 'bg-slate-100 text-slate-500' : 'bg-blue-50 text-blue-600'
|
||||||
Status: {sponsor.subscription_status.toUpperCase()}
|
}`}>
|
||||||
</span>
|
{emp.plan}
|
||||||
</div>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="text-xs font-bold text-slate-400">
|
||||||
<div className="flex items-center space-x-1.5">
|
{emp.join_date}
|
||||||
{sponsor.is_verified ? (
|
</TableCell>
|
||||||
<>
|
<TableCell>
|
||||||
<ShieldCheck className="w-4 h-4 text-emerald-500" />
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-xs font-bold text-emerald-600">OTP Verified</span>
|
{emp.status === 'Active' && <CheckCircle className="w-4 h-4 text-emerald-500" />}
|
||||||
</>
|
{emp.status === 'Pending' && <Clock className="w-4 h-4 text-amber-500" />}
|
||||||
) : (
|
{emp.status === 'Suspended' && <XCircle className="w-4 h-4 text-rose-500" />}
|
||||||
<>
|
<span className={`text-[10px] font-black uppercase tracking-widest ${
|
||||||
<Clock className="w-4 h-4 text-amber-500" />
|
emp.status === 'Active' ? 'text-emerald-600' :
|
||||||
<span className="text-xs font-bold text-amber-600">Unverified</span>
|
emp.status === 'Pending' ? 'text-amber-600' : 'text-rose-600'
|
||||||
</>
|
}`}>
|
||||||
)}
|
{emp.status}
|
||||||
</div>
|
</span>
|
||||||
</TableCell>
|
</div>
|
||||||
<TableCell>
|
</TableCell>
|
||||||
<div className="flex items-center space-x-2">
|
<TableCell className="text-right pr-8">
|
||||||
{sponsor.status === 'active' && <CheckCircle className="w-4 h-4 text-emerald-500" />}
|
<DropdownMenu>
|
||||||
{sponsor.status === 'inactive' && <Clock className="w-4 h-4 text-amber-500" />}
|
<DropdownMenuTrigger asChild>
|
||||||
{sponsor.status === 'suspended' && <XCircle className="w-4 h-4 text-rose-500" />}
|
<button className="p-2 text-slate-400 hover:text-slate-900 hover:bg-slate-50 rounded-lg transition-all outline-none">
|
||||||
<span className={`text-[10px] font-black uppercase tracking-widest ${
|
<MoreVertical className="w-5 h-5" />
|
||||||
sponsor.status === 'active' ? 'text-emerald-600' :
|
|
||||||
sponsor.status === 'inactive' ? 'text-amber-600' : 'text-rose-600'
|
|
||||||
}`}>
|
|
||||||
{sponsor.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<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>
|
</button>
|
||||||
<DropdownMenu>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuContent align="end" className="w-48 p-2 rounded-xl">
|
||||||
<button className="p-2 text-slate-400 hover:text-slate-900 hover:bg-slate-50 rounded-lg transition-all outline-none">
|
<DropdownMenuLabel className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-2 py-2">Actions</DropdownMenuLabel>
|
||||||
<MoreVertical className="w-5 h-5" />
|
<DropdownMenuSeparator />
|
||||||
</button>
|
<DropdownMenuItem className="p-3 rounded-lg focus:bg-slate-50 cursor-pointer" onClick={() => handleAction(emp)}>
|
||||||
</DropdownMenuTrigger>
|
<div className="flex items-center space-x-3">
|
||||||
<DropdownMenuContent align="end" className="w-48 p-2 rounded-xl">
|
<ExternalLink className="w-4 h-4 text-slate-500" />
|
||||||
<DropdownMenuLabel className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-2 py-2">Quick Actions</DropdownMenuLabel>
|
<span className="text-xs font-bold">View Details</span>
|
||||||
<DropdownMenuSeparator />
|
</div>
|
||||||
<DropdownMenuItem
|
</DropdownMenuItem>
|
||||||
className="p-3 rounded-lg focus:bg-blue-50 cursor-pointer text-blue-600 font-bold"
|
{emp.status === 'Pending' && (
|
||||||
onClick={() => handleEditClick(sponsor)}
|
<DropdownMenuItem className="p-3 rounded-lg focus:bg-emerald-50 cursor-pointer text-emerald-600" onClick={() => handleApprove(emp.id)}>
|
||||||
>
|
<div className="flex items-center space-x-3">
|
||||||
<div className="flex items-center space-x-3">
|
<CheckCircle className="w-4 h-4" />
|
||||||
<Edit2 className="w-4 h-4" />
|
<span className="text-xs font-bold">Approve Employer</span>
|
||||||
<span className="text-xs font-bold">Edit Registry</span>
|
</div>
|
||||||
</div>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
)}
|
||||||
{!sponsor.is_verified && (
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem className="p-3 rounded-lg focus:bg-emerald-50 cursor-pointer text-emerald-600 font-bold" onClick={() => handleApprove(sponsor.id)}>
|
<DropdownMenuItem className="p-3 rounded-lg focus:bg-rose-50 cursor-pointer text-rose-600">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<CheckCircle className="w-4 h-4" />
|
<XCircle className="w-4 h-4" />
|
||||||
<span className="text-xs font-bold">Approve / Verify</span>
|
<span className="text-xs font-bold">Suspend Account</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
</DropdownMenuContent>
|
||||||
{sponsor.status === 'active' ? (
|
</DropdownMenu>
|
||||||
<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>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<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">
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
<span className="text-xs font-bold">Delete Sponsor</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</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>
|
</TableCell>
|
||||||
</TableRow>
|
</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>
|
||||||
|
|
||||||
{/* Advanced Sponsor Detail Modal */}
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<Dialog open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
|
<DialogContent className="max-w-2xl bg-white p-0 overflow-hidden rounded-[32px] border-none shadow-2xl">
|
||||||
<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="bg-[#0F6E56] p-8 text-white relative">
|
||||||
<div className="bg-gradient-to-r from-[#0F6E56] to-[#085041] p-6 text-white relative flex-shrink-0">
|
<div className="absolute top-0 right-0 p-8 opacity-10">
|
||||||
|
<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-14 h-14 rounded-2xl bg-white/10 backdrop-blur-md flex items-center justify-center border border-white/20 font-black text-xl">
|
<div className="w-16 h-16 rounded-2xl bg-white/20 backdrop-blur-md flex items-center justify-center border border-white/20">
|
||||||
{selectedSponsor?.full_name.charAt(0)}
|
<Briefcase className="w-8 h-8 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-black tracking-tight">{selectedSponsor?.full_name}</h2>
|
<h2 className="text-2xl font-black tracking-tight">{selectedEmployer?.name}</h2>
|
||||||
<p className="text-teal-100/80 text-xs font-semibold mt-1">📍 {selectedSponsor?.city || 'Dubai'} • Registered {new Date(selectedSponsor?.created_at).toLocaleDateString()}</p>
|
<p className="text-teal-100 text-sm font-medium">{selectedEmployer?.location} • Registered {selectedEmployer?.join_date}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 space-y-6 overflow-y-auto flex-1 bg-slate-50/50">
|
<div className="p-8 space-y-8">
|
||||||
{/* Vetting info */}
|
<div className="grid grid-cols-2 gap-8">
|
||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between border-b border-slate-100 pb-2">
|
<div className="space-y-1">
|
||||||
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest flex items-center gap-1.5">
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Primary Email</label>
|
||||||
<Award className="w-4.5 h-4.5 text-[#0F6E56]" />
|
<div className="flex items-center space-x-2 text-sm font-bold text-slate-900">
|
||||||
<span>UAE Sponsor Vetting Dossier</span>
|
<Mail className="w-4 h-4 text-teal-600" />
|
||||||
</h3>
|
<span>{selectedEmployer?.email}</span>
|
||||||
<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 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="flex items-center gap-2 text-slate-800">
|
|
||||||
<Phone className="w-4 h-4 text-slate-400" />
|
|
||||||
<span>Mobile: {selectedSponsor?.mobile}</span>
|
|
||||||
</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 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">Phone Number</label>
|
||||||
{selectedSponsor?.otp_verified_at && (
|
<div className="flex items-center space-x-2 text-sm font-bold text-slate-900">
|
||||||
<div>Verified At: {new Date(selectedSponsor.otp_verified_at).toLocaleString()}</div>
|
<Clock className="w-4 h-4 text-teal-600" />
|
||||||
)}
|
<span>{selectedEmployer?.phone}</span>
|
||||||
<div>Nationality: {selectedSponsor?.nationality || 'N/A'}</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Contact Person</label>
|
||||||
|
<div className="text-sm font-bold text-slate-900">{selectedEmployer?.contact_person}</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<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>
|
||||||
|
|
||||||
{/* Subscription monitoring */}
|
<div className="bg-slate-50 rounded-2xl p-6 border border-slate-100 flex items-center justify-between">
|
||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-3">
|
|
||||||
<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">
|
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Sponsor Verification Action:</span>
|
|
||||||
{!selectedSponsor?.is_verified ? (
|
|
||||||
<button
|
|
||||||
onClick={() => handleApprove(selectedSponsor.id)}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
Verify Sponsor OTP
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<Badge className="bg-emerald-100 text-emerald-800 border-none font-black uppercase text-[9px] px-3 py-1.5 rounded-lg shadow-sm">
|
|
||||||
OTP Verified
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
{selectedSponsor?.status !== 'suspended' ? (
|
|
||||||
<button
|
|
||||||
onClick={() => handleSuspend(selectedSponsor.id)}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
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
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</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">
|
<div className="space-y-1">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Subscription Plan</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Subscription Plan</label>
|
||||||
<select
|
<div className="flex items-center space-x-2">
|
||||||
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"
|
<BadgeDollarSign className="w-4 h-4 text-blue-600" />
|
||||||
value={editForm.subscription_plan}
|
<span className="text-sm font-black text-blue-600">{selectedEmployer?.plan}</span>
|
||||||
onChange={e => setEditForm({ ...editForm, subscription_plan: e.target.value })}
|
</div>
|
||||||
>
|
|
||||||
<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>
|
||||||
<div className="space-y-1">
|
<div className="text-right">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Subscription Status</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Current Status</label>
|
||||||
<select
|
<div className={`text-xs font-black uppercase tracking-widest ${
|
||||||
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"
|
selectedEmployer?.status === 'Active' ? 'text-emerald-600' : 'text-amber-600'
|
||||||
value={editForm.subscription_status}
|
}`}>
|
||||||
onChange={e => setEditForm({ ...editForm, subscription_status: e.target.value })}
|
{selectedEmployer?.status}
|
||||||
>
|
</div>
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="expired">Expired</option>
|
|
||||||
<option value="none">None</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-slate-100">
|
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-slate-100">
|
||||||
<button
|
{selectedEmployer?.status === 'Pending' && (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => setIsEditOpen(false)}
|
onClick={() => { handleApprove(selectedEmployer.id); setIsDialogOpen(false); }}
|
||||||
className="px-4 py-2 bg-slate-100 text-slate-600 rounded-xl text-xs font-bold hover:bg-slate-200"
|
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"
|
||||||
>
|
>
|
||||||
Cancel
|
Approve Employer
|
||||||
</button>
|
</button>
|
||||||
<button
|
)}
|
||||||
type="submit"
|
<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">
|
||||||
className="px-5 py-2.5 bg-[#0F6E56] text-white rounded-xl text-xs font-black uppercase tracking-wider"
|
Send Message
|
||||||
>
|
|
||||||
Save Registry
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
|
|||||||
@ -1,213 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,271 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -15,14 +15,7 @@ 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,
|
||||||
@ -41,118 +34,17 @@ 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');
|
||||||
|
|
||||||
// Dialog / Worker selection state
|
|
||||||
const [selectedWorker, setSelectedWorker] = useState(null);
|
|
||||||
const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false);
|
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
|
||||||
|
|
||||||
// Form inputs for Profile Moderation
|
const handleToggleStatus = (id, currentStatus) => {
|
||||||
const [editForm, setEditForm] = useState({
|
const newStatus = currentStatus === 'active' ? 'inactive' : 'active';
|
||||||
name: '',
|
router.post(`/admin/workers/${id}/toggle-status`, { status: newStatus });
|
||||||
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 handleAvailabilityOverride = (id, availability) => {
|
const filteredWorkers = workers.filter(worker => {
|
||||||
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;
|
||||||
@ -167,34 +59,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 font-sans">Worker Profile Directory</h1>
|
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Worker Management</h1>
|
||||||
<p className="text-sm text-gray-500 mt-1">Full profile moderation, availability overrides, fraud alerts, and suspension management.</p>
|
<p className="text-sm text-gray-500 mt-1">View and manage worker availability and account status.</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.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">
|
<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">
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
<span>Export CSV</span>
|
<span>Export Data</span>
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/workers/verifications"
|
href="/admin/workers/verifications"
|
||||||
className="inline-flex items-center px-4 py-2.5 bg-[#0F6E56] text-white rounded-xl text-xs font-bold hover:bg-[#085041] transition-colors shadow-sm uppercase tracking-wider"
|
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"
|
||||||
>
|
>
|
||||||
Vetting OCR Queue
|
View Verifications
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters & Search */}
|
{/* Filters & Search */}
|
||||||
<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="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="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, email or nationality..."
|
placeholder="Search by name or email..."
|
||||||
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-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"
|
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"
|
||||||
/>
|
/>
|
||||||
</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">
|
||||||
@ -203,13 +95,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">Lifecycle Filter:</span>
|
<span className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mr-2">Status:</span>
|
||||||
<div className="flex bg-slate-100 p-1 rounded-xl">
|
<div className="flex bg-gray-100 p-1 rounded-xl">
|
||||||
{['all', 'active', 'inactive', 'suspended'].map((f) => (
|
{['all', 'active', 'inactive'].map((f) => (
|
||||||
<button
|
<button
|
||||||
key={f}
|
key={f}
|
||||||
onClick={() => setStatusFilter(f)}
|
onClick={() => setStatusFilter(f)}
|
||||||
className={`px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all ${
|
className={`px-4 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'
|
||||||
@ -223,133 +115,112 @@ export default function WorkerManagement({ workers }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Workers Table */}
|
{/* Workers Table */}
|
||||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
<div className="bg-white rounded-2xl border border-gray-100 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-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 pl-8">Worker Information</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">Contact Info</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">Category & Exp</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">Documents</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">Status</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4 text-right pr-8">Actions</TableHead>
|
<TableHead className="font-black text-gray-400 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-slate-50/50 transition-colors">
|
<TableRow key={worker.id} className="group hover:bg-gray-50/50 transition-colors">
|
||||||
<TableCell className="py-4 pl-8">
|
<TableCell className="py-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3 pl-4">
|
||||||
<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 flex items-center gap-1.5">
|
<div className="font-bold text-gray-900 text-sm">{worker.name}</div>
|
||||||
<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>
|
||||||
<span className={`px-2.5 py-1 rounded-lg text-[10px] font-black uppercase tracking-wider ${
|
<div className="flex items-center space-x-2 text-slate-600">
|
||||||
worker.availability === 'Available Now' ? 'bg-emerald-50 text-emerald-600' :
|
<Phone className="w-3.5 h-3.5 text-slate-400" />
|
||||||
worker.availability === 'In Interview' ? 'bg-blue-50 text-blue-600' : 'bg-slate-100 text-slate-500'
|
<span className="text-xs font-bold">{worker.phone || '+971 50 000 0000'}</span>
|
||||||
}`}>
|
|
||||||
{worker.availability}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center space-x-1.5">
|
|
||||||
<div className={`p-1 bg-${worker.verified ? 'emerald' : 'amber'}-50 rounded-lg`}>
|
|
||||||
<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>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-1">
|
||||||
<div className="text-xs font-bold text-slate-800 flex items-center gap-1.5">
|
<div className="text-sm font-medium text-gray-700 flex items-center gap-1.5">
|
||||||
<Briefcase className="w-3.5 h-3.5 text-slate-400" /> {worker.category}
|
<Briefcase className="w-3.5 h-3.5 text-gray-400" /> {worker.category}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-slate-400 font-bold flex items-center gap-1.5">
|
<div className="text-xs text-gray-500 flex items-center gap-1.5">
|
||||||
<Globe className="w-3.5 h-3.5 text-slate-400" /> {worker.nationality} • {worker.experience}
|
<Globe className="w-3.5 h-3.5 text-gray-400" /> {worker.nationality} • {worker.experience}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center space-x-1.5">
|
||||||
|
<div className="p-1.5 bg-emerald-50 rounded-lg">
|
||||||
|
<FileText className="w-4 h-4 text-emerald-500" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-black text-emerald-600 uppercase">Verified</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`px-3 py-0.5 rounded-full font-black text-[9px] uppercase tracking-wider ${
|
className={`px-3 py-1 rounded-full font-bold text-[10px] 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'
|
||||||
: worker.status === 'suspended' ? 'bg-red-100 text-red-800 border-none' : 'bg-slate-50 text-slate-700 border-slate-200'
|
: 'bg-red-50 text-red-700 border-red-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">
|
<DropdownMenu>
|
||||||
<button
|
<DropdownMenuTrigger className="p-2 hover:bg-gray-100 rounded-lg transition-colors focus:outline-none">
|
||||||
onClick={() => openWorkerDetails(worker)}
|
<MoreHorizontal className="w-5 h-5 text-gray-400" />
|
||||||
className="p-2 hover:bg-slate-100 rounded-lg transition-colors text-[#0F6E56] font-bold"
|
</DropdownMenuTrigger>
|
||||||
title="Manage Profile"
|
<DropdownMenuContent align="end" className="w-48 p-2 rounded-xl shadow-xl border-gray-100">
|
||||||
>
|
<DropdownMenuLabel className="text-[10px] font-bold text-gray-400 uppercase px-2 mb-1">Account Actions</DropdownMenuLabel>
|
||||||
<Eye className="w-4 h-4" />
|
<DropdownMenuItem
|
||||||
</button>
|
onClick={() => handleToggleStatus(worker.id, worker.status)}
|
||||||
<DropdownMenu>
|
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer font-semibold text-xs ${
|
||||||
<DropdownMenuTrigger className="p-2 hover:bg-slate-100 rounded-lg transition-colors focus:outline-none">
|
worker.status === 'active'
|
||||||
<MoreHorizontal className="w-4 h-4 text-slate-400" />
|
? 'text-red-600 hover:bg-red-50'
|
||||||
</DropdownMenuTrigger>
|
: 'text-emerald-600 hover:bg-emerald-50'
|
||||||
<DropdownMenuContent align="end" className="w-52 p-2 rounded-xl shadow-xl border-slate-100">
|
}`}
|
||||||
<DropdownMenuLabel className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-2 mb-1">Quick Actions</DropdownMenuLabel>
|
>
|
||||||
|
{worker.status === 'active' ? (
|
||||||
<DropdownMenuItem
|
<><UserX className="w-4 h-4" /> Deactivate Account</>
|
||||||
onClick={() => handleToggleStatus(worker.id, worker.status === 'active' ? 'suspended' : 'active')}
|
) : (
|
||||||
className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-red-600 hover:bg-red-50"
|
<><UserCheck className="w-4 h-4" /> Activate Account</>
|
||||||
>
|
)}
|
||||||
{worker.status === 'active' ? (
|
</DropdownMenuItem>
|
||||||
<><Ban className="w-3.5 h-3.5" /> Suspend Account</>
|
<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">
|
||||||
<><UserCheck className="w-3.5 h-3.5" /> Activate Account</>
|
<Mail className="w-4 h-4" /> Send Message
|
||||||
)}
|
</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
|
||||||
<DropdownMenuSeparator className="my-1" />
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
<DropdownMenuItem
|
</DropdownMenu>
|
||||||
onClick={() => handleAvailabilityOverride(worker.id, 'Available Now')}
|
</TableCell>
|
||||||
className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-bold text-xs text-slate-700 hover:bg-slate-50"
|
</TableRow>
|
||||||
>
|
|
||||||
<CheckSquare className="w-3.5 h-3.5 text-emerald-500" /> Mark Available
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="py-20 text-center">
|
<TableCell colSpan={5} className="py-20 text-center">
|
||||||
<Users className="w-12 h-12 text-slate-200 mx-auto mb-3" />
|
<Users className="w-12 h-12 text-gray-200 mx-auto mb-3" />
|
||||||
<div className="font-bold text-slate-400">No workers found matching your criteria.</div>
|
<div className="font-bold text-gray-400">No workers found matching your criteria.</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
@ -357,271 +228,6 @@ 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,12 +11,7 @@ import {
|
|||||||
Edit2,
|
Edit2,
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
X,
|
X,
|
||||||
AlertTriangle,
|
Loader2
|
||||||
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 {
|
||||||
@ -25,141 +20,65 @@ 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);
|
||||||
|
|
||||||
// Vetting form overrides
|
// Dialog state
|
||||||
const [ocrOverrides, setOcrOverrides] = useState({});
|
const [ocrOverrides, setOcrOverrides] = useState({});
|
||||||
const [isEditingOcr, setIsEditingOcr] = useState(false);
|
const [editingField, setEditingField] = useState(null);
|
||||||
const [rejectionReason, setRejectionReason] = useState('');
|
const [rejectionReason, setRejectionReason] = useState('');
|
||||||
const [showRejectionForm, setShowRejectionForm] = useState(false);
|
const [showRejectionInput, setShowRejectionInput] = useState(false);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
// Local mock verification queue containing OCR score mismatches
|
const tabs = [
|
||||||
const localQueue = [
|
{ label: 'All', value: 'all' },
|
||||||
{
|
{ label: 'Auto-Approved', value: 'approved' },
|
||||||
id: 101,
|
{ label: 'Flagged/Manual', value: 'rejected' },
|
||||||
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 activeList = localQueue.filter(item => {
|
const handleTabClick = (tabValue) => {
|
||||||
if (status === 'approved') return item.status === 'approved';
|
router.get('/admin/workers/verifications', { status: tabValue }, { preserveState: true });
|
||||||
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 || {});
|
||||||
setIsEditingOcr(false);
|
setEditingField(null);
|
||||||
setShowRejectionForm(false);
|
setShowRejectionInput(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="Vetting & OCR Review Hub">
|
<AdminLayout title="Worker Identity Verifications">
|
||||||
<Head title="OCR Verifications" />
|
<Head title="Worker Verifications - Admin Portal" />
|
||||||
|
|
||||||
<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">OCR Vetting Queue & Verification System</h1>
|
<h1 className="text-xl font-bold text-gray-900 tracking-tight">Identity Audit Logs</h1>
|
||||||
<p className="text-xs text-gray-500 mt-0.5">High-confidence auto-approvals, low-confidence warning audits, and inline OCR override triggers.</p>
|
<p className="text-xs text-gray-500 mt-0.5">Automated document verification system logs and OCR extractions.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter Tabs */}
|
{/* Filter Tabs */}
|
||||||
<div className="flex bg-slate-200/60 p-1 rounded-xl w-fit border border-slate-200 font-sans">
|
<div className="flex bg-slate-200/60 p-1 rounded-xl w-fit border border-slate-200">
|
||||||
{[
|
{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={() => router.get('/admin/workers/verifications', { status: tab.value })}
|
onClick={() => handleTabClick(tab.value)}
|
||||||
className={`px-4 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all ${
|
className={`px-4 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||||
(status || 'all') === tab.value
|
status === 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'
|
||||||
}`}
|
}`}
|
||||||
@ -170,103 +89,39 @@ export default function Verifications({ verifications, status }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Queue Summary widgets */}
|
{/* Desktop Table / Mobile Card List */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
|
<div className="hidden sm:block overflow-x-auto">
|
||||||
<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-slate-100 text-[10px] font-black text-slate-500 uppercase tracking-widest">
|
<tr className="bg-slate-50/70 border-b border-gray-100 text-xs font-semibold text-slate-500">
|
||||||
<th className="py-4 px-6">Worker Name</th>
|
<th className="py-3.5 px-5">Worker Name</th>
|
||||||
<th className="py-4 px-6">Passport ID</th>
|
<th className="py-3.5 px-5">Nationality</th>
|
||||||
<th className="py-4 px-6">OCR Confidence Score</th>
|
<th className="py-3.5 px-5">Passport No.</th>
|
||||||
<th className="py-4 px-6">Vetting Warnings</th>
|
<th className="py-3.5 px-5">Processed</th>
|
||||||
<th className="py-4 px-6">Processed Date</th>
|
<th className="py-3.5 px-5">Status</th>
|
||||||
<th className="py-4 px-6 text-right">Action</th>
|
<th className="py-3.5 px-5 text-right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100 text-xs font-bold text-slate-700">
|
<tbody className="divide-y divide-gray-100 text-sm">
|
||||||
{activeList.map((item) => (
|
{verifications?.data?.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-6">
|
<td className="py-4 px-5 font-medium text-gray-900">{item.worker_name}</td>
|
||||||
<div>
|
<td className="py-4 px-5 text-gray-600">{item.nationality}</td>
|
||||||
<div className="text-sm font-black text-slate-900">{item.worker_name}</div>
|
<td className="py-4 px-5 font-mono text-xs text-gray-600">{item.passport_number}</td>
|
||||||
<div className="text-[10px] text-slate-400 font-semibold">{item.nationality} • {item.document_type}</div>
|
<td className="py-4 px-5 text-xs text-gray-500">{item.processed_at}</td>
|
||||||
</div>
|
<td className="py-4 px-5">
|
||||||
|
<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-6 font-mono font-medium">{item.passport_number}</td>
|
<td className="py-4 px-5 text-right">
|
||||||
<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-4 py-2 border rounded-xl text-[10px] font-black uppercase tracking-wider transition-colors focus:outline-none ${
|
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"
|
||||||
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'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{item.status === 'flagged' ? 'Verify OCR' : 'Audit Log'}
|
<Eye className="w-3.5 h-3.5 mr-1.5" /> View Log
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -274,187 +129,152 @@ export default function Verifications({ verifications, status }) {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Audit logs details grid */}
|
{/* Mobile Card List */}
|
||||||
<div className="bg-slate-50 rounded-2xl p-5 border border-slate-200">
|
<div className="block sm:hidden divide-y divide-gray-100">
|
||||||
<h3 className="text-xs font-black text-slate-800 uppercase tracking-widest flex items-center gap-1.5 mb-3">
|
{verifications?.data?.map((item) => (
|
||||||
<History className="w-4 h-4 text-slate-600" />
|
<div key={item.id} className="p-4 space-y-3">
|
||||||
<span>OCR Vetting Action Audit Logs</span>
|
<div className="flex items-center justify-between">
|
||||||
</h3>
|
<span className="font-semibold text-gray-900">{item.worker_name}</span>
|
||||||
<div className="space-y-2">
|
<Badge className="bg-emerald-50 text-emerald-700 border-emerald-200">Auto-Verified</Badge>
|
||||||
{[
|
|
||||||
{ 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>
|
||||||
<span className="text-[10px] text-slate-400">{log.time} • IP: {log.ip}</span>
|
<div className="grid grid-cols-2 text-xs text-gray-600 gap-1">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Pagination Component */}
|
||||||
|
{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>
|
</div>
|
||||||
|
|
||||||
{/* Vetting Vetting Audit Dialog */}
|
{/* Review Dialog */}
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<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">
|
<DialogContent className="max-w-4xl bg-white p-0 overflow-hidden font-sans rounded-2xl">
|
||||||
<DialogHeader className="p-6 border-b border-slate-100 bg-[#0f6e56]/5 flex-shrink-0">
|
<DialogHeader className="p-6 pb-4 border-b border-slate-100 bg-slate-50/50">
|
||||||
<DialogTitle className="text-lg font-black text-slate-800 flex items-center justify-between tracking-tight">
|
<DialogTitle className="text-lg font-semibold text-gray-800 flex items-center justify-between tracking-tight">
|
||||||
<span className="flex items-center gap-2">
|
<span>Verification Log: <span className="text-[#0F6E56]">{selectedItem?.worker_name}</span></span>
|
||||||
<ShieldCheck className="w-5 h-5 text-[#0F6E56]" />
|
<Badge className="bg-emerald-100 text-emerald-800 border-none px-3 py-1">System Verified</Badge>
|
||||||
<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-8 p-6 overflow-y-auto flex-1 bg-slate-50/30">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6 max-h-[80vh] overflow-y-auto">
|
||||||
|
{/* Left: Document Images */}
|
||||||
{/* Scanned proof & Warnings Column */}
|
<div className="space-y-4">
|
||||||
<div className="space-y-5">
|
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider flex items-center">
|
||||||
<div className="bg-white p-4 rounded-2xl border border-slate-200 shadow-sm space-y-3">
|
<FileText className="w-3.5 h-3.5 mr-1.5" /> Scanned Proof Documents ({selectedItem?.document_images?.length || 0})
|
||||||
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest flex items-center gap-1.5">
|
</h3>
|
||||||
<FileText className="w-4 h-4 text-[#0F6E56]" />
|
<div className="grid grid-cols-1 gap-4">
|
||||||
<span>Scanned Passport Scan Document</span>
|
{selectedItem?.document_images?.map((imgUrl, i) => (
|
||||||
</h3>
|
<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={selectedItem?.document_images[0]}
|
src={imgUrl}
|
||||||
alt="Verification Scan"
|
alt="Document scan"
|
||||||
className="w-full h-full object-cover group-hover:scale-102 transition-transform duration-300"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||||
/>
|
/>
|
||||||
<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]">
|
<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]">
|
||||||
<ZoomIn className="w-4.5 h-4.5 mr-1" /> Click to Zoom Document
|
<ZoomIn className="w-5 h-5 mr-1.5" /> Click to View
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
<p className="text-[10px] text-slate-400 font-bold text-center uppercase tracking-wide">
|
|
||||||
Type: {selectedItem?.document_type}
|
|
||||||
</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 */}
|
{/* Right: OCR Data & Status */}
|
||||||
<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-6 flex flex-col justify-between">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between border-b border-slate-100 pb-2">
|
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider flex items-center justify-between">
|
||||||
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest">Extracted Text Vitals</h3>
|
<span>System Extracted Data</span>
|
||||||
<button
|
<span className="text-[10px] text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-full font-medium">Confidence: 98%</span>
|
||||||
onClick={() => setIsEditingOcr(!isEditingOcr)}
|
</h3>
|
||||||
className="text-xs text-[#0F6E56] hover:text-[#085041] font-black flex items-center gap-1 uppercase tracking-wider text-[10px]"
|
|
||||||
>
|
|
||||||
<Edit3 className="w-3.5 h-3.5" />
|
|
||||||
<span>{isEditingOcr ? 'Cancel Edit' : 'Edit OCR Values'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-2.5">
|
||||||
{Object.entries(ocrOverrides).map(([key, value]) => (
|
{Object.entries(ocrOverrides).map(([key, value]) => (
|
||||||
<div key={key} className="space-y-1.5">
|
<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">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">{key}</label>
|
<span className="font-medium text-slate-600 text-xs">{key}:</span>
|
||||||
{isEditingOcr ? (
|
<span className="text-gray-900 font-semibold text-xs font-mono">{value}</span>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Manual Rejection reason input */}
|
{/* Status Info */}
|
||||||
{showRejectionForm ? (
|
<div className="space-y-4 pt-4 border-t border-slate-100">
|
||||||
<div className="bg-rose-50/30 p-4 border border-rose-100 rounded-xl space-y-3">
|
<div className="bg-emerald-50 p-4 rounded-xl border border-emerald-100 flex items-start">
|
||||||
<label className="text-[10px] font-black text-rose-700 uppercase tracking-widest block">Rejection compliance reason</label>
|
<CheckCircle className="w-5 h-5 text-emerald-600 mr-3 mt-0.5" />
|
||||||
<textarea
|
<div>
|
||||||
rows="2"
|
<p className="text-xs font-bold text-emerald-800">Verified Automatically</p>
|
||||||
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"
|
<p className="text-[11px] text-emerald-700 mt-1">
|
||||||
placeholder="Explain to worker what document error occurred (e.g., expiry date mismatch, blurred image)..."
|
This profile was verified by the system on {selectedItem?.processed_at}. No further action is required from administrators.
|
||||||
value={rejectionReason}
|
</p>
|
||||||
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>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="flex gap-3 pt-4 border-t border-slate-100 flex-shrink-0">
|
<button
|
||||||
<button
|
type="button"
|
||||||
onClick={() => handleVerifySubmit('approve')}
|
onClick={() => setIsDialogOpen(false)}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
<CheckCircle className="w-4 h-4" />
|
Close Audit Log
|
||||||
<span>Approve Verification</span>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
<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>
|
||||||
@ -464,18 +284,19 @@ 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"
|
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"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="absolute top-6 right-6 text-white/80 hover:text-white p-2 bg-white/10 rounded-full transition-all focus:outline-none"
|
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"
|
||||||
|
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 scan"
|
alt="Zoomed document"
|
||||||
className="max-w-full max-h-[90vh] object-contain rounded-xl shadow-2xl border border-white/20"
|
className="max-w-full max-h-[90vh] object-contain rounded-xl shadow-2xl border border-white/20 animate-in zoom-in-95 duration-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -12,12 +12,7 @@ import {
|
|||||||
Filter,
|
Filter,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Info,
|
Info
|
||||||
Heart,
|
|
||||||
MapPin,
|
|
||||||
Clock,
|
|
||||||
Sparkles,
|
|
||||||
BellRing
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -33,7 +28,6 @@ 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(() => {
|
||||||
@ -43,22 +37,12 @@ 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: 'Charity',
|
audience: 'Shortlisted'
|
||||||
type: 'Charity',
|
|
||||||
provided_items: '',
|
|
||||||
event_date: '',
|
|
||||||
event_time: '',
|
|
||||||
location_details: '',
|
|
||||||
location_pin: ''
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const showToast = (message, subMessage = null) => {
|
const showToast = (message) => {
|
||||||
setToastMessage(message);
|
setToastMessage(message);
|
||||||
setToastSub(subMessage);
|
setTimeout(() => setToastMessage(null), 3000);
|
||||||
setTimeout(() => {
|
|
||||||
setToastMessage(null);
|
|
||||||
setToastSub(null);
|
|
||||||
}, 5000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = (e) => {
|
const handleCreate = (e) => {
|
||||||
@ -68,10 +52,7 @@ export default function Announcements({ initialAnnouncements }) {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
reset();
|
reset();
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
showToast(
|
showToast('Announcement posted successfully');
|
||||||
'💖 COMMUNITY CHARITY EVENT POSTED!',
|
|
||||||
'🔔 Instant Push Notification sent to all workers in Dubai! ⏰ Morning-of reminder notification scheduled successfully.'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -81,44 +62,32 @@ 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="Charity Events & Support Drives">
|
<EmployerLayout title="Announcements">
|
||||||
<Head title="Community Charity Events - Sponsor Hub" />
|
<Head title="Announcements - Employer Portal" />
|
||||||
|
|
||||||
{toastMessage && (
|
{toastMessage && (
|
||||||
<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="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="bg-emerald-500 p-2 rounded-xl shrink-0 mt-0.5 shadow-md shadow-emerald-500/20">
|
<div className="bg-emerald-500 p-1 rounded-full">
|
||||||
<CheckCircle2 className="w-5 h-5 text-white" />
|
<CheckCircle2 className="w-4 h-4 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 charity events & drives..."
|
placeholder="Search announcements..."
|
||||||
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"
|
||||||
@ -127,82 +96,38 @@ export default function Announcements({ initialAnnouncements }) {
|
|||||||
|
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<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">
|
<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">
|
||||||
<Heart className="w-4 h-4 fill-white" />
|
<Megaphone className="w-4 h-4" />
|
||||||
<span>Post Charity Event</span>
|
<span>New Announcement</span>
|
||||||
</button>
|
</button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-[550px] rounded-3xl p-0 overflow-hidden border-none shadow-2xl">
|
<DialogContent className="sm:max-w-[500px] rounded-3xl p-0 overflow-hidden border-none shadow-2xl">
|
||||||
<div className="bg-gradient-to-r from-rose-500 to-pink-600 p-8 text-white relative">
|
<div className="bg-[#185FA5] 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 Charity Event</DialogTitle>
|
<DialogTitle className="text-2xl font-black relative z-10">Post Announcement</DialogTitle>
|
||||||
<DialogDescription className="text-pink-100 font-semibold mt-2 relative z-10">
|
<DialogDescription className="text-blue-100 font-medium mt-2 relative z-10">
|
||||||
Broadcast a support program, medical camp, or distribution drive to the worker community.
|
This will be visible to your selected candidates immediately.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleCreate} className="p-8 space-y-5 bg-white max-h-[80vh] overflow-y-auto">
|
<form onSubmit={handleCreate} className="p-8 space-y-6 bg-white">
|
||||||
|
<div className="space-y-2">
|
||||||
<div className="space-y-4 p-4 bg-rose-50/50 rounded-2xl border border-rose-100 animate-in fade-in duration-200">
|
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Target Audience</label>
|
||||||
<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">
|
||||||
<div className="space-y-1">
|
{['Shortlisted', 'Selected Candidates'].map(aud => (
|
||||||
<label className="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1">Event Date</label>
|
<button
|
||||||
<input
|
key={aud}
|
||||||
type="date"
|
type="button"
|
||||||
required
|
onClick={() => setNewAnnouncement({ ...newAnnouncement, audience: aud })}
|
||||||
value={newAnnouncement.event_date}
|
className={`px-4 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest border transition-all ${
|
||||||
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, event_date: e.target.value })}
|
newAnnouncement.audience === aud
|
||||||
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-blue-50 border-[#185FA5] text-[#185FA5]'
|
||||||
/>
|
: 'bg-slate-50 border-slate-200 text-slate-500 hover:bg-white'
|
||||||
</div>
|
}`}
|
||||||
<div className="space-y-1">
|
>
|
||||||
<label className="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1">Event Time</label>
|
{aud}
|
||||||
<input
|
</button>
|
||||||
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>
|
||||||
|
|
||||||
@ -214,30 +139,29 @@ 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. Free Dental checkup by Emirates Charity"
|
placeholder="e.g. Schedule for Video Interview"
|
||||||
/>
|
/>
|
||||||
</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">Event Description & Instructions</label>
|
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Message Content</label>
|
||||||
<textarea
|
<textarea
|
||||||
required
|
required
|
||||||
rows="3"
|
rows="4"
|
||||||
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 details of the charity event here..."
|
placeholder="Enter your message..."
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="pt-2">
|
<DialogFooter className="pt-4">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={processing}
|
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"
|
||||||
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>Publish Charity Event</span>
|
<span>Post Announcement</span>
|
||||||
</button>
|
</button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
@ -245,7 +169,7 @@ export default function Announcements({ initialAnnouncements }) {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-5">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
{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">
|
||||||
@ -255,100 +179,36 @@ 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) => (
|
||||||
const details = ann.charityDetails;
|
<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">
|
||||||
|
<div className="flex-1 space-y-3">
|
||||||
return (
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<div
|
<span className={`px-3 py-1.5 rounded-xl text-[9px] font-black uppercase tracking-widest border ${
|
||||||
key={ann.id}
|
ann.audience === 'Selected Candidates'
|
||||||
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"
|
? 'bg-emerald-50 border-emerald-100 text-emerald-700'
|
||||||
>
|
: 'bg-blue-50 border-blue-100 text-[#185FA5]'
|
||||||
<div className="absolute right-0 top-0 w-24 h-24 bg-rose-500/5 rounded-full blur-2xl pointer-events-none" />
|
}`}>
|
||||||
|
To: {ann.audience}
|
||||||
<div className="flex-1 space-y-4">
|
</span>
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex items-center text-slate-400 text-[10px] font-black uppercase tracking-widest">
|
||||||
<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">
|
<Calendar className="w-3.5 h-3.5 mr-1.5" />
|
||||||
<Heart className="w-3 h-3 fill-rose-500 text-rose-500" />
|
{ann.created_at}
|
||||||
<span>COMMUNITY CHARITY DRIVE</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<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" />
|
|
||||||
{ann.created_at}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
<BellRing className="w-3 h-3 text-emerald-600 animate-bounce" />
|
|
||||||
<span>Push & Reminder Scheduled</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<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>
|
||||||
|
|
||||||
<div className="flex items-center space-x-3 md:border-l md:border-slate-50 md:pl-6 shrink-0 h-full self-center">
|
<div className="space-y-1">
|
||||||
<button className="p-3 rounded-2xl bg-slate-50 text-slate-400 hover:text-rose-600 transition-all">
|
<h4 className="text-lg font-black text-slate-900 tracking-tight">{ann.title}</h4>
|
||||||
<ChevronRight className="w-5 h-5" />
|
<p className="text-sm font-medium text-slate-600 leading-relaxed">{ann.content}</p>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
})
|
<div className="flex items-center space-x-3 md:border-l md:border-slate-50 md:pl-6 shrink-0">
|
||||||
|
<button className="p-3 rounded-2xl bg-slate-50 text-slate-400 hover:text-[#185FA5] transition-all">
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -104,32 +104,26 @@ export default function CreatePassword() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderStepIndicator = () => (
|
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">
|
<div className="flex items-center justify-between mb-6 relative max-w-xs mx-auto px-2 select-none w-48">
|
||||||
|
<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: 'Payment' },
|
{ step: 3, label: 'Password' }
|
||||||
{ step: 4, label: 'Password' }
|
|
||||||
].map((s) => (
|
].map((s) => (
|
||||||
<React.Fragment key={s.step}>
|
<div key={s.step} className="flex flex-col items-center">
|
||||||
<div className="flex items-center space-x-1 sm:space-x-1.5">
|
<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={`w-5 h-5 rounded-full flex items-center justify-center font-bold text-[10px] border transition-all duration-300 ${
|
s.step === 3
|
||||||
s.step === 4
|
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-sm'
|
||||||
? '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>
|
|
||||||
<span className={`text-[10px] font-bold tracking-tight ${
|
|
||||||
s.step === 4 ? 'text-[#185FA5]' : 'text-slate-400'
|
|
||||||
}`}>
|
|
||||||
{s.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{s.step < 4 && (
|
<span className={`text-[9px] font-semibold uppercase mt-1 tracking-wider ${
|
||||||
<div className="w-4 sm:w-6 h-0.5 bg-slate-100" />
|
s.step === 3 ? 'text-[#185FA5]' : 'text-slate-400'
|
||||||
)}
|
}`}>{s.label}</span>
|
||||||
</React.Fragment>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -200,11 +194,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">
|
||||||
{renderStepIndicator()}
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
|
<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>
|
||||||
|
{renderStepIndicator()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} noValidate className="space-y-4">
|
<form onSubmit={handleSubmit} noValidate className="space-y-4">
|
||||||
|
|||||||
@ -11,6 +11,7 @@ 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: '',
|
||||||
@ -29,8 +30,12 @@ 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 = 'Sponsor name is required.';
|
newErrors.name = 'Contact name is required.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.email.trim()) {
|
if (!data.email.trim()) {
|
||||||
@ -85,34 +90,26 @@ export default function Register() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderStepIndicator = () => (
|
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">
|
<div className="flex items-center justify-between mb-6 relative max-w-xs mx-auto px-2 select-none w-48">
|
||||||
|
<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: 'Payment' },
|
{ step: 3, label: 'Password' }
|
||||||
{ step: 4, label: 'Password' }
|
|
||||||
].map((s) => (
|
].map((s) => (
|
||||||
<React.Fragment key={s.step}>
|
<div key={s.step} className="flex flex-col items-center">
|
||||||
<div className="flex items-center space-x-1 sm:space-x-1.5">
|
<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={`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'
|
: 'bg-white text-slate-400 border-slate-200'
|
||||||
: s.step < 1
|
}`}>
|
||||||
? 'bg-blue-50 text-[#185FA5] border-blue-200'
|
{s.step}
|
||||||
: 'bg-white text-slate-400 border-slate-200'
|
|
||||||
}`}>
|
|
||||||
{s.step}
|
|
||||||
</div>
|
|
||||||
<span className={`text-[10px] font-bold tracking-tight ${
|
|
||||||
s.step === 1 ? 'text-[#185FA5]' : 'text-slate-400'
|
|
||||||
}`}>
|
|
||||||
{s.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{s.step < 4 && (
|
<span className={`text-[9px] font-semibold uppercase mt-1 tracking-wider ${
|
||||||
<div className="w-4 sm:w-6 h-0.5 bg-slate-100" />
|
s.step === 1 ? 'text-[#185FA5]' : 'text-slate-400'
|
||||||
)}
|
}`}>{s.label}</span>
|
||||||
</React.Fragment>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -175,73 +172,96 @@ 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">
|
||||||
{renderStepIndicator()}
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
|
<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>
|
||||||
|
{renderStepIndicator()}
|
||||||
</div>
|
</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-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Sponsor Name</label>
|
<label className="block text-xs font-medium text-gray-700 mb-1">Company / Household Name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={data.name}
|
value={data.company_name}
|
||||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
onChange={(e) => handleInputChange('company_name', e.target.value)}
|
||||||
placeholder="e.g. Abdullah Bin Ahmed"
|
placeholder="e.g. Al Mansoor Household"
|
||||||
className={`w-full px-3.5 py-3 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all ${
|
className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 ${
|
||||||
errors.name
|
errors.company_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.company_name && (
|
||||||
<p className="mt-1.5 text-xs text-red-500 font-medium">
|
<p className="mt-1 text-xs text-red-500">
|
||||||
{Array.isArray(errors.name) ? errors.name[0] : errors.name}
|
{Array.isArray(errors.company_name) ? errors.company_name[0] : errors.company_name}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Email */}
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
{/* Contact Name */}
|
||||||
<label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Email</label>
|
<div>
|
||||||
<input
|
<label className="block text-xs font-medium text-gray-700 mb-1">Contact Name</label>
|
||||||
type="email"
|
<input
|
||||||
value={data.email}
|
type="text"
|
||||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
value={data.name}
|
||||||
placeholder="employer@domain.com"
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
className={`w-full px-3.5 py-3 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all ${
|
placeholder="e.g. Abdullah Bin Ahmed"
|
||||||
errors.email
|
className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 ${
|
||||||
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'
|
errors.name
|
||||||
: 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]'
|
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'
|
||||||
}`}
|
: 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]'
|
||||||
/>
|
}`}
|
||||||
{errors.email && (
|
/>
|
||||||
<p className="mt-1.5 text-xs text-red-500 font-medium">
|
{errors.name && (
|
||||||
{Array.isArray(errors.email) ? errors.email[0] : errors.email}
|
<p className="mt-1 text-xs text-red-500">
|
||||||
</p>
|
{Array.isArray(errors.name) ? errors.name[0] : errors.name}
|
||||||
)}
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Work Email */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">Work Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={data.email}
|
||||||
|
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||||
|
placeholder="employer@domain.com"
|
||||||
|
className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 ${
|
||||||
|
errors.email
|
||||||
|
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'
|
||||||
|
: 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="mt-1 text-xs text-red-500">
|
||||||
|
{Array.isArray(errors.email) ? errors.email[0] : errors.email}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Number */}
|
{/* Mobile Number */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Mobile Number</label>
|
<label className="block text-xs font-medium text-gray-700 mb-1">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-3 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all ${
|
className={`w-full px-3.5 py-2.5 rounded-xl border text-sm focus:outline-none focus:ring-2 ${
|
||||||
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.5 text-xs text-red-500 font-medium">
|
<p className="mt-1 text-xs text-red-500">
|
||||||
{Array.isArray(errors.phone) ? errors.phone[0] : errors.phone}
|
{Array.isArray(errors.phone) ? errors.phone[0] : errors.phone}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,313 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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 choose a subscription plan.');
|
toast.success('Email verified successfully! Let\'s secure your account.');
|
||||||
router.visit('/employer/register-payment');
|
router.visit('/employer/create-password');
|
||||||
} 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,34 +95,28 @@ export default function VerifyEmail({ email }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderStepIndicator = () => (
|
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">
|
<div className="flex items-center justify-between mb-6 relative max-w-xs mx-auto px-2 select-none">
|
||||||
|
<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: 'Payment' },
|
{ step: 3, label: 'Password' }
|
||||||
{ step: 4, label: 'Password' }
|
|
||||||
].map((s) => (
|
].map((s) => (
|
||||||
<React.Fragment key={s.step}>
|
<div key={s.step} className="flex flex-col items-center">
|
||||||
<div className="flex items-center space-x-1 sm:space-x-1.5">
|
<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={`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>
|
|
||||||
<span className={`text-[10px] font-bold tracking-tight ${
|
|
||||||
s.step === 2 ? 'text-[#185FA5]' : 'text-slate-400'
|
|
||||||
}`}>
|
|
||||||
{s.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{s.step < 4 && (
|
<span className={`text-[9px] font-semibold uppercase mt-1 tracking-wider ${
|
||||||
<div className="w-4 sm:w-6 h-0.5 bg-slate-100" />
|
s.step === 2 ? 'text-[#185FA5]' : 'text-slate-400'
|
||||||
)}
|
}`}>{s.label}</span>
|
||||||
</React.Fragment>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -185,11 +179,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">
|
||||||
{renderStepIndicator()}
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
|
<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>
|
||||||
|
{renderStepIndicator()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-2 text-center">
|
<div className="bg-white p-2 text-center">
|
||||||
|
|||||||
@ -8,467 +8,115 @@ 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({
|
export default function Dashboard({ employer, stats, shortlisted_workers, recent_messages, announcements }) {
|
||||||
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="Sponsor Control Center">
|
<EmployerLayout title="Employer Dashboard">
|
||||||
<Head title="Sponsor Dashboard - Verified UAE Domestic Workers" />
|
<Head title="Employer Dashboard - Marketplace Portal" />
|
||||||
|
|
||||||
<div className="space-y-8 pb-12">
|
<div className="space-y-8">
|
||||||
{/* 1. Alerts & Warning Banners */}
|
{/* Section 1: Welcome Banner */}
|
||||||
{!isSubActive && (
|
<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">
|
||||||
<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="absolute -right-10 -bottom-10 w-48 h-48 bg-white/10 rounded-full blur-2xl pointer-events-none" />
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isExpiringSoon && isSubActive && (
|
|
||||||
<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="space-y-3 z-10">
|
||||||
<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">
|
<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">
|
||||||
<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 Status: <span className="text-emerald-300 font-bold uppercase">{employer.subscription_status}</span></span>
|
<span>Subscription Active</span>
|
||||||
<span className="text-blue-300">|</span>
|
<span className="text-blue-200">|</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-black tracking-tight">
|
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight">
|
||||||
Welcome Back, {employer.name}
|
Good morning, {employer.name}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-blue-100 text-sm leading-relaxed font-medium">
|
<p className="text-blue-100 text-sm max-w-xl font-medium">
|
||||||
Hire direct, MOHRE-compliant domestic workers with zero middleman commissions. Browse our updated database of candidates with OCR passport checks.
|
Welcome to your employer control center. You have {stats.days_remaining} days remaining on your {employer.plan_name}.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="z-10 flex flex-wrap gap-3 flex-shrink-0">
|
<div className="z-10 flex-shrink-0">
|
||||||
<Link
|
<Link
|
||||||
href="/employer/workers"
|
href="/employer/workers"
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
<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>Browse 500+ Verified Workers</span>
|
<span>Find 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>
|
||||||
|
|
||||||
{/* 2. Top-level Stats & Analytics Grid */}
|
{/* Section 2: Stat Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{/* Subscription Pass status card */}
|
{/* Shortlisted */}
|
||||||
<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="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center flex-shrink-0">
|
||||||
<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">
|
<Bookmark className="w-6 h-6" />
|
||||||
<CreditCard className="w-6 h-6" />
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] font-black text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-md border border-emerald-100">
|
|
||||||
ACTIVE BILLING
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">Sponsor Pass Tier</div>
|
<div className="text-2xl font-bold text-slate-800">{stats.shortlisted_count}</div>
|
||||||
<div className="text-lg font-bold text-slate-800 mt-1">{employer.plan_name}</div>
|
<div className="text-xs text-slate-500 font-medium">Workers Shortlisted</div>
|
||||||
<div className="text-[11px] text-slate-500 font-medium mt-1">Renews: {employer.subscription_expires_at}</div>
|
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Pending Offers Stat Card */}
|
{/* Messages Sent */}
|
||||||
<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="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="w-12 h-12 rounded-xl bg-teal-50 text-teal-600 flex items-center justify-center flex-shrink-0">
|
||||||
<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">
|
<MessageSquare className="w-6 h-6" />
|
||||||
<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>
|
<div>
|
||||||
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">Pending Job Offers</div>
|
<div className="text-2xl font-bold text-slate-800">{stats.messages_sent}</div>
|
||||||
<div className="text-3xl font-black text-slate-800 mt-1">{stats.pending_offers}</div>
|
<div className="text-xs text-slate-500 font-medium">Messages Sent</div>
|
||||||
<div className="text-[11px] text-slate-500 font-medium mt-1">Direct recruitment proposals</div>
|
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Hired Count Stat Card */}
|
{/* Days Remaining */}
|
||||||
<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="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
|
||||||
<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">
|
stats.days_remaining < 7 ? 'bg-red-50 text-red-600' : 'bg-amber-50 text-amber-600'
|
||||||
<UserCheck className="w-5 h-5" />
|
}`}>
|
||||||
</div>
|
<CalendarDays className="w-6 h-6" />
|
||||||
<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>
|
<div>
|
||||||
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">Total Hired Workers</div>
|
<div className={`text-2xl font-bold ${stats.days_remaining < 7 ? 'text-red-600' : 'text-slate-800'}`}>
|
||||||
<div className="text-3xl font-black text-slate-800 mt-1">{stats.hired_count}</div>
|
{stats.days_remaining}
|
||||||
<div className="text-[11px] text-slate-500 font-medium mt-1">Direct contracts initiated</div>
|
</div>
|
||||||
|
<div className="text-xs text-slate-500 font-medium">Days Remaining</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Shortlist Counter */}
|
{/* Plan Name */}
|
||||||
<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="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="w-12 h-12 rounded-xl bg-slate-100 text-slate-600 flex items-center justify-center flex-shrink-0">
|
||||||
<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">
|
<CreditCard className="w-6 h-6" />
|
||||||
<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>
|
<div className="truncate">
|
||||||
<div className="text-xs font-semibold text-slate-400 uppercase tracking-widest">Shortlisted Candidates</div>
|
<div className="text-lg font-bold text-slate-800 truncate">{employer.plan_name}</div>
|
||||||
<div className="text-3xl font-black text-slate-800 mt-1">{stats.shortlisted_count}</div>
|
<div className="text-xs text-slate-500 font-medium">Current Plan</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>
|
|
||||||
|
|
||||||
{/* 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>
|
</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">
|
||||||
{/* Left Column: Recommended Workers, Shortlist, Chats */}
|
{/* Main Content Column (Col 8) */}
|
||||||
<div className="lg:col-span-8 space-y-8">
|
<div className="lg:col-span-8 space-y-8">
|
||||||
{/* Recommended Workers Module */}
|
{/* Section 3: My Shortlist */}
|
||||||
<div className="bg-white p-6 rounded-3xl border border-slate-200 shadow-xs space-y-6">
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden p-6 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]" />
|
||||||
<h3 className="font-bold text-base text-slate-900">Active Shortlist ({shortlisted_workers.length})</h3>
|
<h2 className="text-lg font-bold text-slate-800">My Shortlist</h2>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/employer/shortlist" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-0.5">
|
<Link href="/employer/shortlist" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1">
|
||||||
<span>View book</span>
|
<span>View all</span>
|
||||||
<ChevronRight className="w-4 h-4" />
|
<ChevronRight className="w-4 h-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -476,9 +124,9 @@ export default function Dashboard({
|
|||||||
{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-2xl p-4 border border-slate-200/70 flex-shrink-0 space-y-3 relative hover:border-[#185FA5] hover:bg-white transition-all">
|
<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 className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<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">
|
<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">
|
||||||
{worker.name.charAt(0)}
|
{worker.name.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<div className="truncate">
|
<div className="truncate">
|
||||||
@ -486,192 +134,97 @@ export default function Dashboard({
|
|||||||
<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-[10px] text-slate-500 truncate font-semibold">{worker.nationality}</div>
|
<div className="text-xs text-slate-500 truncate">{worker.nationality}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="text-[10px] font-semibold text-slate-400 uppercase tracking-tighter">Category:</div>
|
<div className="text-[11px] font-semibold text-slate-500">Top Skill:</div>
|
||||||
<div className="inline-block px-2 py-0.5 bg-white border border-slate-200 rounded-md text-xs font-semibold text-slate-700">
|
<div className="inline-block px-2.5 py-1 bg-white border border-slate-200 rounded-lg text-xs font-medium text-slate-700">
|
||||||
{worker.category || 'General Helper'}
|
{worker.skills[0] || 'General Work'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-2 border-t border-slate-200/80 flex items-center justify-between text-xs">
|
<div className="pt-2 border-t border-slate-200 flex items-center justify-between text-xs">
|
||||||
<span className="text-[10px] text-slate-400 font-bold uppercase">AVAILABILITY:</span>
|
<span className="text-slate-500 font-medium">Availability:</span>
|
||||||
<span className="font-bold text-[#185FA5] bg-blue-50 px-2 py-0.5 rounded text-[10px] border border-blue-100">
|
<span className="font-bold text-[#185FA5] bg-blue-50 px-2 py-0.5 rounded 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-10 bg-slate-50 rounded-2xl border-2 border-dashed border-slate-200 space-y-2">
|
<div className="text-center py-12 bg-slate-50 rounded-xl 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-xs font-bold text-slate-500">Shortlist candidates to compare their documents side-by-side</div>
|
<div className="text-sm font-semibold text-slate-600">Shortlist workers to see them here</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recent Chats / Message Center */}
|
{/* Section 4: Recent Messages */}
|
||||||
<div className="bg-white rounded-3xl border border-slate-200 shadow-xs p-6 space-y-6">
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden 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]" />
|
||||||
<h3 className="font-bold text-base text-slate-900">Recent Chats</h3>
|
<h2 className="text-lg font-bold text-slate-800">Recent Messages</h2>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/employer/messages" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-0.5">
|
<Link href="/employer/messages" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1">
|
||||||
<span>All channels</span>
|
<span>View all messages</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?.length > 0 ? (
|
{recent_messages?.map((msg) => (
|
||||||
recent_messages.map((msg) => (
|
|
||||||
<Link
|
|
||||||
key={msg.id}
|
|
||||||
href={`/employer/messages/${msg.id}`}
|
|
||||||
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-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)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="font-bold text-xs text-slate-800 group-hover:text-[#185FA5] transition-colors truncate">
|
|
||||||
{msg.worker_name}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<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 border border-white" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-slate-600 truncate mt-0.5 font-medium">
|
|
||||||
{msg.last_message}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</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
|
<Link
|
||||||
key={search.id}
|
key={msg.id}
|
||||||
href={`/employer/workers?${search.query}`}
|
href={`/employer/messages/${msg.id}`}
|
||||||
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"
|
className="flex items-start space-x-4 py-4 hover:bg-slate-50 px-3 rounded-xl transition-colors group"
|
||||||
>
|
>
|
||||||
<span className="text-xs font-bold text-slate-700 group-hover:text-[#185FA5] transition-colors truncate">
|
<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">
|
||||||
{search.name}
|
{msg.worker_name.charAt(0)}
|
||||||
</span>
|
</div>
|
||||||
<ChevronRight className="w-4 h-4 text-slate-400 group-hover:translate-x-0.5 transition-transform" />
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="font-bold text-sm text-slate-800 group-hover:text-[#185FA5] transition-colors truncate">
|
||||||
|
{msg.worker_name}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-[10px] font-medium text-slate-400">{msg.sent_at}</span>
|
||||||
|
{msg.unread && <span className="w-2 h-2 rounded-full bg-red-500 animate-pulse" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-600 truncate mt-0.5">{msg.last_message}</p>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Charity Events Module */}
|
{/* Right Column (Col 4) */}
|
||||||
<div className="bg-white rounded-3xl border border-slate-200 shadow-xs p-6 space-y-6">
|
<div className="lg:col-span-4 space-y-8">
|
||||||
<div className="flex items-center justify-between">
|
{/* Section 5: Announcements */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6 space-y-6">
|
||||||
<Heart className="w-5 h-5 text-rose-500 fill-rose-500/20" />
|
<div className="flex items-center space-x-2">
|
||||||
<h3 className="font-extrabold text-base text-slate-900">Community Charity Drives</h3>
|
<Info className="w-5 h-5 text-[#185FA5]" />
|
||||||
</div>
|
<h2 className="text-lg font-bold text-slate-800">Announcements</h2>
|
||||||
<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-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 key={ann.id} className="p-4 bg-blue-50/70 border border-blue-100 rounded-xl space-y-2">
|
||||||
<div className="flex items-center justify-between text-[9px] font-bold text-rose-600 uppercase tracking-wider">
|
<div className="flex items-center justify-between text-[10px] font-bold text-blue-600 uppercase tracking-wider">
|
||||||
<span>❤️ Charity Drive</span>
|
<span>System Alert</span>
|
||||||
<span>{ann.created_at}</span>
|
<span>{ann.created_at}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<h3 className="font-bold text-xs text-slate-900 tracking-tight">{ann.title}</h3>
|
||||||
<h4 className="font-black text-xs text-slate-900 tracking-tight group-hover:text-rose-600 transition-colors">{ann.title}</h4>
|
<p className="text-xs text-slate-600 leading-relaxed">{ann.body}</p>
|
||||||
<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>
|
||||||
|
|||||||
@ -19,23 +19,15 @@ 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(true);
|
const [showProfile, setShowProfile] = useState(false);
|
||||||
const [showAttachmentModal, setShowAttachmentModal] = useState(false);
|
|
||||||
const [uploadedFiles, setUploadedFiles] = useState([]);
|
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
@ -50,13 +42,6 @@ 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())
|
||||||
);
|
);
|
||||||
@ -64,48 +49,12 @@ 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: () => {
|
||||||
// Show immediate confirmation of the push alert popping up on worker mobile app
|
setInput('');
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -115,24 +64,6 @@ 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?",
|
||||||
@ -143,23 +74,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 w-full">
|
<div className="flex h-full bg-[#FAFBFF] overflow-hidden select-none">
|
||||||
{/* Conversation List Sidebar (Desktop) */}
|
{/* Conversation List Sidebar (Desktop) */}
|
||||||
<aside className="hidden xl:flex w-90 border-r border-slate-200 flex-col flex-shrink-0 bg-white shadow-sm z-30">
|
<aside className="hidden xl:flex w-96 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 Hub</h2>
|
<h2 className="font-black text-2xl text-slate-900 tracking-tight">Messages</h2>
|
||||||
<span className="text-[9px] font-black bg-emerald-50 text-emerald-700 px-2 py-0.5 rounded uppercase tracking-wider">
|
<button className="p-2 bg-slate-50 text-slate-400 rounded-xl hover:text-[#185FA5] transition-colors">
|
||||||
SSL SECURE
|
<Search className="w-5 h-5" />
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search conversations..."
|
placeholder="Search by name..."
|
||||||
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 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -174,26 +105,28 @@ 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 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.5 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-12 h-12 rounded-[20px] text-white flex items-center justify-center font-black text-lg shadow-sm ${
|
<div className={`w-14 h-14 rounded-[20px] text-white flex items-center justify-center font-black text-xl shadow-lg ${
|
||||||
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>
|
||||||
<span className="absolute -bottom-1 -right-1 w-3.5 h-3.5 bg-emerald-500 border-4 border-white rounded-full shadow-sm" />
|
{c.online && (
|
||||||
|
<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-xs font-black truncate ${c.id === conversation.id ? 'text-[#185FA5]' : 'text-slate-900'}`}>
|
<span className={`text-sm font-black truncate ${c.id === conversation.id ? 'text-[#185FA5]' : 'text-slate-900'}`}>
|
||||||
{c.worker_name}
|
{c.worker_name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[9px] text-slate-400 font-bold uppercase tracking-tighter">{c.sent_at}</span>
|
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">{c.sent_at}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className={`text-[11px] font-bold truncate leading-tight ${c.unread ? 'text-slate-900' : 'text-slate-400'}`}>
|
<p className={`text-xs font-bold truncate leading-tight ${c.unread ? 'text-slate-900' : 'text-slate-400'}`}>
|
||||||
{c.last_message}
|
{c.last_message}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -207,70 +140,58 @@ 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 overflow-hidden">
|
<div className="flex-1 flex flex-col h-full bg-white relative z-10 shadow-2xl">
|
||||||
{/* Header */}
|
{/* Glassmorphism Header */}
|
||||||
<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="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="flex items-center space-x-3">
|
<div className="flex items-center space-x-4">
|
||||||
<Link
|
<Link
|
||||||
href="/employer/messages"
|
href="/employer/messages"
|
||||||
className="xl:hidden p-2 text-slate-400 hover:text-[#185FA5] hover:bg-blue-50 rounded-2xl transition-all"
|
className="lg:hidden p-2.5 text-slate-400 hover:text-[#185FA5] hover:bg-blue-50 rounded-2xl transition-all"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-6 h-6" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="relative group cursor-pointer" onClick={() => setShowProfile(!showProfile)}>
|
<div className="relative group">
|
||||||
<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">
|
<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">
|
||||||
{conversation.worker_name.charAt(0)}
|
{conversation.worker_name.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<span className="absolute -bottom-1 -right-1 w-3.5 h-3.5 bg-emerald-500 border-4 border-white rounded-full shadow-lg" />
|
{conversation.online && (
|
||||||
|
<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-extrabold text-slate-900 text-sm tracking-tight">{conversation.worker_name}</h3>
|
<h3 className="font-black text-slate-900 text-lg tracking-tight">{conversation.worker_name}</h3>
|
||||||
<span className="bg-emerald-50 text-emerald-600 p-0.5 rounded-full shadow-sm" title="OCR Verified Passport">
|
<span className="bg-emerald-50 text-emerald-600 p-0.5 rounded-full shadow-sm">
|
||||||
<ShieldCheck className="w-3.5 h-3.5" />
|
<ShieldCheck className="w-4 h-4" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-1.5 mt-0.5">
|
<div className="flex items-center space-x-2 mt-0.5">
|
||||||
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">
|
<div className={`w-2 h-2 rounded-full ${conversation.online ? 'bg-emerald-500' : 'bg-slate-300'}`} />
|
||||||
Active Now
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
|
||||||
|
{conversation.online ? 'Active Now' : 'Last seen 2h ago'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* WhatsApp Bridge Widget & Dossier Button */}
|
<div className="flex items-center space-x-3">
|
||||||
<div className="flex items-center space-x-2">
|
<Link
|
||||||
<a
|
href={`/employer/workers/${conversation.id}`}
|
||||||
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.`}
|
className="p-3 bg-slate-50 text-slate-400 hover:text-[#185FA5] hover:bg-blue-50 rounded-2xl transition-all shadow-sm"
|
||||||
target="_blank"
|
title="View Full Profile"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
<MessageSquare className="w-4 h-4 text-emerald-600 fill-emerald-600" />
|
<Info className="w-5 h-5" />
|
||||||
<span className="hidden sm:inline">WhatsApp Bridge</span>
|
</Link>
|
||||||
</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 Log */}
|
{/* Chat Messages */}
|
||||||
<div className="flex-1 px-6 py-6 overflow-y-auto bg-[#F8FAFC]/60 space-y-6 scroll-smooth">
|
<div className="flex-1 px-6 py-8 overflow-y-auto bg-[#F8FAFC]/50 space-y-8 scroll-smooth">
|
||||||
<div className="flex flex-col items-center mb-6">
|
<div className="flex flex-col items-center mb-10">
|
||||||
<div className="bg-white border border-slate-200 px-4 py-1.5 rounded-full shadow-xs">
|
<div className="bg-white border border-slate-200 px-6 py-2 rounded-full shadow-sm">
|
||||||
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Today • Realtime Chat Logs</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Today, Dec 15</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -279,25 +200,19 @@ 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-300`}
|
className={`flex w-full ${isEmp ? 'justify-end' : 'justify-start'} animate-in fade-in slide-in-from-bottom-4 duration-500`}
|
||||||
>
|
>
|
||||||
<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-5 py-3 rounded-2xl text-xs font-semibold leading-relaxed shadow-xs relative ${
|
<div className={`px-6 py-4 rounded-[32px] text-[13px] font-bold leading-relaxed shadow-xl relative ${
|
||||||
isEmp
|
isEmp
|
||||||
? 'bg-[#185FA5] text-white rounded-tr-none'
|
? 'bg-[#185FA5] text-white rounded-br-lg shadow-blue-500/10'
|
||||||
: 'bg-white border border-slate-100 text-slate-800 rounded-tl-none'
|
: 'bg-white border border-slate-100 text-slate-800 rounded-bl-lg shadow-slate-200/40'
|
||||||
}`}>
|
}`}>
|
||||||
{msg.text}
|
{msg.text}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-1 mt-1 px-1 text-[9px] font-bold text-slate-400 uppercase">
|
<div className="flex items-center space-x-2 mt-2 px-2">
|
||||||
<span>{msg.time}</span>
|
<span className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">{msg.time}</span>
|
||||||
{isEmp && (
|
{isEmp && <CheckCircle2 className="w-3 h-3 text-emerald-500" />}
|
||||||
<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>
|
||||||
@ -306,7 +221,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-4 py-3 rounded-2xl rounded-tl-none shadow-xs flex items-center space-x-1.5">
|
<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="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>
|
||||||
@ -316,44 +231,44 @@ export default function Show({ conversation, initialMessages, conversations = []
|
|||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick replies & typing input */}
|
{/* Quick Replies & Input */}
|
||||||
<div className="bg-white border-t border-slate-100 p-5 space-y-4 shadow-sm shrink-0">
|
<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="flex items-center space-x-2.5 overflow-x-auto scrollbar-none pb-1.5">
|
<div className="flex items-center space-x-3 overflow-x-auto scrollbar-none pb-2">
|
||||||
<div className="flex-shrink-0 p-1.5 bg-blue-50 text-[#185FA5] rounded-lg">
|
<div className="flex-shrink-0 p-2 bg-indigo-50 text-indigo-500 rounded-xl">
|
||||||
<Sparkles className="w-3.5 h-3.5" />
|
<Sparkles className="w-4 h-4" />
|
||||||
</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-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider flex-shrink-0 transition-all border border-slate-100"
|
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"
|
||||||
>
|
>
|
||||||
{chip}
|
{chip}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleFormSubmit} className="flex items-center gap-3">
|
<form onSubmit={handleFormSubmit} className="flex items-center gap-4">
|
||||||
<button
|
<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">
|
||||||
type="button"
|
<Paperclip className="w-6 h-6" />
|
||||||
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>
|
||||||
<input
|
<div className="flex-1 relative group">
|
||||||
value={input}
|
<input
|
||||||
onChange={(e) => setInput(e.target.value)}
|
value={input}
|
||||||
placeholder="Type a compliant secure message..."
|
onChange={(e) => setInput(e.target.value)}
|
||||||
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"
|
placeholder="Type a 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"
|
||||||
|
/>
|
||||||
|
<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-3.5 rounded-xl transition-all shadow-xs flex items-center justify-center flex-shrink-0"
|
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"
|
||||||
>
|
>
|
||||||
<Send className="w-5 h-5" />
|
<Send className="w-6 h-6 group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform" />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -361,109 +276,66 @@ export default function Show({ conversation, initialMessages, conversations = []
|
|||||||
|
|
||||||
{/* Candidate Context Sidebar (Right) */}
|
{/* Candidate Context Sidebar (Right) */}
|
||||||
{showProfile && (
|
{showProfile && (
|
||||||
<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">
|
<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">
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-8 space-y-8 overflow-y-auto">
|
||||||
<div className="text-center space-y-3 pb-4 border-b border-slate-100">
|
<div className="text-center space-y-4">
|
||||||
<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">
|
<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">
|
||||||
{conversation.worker_name.charAt(0)}
|
{conversation.worker_name.charAt(0)}
|
||||||
<span className="absolute -bottom-1 -right-1 bg-emerald-500 p-1 rounded-lg border-2 border-white shadow">
|
<span className="absolute -bottom-2 -right-2 bg-emerald-500 p-2 rounded-2xl border-4 border-white shadow-lg">
|
||||||
<ShieldCheck className="w-4 h-4 text-white" />
|
<ShieldCheck className="w-5 h-5 text-white" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-extrabold text-base text-slate-900 tracking-tight">{conversation.worker_name}</h4>
|
<h4 className="font-black text-2xl text-slate-900 tracking-tight">{conversation.worker_name}</h4>
|
||||||
<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">
|
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] mt-1">{conversation.category}</p>
|
||||||
{conversation.category}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2 text-center text-xs font-bold">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="bg-slate-50/70 p-3 rounded-xl border border-slate-100">
|
<div className="bg-slate-50 p-4 rounded-3xl text-center">
|
||||||
<DollarSign className="w-3.5 h-3.5 text-emerald-600 mx-auto mb-1" />
|
<DollarSign className="w-4 h-4 text-emerald-600 mx-auto mb-1.5" />
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Expected</div>
|
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Expected</div>
|
||||||
<div className="text-slate-800 mt-0.5 truncate">{conversation.salary}</div>
|
<div className="text-sm font-black text-slate-900 mt-1 truncate">{conversation.salary}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-50/70 p-3 rounded-xl border border-slate-100">
|
<div className="bg-slate-50 p-4 rounded-3xl text-center">
|
||||||
<MapPin className="w-3.5 h-3.5 text-blue-600 mx-auto mb-1" />
|
<MapPin className="w-4 h-4 text-blue-600 mx-auto mb-1.5" />
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Region</div>
|
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Region</div>
|
||||||
<div className="text-slate-800 mt-0.5 truncate">{conversation.nationality}</div>
|
<div className="text-sm font-black text-slate-900 mt-1 truncate">{conversation.nationality}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-4">
|
||||||
<h5 className="text-[9px] font-black text-slate-400 uppercase tracking-widest pl-1">Compliance Profile</h5>
|
<h5 className="text-[10px] font-black text-slate-400 uppercase tracking-widest pl-2">Recruitment Context</h5>
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
{[
|
{[
|
||||||
{ label: 'Visa Transfer', value: 'Transferable', icon: Briefcase, color: 'text-blue-500' },
|
{ label: 'Visa Status', value: 'Transferable', icon: Briefcase, color: 'text-blue-500' },
|
||||||
{ label: 'Medical Status', value: 'Cleared', icon: ShieldCheck, color: 'text-emerald-500' },
|
{ label: 'Medical Test', value: 'Cleared', icon: ShieldCheck, color: 'text-emerald-500' },
|
||||||
{ label: 'Passport OCR', value: 'Verified', icon: FileText, color: 'text-slate-500' }
|
{ label: 'Last Active', value: 'Today, 10:15 AM', icon: Calendar, color: 'text-slate-500' }
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<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 key={i} className="flex items-center justify-between p-4 bg-slate-50/50 border border-slate-100 rounded-3xl">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-3">
|
||||||
<item.icon className={`w-3.5 h-3.5 ${item.color}`} />
|
<item.icon className={`w-4 h-4 ${item.color}`} />
|
||||||
<span className="text-slate-500">{item.label}</span>
|
<span className="text-[11px] font-black text-slate-500 uppercase tracking-tighter">{item.label}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-slate-900 font-extrabold">{item.value}</span>
|
<span className="text-[11px] font-black text-slate-900">{item.value}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-2">
|
<div className="pt-4">
|
||||||
<Link
|
<Link
|
||||||
href={`/employer/workers/${conversation.id}`}
|
href={`/employer/workers/${conversation.id}`}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
<Eye className="w-4 h-4" />
|
<span>Full Dossier</span>
|
||||||
<span>Open Worker Profile</span>
|
<ChevronRight className="w-4 h-4" />
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,26 +14,16 @@ 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 || 'Fatima Al Mansoori',
|
name: employerProfile?.name || '',
|
||||||
company_name: employerProfile?.company_name || 'Household Sponsor Account',
|
company_name: employerProfile?.company_name || '',
|
||||||
email: employerProfile?.email || 'fatima.sponsor@marketplace.ae',
|
email: employerProfile?.email || '',
|
||||||
phone: employerProfile?.phone || '+971 50 111 2222',
|
phone: employerProfile?.phone || '',
|
||||||
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: '',
|
||||||
@ -45,42 +35,39 @@ export default function Profile({ employerProfile }) {
|
|||||||
|
|
||||||
const handleSave = (e) => {
|
const handleSave = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSaved(true);
|
post('/employer/profile/update', {
|
||||||
toast.success("🛡️ Profile updated successfully", {
|
preserveScroll: true,
|
||||||
description: "Household sponsor preferences and vetted credentials synchronized successfully."
|
onSuccess: () => {
|
||||||
|
setSaved(true);
|
||||||
|
setTimeout(() => setSaved(false), 3000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
setTimeout(() => setSaved(false), 3000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'personal', label: 'Personal & Household', icon: User },
|
{ id: 'personal', label: 'Personal Information', icon: User },
|
||||||
{ id: 'company', label: 'Verification & History', icon: ShieldCheck },
|
{ id: 'company', label: 'Company & Verification', icon: Building2 },
|
||||||
{ id: 'security', label: 'Security & Password', icon: Lock },
|
{ id: 'security', label: 'Security & Password', icon: Lock },
|
||||||
{ id: 'billing', label: 'Billing & Receipts', icon: CreditCard },
|
{ id: 'billing', label: 'Billing & History', icon: CreditCard },
|
||||||
{ id: 'notifications', label: 'Notifications Hub', icon: Bell },
|
{ id: 'notifications', label: 'Notification Settings', 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 w-full pb-16">
|
<div className="flex flex-col lg:flex-row gap-8 select-none">
|
||||||
{/* Left Side: Navigation Tabs */}
|
{/* Left Side: Navigation Tabs */}
|
||||||
<aside className="w-full lg:w-72 space-y-2 flex-shrink-0">
|
<aside className="w-full lg:w-72 space-y-2">
|
||||||
{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-xs font-black uppercase tracking-wider transition-all ${
|
className={`w-full flex items-center justify-between p-4 rounded-2xl text-sm font-bold transition-all ${
|
||||||
activeTab === tab.id
|
activeTab === tab.id
|
||||||
? 'bg-[#185FA5] text-white shadow-md shadow-blue-900/10'
|
? 'bg-[#185FA5] text-white shadow-lg shadow-blue-200'
|
||||||
: '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'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -98,30 +85,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 pointer-events-none" />
|
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-50 rounded-full -mr-16 -mt-16 opacity-50" />
|
||||||
|
|
||||||
<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-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">
|
<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">
|
||||||
{profile.name?.charAt(0)}
|
{profile.name?.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<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">
|
||||||
<Camera className="w-3.5 h-3.5" />
|
<Camera className="w-4 h-4" />
|
||||||
</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-xl font-black text-slate-900 leading-tight">{profile.name}</h2>
|
<h2 className="text-2xl font-black text-slate-900 leading-tight">{profile.name}</h2>
|
||||||
<p className="text-slate-500 font-bold text-xs mt-1">{profile.company_name}</p>
|
<p className="text-slate-500 font-bold text-sm mt-1">{profile.company_name}</p>
|
||||||
|
|
||||||
<div className="flex flex-wrap justify-center md:justify-start gap-2.5 mt-4">
|
<div className="flex flex-wrap justify-center md:justify-start gap-3 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>Direct Sponsor Portal</span>
|
<span>Verified Employer</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -134,161 +121,107 @@ 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 & Household details</h3>
|
<h3 className="text-lg font-black text-slate-900">Personal Information</h3>
|
||||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Manage sponsor bio and residence style</p>
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Manage your basic account details</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 Sponsor Name</label>
|
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Full 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-xs 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"
|
||||||
/>
|
/>
|
||||||
</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">Invoicing Email</label>
|
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Email Address</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-xs 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"
|
||||||
/>
|
/>
|
||||||
</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">UAE Mobile Contact</label>
|
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Phone Number</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-xs 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"
|
||||||
/>
|
/>
|
||||||
</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">Sponsor Nationality</label>
|
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Preferred Language</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.nationality}
|
value={profile.language}
|
||||||
onChange={(e) => setProfile({ ...profile, nationality: e.target.value })}
|
onChange={(e) => setProfile({ ...profile, language: 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"
|
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"
|
||||||
>
|
>
|
||||||
<option>UAE National</option>
|
<option>English</option>
|
||||||
<option>Expat (UK / Europe)</option>
|
<option>Arabic</option>
|
||||||
<option>Expat (USA / Canada)</option>
|
|
||||||
<option>Expat (Asia / GCC)</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{errors.language && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.language}</p>}
|
||||||
|
|
||||||
<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">
|
<div className="space-y-6">
|
||||||
{/* Emirates ID Verification */}
|
<div className="border-b border-slate-100 pb-4">
|
||||||
<div className="space-y-6">
|
<h3 className="text-lg font-black text-slate-900">Company & Verification</h3>
|
||||||
<div className="border-b border-slate-100 pb-4">
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Manage your professional credentials</p>
|
||||||
<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">Regulatory compliance credentials</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 bg-slate-50 rounded-3xl border border-slate-150 flex items-start space-x-4">
|
|
||||||
<div className="p-3 bg-emerald-100 text-emerald-600 rounded-2xl">
|
|
||||||
<ShieldCheck className="w-6 h-6" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-extrabold text-slate-900">Emirates ID Vetted Status</h4>
|
|
||||||
<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">
|
|
||||||
Your Emirates ID is active and fully validated under Ministry of Human Resources (MOHRE) guidelines. Direct hiring is legally enabled.
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center space-x-2 mt-4 text-emerald-600 font-bold text-[10px] uppercase">
|
|
||||||
<CheckCircle2 className="w-4 h-4" />
|
|
||||||
<span>Verified on Jan 15, 2026</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hiring History Logs */}
|
<div className="space-y-2">
|
||||||
<div className="space-y-4">
|
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Company / Household Name</label>
|
||||||
<div className="flex items-center space-x-2 text-slate-800 font-bold">
|
<div className="relative">
|
||||||
<History className="w-5 h-5 text-[#185FA5]" />
|
<Building2 className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
|
||||||
<h3 className="font-extrabold text-sm uppercase">Sponsorship Hiring History Logs</h3>
|
<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>
|
</div>
|
||||||
|
{errors.company_name && <p className="text-rose-500 text-[10px] font-bold mt-1 ml-1">{errors.company_name}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="bg-slate-50 rounded-2xl border border-slate-150 overflow-hidden divide-y divide-slate-150">
|
<div className="p-6 bg-slate-50 rounded-3xl border border-slate-100 flex items-start space-x-4">
|
||||||
{pastHires.map((hire) => (
|
<div className="p-3 bg-emerald-100 rounded-2xl">
|
||||||
<div key={hire.id} className="p-4 flex items-center justify-between text-xs font-semibold text-slate-700">
|
<ShieldCheck className="w-6 h-6 text-emerald-600" />
|
||||||
<div className="space-y-0.5">
|
</div>
|
||||||
<div className="font-bold text-slate-800">{hire.name} ({hire.nationality})</div>
|
<div>
|
||||||
<div className="text-[9px] text-slate-400 uppercase">{hire.category} • Hired: {hire.date}</div>
|
<h4 className="font-black text-slate-900">Emirates ID Verification</h4>
|
||||||
</div>
|
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mt-0.5">Reference: EID-9182-XJ</p>
|
||||||
<span className="px-2.5 py-0.5 bg-blue-50 border border-blue-100 text-[#185FA5] text-[9px] font-black rounded uppercase">
|
<p className="text-xs text-slate-600 mt-2 font-medium leading-relaxed">
|
||||||
{hire.status}
|
Your Emirates ID has been successfully vetted and matched with our internal records. This allows you to initiate direct contact with all verified workers.
|
||||||
</span>
|
</p>
|
||||||
</div>
|
<div className="flex items-center space-x-2 mt-4 text-emerald-600">
|
||||||
))}
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
|
<span className="text-[10px] font-black uppercase">Verified on Jan 15, 2026</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -298,7 +231,7 @@ export default function Profile({ employerProfile }) {
|
|||||||
<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 sponsor account secure</p>
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Keep your account protected</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 max-w-md">
|
<div className="space-y-4 max-w-md">
|
||||||
@ -309,8 +242,9 @@ 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-xs font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
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"
|
||||||
/>
|
/>
|
||||||
|
{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>
|
||||||
@ -319,8 +253,9 @@ 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-xs font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
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"
|
||||||
/>
|
/>
|
||||||
|
{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>
|
||||||
@ -329,8 +264,9 @@ 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-xs font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
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"
|
||||||
/>
|
/>
|
||||||
|
{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>
|
||||||
@ -339,13 +275,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 & Receipts</h3>
|
<h3 className="text-lg font-black text-slate-900">Billing & History</h3>
|
||||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Manage active sponsorship access</p>
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Manage your subscriptions and payments</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 pointer-events-none" />
|
<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="relative z-10">
|
<div className="relative z-10">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@ -359,10 +295,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">Jun 15, 2026</div>
|
<div className="text-sm font-bold italic">Dec 15, 2026</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-3xl font-black tracking-tighter">
|
<div className="text-3xl font-black tracking-tighter">
|
||||||
199 <span className="text-sm font-bold text-slate-400">AED/mo</span>
|
499 <span className="text-sm font-bold text-slate-400">AED/mo</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -383,19 +319,15 @@ export default function Profile({ employerProfile }) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100">
|
<tbody className="divide-y divide-slate-100">
|
||||||
{[
|
{[
|
||||||
{ date: 'May 01, 2026', desc: 'Premium Monthly Subscription', amount: '199 AED' },
|
{ date: 'Nov 15, 2026', desc: 'Premium Monthly Subscription', amount: '499 AED' },
|
||||||
{ date: 'Apr 01, 2026', desc: 'Premium Monthly Subscription', amount: '199 AED' }
|
{ date: 'Oct 15, 2026', desc: 'Premium Monthly Subscription', amount: '499 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
|
<button className="p-2 text-slate-400 hover:text-[#185FA5] transition-colors">
|
||||||
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>
|
||||||
@ -411,19 +343,20 @@ 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 Hub</h3>
|
<h3 className="text-lg font-black text-slate-900">Notification Settings</h3>
|
||||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Choose how you want to receive alerts</p>
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Choose how you want to be notified</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[
|
{[
|
||||||
{ title: 'Email Alerts', desc: 'Receive vetted worker recommendations and visa updates.' },
|
{ title: 'Email Notifications', desc: 'Receive daily updates and recruitment alerts via email.' },
|
||||||
{ title: 'SMS Instant Receipts', desc: 'Get SMS notifications for interview schedules.' },
|
{ title: 'SMS Alerts', desc: 'Get instant text messages for direct message replies.' },
|
||||||
{ title: 'Push Notifications', desc: 'Get desktop sound alerts for new direct messages.' }
|
{ title: 'Browser Notifications', desc: 'Real-time alerts while using the portal.' },
|
||||||
|
{ 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-xs font-black text-slate-900">{n.title}</div>
|
<div className="text-sm 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
|
||||||
@ -447,13 +380,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 Emirates ID re-verification.
|
Some updates may require additional verification before taking effect.
|
||||||
</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-md 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-lg shadow-blue-200/50"
|
||||||
>
|
>
|
||||||
<Save className="w-4 h-4" />
|
<Save className="w-4 h-4" />
|
||||||
<span>Update Settings</span>
|
<span>Update Settings</span>
|
||||||
|
|||||||
@ -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 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 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}`}>
|
||||||
|
|||||||
@ -1,197 +1,71 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Head } from '@inertiajs/react';
|
import { Head, Link } from '@inertiajs/react';
|
||||||
import EmployerLayout from '../../Layouts/EmployerLayout';
|
import EmployerLayout from '../../Layouts/EmployerLayout';
|
||||||
import {
|
import { CreditCard, CheckCircle2, AlertCircle, ArrowRight, Sparkles } from 'lucide-react';
|
||||||
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 [selectedPlan, setSelectedPlan] = useState(null);
|
const [selectedId, setSelectedId] = useState('premium');
|
||||||
const [showPayTabsModal, setShowPayTabsModal] = useState(false);
|
|
||||||
const [showSuccess, setShowSuccess] = useState(false);
|
const [showSuccess, setShowSuccess] = useState(false);
|
||||||
|
|
||||||
// Card inputs for PayTabs Gateway
|
|
||||||
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 handleSelect = (id) => {
|
||||||
const [simulatedFailedState, setSimulatedFailedState] = useState(false);
|
setSelectedId(id);
|
||||||
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);
|
||||||
setActivePlan(selectedPlan.name);
|
setTimeout(() => setShowSuccess(false), 4000);
|
||||||
|
|
||||||
// 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="Sponsor Pass & Billing">
|
<EmployerLayout title="Manage Subscription">
|
||||||
<Head title="Subscription Billing & Invoices - Employer Portal" />
|
<Head title="Subscription - Employer Portal" />
|
||||||
|
|
||||||
<div className="space-y-8 select-none w-full pb-16">
|
<div className="space-y-8 select-none">
|
||||||
|
{/* Current Status Banner */}
|
||||||
{/* Simulated payment failed retry block */}
|
<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">
|
||||||
{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 border border-blue-100 flex-shrink-0">
|
<div className="w-12 h-12 rounded-xl bg-blue-50 text-[#185FA5] flex items-center justify-center font-bold">
|
||||||
<CreditCard className="w-6 h-6" />
|
<CreditCard className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Active Plan Pass</div>
|
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Current Active Plan</div>
|
||||||
<h3 className="text-lg font-black text-slate-900 tracking-tight mt-0.5">{activePlan}</h3>
|
<div className="text-lg font-bold text-slate-900">{currentPlan}</div>
|
||||||
<p className="text-xs text-slate-500 font-semibold mt-0.5">
|
<div className="text-xs text-slate-500 mt-0.5">Renews automatically on <span className="font-bold text-slate-700">{expiresAt}</span></div>
|
||||||
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-3.5">
|
<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">
|
||||||
<button
|
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
||||||
onClick={triggerFailedPaymentSimulation}
|
<span>Active & Verified</span>
|
||||||
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" />
|
|
||||||
<span>Verified Sponsor Status</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showSuccess && (
|
{showSuccess && (
|
||||||
<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">
|
<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">
|
||||||
<Sparkles className="w-4 h-4 text-amber-300" />
|
<Sparkles className="w-4 h-4" />
|
||||||
<span>Sponsor Pass updated successfully! PayTabs transaction logged.</span>
|
<span>Plan successfully updated! Mock PayTabs billing cycle applied.</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Available Plan Tiers Pricing Grid */}
|
{/* Pricing Grid */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-black text-slate-900 tracking-tight">Available Subscription Tiers</h2>
|
<h2 className="text-lg font-bold text-slate-900 tracking-tight">Available Subscription Tiers</h2>
|
||||||
<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>
|
<p className="text-xs text-slate-500 mt-0.5">Upgrade or change your plan anytime. Zero agency recruitment commissions.</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 isCurrent = activePlan.toLowerCase().includes(plan.id.toLowerCase());
|
const isSelected = selectedId === plan.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={plan.id}
|
key={plan.id}
|
||||||
className={`bg-white rounded-3xl border shadow-sm flex flex-col justify-between overflow-hidden transition-all relative ${
|
className={`bg-white rounded-2xl border-2 shadow-sm flex flex-col justify-between overflow-hidden transition-all relative ${
|
||||||
isCurrent
|
isSelected
|
||||||
? 'border-[#185FA5] ring-4 ring-[#185FA5]/10 scale-[1.02]'
|
? 'border-[#185FA5] ring-4 ring-[#185FA5]/10'
|
||||||
: '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-[9px] font-black uppercase tracking-wider text-center py-1.5 shadow-xs">
|
<div className="bg-[#185FA5] text-white text-[10px] font-bold uppercase tracking-wider text-center py-1 shadow-xs">
|
||||||
Most Popular Tier
|
Most Popular Tier
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -199,17 +73,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-extrabold text-base text-slate-900">{plan.name}</h3>
|
<h3 className="font-bold text-base text-slate-900">{plan.name}</h3>
|
||||||
<div className="mt-2.5 flex items-baseline space-x-1">
|
<div className="mt-2 flex items-baseline space-x-1">
|
||||||
<span className="text-3xl font-black text-slate-900 tracking-tight">{plan.price}</span>
|
<span className="text-3xl font-extrabold text-slate-900">{plan.price}</span>
|
||||||
<span className="text-xs font-semibold text-slate-400">/ {plan.period}</span>
|
<span className="text-xs font-semibold text-slate-500">/ {plan.period}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="space-y-3 pt-5 border-t border-slate-100">
|
<ul className="space-y-3 pt-4 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 font-medium">
|
<li key={f} className="flex items-start space-x-3 text-xs text-slate-600">
|
||||||
<Check className="w-4 h-4 text-emerald-600 flex-shrink-0 mt-0.5" />
|
<CheckCircle2 className="w-4 h-4 text-emerald-600 flex-shrink-0 mt-0.5" />
|
||||||
<span>{f}</span>
|
<span>{f}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@ -218,14 +92,14 @@ export default function Subscription({ currentPlan, expiresAt, plans }) {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleSelectPlan(plan)}
|
onClick={() => handleSelect(plan.id)}
|
||||||
className={`w-full h-11 rounded-xl font-bold text-xs flex items-center justify-center transition-all shadow-xs mt-6 ${
|
className={`w-full h-11 rounded-xl font-bold text-xs flex items-center justify-center transition-colors shadow-sm ${
|
||||||
isCurrent
|
isSelected
|
||||||
? 'bg-emerald-600 hover:bg-emerald-700 text-white cursor-default'
|
? 'bg-emerald-600 hover:bg-emerald-700 text-white'
|
||||||
: 'bg-[#185FA5] hover:bg-[#144f8a] text-white'
|
: 'bg-[#185FA5] hover:bg-[#144f8a] text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isCurrent ? 'Current Active Tier ✓' : 'Select Plan Tier'}
|
{isSelected ? 'Current Active Tier' : 'Select Plan Tier'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -233,200 +107,7 @@ 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>
|
|
||||||
<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>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { Head, Link, router } from '@inertiajs/react';
|
import { Head, Link } from '@inertiajs/react';
|
||||||
import EmployerLayout from '../../../Layouts/EmployerLayout';
|
import EmployerLayout from '../../../Layouts/EmployerLayout';
|
||||||
import {
|
import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@ -15,240 +15,72 @@ 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 }) {
|
||||||
const [showReportModal, setShowReportModal] = useState(false);
|
|
||||||
const [reportReason, setReportReason] = useState('');
|
|
||||||
const [reportDetails, setReportDetails] = useState('');
|
|
||||||
const [isShortlisted, setIsShortlisted] = useState(false);
|
|
||||||
|
|
||||||
// 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 (
|
return (
|
||||||
<EmployerLayout title="Candidate Profile Detail">
|
<EmployerLayout title="Worker Profile">
|
||||||
<Head title={`${worker.name} - ${worker.category} Profile`} />
|
<Head title={`${worker.name} - Worker Profile`} />
|
||||||
|
|
||||||
<div className="space-y-6 select-none w-full pb-16">
|
<div className="space-y-6 select-none w-full">
|
||||||
|
<Link
|
||||||
{/* Back Link */}
|
href="/employer/workers"
|
||||||
<div className="flex items-center justify-between">
|
className="inline-flex items-center space-x-2 text-xs font-bold text-slate-600 hover:text-[#185FA5] transition-colors"
|
||||||
<Link
|
>
|
||||||
href="/employer/workers"
|
<ArrowLeft className="w-4 h-4" />
|
||||||
className="inline-flex items-center space-x-2 text-xs font-black text-slate-500 hover:text-[#185FA5] transition-colors"
|
<span>Back to Find Workers</span>
|
||||||
>
|
</Link>
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
<span>BACK TO FIND WORKERS</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Quick Profile Actions */}
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
||||||
<div className="flex items-center space-x-2">
|
{/* Header Card */}
|
||||||
<button
|
<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">
|
||||||
onClick={handleShare}
|
<div className="absolute -right-10 -bottom-10 w-48 h-48 bg-white/10 rounded-full blur-2xl pointer-events-none" />
|
||||||
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-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">
|
<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">
|
||||||
{worker.photo ? (
|
{worker.name.charAt(0)}
|
||||||
<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.5">
|
<div className="space-y-1">
|
||||||
<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 text-slate-900">{worker.name}</h1>
|
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight">{worker.name}</h1>
|
||||||
{worker.verified && (
|
{worker.verified && (
|
||||||
<span className="bg-emerald-500 text-white p-0.5 rounded-full shadow-sm animate-pulse" title="OCR Verified">
|
<span className="bg-emerald-500 text-white p-0.5 rounded-full shadow-sm" 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">
|
||||||
{/* Emirates ID verification status bar & Visa Status */}
|
<Globe2 className="w-4 h-4" />
|
||||||
<div className="flex flex-wrap gap-2">
|
<span>{worker.nationality}</span>
|
||||||
<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>
|
|
||||||
<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>
|
||||||
|
|
||||||
{/* Sponsor Actions Block */}
|
<div className="flex items-center space-x-3 z-10 w-full sm:w-auto justify-end">
|
||||||
<div className="flex flex-wrap items-center gap-3 z-10 w-full sm:w-auto justify-end">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleToggleShortlist}
|
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"
|
||||||
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 ${isShortlisted ? 'fill-[#185FA5]' : ''}`} />
|
<Bookmark className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Stage 3 Connect: Start In-App Chat */}
|
|
||||||
<Link
|
<Link
|
||||||
href={`/employer/messages/start/${worker.id}`}
|
href={`/employer/workers/${worker.id}/hire`}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
<MessageSquare className="w-4 h-4 text-slate-500" />
|
<Sparkles className="w-4 h-4" />
|
||||||
<span>Start Direct Chat</span>
|
<span>Hire Now</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
{/* Stage 4 Hire: Mark Hired (Finalize Hiring Direct Flow) */}
|
href="/employer/messages"
|
||||||
<button
|
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"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="w-4 h-4 text-white" />
|
<MessageSquare className="w-4 h-4" />
|
||||||
<span>Mark Hired</span>
|
<span>Message</span>
|
||||||
</button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -257,80 +89,63 @@ 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-black text-xs text-slate-500 uppercase tracking-widest">Sponsorship Details</h3>
|
<h3 className="font-bold text-sm text-slate-800 uppercase tracking-wider">Verification & Cost</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-bold">
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium">
|
||||||
<DollarSign className="w-4 h-4 text-emerald-600" />
|
<DollarSign className="w-4 h-4 text-emerald-600" />
|
||||||
<span>Salary Expectations</span>
|
<span>Monthly Salary</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-extrabold text-slate-900 text-sm">{worker.salary} AED / mo</span>
|
<span className="font-bold text-slate-900 text-sm">{worker.salary} AED</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-bold">
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium">
|
||||||
<Calendar className="w-4 h-4 text-[#185FA5]" />
|
<Calendar className="w-4 h-4 text-[#185FA5]" />
|
||||||
<span>Availability Status</span>
|
<span>Availability</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-black text-[#185FA5] bg-blue-50 px-2 py-0.5 rounded text-xs border border-blue-100">
|
<span className="font-bold text-[#185FA5] bg-blue-50 px-2 py-0.5 rounded text-xs">
|
||||||
{availabilityStatus}
|
{worker.availability}
|
||||||
</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-bold">
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium">
|
||||||
<Briefcase className="w-4 h-4 text-amber-600" />
|
<Briefcase className="w-4 h-4 text-amber-600" />
|
||||||
<span>Work Experience</span>
|
<span>Experience</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-extrabold text-slate-900 text-xs">{worker.experience}</span>
|
<span className="font-bold 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-bold">
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium">
|
||||||
<Sparkles className="w-4 h-4 text-purple-600" />
|
<HeartHandshake className="w-4 h-4 text-purple-600" />
|
||||||
<span>Preferred Job Type</span>
|
<span>Religion</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-black text-slate-900 text-xs uppercase">{worker.preferred_job_type}</span>
|
<span className="font-bold text-slate-900 text-xs">{worker.religion}</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-bold">
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium">
|
||||||
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
<ShieldCheck className="w-4 h-4 text-emerald-600" />
|
||||||
<span>Medical Health Test</span>
|
<span>Medical Cert</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-black text-emerald-700 bg-emerald-50 px-2.5 py-0.5 rounded text-[10px] uppercase border border-emerald-200">
|
<span className="font-bold text-emerald-700 bg-emerald-50 px-2 py-0.5 rounded text-xs">
|
||||||
PASSED CERTIFIED
|
Passed
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Languages Spoken with visual flags */}
|
{/* Languages Spoken */}
|
||||||
<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-extrabold shadow-xs uppercase flex items-center space-x-1.5">
|
<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>{getLanguageFlag(lang)}</span>
|
{lang}
|
||||||
<span>{lang}</span>
|
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -341,7 +156,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-extrabold text-base text-slate-900">Professional Summary</h3>
|
<h3 className="font-bold 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>
|
||||||
@ -349,12 +164,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-extrabold text-base text-slate-900">Verified Skills & Competencies</h3>
|
<h3 className="font-bold 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-xs">
|
<div key={skill} className="flex items-center space-x-3 p-3.5 bg-white border border-slate-200 rounded-xl shadow-sm">
|
||||||
<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 uppercase tracking-wide">{skill}</span>
|
<span className="font-bold text-xs text-slate-800">{skill}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -362,46 +177,51 @@ export default function Show({ worker }) {
|
|||||||
|
|
||||||
{/* Verified Documents Section */}
|
{/* Verified Documents Section */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="font-extrabold text-base text-slate-900 flex items-center space-x-2">
|
<h3 className="font-bold 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 (UAE MOHRE Vetted)</span>
|
<span>Verified Legal Documents</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-xs hover:shadow-sm">
|
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow">
|
||||||
<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-[10px] font-black text-slate-500 uppercase tracking-widest">Passport (Verified)</span>
|
<span className="text-[11px] 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">
|
||||||
<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">
|
{/* Document Thumbnail Placeholder */}
|
||||||
<Search className="w-5 h-5 text-slate-300" />
|
<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">
|
||||||
<span className="absolute bottom-1 text-[7px] font-bold text-slate-400 uppercase">Click zoom</span>
|
<div className="w-full h-full bg-blue-50/50 rounded flex items-center justify-center">
|
||||||
|
<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>
|
||||||
|
|
||||||
<div className="flex-1 space-y-2">
|
{/* Extracted Data */}
|
||||||
|
<div className="flex-1 space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">Document ID</div>
|
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Document Number</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-1">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">Issue</div>
|
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Issue Date</div>
|
||||||
<div className="text-[10px] font-bold text-slate-800">12 Jan 2021</div>
|
<div className="text-xs font-bold text-slate-800">12 Jan 2021</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">Expiry</div>
|
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Expiry Date</div>
|
||||||
<div className="text-[10px] font-bold text-emerald-600">11 Jan 2031</div>
|
<div className="text-xs font-bold text-emerald-600">11 Jan 2031</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">OCR Match</div>
|
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">OCR Match Accuracy</div>
|
||||||
<div className="flex items-center space-x-2 mt-0.5">
|
<div className="flex items-center space-x-2 mt-1">
|
||||||
<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-[95%]" />
|
<div className="h-full bg-emerald-500 w-[98%]" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[9px] font-bold text-emerald-600">95%</span>
|
<span className="text-[10px] font-black text-emerald-600">98%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -409,27 +229,42 @@ 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-xs hover:shadow-sm">
|
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow">
|
||||||
<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-[10px] font-black text-slate-500 uppercase tracking-widest">Entry Visa (Verified)</span>
|
<span className="text-[11px] 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">
|
||||||
<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">
|
{/* Document Thumbnail Placeholder */}
|
||||||
<Search className="w-5 h-5 text-slate-300" />
|
<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">
|
||||||
<span className="absolute bottom-1 text-[7px] font-bold text-slate-400 uppercase">Click zoom</span>
|
<div className="w-full h-full bg-blue-50/50 rounded flex items-center justify-center">
|
||||||
|
<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>
|
||||||
|
|
||||||
<div className="flex-1 space-y-2">
|
{/* Extracted Data */}
|
||||||
|
<div className="flex-1 space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">Emirates ID status</div>
|
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Visa Reference</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{worker.emirates_id_status}</div>
|
<div className="text-xs font-bold text-slate-800">UAE-2024-918273</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 className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Expiry Date</div>
|
||||||
|
<div className="text-xs font-bold text-amber-600">30 Dec 2026</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">Vetting status</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-0.5">
|
<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-[9px] font-bold text-slate-700">Clear Records</span>
|
<span className="text-[10px] font-bold text-slate-700">No Criminal Record Found</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -441,262 +276,7 @@ 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>
|
|
||||||
<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>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,15 +27,6 @@
|
|||||||
|
|
||||||
// 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)
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
<?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;
|
||||||
|
|
||||||
@ -26,19 +25,12 @@
|
|||||||
|
|
||||||
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', [SponsorController::class, 'index'])->name('admin.employers');
|
Route::get('/employers', function () {
|
||||||
Route::post('/employers/{id}/verify', [SponsorController::class, 'verify'])->name('admin.employers.verify');
|
return Inertia::render('Admin/Employers/Index');
|
||||||
Route::post('/employers/{id}/suspend', [SponsorController::class, 'suspend'])->name('admin.employers.suspend');
|
})->name('admin.employers');
|
||||||
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');
|
||||||
@ -48,8 +40,6 @@
|
|||||||
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');
|
||||||
@ -57,20 +47,6 @@
|
|||||||
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
|
||||||
@ -81,8 +57,6 @@
|
|||||||
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');
|
||||||
|
|||||||
@ -20,11 +20,5 @@ 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',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user