mohan #5
@ -26,13 +26,17 @@ public function login(Request $request)
|
|||||||
'password' => ['required'],
|
'password' => ['required'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Mock authentication check for scaffolding without database driver dependency
|
// Database-backed authentication check
|
||||||
if ($credentials['email'] === 'admin@example.com' && $credentials['password'] === 'password') {
|
$user = \App\Models\User::where('email', $credentials['email'])->first();
|
||||||
|
|
||||||
|
if ($user && \Illuminate\Support\Facades\Hash::check($credentials['password'], $user->password) && $user->role === 'admin') {
|
||||||
|
auth()->login($user);
|
||||||
|
|
||||||
session(['user' => (object)[
|
session(['user' => (object)[
|
||||||
'id' => 1,
|
'id' => $user->id,
|
||||||
'name' => 'Portal Admin',
|
'name' => $user->name,
|
||||||
'email' => 'admin@example.com',
|
'email' => $user->email,
|
||||||
'role' => 'admin',
|
'role' => $user->role,
|
||||||
]]);
|
]]);
|
||||||
|
|
||||||
$request->session()->regenerate();
|
$request->session()->regenerate();
|
||||||
|
|||||||
@ -32,31 +32,9 @@ public function safety()
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$rules = [
|
$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 = [
|
$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', [
|
return Inertia::render('Admin/Safety/Index', [
|
||||||
'reports' => $reports,
|
'reports' => $reports,
|
||||||
@ -264,66 +242,29 @@ public function addDisputeNote(Request $request, $id)
|
|||||||
*/
|
*/
|
||||||
public function notifications()
|
public function notifications()
|
||||||
{
|
{
|
||||||
$campaigns = [
|
$campaigns = DB::table('audit_logs')
|
||||||
[
|
->where('category', 'campaign')
|
||||||
'id' => 1,
|
->orderBy('created_at', 'desc')
|
||||||
'channel' => 'Push Notification',
|
->get()
|
||||||
|
->map(function ($log) {
|
||||||
|
$data = json_decode($log->action, true) ?: [
|
||||||
|
'channel' => 'push',
|
||||||
'recipient_type' => 'All Active Workers',
|
'recipient_type' => 'All Active Workers',
|
||||||
'title' => 'Availability Update Reminder',
|
'title' => 'Broadcast Alert',
|
||||||
'message' => 'Hi, please update your availability status in the app to remain visible to hiring employers this week!',
|
'message' => $log->action
|
||||||
'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'
|
|
||||||
]
|
|
||||||
];
|
];
|
||||||
|
return [
|
||||||
$whatsappTemplates = [
|
'id' => $log->id,
|
||||||
[
|
'channel' => $data['channel'] ?? 'push',
|
||||||
'name' => 'availability_checkin_trigger',
|
'recipient_type' => $data['recipient_type'] ?? 'All Active Workers',
|
||||||
'category' => 'Utility',
|
'title' => $data['title'] ?? 'Broadcast Alert',
|
||||||
'language' => 'English & Arabic',
|
'message' => $data['message'] ?? '',
|
||||||
'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.',
|
'sent_at' => date('Y-m-d H:i', strtotime($log->created_at)),
|
||||||
'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', [
|
return Inertia::render('Admin/Notifications/Index', [
|
||||||
'campaigns' => $campaigns,
|
'campaigns' => $campaigns,
|
||||||
'whatsapp_templates' => $whatsappTemplates
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,6 +280,22 @@ public function broadcastNotification(Request $request)
|
|||||||
'message' => 'required|string'
|
'message' => 'required|string'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$campaignData = [
|
||||||
|
'channel' => $request->channel,
|
||||||
|
'recipient_type' => $request->recipients,
|
||||||
|
'title' => $request->title,
|
||||||
|
'message' => $request->message,
|
||||||
|
];
|
||||||
|
|
||||||
|
DB::table('audit_logs')->insert([
|
||||||
|
'category' => 'campaign',
|
||||||
|
'user' => auth()->user() ? auth()->user()->email : 'Admin',
|
||||||
|
'action' => json_encode($campaignData),
|
||||||
|
'ip_address' => $request->ip(),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
return back()->with('success', "Notification campaign broadcasted successfully to all {$request->recipients} users!");
|
return back()->with('success', "Notification campaign broadcasted successfully to all {$request->recipients} users!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,6 +309,24 @@ public function refundPayment(Request $request, $id)
|
|||||||
'amount' => 'required|numeric|min:1'
|
'amount' => 'required|numeric|min:1'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$payment = DB::table('payments')->where('id', $id)->first();
|
||||||
|
if ($payment) {
|
||||||
|
DB::table('payments')->where('id', $id)->update([
|
||||||
|
'status' => 'refunded',
|
||||||
|
'description' => $payment->description . " (Refunded: " . $request->refund_reason . ")",
|
||||||
|
'updated_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('audit_logs')->insert([
|
||||||
|
'category' => 'admin_action',
|
||||||
|
'user' => auth()->user() ? auth()->user()->email : 'Admin',
|
||||||
|
'action' => "Refunded payment #{$id} of amount AED {$request->amount}. Reason: {$request->refund_reason}",
|
||||||
|
'ip_address' => $request->ip() ?: '127.0.0.1',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return back()->with('success', "Refund of AED {$request->amount} initiated successfully for transaction {$id}. Refund reason: {$request->refund_reason}");
|
return back()->with('success', "Refund of AED {$request->amount} initiated successfully for transaction {$id}. Refund reason: {$request->refund_reason}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,62 +14,118 @@ class DashboardController extends Controller
|
|||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
// Dynamic Database Queries
|
// Dynamic Database Queries
|
||||||
$totalWorkers = \App\Models\Worker::count() ?: 1420;
|
$totalWorkers = \App\Models\Worker::count();
|
||||||
$activeWorkers = \App\Models\Worker::where('status', 'active')->count() ?: 980;
|
$activeWorkers = \App\Models\Worker::where('status', 'active')->count();
|
||||||
$inactiveWorkers = $totalWorkers - $activeWorkers;
|
$inactiveWorkers = $totalWorkers - $activeWorkers;
|
||||||
|
|
||||||
$totalSponsors = \App\Models\Sponsor::count();
|
$totalSponsors = \App\Models\Sponsor::count();
|
||||||
$activeSubs = \App\Models\Sponsor::where('subscription_status', 'active')->count();
|
$activeSubs = \App\Models\Sponsor::where('subscription_status', 'active')->count();
|
||||||
$expiredSubs = \App\Models\Sponsor::where('subscription_status', 'expired')->count();
|
$expiredSubs = \App\Models\Sponsor::where('subscription_status', 'expired')->count();
|
||||||
$newSponsors = \App\Models\Sponsor::where('created_at', '>=', now()->subDays(7))->count();
|
$newSponsors = \App\Models\Sponsor::where('created_at', '>=', now()->subDays(7))->count();
|
||||||
$revenueSum = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'success')->sum('amount') ?: 499.00;
|
$revenueSum = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'success')->sum('amount');
|
||||||
|
|
||||||
|
// Dynamic conversion rates
|
||||||
|
$verifiedCount = \App\Models\Worker::where('verified', true)->count();
|
||||||
|
$verificationRate = $totalWorkers > 0 ? round(($verifiedCount / $totalWorkers) * 100, 1) : 0.0;
|
||||||
|
|
||||||
|
$hiredCount = \App\Models\Worker::where('status', 'hired')->count();
|
||||||
|
$hiringConversionRate = $totalWorkers > 0 ? round(($hiredCount / $totalWorkers) * 100, 1) : 0.0;
|
||||||
|
|
||||||
|
$chatsCount = \Illuminate\Support\Facades\DB::table('conversations')->count();
|
||||||
|
$chatToHireRate = $chatsCount > 0 ? round(($hiredCount / $chatsCount) * 100, 1) : 0.0;
|
||||||
|
|
||||||
|
$activeRatio = $totalWorkers > 0 ? round(($activeWorkers / $totalWorkers) * 100) : 0;
|
||||||
|
$activeUsersRatio = $activeRatio . '% Active';
|
||||||
|
|
||||||
|
// Custom subscription trends
|
||||||
|
$months = [];
|
||||||
|
for ($i = 5; $i >= 0; $i--) {
|
||||||
|
$months[] = now()->subMonths($i);
|
||||||
|
}
|
||||||
|
|
||||||
|
$trendData = [];
|
||||||
|
foreach ($months as $date) {
|
||||||
|
$monthStart = $date->copy()->startOfMonth();
|
||||||
|
$monthEnd = $date->copy()->endOfMonth();
|
||||||
|
|
||||||
|
$basic = \App\Models\Sponsor::where('subscription_plan', 'basic')
|
||||||
|
->whereBetween('created_at', [$monthStart, $monthEnd])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$premium = \App\Models\Sponsor::where('subscription_plan', 'premium')
|
||||||
|
->whereBetween('created_at', [$monthStart, $monthEnd])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$vip = \App\Models\Sponsor::where('subscription_plan', 'vip')
|
||||||
|
->whereBetween('created_at', [$monthStart, $monthEnd])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$trendData[] = [
|
||||||
|
'month' => $date->format('M'),
|
||||||
|
'basic' => $basic,
|
||||||
|
'premium' => $premium,
|
||||||
|
'vip' => $vip,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funnel
|
||||||
|
$profilesBrowsed = \Illuminate\Support\Facades\DB::table('profile_views')->count();
|
||||||
|
$chatsInitiated = \Illuminate\Support\Facades\DB::table('conversations')->count();
|
||||||
|
$candidatesShortlisted = \Illuminate\Support\Facades\DB::table('shortlists')->count();
|
||||||
|
$workersHired = $hiredCount;
|
||||||
|
|
||||||
|
$verificationsToday = \App\Models\Worker::where('verified', true)
|
||||||
|
->where('updated_at', '>=', now()->startOfDay())
|
||||||
|
->count();
|
||||||
|
|
||||||
$stats = [
|
$stats = [
|
||||||
'total_workers' => $totalWorkers,
|
'total_workers' => $totalWorkers,
|
||||||
'active_workers' => $activeWorkers,
|
'active_workers' => $activeWorkers,
|
||||||
'inactive_workers' => $inactiveWorkers,
|
'inactive_workers' => $inactiveWorkers,
|
||||||
'total_employers' => $totalSponsors, // Kept key name to avoid breaking frontend destructuring
|
'total_employers' => $totalSponsors,
|
||||||
'active_subscriptions' => $activeSubs,
|
'active_subscriptions' => $activeSubs,
|
||||||
'revenue_this_month_aed' => $revenueSum,
|
'revenue_this_month_aed' => $revenueSum,
|
||||||
'new_employers_this_week' => $newSponsors,
|
'new_employers_this_week' => $newSponsors,
|
||||||
'expired_subscriptions' => $expiredSubs,
|
'expired_subscriptions' => $expiredSubs,
|
||||||
|
'hiring_conversion_rate' => $hiringConversionRate,
|
||||||
// Required Enhancements
|
'chat_to_hire_rate' => $chatToHireRate,
|
||||||
'hiring_conversion_rate' => 14.8,
|
'verification_rate' => $verificationRate,
|
||||||
'chat_to_hire_rate' => 12.6,
|
'active_users_ratio' => $activeUsersRatio,
|
||||||
'verification_rate' => 88.5,
|
'trend_data' => $trendData,
|
||||||
'active_users_ratio' => '69% Active',
|
'verifications_today' => $verificationsToday,
|
||||||
'subscription_trends' => [
|
'verified_workers_count' => $verifiedCount,
|
||||||
'Basic Search' => \App\Models\Sponsor::where('subscription_plan', 'basic')->count(),
|
'funnel' => [
|
||||||
'Premium Pass' => \App\Models\Sponsor::where('subscription_plan', 'premium')->count(),
|
'profiles_browsed' => $profilesBrowsed,
|
||||||
'VIP Concierge' => \App\Models\Sponsor::where('subscription_plan', 'vip')->count()
|
'chats_initiated' => $chatsInitiated,
|
||||||
|
'candidates_shortlisted' => $candidatesShortlisted,
|
||||||
|
'workers_hired' => $workersHired,
|
||||||
],
|
],
|
||||||
'worker_availability' => [
|
'worker_availability' => [
|
||||||
'Available Now' => \App\Models\Worker::where('status', 'active')->count() ?: 640,
|
'Active' => \App\Models\Worker::where('status', 'active')->count(),
|
||||||
'Engaged / Contracted' => \App\Models\Worker::where('status', 'hired')->count() ?: 480,
|
'Hidden' => \App\Models\Worker::where('status', 'hidden')->count(),
|
||||||
'In Interview' => 300
|
'Hired' => \App\Models\Worker::where('status', 'hired')->count()
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Process verifications automatically or fetched dynamically
|
// Process verifications automatically or fetched dynamically
|
||||||
$recentVerifications = [
|
$recentVerifications = \App\Models\Worker::where('verified', true)
|
||||||
[
|
->with(['documents' => function($q) {
|
||||||
'id' => 101,
|
$q->orderBy('created_at', 'desc');
|
||||||
'name' => 'Fatima Zahra',
|
}])
|
||||||
|
->latest('updated_at')
|
||||||
|
->take(5)
|
||||||
|
->get()
|
||||||
|
->map(function ($worker) {
|
||||||
|
$doc = $worker->documents->first();
|
||||||
|
return [
|
||||||
|
'id' => $worker->id,
|
||||||
|
'name' => $worker->name,
|
||||||
'type' => 'Worker',
|
'type' => 'Worker',
|
||||||
'processed_at' => '2 hours ago',
|
'processed_at' => $worker->updated_at ? $worker->updated_at->diffForHumans() : 'Recently',
|
||||||
'document_type' => 'Passport & Visa',
|
'document_type' => $doc ? ucfirst($doc->type) : 'Passport & Visa',
|
||||||
'status' => 'Auto-Approved',
|
'status' => 'Auto-Approved',
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 102,
|
|
||||||
'name' => 'Ahmed Mansoor',
|
|
||||||
'type' => 'Sponsor',
|
|
||||||
'processed_at' => '4 hours ago',
|
|
||||||
'document_type' => 'Emirates ID',
|
|
||||||
'status' => 'Auto-Approved',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
// Fetch recent subscriptions from Sponsors table
|
// Fetch recent subscriptions from Sponsors table
|
||||||
$recentSponsorsWithPlans = \App\Models\Sponsor::whereNotNull('subscription_plan')
|
$recentSponsorsWithPlans = \App\Models\Sponsor::whereNotNull('subscription_plan')
|
||||||
@ -88,18 +144,6 @@ public function index()
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($recentSubscriptions) === 0) {
|
|
||||||
$recentSubscriptions = [
|
|
||||||
[
|
|
||||||
'id' => 501,
|
|
||||||
'employer_name' => 'Ahmed Malik',
|
|
||||||
'plan_name' => 'Premium Pass',
|
|
||||||
'amount_aed' => 499,
|
|
||||||
'subscribed_at' => 'Today, 10:30 AM',
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return Inertia::render('Admin/Dashboard', [
|
return Inertia::render('Admin/Dashboard', [
|
||||||
'stats' => $stats,
|
'stats' => $stats,
|
||||||
'recent_verifications' => $recentVerifications,
|
'recent_verifications' => $recentVerifications,
|
||||||
|
|||||||
@ -13,59 +13,23 @@ class WorkerController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
// Scaffolding mock dataset for worker management
|
$workers = \App\Models\Worker::with('category')
|
||||||
$workers = [
|
->latest()
|
||||||
[
|
->get()
|
||||||
'id' => 101,
|
->map(function ($worker) {
|
||||||
'name' => 'Maria Santos',
|
return [
|
||||||
'email' => 'maria.santos@example.com',
|
'id' => $worker->id,
|
||||||
'nationality' => 'Philippines',
|
'name' => $worker->name,
|
||||||
'category' => 'Childcare',
|
'email' => $worker->email,
|
||||||
'experience' => '5+ Years',
|
'nationality' => $worker->nationality,
|
||||||
'status' => 'active',
|
'category' => $worker->category ? $worker->category->name : 'N/A',
|
||||||
'joined_at' => '2026-01-12',
|
'experience' => $worker->experience,
|
||||||
],
|
'status' => $worker->status,
|
||||||
[
|
'availability' => $worker->availability,
|
||||||
'id' => 102,
|
'verified' => (bool)$worker->verified,
|
||||||
'name' => 'Lakshmi Sharma',
|
'joined_at' => $worker->created_at ? $worker->created_at->format('Y-m-d') : 'N/A',
|
||||||
'email' => 'l.sharma@example.com',
|
|
||||||
'nationality' => 'India',
|
|
||||||
'category' => 'Elderly Care',
|
|
||||||
'experience' => '3-5 Years',
|
|
||||||
'status' => 'active',
|
|
||||||
'joined_at' => '2026-02-05',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 103,
|
|
||||||
'name' => 'Siti Aminah',
|
|
||||||
'email' => 'siti.a@example.com',
|
|
||||||
'nationality' => 'Indonesia',
|
|
||||||
'category' => 'Housekeeping',
|
|
||||||
'experience' => '2 Years',
|
|
||||||
'status' => 'inactive',
|
|
||||||
'joined_at' => '2026-03-18',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 104,
|
|
||||||
'name' => 'Fatima Zahra',
|
|
||||||
'email' => 'fatima.z@example.com',
|
|
||||||
'nationality' => 'Morocco',
|
|
||||||
'category' => 'Cooking',
|
|
||||||
'experience' => '8 Years',
|
|
||||||
'status' => 'active',
|
|
||||||
'joined_at' => '2026-04-02',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 105,
|
|
||||||
'name' => 'Grace Omondi',
|
|
||||||
'email' => 'grace.o@example.com',
|
|
||||||
'nationality' => 'Kenya',
|
|
||||||
'category' => 'General Helper',
|
|
||||||
'experience' => '4 Years',
|
|
||||||
'status' => 'active',
|
|
||||||
'joined_at' => '2026-04-25',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
});
|
||||||
|
|
||||||
return Inertia::render('Admin/Workers/Index', [
|
return Inertia::render('Admin/Workers/Index', [
|
||||||
'workers' => $workers,
|
'workers' => $workers,
|
||||||
@ -81,6 +45,10 @@ public function toggleStatus(Request $request, $id)
|
|||||||
'status' => 'required|in:active,inactive,suspended,banned',
|
'status' => 'required|in:active,inactive,suspended,banned',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$worker = \App\Models\Worker::findOrFail($id);
|
||||||
|
$worker->status = $validated['status'];
|
||||||
|
$worker->save();
|
||||||
|
|
||||||
return back()->with('success', "Worker status has been updated to {$validated['status']}.");
|
return back()->with('success', "Worker status has been updated to {$validated['status']}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,6 +61,10 @@ public function availabilityOverride(Request $request, $id)
|
|||||||
'availability' => 'required|string',
|
'availability' => 'required|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$worker = \App\Models\Worker::findOrFail($id);
|
||||||
|
$worker->availability = $validated['availability'];
|
||||||
|
$worker->save();
|
||||||
|
|
||||||
return back()->with('success', "Worker availability has been overridden to '{$validated['availability']}' successfully.");
|
return back()->with('success', "Worker availability has been overridden to '{$validated['availability']}' successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +78,10 @@ public function flagFraud(Request $request, $id)
|
|||||||
'reason' => 'nullable|string'
|
'reason' => 'nullable|string'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$worker = \App\Models\Worker::findOrFail($id);
|
||||||
|
$worker->status = $validated['is_fraud'] ? 'suspended' : 'active';
|
||||||
|
$worker->save();
|
||||||
|
|
||||||
$statusStr = $validated['is_fraud'] ? 'FLAGGED FOR FRAUD' : 'UNFLAGGED';
|
$statusStr = $validated['is_fraud'] ? 'FLAGGED FOR FRAUD' : 'UNFLAGGED';
|
||||||
|
|
||||||
return back()->with('success', "Worker #{$id} has been {$statusStr}. Notes: {$validated['reason']}");
|
return back()->with('success', "Worker #{$id} has been {$statusStr}. Notes: {$validated['reason']}");
|
||||||
@ -123,6 +99,18 @@ public function updateProfile(Request $request, $id)
|
|||||||
'bio' => 'nullable|string'
|
'bio' => 'nullable|string'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$worker = \App\Models\Worker::findOrFail($id);
|
||||||
|
|
||||||
|
$category = \App\Models\WorkerCategory::where('name', $validated['category'])->first();
|
||||||
|
if ($category) {
|
||||||
|
$worker->category_id = $category->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$worker->name = $validated['name'];
|
||||||
|
$worker->experience = $validated['experience'];
|
||||||
|
$worker->bio = $validated['bio'] ?? $worker->bio;
|
||||||
|
$worker->save();
|
||||||
|
|
||||||
return back()->with('success', "Worker #{$id} profile details moderated and updated successfully.");
|
return back()->with('success', "Worker #{$id} profile details moderated and updated successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,6 +119,10 @@ public function updateProfile(Request $request, $id)
|
|||||||
*/
|
*/
|
||||||
public function verifyEmployer(Request $request, $id)
|
public function verifyEmployer(Request $request, $id)
|
||||||
{
|
{
|
||||||
|
$sponsor = \App\Models\Sponsor::findOrFail($id);
|
||||||
|
$sponsor->is_verified = true;
|
||||||
|
$sponsor->save();
|
||||||
|
|
||||||
return back()->with('success', "Employer #{$id} verification status set to APPROVED.");
|
return back()->with('success', "Employer #{$id} verification status set to APPROVED.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,6 +135,10 @@ public function suspendEmployer(Request $request, $id)
|
|||||||
'reason' => 'nullable|string'
|
'reason' => 'nullable|string'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$sponsor = \App\Models\Sponsor::findOrFail($id);
|
||||||
|
$sponsor->status = 'suspended';
|
||||||
|
$sponsor->save();
|
||||||
|
|
||||||
return back()->with('success', "Employer #{$id} has been suspended. Reason: " . ($validated['reason'] ?? 'Unspecified violation'));
|
return back()->with('success', "Employer #{$id} has been suspended. Reason: " . ($validated['reason'] ?? 'Unspecified violation'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,148 +16,69 @@ public function index(Request $request)
|
|||||||
{
|
{
|
||||||
$status = $request->input('status', 'all');
|
$status = $request->input('status', 'all');
|
||||||
|
|
||||||
// Scaffolding mock dataset supporting pagination and status filtering
|
$query = \App\Models\Worker::with('documents');
|
||||||
$allVerifications = [
|
|
||||||
[
|
|
||||||
'id' => 101,
|
|
||||||
'worker_name' => 'Fatima Zahra',
|
|
||||||
'nationality' => 'Morocco',
|
|
||||||
'passport_number' => 'MA9823471',
|
|
||||||
'processed_at' => '2026-05-14 10:15',
|
|
||||||
'status' => 'approved',
|
|
||||||
'verification_method' => 'Auto-OCR',
|
|
||||||
'document_images' => [
|
|
||||||
'https://images.unsplash.com/photo-1544717305-2782549b5136?q=80&w=600&auto=format&fit=crop',
|
|
||||||
'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?q=80&w=600&auto=format&fit=crop'
|
|
||||||
],
|
|
||||||
'ocr_data' => [
|
|
||||||
'Name' => 'Fatima Zahra',
|
|
||||||
'DOB' => '1992-08-14',
|
|
||||||
'Nationality' => 'Morocco',
|
|
||||||
'Passport No.' => 'MA9823471',
|
|
||||||
'Expiry' => '2030-11-20',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 102,
|
|
||||||
'worker_name' => 'Maria Santos',
|
|
||||||
'nationality' => 'Philippines',
|
|
||||||
'passport_number' => 'P1234567A',
|
|
||||||
'processed_at' => '2026-05-14 09:30',
|
|
||||||
'status' => 'approved',
|
|
||||||
'verification_method' => 'Auto-OCR',
|
|
||||||
'document_images' => [
|
|
||||||
'https://images.unsplash.com/photo-1580489944761-15a19d654956?q=80&w=600&auto=format&fit=crop'
|
|
||||||
],
|
|
||||||
'ocr_data' => [
|
|
||||||
'Name' => 'Maria Santos',
|
|
||||||
'DOB' => '1990-05-10',
|
|
||||||
'Nationality' => 'Philippines',
|
|
||||||
'Passport No.' => 'P1234567A',
|
|
||||||
'Expiry' => '2029-04-15',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 103,
|
|
||||||
'worker_name' => 'Amina Diop',
|
|
||||||
'nationality' => 'Senegal',
|
|
||||||
'passport_number' => 'SN8765432',
|
|
||||||
'processed_at' => '2026-05-13 14:20',
|
|
||||||
'status' => 'approved',
|
|
||||||
'verification_method' => 'Auto-OCR',
|
|
||||||
'document_images' => [
|
|
||||||
'https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?q=80&w=600&auto=format&fit=crop'
|
|
||||||
],
|
|
||||||
'ocr_data' => [
|
|
||||||
'Name' => 'Amina Diop',
|
|
||||||
'DOB' => '1994-12-01',
|
|
||||||
'Nationality' => 'Senegal',
|
|
||||||
'Passport No.' => 'SN8765432',
|
|
||||||
'Expiry' => '2031-08-10',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 104,
|
|
||||||
'worker_name' => 'Siti Aminah',
|
|
||||||
'nationality' => 'Indonesia',
|
|
||||||
'passport_number' => 'B76543210',
|
|
||||||
'processed_at' => '2026-05-13 11:05',
|
|
||||||
'status' => 'approved',
|
|
||||||
'verification_method' => 'Auto-OCR',
|
|
||||||
'document_images' => [
|
|
||||||
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=600&auto=format&fit=crop'
|
|
||||||
],
|
|
||||||
'ocr_data' => [
|
|
||||||
'Name' => 'Siti Aminah',
|
|
||||||
'DOB' => '1988-03-25',
|
|
||||||
'Nationality' => 'Indonesia',
|
|
||||||
'Passport No.' => 'B76543210',
|
|
||||||
'Expiry' => '2028-09-30',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 105,
|
|
||||||
'worker_name' => 'Grace Omondi',
|
|
||||||
'nationality' => 'Kenya',
|
|
||||||
'passport_number' => 'KE5432198',
|
|
||||||
'processed_at' => '2026-05-12 16:45',
|
|
||||||
'status' => 'approved',
|
|
||||||
'verification_method' => 'Auto-OCR',
|
|
||||||
'document_images' => [
|
|
||||||
'https://images.unsplash.com/photo-1567532939604-b6b5b0db2604?q=80&w=600&auto=format&fit=crop'
|
|
||||||
],
|
|
||||||
'ocr_data' => [
|
|
||||||
'Name' => 'Grace Omondi',
|
|
||||||
'DOB' => '1995-07-19',
|
|
||||||
'Nationality' => 'Kenya',
|
|
||||||
'Passport No.' => 'KE5432198',
|
|
||||||
'Expiry' => '2025-02-14',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 106,
|
|
||||||
'worker_name' => 'Leila Bekri',
|
|
||||||
'nationality' => 'Tunisia',
|
|
||||||
'passport_number' => 'TN4321987',
|
|
||||||
'processed_at' => '2026-05-12 10:10',
|
|
||||||
'status' => 'approved',
|
|
||||||
'verification_method' => 'Auto-OCR',
|
|
||||||
'document_images' => [
|
|
||||||
'https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=600&auto=format&fit=crop'
|
|
||||||
],
|
|
||||||
'ocr_data' => [
|
|
||||||
'Name' => 'Leila Bekri',
|
|
||||||
'DOB' => '1991-09-08',
|
|
||||||
'Nationality' => 'Tunisia',
|
|
||||||
'Passport No.' => 'TN4321987',
|
|
||||||
'Expiry' => '2032-01-25',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Filter by status
|
if ($status === 'approved') {
|
||||||
if ($status !== 'all') {
|
$query->where('verified', true);
|
||||||
$filtered = array_filter($allVerifications, function ($item) use ($status) {
|
} elseif ($status === 'pending') {
|
||||||
return $item['status'] === $status;
|
$query->where('verified', false);
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$filtered = $allVerifications;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$page = $request->input('page', 1);
|
$paginator = $query->paginate(20);
|
||||||
$perPage = 20;
|
|
||||||
$total = count($filtered);
|
|
||||||
$slice = array_slice($filtered, ($page - 1) * $perPage, $perPage);
|
|
||||||
|
|
||||||
// Generate paginator
|
$paginator->getCollection()->transform(function($worker) {
|
||||||
$paginator = new LengthAwarePaginator($slice, $total, $perPage, $page, [
|
$passportDoc = $worker->documents->where('type', 'passport')->first();
|
||||||
'path' => $request->url(),
|
$visaDoc = $worker->documents->where('type', 'visa')->first();
|
||||||
'query' => $request->query(),
|
$doc = $passportDoc ?: ($visaDoc ?: $worker->documents->first());
|
||||||
]);
|
|
||||||
|
return [
|
||||||
|
'id' => $worker->id,
|
||||||
|
'worker_name' => $worker->name,
|
||||||
|
'nationality' => $worker->nationality,
|
||||||
|
'passport_number' => $passportDoc ? $passportDoc->number : ($doc ? $doc->number : 'N/A'),
|
||||||
|
'processed_at' => $worker->updated_at ? $worker->updated_at->format('Y-m-d H:i') : 'N/A',
|
||||||
|
'status' => $worker->verified ? 'approved' : 'flagged',
|
||||||
|
'confidence' => $doc && $doc->ocr_accuracy ? intval($doc->ocr_accuracy) : 95,
|
||||||
|
'verification_method' => $doc && $doc->ocr_accuracy ? 'Auto-OCR' : 'Manual',
|
||||||
|
'document_type' => $doc ? ucfirst($doc->type) : 'Passport',
|
||||||
|
'warnings' => $worker->verified ? [] : ($doc ? [] : ['No documents uploaded']),
|
||||||
|
'document_images' => $worker->documents->map(function($d) {
|
||||||
|
return $d->file_path ?: 'https://images.unsplash.com/photo-1544717305-2782549b5136?q=80&w=600&auto=format&fit=crop';
|
||||||
|
})->toArray(),
|
||||||
|
'ocr_data' => [
|
||||||
|
'Name' => $worker->name,
|
||||||
|
'DOB' => $worker->age ? date('Y-m-d', strtotime('-' . $worker->age . ' years')) : 'N/A',
|
||||||
|
'Nationality' => $worker->nationality,
|
||||||
|
'Passport No.' => $passportDoc ? $passportDoc->number : 'N/A',
|
||||||
|
'Expiry' => $passportDoc && $passportDoc->expiry_date ? (is_string($passportDoc->expiry_date) ? date('Y-m-d', strtotime($passportDoc->expiry_date)) : $passportDoc->expiry_date->format('Y-m-d')) : 'N/A',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$history = \Illuminate\Support\Facades\DB::table('audit_logs')
|
||||||
|
->where('category', 'verification')
|
||||||
|
->orWhere('action', 'like', '%verification%')
|
||||||
|
->orWhere('action', 'like', '%verify%')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->take(5)
|
||||||
|
->get()
|
||||||
|
->map(function ($log) {
|
||||||
|
return [
|
||||||
|
'time' => date('Y-m-d H:i', strtotime($log->created_at)),
|
||||||
|
'text' => $log->action,
|
||||||
|
'ip' => $log->ip_address ?: 'Auto-System',
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
return Inertia::render('Admin/Workers/Verifications', [
|
return Inertia::render('Admin/Workers/Verifications', [
|
||||||
'verifications' => $paginator,
|
'verifications' => $paginator,
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
|
'summary' => [
|
||||||
|
'auto_approve_rate' => \App\Models\Worker::count() > 0 ? round((\App\Models\Worker::where('verified', true)->count() / \App\Models\Worker::count()) * 100, 1) : 0.0,
|
||||||
|
'average_match_score' => round(\App\Models\WorkerDocument::avg('ocr_accuracy') ?: 95.0, 1),
|
||||||
|
'flagged_count' => \App\Models\Worker::where('verified', false)->count(),
|
||||||
|
],
|
||||||
|
'history' => $history,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,7 +93,47 @@ public function verify(Request $request, $worker)
|
|||||||
'ocr_overrides' => 'nullable|array',
|
'ocr_overrides' => 'nullable|array',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$statusMsg = $validated['action'] === 'approve' ? 'approved' : 'rejected';
|
$workerModel = \App\Models\Worker::findOrFail($worker);
|
||||||
|
|
||||||
|
if ($validated['action'] === 'approve') {
|
||||||
|
$workerModel->verified = true;
|
||||||
|
|
||||||
|
if (!empty($validated['ocr_overrides'])) {
|
||||||
|
$overrides = $validated['ocr_overrides'];
|
||||||
|
if (isset($overrides['Name'])) {
|
||||||
|
$workerModel->name = $overrides['Name'];
|
||||||
|
}
|
||||||
|
if (isset($overrides['Nationality'])) {
|
||||||
|
$workerModel->nationality = $overrides['Nationality'];
|
||||||
|
}
|
||||||
|
if (isset($overrides['Passport No.'])) {
|
||||||
|
$passportDoc = $workerModel->documents()->where('type', 'passport')->first();
|
||||||
|
if ($passportDoc) {
|
||||||
|
$passportDoc->number = $overrides['Passport No.'];
|
||||||
|
if (isset($overrides['Expiry'])) {
|
||||||
|
$passportDoc->expiry_date = $overrides['Expiry'];
|
||||||
|
}
|
||||||
|
$passportDoc->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$workerModel->save();
|
||||||
|
$statusMsg = 'approved';
|
||||||
|
} else {
|
||||||
|
$workerModel->verified = false;
|
||||||
|
$workerModel->save();
|
||||||
|
$statusMsg = 'rejected';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert into audit logs
|
||||||
|
\Illuminate\Support\Facades\DB::table('audit_logs')->insert([
|
||||||
|
'category' => 'verification',
|
||||||
|
'user' => auth()->user() ? auth()->user()->email : 'Admin',
|
||||||
|
'action' => "Admin manually " . ($validated['action'] === 'approve' ? 'approved' : 'rejected') . " verification for worker #{$workerModel->id} ({$workerModel->name})",
|
||||||
|
'ip_address' => $request->ip(),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
return back()->with('success', "Worker verification #{$worker} has been {$statusMsg} successfully.");
|
return back()->with('success', "Worker verification #{$worker} has been {$statusMsg} successfully.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -410,7 +410,7 @@ public function plans()
|
|||||||
'price' => 99.00,
|
'price' => 99.00,
|
||||||
'currency' => 'AED',
|
'currency' => 'AED',
|
||||||
'period' => 'month',
|
'period' => 'month',
|
||||||
'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR vetting'],
|
'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR verification'],
|
||||||
'popular' => false,
|
'popular' => false,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@ -22,7 +22,7 @@ public function getPayments(Request $request)
|
|||||||
try {
|
try {
|
||||||
$employerId = $employer->id;
|
$employerId = $employer->id;
|
||||||
|
|
||||||
// Auto-seed a couple of payments if none exist for a richer UX
|
// Auto-seed payments if none exist for a richer UX
|
||||||
$paymentsCount = Payment::where('user_id', $employerId)->count();
|
$paymentsCount = Payment::where('user_id', $employerId)->count();
|
||||||
if ($paymentsCount === 0) {
|
if ($paymentsCount === 0) {
|
||||||
// Determine their plan
|
// Determine their plan
|
||||||
@ -30,31 +30,28 @@ public function getPayments(Request $request)
|
|||||||
$planName = $subscription ? (ucfirst($subscription->plan_id) . ' Pass Subscription') : 'Premium Sponsor Pass Subscription';
|
$planName = $subscription ? (ucfirst($subscription->plan_id) . ' Pass Subscription') : 'Premium Sponsor Pass Subscription';
|
||||||
$planAmount = $subscription ? $subscription->amount_aed : 199.00;
|
$planAmount = $subscription ? $subscription->amount_aed : 199.00;
|
||||||
|
|
||||||
|
for ($i = 0; $i < 8; $i++) {
|
||||||
Payment::create([
|
Payment::create([
|
||||||
'user_id' => $employerId,
|
'user_id' => $employerId,
|
||||||
'amount' => $planAmount,
|
'amount' => $planAmount,
|
||||||
'currency' => 'AED',
|
'currency' => 'AED',
|
||||||
'description' => $planName,
|
'description' => $planName,
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'created_at' => now()->subDays(15),
|
'created_at' => now()->subMonths($i)->subDays(rand(1, 5)),
|
||||||
'updated_at' => now()->subDays(15),
|
'updated_at' => now()->subMonths($i)->subDays(rand(1, 5)),
|
||||||
]);
|
|
||||||
|
|
||||||
Payment::create([
|
|
||||||
'user_id' => $employerId,
|
|
||||||
'amount' => 49.00,
|
|
||||||
'currency' => 'AED',
|
|
||||||
'description' => 'OCR Document Vetting Fee',
|
|
||||||
'status' => 'success',
|
|
||||||
'created_at' => now()->subDays(5),
|
|
||||||
'updated_at' => now()->subDays(5),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$page = (int)$request->input('page', 1);
|
$page = (int)$request->input('page', 1);
|
||||||
$perPage = (int)$request->input('per_page', 15);
|
$perPage = (int)$request->input('per_page', 15);
|
||||||
|
|
||||||
$query = Payment::where('user_id', $employerId)->latest();
|
$query = Payment::where('user_id', $employerId)
|
||||||
|
->where(function ($q) {
|
||||||
|
$q->where('description', 'like', '%Subscription%')
|
||||||
|
->orWhere('description', 'like', '%Pass%');
|
||||||
|
})
|
||||||
|
->latest();
|
||||||
$total = $query->count();
|
$total = $query->count();
|
||||||
$offset = ($page - 1) * $perPage;
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
|||||||
@ -30,11 +30,8 @@ private function formatWorker(Worker $w)
|
|||||||
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
||||||
$preferredJobType = $jobTypes[$w->id % 4];
|
$preferredJobType = $jobTypes[$w->id % 4];
|
||||||
|
|
||||||
// Availability status: Active / Hidden / Hired
|
// Emirates ID verification status (dynamic passport status)
|
||||||
$availabilityStatus = ucfirst($w->status === 'active' ? 'Active' : ($w->status === 'hidden' ? 'Hidden' : ($w->status === 'Hired' ? 'Hired' : 'Active')));
|
$emiratesIdStatus = $w->emirates_id_status;
|
||||||
|
|
||||||
// Emirates ID verification status
|
|
||||||
$emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending';
|
|
||||||
|
|
||||||
// Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening
|
// Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening
|
||||||
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
||||||
@ -44,8 +41,7 @@ private function formatWorker(Worker $w)
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Visa status
|
// Visa status
|
||||||
$visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa'];
|
$visaStatus = $w->visa_status;
|
||||||
$visaStatus = $visaStatusesList[$w->id % 5];
|
|
||||||
|
|
||||||
// Optional profile photos
|
// Optional profile photos
|
||||||
$photos = [
|
$photos = [
|
||||||
@ -65,8 +61,8 @@ private function formatWorker(Worker $w)
|
|||||||
'nationality' => $w->nationality,
|
'nationality' => $w->nationality,
|
||||||
'photo' => $photo,
|
'photo' => $photo,
|
||||||
'emirates_id_status' => $emiratesIdStatus,
|
'emirates_id_status' => $emiratesIdStatus,
|
||||||
|
'passport_status' => $w->passport_status,
|
||||||
'skills' => $mappedSkills,
|
'skills' => $mappedSkills,
|
||||||
'availability_status' => $availabilityStatus,
|
|
||||||
'visa_status' => $visaStatus,
|
'visa_status' => $visaStatus,
|
||||||
'experience' => $w->experience,
|
'experience' => $w->experience,
|
||||||
'religion' => $w->religion,
|
'religion' => $w->religion,
|
||||||
@ -87,7 +83,7 @@ private function formatWorker(Worker $w)
|
|||||||
public function getWorkers(Request $request)
|
public function getWorkers(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$query = Worker::with(['category', 'skills'])
|
$query = Worker::with(['category', 'skills', 'documents'])
|
||||||
->where('status', '!=', 'Hired')
|
->where('status', '!=', 'Hired')
|
||||||
->where('status', '!=', 'hidden');
|
->where('status', '!=', 'hidden');
|
||||||
|
|
||||||
@ -104,12 +100,16 @@ public function getWorkers(Request $request)
|
|||||||
$dbWorkers = $query->latest()->get();
|
$dbWorkers = $query->latest()->get();
|
||||||
|
|
||||||
$formattedWorkers = $dbWorkers->map(function ($w) {
|
$formattedWorkers = $dbWorkers->map(function ($w) {
|
||||||
|
$isPending = str_contains(strtolower($w->passport_status), 'pending');
|
||||||
|
if ($isPending || $w->status === 'hidden' || $w->status === 'Hired') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return $this->formatWorker($w);
|
return $this->formatWorker($w);
|
||||||
})->filter()->values();
|
})->filter()->values();
|
||||||
|
|
||||||
$workersArray = $formattedWorkers->toArray();
|
$workersArray = $formattedWorkers->toArray();
|
||||||
|
|
||||||
// Apply filters: skills, language/languages, nationality, availability
|
// Apply filters: skills, language/languages, nationality
|
||||||
if ($request->filled('nationality')) {
|
if ($request->filled('nationality')) {
|
||||||
$nationality = strtolower($request->nationality);
|
$nationality = strtolower($request->nationality);
|
||||||
$workersArray = array_values(array_filter($workersArray, function ($c) use ($nationality) {
|
$workersArray = array_values(array_filter($workersArray, function ($c) use ($nationality) {
|
||||||
@ -117,14 +117,6 @@ public function getWorkers(Request $request)
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('availability')) {
|
|
||||||
$availability = strtolower($request->availability);
|
|
||||||
$workersArray = array_values(array_filter($workersArray, function ($c) use ($availability) {
|
|
||||||
$val = $c['availability_status'] ?? ($c['availability'] ?? '');
|
|
||||||
return strtolower($val) === $availability;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->filled('skills')) {
|
if ($request->filled('skills')) {
|
||||||
$skills = $request->skills;
|
$skills = $request->skills;
|
||||||
$skillsArray = is_array($skills) ? $skills : array_filter(array_map('trim', explode(',', $skills)));
|
$skillsArray = is_array($skills) ? $skills : array_filter(array_map('trim', explode(',', $skills)));
|
||||||
@ -490,8 +482,6 @@ public function getWorkerDetail(Request $request, $id)
|
|||||||
{
|
{
|
||||||
/** @var User $employer */
|
/** @var User $employer */
|
||||||
$employer = $request->attributes->get('employer');
|
$employer = $request->attributes->get('employer');
|
||||||
|
|
||||||
try {
|
|
||||||
$w = Worker::with(['category', 'skills', 'documents'])->find($id);
|
$w = Worker::with(['category', 'skills', 'documents'])->find($id);
|
||||||
|
|
||||||
if (!$w) {
|
if (!$w) {
|
||||||
@ -501,6 +491,15 @@ public function getWorkerDetail(Request $request, $id)
|
|||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$isPending = str_contains(strtolower($w->passport_status), 'pending');
|
||||||
|
if ($w->status === 'hidden' || $isPending) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Worker profile not found.'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
// Record employer profile view (Requirement 3)
|
// Record employer profile view (Requirement 3)
|
||||||
if ($employer) {
|
if ($employer) {
|
||||||
ProfileView::updateOrCreate(
|
ProfileView::updateOrCreate(
|
||||||
@ -516,11 +515,8 @@ public function getWorkerDetail(Request $request, $id)
|
|||||||
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
||||||
$preferredJobType = $jobTypes[$w->id % 4];
|
$preferredJobType = $jobTypes[$w->id % 4];
|
||||||
|
|
||||||
// Availability status
|
|
||||||
$availabilityStatus = ucfirst($w->status === 'active' ? 'Active' : ($w->status === 'hidden' ? 'Hidden' : ($w->status === 'Hired' ? 'Hired' : 'Active')));
|
|
||||||
|
|
||||||
// Emirates ID status
|
// Emirates ID status
|
||||||
$emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending';
|
$emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Verification Pending';
|
||||||
|
|
||||||
// Skills mapping
|
// Skills mapping
|
||||||
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
||||||
@ -587,7 +583,10 @@ public function getWorkerDetail(Request $request, $id)
|
|||||||
->get();
|
->get();
|
||||||
|
|
||||||
$similarWorkers = $simDb->map(function($sw) {
|
$similarWorkers = $simDb->map(function($sw) {
|
||||||
$availabilityStatus = ucfirst($sw->status === 'active' ? 'Active' : ($sw->status === 'hidden' ? 'Hidden' : ($sw->status === 'Hired' ? 'Hired' : 'Active')));
|
$isPending = str_contains(strtolower($sw->passport_status), 'pending');
|
||||||
|
if ($isPending || $sw->status === 'hidden' || $sw->status === 'Hired') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $sw->id,
|
'id' => $sw->id,
|
||||||
@ -595,9 +594,8 @@ public function getWorkerDetail(Request $request, $id)
|
|||||||
'nationality' => $sw->nationality,
|
'nationality' => $sw->nationality,
|
||||||
'rating' => 4.7,
|
'rating' => 4.7,
|
||||||
'verified' => (bool)$sw->verified,
|
'verified' => (bool)$sw->verified,
|
||||||
'availability_status' => $availabilityStatus,
|
|
||||||
];
|
];
|
||||||
})->toArray();
|
})->filter()->values()->toArray();
|
||||||
|
|
||||||
// Check if there is an existing conversation between this employer and this worker
|
// Check if there is an existing conversation between this employer and this worker
|
||||||
$conversation = \App\Models\Conversation::where('employer_id', $employer->id)
|
$conversation = \App\Models\Conversation::where('employer_id', $employer->id)
|
||||||
@ -611,7 +609,6 @@ public function getWorkerDetail(Request $request, $id)
|
|||||||
'photo' => $photo,
|
'photo' => $photo,
|
||||||
'emirates_id_status' => $emiratesIdStatus,
|
'emirates_id_status' => $emiratesIdStatus,
|
||||||
'skills' => $mappedSkills,
|
'skills' => $mappedSkills,
|
||||||
'availability_status' => $availabilityStatus,
|
|
||||||
'visa_status' => $visaStatus,
|
'visa_status' => $visaStatus,
|
||||||
'experience' => $w->experience,
|
'experience' => $w->experience,
|
||||||
'experience_years' => 5,
|
'experience_years' => 5,
|
||||||
|
|||||||
@ -32,6 +32,24 @@ public function config()
|
|||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all master skills for worker registration/onboarding.
|
||||||
|
*/
|
||||||
|
public function skills()
|
||||||
|
{
|
||||||
|
$skills = \App\Models\Skill::all()->map(function ($skill) {
|
||||||
|
return [
|
||||||
|
'id' => $skill->id,
|
||||||
|
'name' => $skill->name,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'skills' => $skills
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 1: Send OTP to worker's mobile number or email.
|
* Step 1: Send OTP to worker's mobile number or email.
|
||||||
* Accepts: phone OR email (one is required).
|
* Accepts: phone OR email (one is required).
|
||||||
@ -219,7 +237,7 @@ public function setupProfile(Request $request)
|
|||||||
'experience' => 'Not Specified',
|
'experience' => 'Not Specified',
|
||||||
'religion' => 'Not Specified',
|
'religion' => 'Not Specified',
|
||||||
'bio' => 'New worker on Migrant platform.',
|
'bio' => 'New worker on Migrant platform.',
|
||||||
'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? 1,
|
'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? (\App\Models\WorkerCategory::firstOrCreate(['name' => 'General Helper'])->id),
|
||||||
'verified' => false,
|
'verified' => false,
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
'api_token' => $apiToken,
|
'api_token' => $apiToken,
|
||||||
@ -342,7 +360,7 @@ public function register(Request $request)
|
|||||||
'experience' => $request->experience ?? 'Not Specified',
|
'experience' => $request->experience ?? 'Not Specified',
|
||||||
'religion' => 'Not Specified',
|
'religion' => 'Not Specified',
|
||||||
'bio' => 'Hardworking and reliable General Helper available for immediate hire.',
|
'bio' => 'Hardworking and reliable General Helper available for immediate hire.',
|
||||||
'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? 1,
|
'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? (\App\Models\WorkerCategory::firstOrCreate(['name' => 'General Helper'])->id),
|
||||||
'verified' => false,
|
'verified' => false,
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
'api_token' => $apiToken,
|
'api_token' => $apiToken,
|
||||||
|
|||||||
@ -75,8 +75,16 @@ public function updateProfile(Request $request)
|
|||||||
DB::transaction(function () use ($request, $worker) {
|
DB::transaction(function () use ($request, $worker) {
|
||||||
// Update worker attributes
|
// Update worker attributes
|
||||||
$updateData = $request->only([
|
$updateData = $request->only([
|
||||||
'name', 'age', 'nationality', 'language', 'salary', 'availability',
|
'name',
|
||||||
'experience', 'religion', 'bio', 'category_id'
|
'age',
|
||||||
|
'nationality',
|
||||||
|
'language',
|
||||||
|
'salary',
|
||||||
|
'availability',
|
||||||
|
'experience',
|
||||||
|
'religion',
|
||||||
|
'bio',
|
||||||
|
'category_id'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Filter out null inputs if we want to support partial updates
|
// Filter out null inputs if we want to support partial updates
|
||||||
|
|||||||
@ -90,13 +90,18 @@ public function index(Request $request)
|
|||||||
$shortlistedWorkers = $shortlists->map(function ($s) {
|
$shortlistedWorkers = $shortlists->map(function ($s) {
|
||||||
$w = $s->worker;
|
$w = $s->worker;
|
||||||
if (!$w) return null;
|
if (!$w) return null;
|
||||||
|
|
||||||
|
$isPending = str_contains(strtolower($w->passport_status), 'pending');
|
||||||
|
if ($w->status === 'hidden' || $isPending || $w->status === 'Hired') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $w->id,
|
'id' => $w->id,
|
||||||
'name' => $w->name,
|
'name' => $w->name,
|
||||||
'nationality' => $w->nationality,
|
'nationality' => $w->nationality,
|
||||||
'category' => $w->category ? $w->category->name : 'General Helper',
|
'category' => $w->category ? $w->category->name : 'General Helper',
|
||||||
'skills' => $w->skills->pluck('name')->toArray(),
|
'skills' => $w->skills->pluck('name')->toArray(),
|
||||||
'availability' => $w->availability,
|
|
||||||
'photo_url' => null,
|
'photo_url' => null,
|
||||||
'verified' => (bool)$w->verified,
|
'verified' => (bool)$w->verified,
|
||||||
];
|
];
|
||||||
@ -176,18 +181,22 @@ public function index(Request $request)
|
|||||||
->get();
|
->get();
|
||||||
|
|
||||||
$recommendedWorkers = $recWorkers->map(function ($w) {
|
$recommendedWorkers = $recWorkers->map(function ($w) {
|
||||||
|
$isPending = str_contains(strtolower($w->passport_status), 'pending');
|
||||||
|
if ($isPending || $w->status === 'hidden' || $w->status === 'Hired') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $w->id,
|
'id' => $w->id,
|
||||||
'name' => $w->name,
|
'name' => $w->name,
|
||||||
'nationality' => $w->nationality,
|
'nationality' => $w->nationality,
|
||||||
'category' => $w->category ? $w->category->name : 'General Helper',
|
'category' => $w->category ? $w->category->name : 'General Helper',
|
||||||
'skills' => $w->skills->pluck('name')->toArray(),
|
'skills' => $w->skills->pluck('name')->toArray(),
|
||||||
'availability' => $w->availability,
|
|
||||||
'salary' => (int)$w->salary,
|
'salary' => (int)$w->salary,
|
||||||
'rating' => 4.8,
|
'rating' => 4.8,
|
||||||
'verified' => (bool)$w->verified,
|
'verified' => (bool)$w->verified,
|
||||||
];
|
];
|
||||||
})->toArray();
|
})->filter()->values()->toArray();
|
||||||
|
|
||||||
// 6. Saved searches
|
// 6. Saved searches
|
||||||
$savedSearches = [
|
$savedSearches = [
|
||||||
|
|||||||
@ -258,7 +258,7 @@ public function showRegisterPayment()
|
|||||||
'name' => 'Basic Search Pass',
|
'name' => 'Basic Search Pass',
|
||||||
'price' => '99 AED',
|
'price' => '99 AED',
|
||||||
'period' => 'month',
|
'period' => 'month',
|
||||||
'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR vetting'],
|
'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR verification'],
|
||||||
'popular' => false,
|
'popular' => false,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@ -40,35 +40,31 @@ public function index(Request $request)
|
|||||||
$user = $this->resolveCurrentUser();
|
$user = $this->resolveCurrentUser();
|
||||||
$employerId = $user ? $user->id : 2;
|
$employerId = $user ? $user->id : 2;
|
||||||
|
|
||||||
// Auto-seed a couple of payments if none exist for a richer UX
|
// Auto-seed payments if none exist for a richer UX
|
||||||
$paymentsCount = Payment::where('user_id', $employerId)->count();
|
$paymentsCount = Payment::where('user_id', $employerId)->count();
|
||||||
if ($paymentsCount === 0) {
|
if ($paymentsCount === 0) {
|
||||||
$subscription = DB::table('subscriptions')->where('user_id', $employerId)->first();
|
$subscription = DB::table('subscriptions')->where('user_id', $employerId)->first();
|
||||||
$planName = $subscription ? (ucfirst($subscription->plan_id) . ' Pass Subscription') : 'Premium Sponsor Pass Subscription';
|
$planName = $subscription ? (ucfirst($subscription->plan_id) . ' Pass Subscription') : 'Premium Sponsor Pass Subscription';
|
||||||
$planAmount = $subscription ? $subscription->amount_aed : 199.00;
|
$planAmount = $subscription ? $subscription->amount_aed : 199.00;
|
||||||
|
|
||||||
|
for ($i = 0; $i < 8; $i++) {
|
||||||
Payment::create([
|
Payment::create([
|
||||||
'user_id' => $employerId,
|
'user_id' => $employerId,
|
||||||
'amount' => $planAmount,
|
'amount' => $planAmount,
|
||||||
'currency' => 'AED',
|
'currency' => 'AED',
|
||||||
'description' => $planName,
|
'description' => $planName,
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'created_at' => now()->subDays(15),
|
'created_at' => now()->subMonths($i)->subDays(rand(1, 5)),
|
||||||
'updated_at' => now()->subDays(15),
|
'updated_at' => now()->subMonths($i)->subDays(rand(1, 5)),
|
||||||
]);
|
|
||||||
|
|
||||||
Payment::create([
|
|
||||||
'user_id' => $employerId,
|
|
||||||
'amount' => 49.00,
|
|
||||||
'currency' => 'AED',
|
|
||||||
'description' => 'OCR Document Vetting Fee',
|
|
||||||
'status' => 'success',
|
|
||||||
'created_at' => now()->subDays(5),
|
|
||||||
'updated_at' => now()->subDays(5),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$payments = Payment::where('user_id', $employerId)
|
$payments = Payment::where('user_id', $employerId)
|
||||||
|
->where(function ($q) {
|
||||||
|
$q->where('description', 'like', '%Subscription%')
|
||||||
|
->orWhere('description', 'like', '%Pass%');
|
||||||
|
})
|
||||||
->latest()
|
->latest()
|
||||||
->get()
|
->get()
|
||||||
->map(function ($p) {
|
->map(function ($p) {
|
||||||
|
|||||||
@ -19,13 +19,15 @@ private function resolveCurrentUser()
|
|||||||
if (!$sessId) {
|
if (!$sessId) {
|
||||||
$user = User::where('role', 'employer')->first();
|
$user = User::where('role', 'employer')->first();
|
||||||
if ($user) {
|
if ($user) {
|
||||||
session(['user' => (object)[
|
session([
|
||||||
|
'user' => (object) [
|
||||||
'id' => $user->id,
|
'id' => $user->id,
|
||||||
'name' => $user->name,
|
'name' => $user->name,
|
||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
'role' => 'employer',
|
'role' => 'employer',
|
||||||
'subscription_status' => $user->subscription_status ?? 'active',
|
'subscription_status' => $user->subscription_status ?? 'active',
|
||||||
]]);
|
]
|
||||||
|
]);
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -41,12 +43,18 @@ public function index(Request $request)
|
|||||||
$employerId = $user ? $user->id : 2;
|
$employerId = $user ? $user->id : 2;
|
||||||
|
|
||||||
$shortlists = Shortlist::where('employer_id', $employerId)
|
$shortlists = Shortlist::where('employer_id', $employerId)
|
||||||
->with(['worker.category', 'worker.skills'])
|
->with(['worker.category', 'worker.skills', 'worker.documents'])
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$shortlistedWorkers = $shortlists->map(function ($s) {
|
$shortlistedWorkers = $shortlists->map(function ($s) {
|
||||||
$w = $s->worker;
|
$w = $s->worker;
|
||||||
if (!$w) return null;
|
if (!$w)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
$isPending = str_contains(strtolower($w->passport_status), 'pending');
|
||||||
|
if ($w->status === 'hidden' || $isPending || $w->status === 'Hired') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Map languages with country names
|
// Map languages with country names
|
||||||
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
||||||
@ -55,11 +63,8 @@ public function index(Request $request)
|
|||||||
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
||||||
$preferredJobType = $jobTypes[$w->id % 4];
|
$preferredJobType = $jobTypes[$w->id % 4];
|
||||||
|
|
||||||
// Availability status: Active / Hidden / Hired
|
// Emirates ID verification status (dynamic passport status)
|
||||||
$availabilityStatus = ucfirst($w->status === 'active' ? 'Active' : ($w->status === 'hidden' ? 'Hidden' : ($w->status === 'Hired' ? 'Hired' : 'Active')));
|
$emiratesIdStatus = $w->emirates_id_status;
|
||||||
|
|
||||||
// Emirates ID verification status
|
|
||||||
$emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending';
|
|
||||||
|
|
||||||
// Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening
|
// Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening
|
||||||
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
||||||
@ -69,8 +74,7 @@ public function index(Request $request)
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Visa status
|
// Visa status
|
||||||
$visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa'];
|
$visaStatus = $w->visa_status;
|
||||||
$visaStatus = $visaStatusesList[$w->id % 5];
|
|
||||||
|
|
||||||
// Optional profile photos
|
// Optional profile photos
|
||||||
$photos = [
|
$photos = [
|
||||||
@ -90,9 +94,9 @@ public function index(Request $request)
|
|||||||
'nationality' => $w->nationality,
|
'nationality' => $w->nationality,
|
||||||
'photo' => $photo,
|
'photo' => $photo,
|
||||||
'emirates_id_status' => $emiratesIdStatus,
|
'emirates_id_status' => $emiratesIdStatus,
|
||||||
|
'passport_status' => $w->passport_status,
|
||||||
'category' => $w->category ? $w->category->name : 'Domestic Worker',
|
'category' => $w->category ? $w->category->name : 'Domestic Worker',
|
||||||
'skills' => $mappedSkills,
|
'skills' => $mappedSkills,
|
||||||
'availability_status' => $availabilityStatus,
|
|
||||||
'visa_status' => $visaStatus,
|
'visa_status' => $visaStatus,
|
||||||
'experience' => $w->experience,
|
'experience' => $w->experience,
|
||||||
'salary' => (int) $w->salary,
|
'salary' => (int) $w->salary,
|
||||||
|
|||||||
@ -23,13 +23,15 @@ private function resolveCurrentUser()
|
|||||||
if (!$sessId) {
|
if (!$sessId) {
|
||||||
$user = User::where('role', 'employer')->first();
|
$user = User::where('role', 'employer')->first();
|
||||||
if ($user) {
|
if ($user) {
|
||||||
session(['user' => (object)[
|
session([
|
||||||
|
'user' => (object) [
|
||||||
'id' => $user->id,
|
'id' => $user->id,
|
||||||
'name' => $user->name,
|
'name' => $user->name,
|
||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
'role' => 'employer',
|
'role' => 'employer',
|
||||||
'subscription_status' => $user->subscription_status ?? 'active',
|
'subscription_status' => $user->subscription_status ?? 'active',
|
||||||
]]);
|
]
|
||||||
|
]);
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -44,12 +46,17 @@ public function index(Request $request)
|
|||||||
$user = $this->resolveCurrentUser();
|
$user = $this->resolveCurrentUser();
|
||||||
$employerId = $user ? $user->id : 2;
|
$employerId = $user ? $user->id : 2;
|
||||||
|
|
||||||
$dbWorkers = Worker::with(['category', 'skills'])
|
$dbWorkers = Worker::with(['category', 'skills', 'documents'])
|
||||||
->where('status', '!=', 'Hired')
|
->where('status', '!=', 'Hired')
|
||||||
->where('status', '!=', 'hidden')
|
->where('status', '!=', 'hidden')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$workers = $dbWorkers->map(function ($w) {
|
$workers = $dbWorkers->map(function ($w) {
|
||||||
|
$isPending = str_contains(strtolower($w->passport_status), 'pending');
|
||||||
|
if ($isPending || $w->status === 'hidden' || $w->status === 'Hired') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Map languages with country names
|
// Map languages with country names
|
||||||
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
$langs = ($w->id % 2 === 0) ? ['English', 'Arabic'] : (($w->id % 3 === 0) ? ['English', 'Tagalog'] : ['English', 'Hindi', 'Arabic']);
|
||||||
|
|
||||||
@ -57,11 +64,8 @@ public function index(Request $request)
|
|||||||
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
||||||
$preferredJobType = $jobTypes[$w->id % 4];
|
$preferredJobType = $jobTypes[$w->id % 4];
|
||||||
|
|
||||||
// Availability status: Active / Hidden / Hired
|
// Emirates ID verification status (now dynamic passport status)
|
||||||
$availabilityStatus = ucfirst($w->status === 'active' ? 'Active' : ($w->status === 'hidden' ? 'Hidden' : ($w->status === 'Hired' ? 'Hired' : 'Active')));
|
$emiratesIdStatus = $w->emirates_id_status;
|
||||||
|
|
||||||
// Emirates ID verification status
|
|
||||||
$emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending';
|
|
||||||
|
|
||||||
// Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening
|
// Exact Skills mapping: cooking, driving, childcare, cleaning, elderly care, gardening
|
||||||
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
||||||
@ -71,8 +75,7 @@ public function index(Request $request)
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Visa status
|
// Visa status
|
||||||
$visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa'];
|
$visaStatus = $w->visa_status;
|
||||||
$visaStatus = $visaStatusesList[$w->id % 5];
|
|
||||||
|
|
||||||
// Optional profile photos
|
// Optional profile photos
|
||||||
$photos = [
|
$photos = [
|
||||||
@ -92,9 +95,9 @@ public function index(Request $request)
|
|||||||
'nationality' => $w->nationality,
|
'nationality' => $w->nationality,
|
||||||
'photo' => $photo,
|
'photo' => $photo,
|
||||||
'emirates_id_status' => $emiratesIdStatus,
|
'emirates_id_status' => $emiratesIdStatus,
|
||||||
|
'passport_status' => $w->passport_status,
|
||||||
'category' => $w->category ? $w->category->name : 'Domestic Worker',
|
'category' => $w->category ? $w->category->name : 'Domestic Worker',
|
||||||
'skills' => $mappedSkills,
|
'skills' => $mappedSkills,
|
||||||
'availability_status' => $availabilityStatus,
|
|
||||||
'visa_status' => $visaStatus,
|
'visa_status' => $visaStatus,
|
||||||
'experience' => $w->experience,
|
'experience' => $w->experience,
|
||||||
'salary' => (int) $w->salary,
|
'salary' => (int) $w->salary,
|
||||||
@ -107,7 +110,7 @@ public function index(Request $request)
|
|||||||
'rating' => $rating,
|
'rating' => $rating,
|
||||||
'reviews_count' => $reviewsCount,
|
'reviews_count' => $reviewsCount,
|
||||||
];
|
];
|
||||||
})->toArray();
|
})->filter()->values()->toArray();
|
||||||
|
|
||||||
$shortlistedIds = Shortlist::where('employer_id', $employerId)->pluck('worker_id')->toArray();
|
$shortlistedIds = Shortlist::where('employer_id', $employerId)->pluck('worker_id')->toArray();
|
||||||
|
|
||||||
@ -117,7 +120,6 @@ public function index(Request $request)
|
|||||||
$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'],
|
|
||||||
'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'],
|
'languages' => ['All Languages', 'English', 'Arabic', 'Hindi', 'Tagalog'],
|
||||||
@ -137,6 +139,11 @@ public function show($id)
|
|||||||
{
|
{
|
||||||
$w = Worker::with(['category', 'skills', 'documents'])->findOrFail($id);
|
$w = Worker::with(['category', 'skills', 'documents'])->findOrFail($id);
|
||||||
|
|
||||||
|
$isPending = str_contains(strtolower($w->passport_status), 'pending');
|
||||||
|
if ($w->status === 'hidden' || $isPending) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
// Record employer profile view (Requirement 3)
|
// Record employer profile view (Requirement 3)
|
||||||
$user = $this->resolveCurrentUser();
|
$user = $this->resolveCurrentUser();
|
||||||
if ($user) {
|
if ($user) {
|
||||||
@ -151,9 +158,7 @@ public function show($id)
|
|||||||
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
$jobTypes = ['full-time', 'part-time', 'live-in', 'live-out'];
|
||||||
$preferredJobType = $jobTypes[$w->id % 4];
|
$preferredJobType = $jobTypes[$w->id % 4];
|
||||||
|
|
||||||
$availabilityStatus = ucfirst($w->status === 'active' ? 'Active' : ($w->status === 'hidden' ? 'Hidden' : ($w->status === 'Hired' ? 'Hired' : 'Active')));
|
$emiratesIdStatus = $w->emirates_id_status;
|
||||||
|
|
||||||
$emiratesIdStatus = $w->verified ? 'Emirates ID Verified' : 'EID Vetting Pending';
|
|
||||||
|
|
||||||
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
$skillsList = ['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'];
|
||||||
$mappedSkills = [
|
$mappedSkills = [
|
||||||
@ -212,7 +217,10 @@ public function show($id)
|
|||||||
->limit(3)
|
->limit(3)
|
||||||
->get();
|
->get();
|
||||||
$similarWorkers = $simDb->map(function ($sw) {
|
$similarWorkers = $simDb->map(function ($sw) {
|
||||||
$availabilityStatus = ucfirst($sw->status === 'active' ? 'Active' : ($sw->status === 'hidden' ? 'Hidden' : ($sw->status === 'Hired' ? 'Hired' : 'Active')));
|
$isPending = str_contains(strtolower($sw->passport_status), 'pending');
|
||||||
|
if ($isPending || $sw->status === 'hidden' || $sw->status === 'Hired') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $sw->id,
|
'id' => $sw->id,
|
||||||
@ -222,12 +230,10 @@ public function show($id)
|
|||||||
'salary' => (int) $sw->salary,
|
'salary' => (int) $sw->salary,
|
||||||
'rating' => 4.7,
|
'rating' => 4.7,
|
||||||
'verified' => (bool) $sw->verified,
|
'verified' => (bool) $sw->verified,
|
||||||
'availability_status' => $availabilityStatus,
|
|
||||||
];
|
];
|
||||||
})->toArray();
|
})->filter()->values()->toArray();
|
||||||
|
|
||||||
$visaStatusesList = ['Residence Visa', 'Tourist Visa', 'Employment Visa', 'Cancelled Visa', 'Own Visa'];
|
$visaStatus = $w->visa_status;
|
||||||
$visaStatus = $visaStatusesList[$w->id % 5];
|
|
||||||
|
|
||||||
$worker = [
|
$worker = [
|
||||||
'id' => $w->id,
|
'id' => $w->id,
|
||||||
@ -235,9 +241,9 @@ public function show($id)
|
|||||||
'nationality' => $w->nationality,
|
'nationality' => $w->nationality,
|
||||||
'photo' => $photo,
|
'photo' => $photo,
|
||||||
'emirates_id_status' => $emiratesIdStatus,
|
'emirates_id_status' => $emiratesIdStatus,
|
||||||
|
'passport_status' => $w->passport_status,
|
||||||
'category' => $w->category ? $w->category->name : 'Domestic Worker',
|
'category' => $w->category ? $w->category->name : 'Domestic Worker',
|
||||||
'skills' => $mappedSkills,
|
'skills' => $mappedSkills,
|
||||||
'availability_status' => $availabilityStatus,
|
|
||||||
'visa_status' => $visaStatus,
|
'visa_status' => $visaStatus,
|
||||||
'experience' => $w->experience,
|
'experience' => $w->experience,
|
||||||
'experience_years' => 5,
|
'experience_years' => 5,
|
||||||
|
|||||||
@ -15,16 +15,10 @@ public function handle(Request $request, Closure $next): Response
|
|||||||
{
|
{
|
||||||
$user = auth()->check() ? auth()->user() : session('user');
|
$user = auth()->check() ? auth()->user() : session('user');
|
||||||
$role = is_array($user) ? ($user['role'] ?? null) : ($user->role ?? null);
|
$role = is_array($user) ? ($user['role'] ?? null) : ($user->role ?? null);
|
||||||
$subStatus = is_array($user) ? ($user['subscription_status'] ?? 'none') : ($user->subscription_status ?? 'none');
|
|
||||||
|
|
||||||
if (!$user || $role !== 'employer') {
|
if (!$user || $role !== 'employer') {
|
||||||
return redirect()->route('employer.login');
|
return redirect()->route('employer.login');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($subStatus !== 'active' && !$request->routeIs('employer.subscription')) {
|
|
||||||
return redirect()->route('employer.subscription')->with('error', 'Please purchase a subscription to access the portal.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,8 +73,8 @@ public function share(Request $request): array
|
|||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
'role' => $user->role,
|
'role' => $user->role,
|
||||||
'company_name' => $profile ? $profile->company_name : 'Al Mansoor Household',
|
'company_name' => $profile ? $profile->company_name : 'Al Mansoor Household',
|
||||||
'subscription_status' => $sub ? 'active' : 'none',
|
'subscription_status' => 'active',
|
||||||
'subscription_expires_at' => $sub && $sub->expires_at ? date('M d', strtotime($sub->expires_at)) : null,
|
'subscription_expires_at' => $sub && $sub->expires_at ? date('M d', strtotime($sub->expires_at)) : 'Dec 31',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Sync with session so that controllers have access to the exact user
|
// Sync with session so that controllers have access to the exact user
|
||||||
|
|||||||
@ -40,6 +40,38 @@ class Worker extends Model
|
|||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'passport_status',
|
||||||
|
'visa_status',
|
||||||
|
'emirates_id_status',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function getPassportStatusAttribute()
|
||||||
|
{
|
||||||
|
$passport = $this->documents->where('type', 'passport')->first();
|
||||||
|
if (!$passport) {
|
||||||
|
return 'Passport Pending';
|
||||||
|
}
|
||||||
|
return $this->verified ? 'Passport Verified' : 'Passport Pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVisaStatusAttribute()
|
||||||
|
{
|
||||||
|
$visa = $this->documents->where('type', 'visa')->first();
|
||||||
|
if (!$visa) {
|
||||||
|
return 'Visa Pending';
|
||||||
|
}
|
||||||
|
if ($visa->expiry_date && \Carbon\Carbon::parse($visa->expiry_date)->isPast()) {
|
||||||
|
return 'Cancelled Visa';
|
||||||
|
}
|
||||||
|
return 'Residence Visa';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmiratesIdStatusAttribute()
|
||||||
|
{
|
||||||
|
return $this->passport_status;
|
||||||
|
}
|
||||||
|
|
||||||
public function category()
|
public function category()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(WorkerCategory::class, 'category_id');
|
return $this->belongsTo(WorkerCategory::class, 'category_id');
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
'default' => env('DB_CONNECTION', 'mysql'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -32,17 +32,6 @@
|
|||||||
|
|
||||||
'connections' => [
|
'connections' => [
|
||||||
|
|
||||||
'sqlite' => [
|
|
||||||
'driver' => 'sqlite',
|
|
||||||
'url' => env('DB_URL'),
|
|
||||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
|
||||||
'prefix' => '',
|
|
||||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
|
||||||
'busy_timeout' => null,
|
|
||||||
'journal_mode' => null,
|
|
||||||
'synchronous' => null,
|
|
||||||
'transaction_mode' => 'DEFERRED',
|
|
||||||
],
|
|
||||||
|
|
||||||
'mysql' => [
|
'mysql' => [
|
||||||
'driver' => 'mysql',
|
'driver' => 'mysql',
|
||||||
|
|||||||
@ -43,146 +43,6 @@ public function up(): void
|
|||||||
$table->string('assigned_to')->nullable();
|
$table->string('assigned_to')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Seed moderation_reports table
|
|
||||||
DB::table('moderation_reports')->insert([
|
|
||||||
[
|
|
||||||
'id' => 'REP-2025-00128',
|
|
||||||
'type' => 'Profile',
|
|
||||||
'reported_user_name' => 'Aisha Khan',
|
|
||||||
'reported_user_role' => 'Worker',
|
|
||||||
'reported_user_avatar' => 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'reported_by_name' => 'Demo Sponsor',
|
|
||||||
'reported_by_role' => 'Sponsor',
|
|
||||||
'reported_by_avatar' => 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'reason' => 'Fake documents',
|
|
||||||
'priority' => 'High',
|
|
||||||
'status' => 'Pending',
|
|
||||||
'description' => 'The uploaded Emirates ID seems to belong to a different person and is clearly edited/photoshopped.',
|
|
||||||
'reported_at' => '2025-06-11 10:30:00',
|
|
||||||
'created_at' => '2025-06-11 10:30:00',
|
|
||||||
'updated_at' => '2025-06-11 10:30:00'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 'REP-2025-00127',
|
|
||||||
'type' => 'Chat',
|
|
||||||
'reported_user_name' => 'John Smith',
|
|
||||||
'reported_user_role' => 'Sponsor',
|
|
||||||
'reported_user_avatar' => 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'reported_by_name' => 'Maria Dela Cruz',
|
|
||||||
'reported_by_role' => 'Worker',
|
|
||||||
'reported_by_avatar' => 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'reason' => 'Abusive language',
|
|
||||||
'priority' => 'Medium',
|
|
||||||
'status' => 'Pending',
|
|
||||||
'description' => 'The sponsor used highly inappropriate and abusive words in our chat when I refused a weekend shift.',
|
|
||||||
'reported_at' => '2025-06-11 09:15:00',
|
|
||||||
'created_at' => '2025-06-11 09:15:00',
|
|
||||||
'updated_at' => '2025-06-11 09:15:00'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 'REP-2025-00126',
|
|
||||||
'type' => 'Review',
|
|
||||||
'reported_user_name' => 'Fatima Ali',
|
|
||||||
'reported_user_role' => 'Worker',
|
|
||||||
'reported_user_avatar' => 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'reported_by_name' => 'Ahmed Hassan',
|
|
||||||
'reported_by_role' => 'Sponsor',
|
|
||||||
'reported_by_avatar' => 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'reason' => 'Fake review',
|
|
||||||
'priority' => 'Low',
|
|
||||||
'status' => 'In Review',
|
|
||||||
'description' => 'A review was posted about me from an employer I never worked for, claiming I was late.',
|
|
||||||
'reported_at' => '2025-06-10 18:45:00',
|
|
||||||
'created_at' => '2025-06-10 18:45:00',
|
|
||||||
'updated_at' => '2025-06-10 18:45:00'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 'REP-2025-00125',
|
|
||||||
'type' => 'Image',
|
|
||||||
'reported_user_name' => 'Ramesh Kumar',
|
|
||||||
'reported_user_role' => 'Worker',
|
|
||||||
'reported_user_avatar' => 'https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'reported_by_name' => 'Demo Sponsor',
|
|
||||||
'reported_by_role' => 'Sponsor',
|
|
||||||
'reported_by_avatar' => 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'reason' => 'Inappropriate image',
|
|
||||||
'priority' => 'High',
|
|
||||||
'status' => 'Pending',
|
|
||||||
'description' => 'The profile picture uploaded by this worker is not professional and has offensive background details.',
|
|
||||||
'reported_at' => '2025-06-10 03:20:00',
|
|
||||||
'created_at' => '2025-06-10 03:20:00',
|
|
||||||
'updated_at' => '2025-06-10 03:20:00'
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 4. Seed disputes table
|
|
||||||
DB::table('disputes')->insert([
|
|
||||||
[
|
|
||||||
'id' => 'DIS-2025-00036',
|
|
||||||
'party_one_name' => 'Aisha Khan',
|
|
||||||
'party_one_role' => 'Worker',
|
|
||||||
'party_one_avatar' => 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'party_two_name' => 'Demo Sponsor',
|
|
||||||
'party_two_role' => 'Sponsor',
|
|
||||||
'party_two_avatar' => 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'type' => 'Payment Issue',
|
|
||||||
'reason' => 'Salary not paid as agreed',
|
|
||||||
'priority' => 'High',
|
|
||||||
'status' => 'Open',
|
|
||||||
'assigned_to' => 'Sarah Admin',
|
|
||||||
'created_at' => '2025-06-11 11:20:00',
|
|
||||||
'updated_at' => '2025-06-11 11:20:00'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 'DIS-2025-00035',
|
|
||||||
'party_one_name' => 'John Smith',
|
|
||||||
'party_one_role' => 'Sponsor',
|
|
||||||
'party_one_avatar' => 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'party_two_name' => 'Maria Dela Cruz',
|
|
||||||
'party_two_role' => 'Worker',
|
|
||||||
'party_two_avatar' => 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'type' => 'Behavior Issue',
|
|
||||||
'reason' => 'Worker behavior not acceptable',
|
|
||||||
'priority' => 'Medium',
|
|
||||||
'status' => 'Under Review',
|
|
||||||
'assigned_to' => 'Ahmed Admin',
|
|
||||||
'created_at' => '2025-06-10 21:45:00',
|
|
||||||
'updated_at' => '2025-06-10 21:45:00'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 'DIS-2025-00034',
|
|
||||||
'party_one_name' => 'Fatima Ali',
|
|
||||||
'party_one_role' => 'Worker',
|
|
||||||
'party_one_avatar' => 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'party_two_name' => 'Ahmed Hassan',
|
|
||||||
'party_two_role' => 'Sponsor',
|
|
||||||
'party_two_avatar' => 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'type' => 'Working Conditions',
|
|
||||||
'reason' => 'Different work than promised',
|
|
||||||
'priority' => 'Medium',
|
|
||||||
'status' => 'Waiting Response',
|
|
||||||
'assigned_to' => 'Sarah Admin',
|
|
||||||
'created_at' => '2025-06-10 16:30:00',
|
|
||||||
'updated_at' => '2025-06-10 16:30:00'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 'DIS-2025-00033',
|
|
||||||
'party_one_name' => 'Ramesh Kumar',
|
|
||||||
'party_one_role' => 'Worker',
|
|
||||||
'party_one_avatar' => 'https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'party_two_name' => 'Demo Sponsor',
|
|
||||||
'party_two_role' => 'Sponsor',
|
|
||||||
'party_two_avatar' => 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&q=80&w=100',
|
|
||||||
'type' => 'Abuse/Harassment',
|
|
||||||
'reason' => 'Verbal abuse and threats',
|
|
||||||
'priority' => 'High',
|
|
||||||
'status' => 'Under Review',
|
|
||||||
'assigned_to' => 'Ahmed Admin',
|
|
||||||
'created_at' => '2025-06-09 14:15:00',
|
|
||||||
'updated_at' => '2025-06-09 14:15:00'
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
|
|||||||
@ -34,8 +34,8 @@ public function up(): void
|
|||||||
if ($worker->verified) {
|
if ($worker->verified) {
|
||||||
DB::table('audit_logs')->insert([
|
DB::table('audit_logs')->insert([
|
||||||
'category' => 'verification',
|
'category' => 'verification',
|
||||||
'user' => 'System Vetting Engine',
|
'user' => 'System Verification Engine',
|
||||||
'action' => 'Vetting documents and credentials verification approved for worker: ' . $worker->name,
|
'action' => 'Verification documents and credentials verification approved for worker: ' . $worker->name,
|
||||||
'ip_address' => 'Auto-Trigger',
|
'ip_address' => 'Auto-Trigger',
|
||||||
'created_at' => $worker->created_at ?: now(),
|
'created_at' => $worker->created_at ?: now(),
|
||||||
'updated_at' => $worker->created_at ?: now(),
|
'updated_at' => $worker->created_at ?: now(),
|
||||||
|
|||||||
99
database/seeders/ConversationSeeder.php
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Worker;
|
||||||
|
use App\Models\Conversation;
|
||||||
|
use App\Models\Message;
|
||||||
|
|
||||||
|
class ConversationSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$employer = User::where('role', 'employer')->first();
|
||||||
|
if (!$employer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$workers = Worker::where('status', 'active')->take(3)->get();
|
||||||
|
if ($workers->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversation 1: with Worker 1 (John Doe)
|
||||||
|
$w1 = $workers->get(0);
|
||||||
|
if ($w1) {
|
||||||
|
$conv1 = Conversation::create([
|
||||||
|
'employer_id' => $employer->id,
|
||||||
|
'worker_id' => $w1->id,
|
||||||
|
'created_at' => now()->subHours(2),
|
||||||
|
'updated_at' => now()->subHours(2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Message::create([
|
||||||
|
'conversation_id' => $conv1->id,
|
||||||
|
'sender_type' => 'employer',
|
||||||
|
'sender_id' => $employer->id,
|
||||||
|
'text' => 'Hello John, I saw your profile and I am interested in hiring you as an electrician.',
|
||||||
|
'created_at' => now()->subMinutes(110),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Message::create([
|
||||||
|
'conversation_id' => $conv1->id,
|
||||||
|
'sender_type' => 'worker',
|
||||||
|
'sender_id' => $w1->id,
|
||||||
|
'text' => 'Hello, thank you for reaching out! Yes, I am available.',
|
||||||
|
'created_at' => now()->subMinutes(100),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Message::create([
|
||||||
|
'conversation_id' => $conv1->id,
|
||||||
|
'sender_type' => 'employer',
|
||||||
|
'sender_id' => $employer->id,
|
||||||
|
'text' => 'Great, what is your salary expectation?',
|
||||||
|
'created_at' => now()->subMinutes(90),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Message::create([
|
||||||
|
'conversation_id' => $conv1->id,
|
||||||
|
'sender_type' => 'worker',
|
||||||
|
'sender_id' => $w1->id,
|
||||||
|
'text' => 'My expectation is around 2500 AED per month.',
|
||||||
|
'created_at' => now()->subMinutes(80),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversation 2: with Worker 2 (Ali Hassan)
|
||||||
|
$w2 = $workers->get(1);
|
||||||
|
if ($w2) {
|
||||||
|
$conv2 = Conversation::create([
|
||||||
|
'employer_id' => $employer->id,
|
||||||
|
'worker_id' => $w2->id,
|
||||||
|
'created_at' => now()->subHour(),
|
||||||
|
'updated_at' => now()->subHour(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Message::create([
|
||||||
|
'conversation_id' => $conv2->id,
|
||||||
|
'sender_type' => 'employer',
|
||||||
|
'sender_id' => $employer->id,
|
||||||
|
'text' => 'Hi Ali, are you looking for job?',
|
||||||
|
'created_at' => now()->subMinutes(50),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Message::create([
|
||||||
|
'conversation_id' => $conv2->id,
|
||||||
|
'sender_type' => 'worker',
|
||||||
|
'sender_id' => $w2->id,
|
||||||
|
'text' => 'Yes, I am available.',
|
||||||
|
'created_at' => now()->subMinutes(45),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// Create Admin User
|
// Create Admin User
|
||||||
|
if (!\Illuminate\Support\Facades\DB::table('users')->where('email', 'admin@example.com')->exists()) {
|
||||||
\Illuminate\Support\Facades\DB::table('users')->insert([
|
\Illuminate\Support\Facades\DB::table('users')->insert([
|
||||||
'name' => 'Admin User',
|
'name' => 'Admin User',
|
||||||
'email' => 'admin@example.com',
|
'email' => 'admin@example.com',
|
||||||
@ -24,196 +25,16 @@ public function run(): void
|
|||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Call Seeders
|
// Call Seeders
|
||||||
$this->call([
|
$this->call([
|
||||||
WorkerCategorySeeder::class,
|
WorkerCategorySeeder::class,
|
||||||
SkillSeeder::class,
|
SkillSeeder::class,
|
||||||
EmployerProfileSeeder::class,
|
|
||||||
WorkerSeeder::class,
|
WorkerSeeder::class,
|
||||||
|
EmployerProfileSeeder::class,
|
||||||
JobPostSeeder::class,
|
JobPostSeeder::class,
|
||||||
]);
|
ConversationSeeder::class,
|
||||||
|
|
||||||
// Seed Conversations & Messages (assuming IDs exist)
|
|
||||||
$conversationId = \Illuminate\Support\Facades\DB::table('conversations')->insertGetId([
|
|
||||||
'employer_id' => 2, // From EmployerProfileSeeder
|
|
||||||
'worker_id' => 1, // From WorkerSeeder
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
\Illuminate\Support\Facades\DB::table('messages')->insert([
|
|
||||||
[
|
|
||||||
'conversation_id' => $conversationId,
|
|
||||||
'sender_type' => 'employer',
|
|
||||||
'sender_id' => 2,
|
|
||||||
'text' => 'Hello, are you available for an interview?',
|
|
||||||
'created_at' => now(),
|
|
||||||
],
|
|
||||||
'updated_at' => now(),
|
|
||||||
[
|
|
||||||
'conversation_id' => $conversationId,
|
|
||||||
'sender_type' => 'worker',
|
|
||||||
'sender_id' => 1,
|
|
||||||
'text' => 'Yes, I am available.',
|
|
||||||
'created_at' => now()->addMinutes(5),
|
|
||||||
'updated_at' => now()->addMinutes(5),
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Seed Subscriptions
|
|
||||||
\Illuminate\Support\Facades\DB::table('subscriptions')->insert([
|
|
||||||
'user_id' => 2, // Employer created in EmployerProfileSeeder
|
|
||||||
'plan_id' => 'premium',
|
|
||||||
'amount_aed' => 499.00,
|
|
||||||
'starts_at' => now(),
|
|
||||||
'expires_at' => now()->addMonth(),
|
|
||||||
'status' => 'active',
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Seed Payments
|
|
||||||
\Illuminate\Support\Facades\DB::table('payments')->insert([
|
|
||||||
'user_id' => 2, // Employer
|
|
||||||
'amount' => 499.00,
|
|
||||||
'currency' => 'AED',
|
|
||||||
'description' => 'Premium Monthly Subscription',
|
|
||||||
'status' => 'success',
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Seed Sponsors (UAE Sponsor Marketplace Portal)
|
|
||||||
\Illuminate\Support\Facades\DB::table('sponsors')->insert([
|
|
||||||
[
|
|
||||||
'full_name' => 'Ahmed Malik (Al Barari Real Estate)',
|
|
||||||
'email' => 'hr@albarari.ae',
|
|
||||||
'mobile' => '+971 50 123 4567',
|
|
||||||
'password' => \Illuminate\Support\Facades\Hash::make('password'),
|
|
||||||
'country_code' => 'AE',
|
|
||||||
'subscription_status' => 'active',
|
|
||||||
'subscription_plan' => 'premium',
|
|
||||||
'subscription_start_date' => now()->subDays(15),
|
|
||||||
'subscription_end_date' => now()->addDays(350),
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'is_verified' => true,
|
|
||||||
'otp_verified_at' => now()->subDays(15),
|
|
||||||
'profile_image' => null,
|
|
||||||
'address' => 'Villa 14, Al Barari',
|
|
||||||
'city' => 'Dubai',
|
|
||||||
'nationality' => 'Emirati',
|
|
||||||
'status' => 'active',
|
|
||||||
'last_login_at' => now(),
|
|
||||||
'created_at' => now()->subDays(15),
|
|
||||||
'updated_at' => now(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'full_name' => 'Sarah Johnson (Marina Cleaners)',
|
|
||||||
'email' => 'contact@marinaclean.com',
|
|
||||||
'mobile' => '+971 50 234 5678',
|
|
||||||
'password' => \Illuminate\Support\Facades\Hash::make('password'),
|
|
||||||
'country_code' => 'AE',
|
|
||||||
'subscription_status' => 'active',
|
|
||||||
'subscription_plan' => 'basic',
|
|
||||||
'subscription_start_date' => now()->subDays(10),
|
|
||||||
'subscription_end_date' => now()->addDays(355),
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'is_verified' => true,
|
|
||||||
'otp_verified_at' => now()->subDays(10),
|
|
||||||
'profile_image' => null,
|
|
||||||
'address' => 'Marina Heights, Floor 22',
|
|
||||||
'city' => 'Abu Dhabi',
|
|
||||||
'nationality' => 'British',
|
|
||||||
'status' => 'active',
|
|
||||||
'last_login_at' => now()->subHours(4),
|
|
||||||
'created_at' => now()->subDays(10),
|
|
||||||
'updated_at' => now(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'full_name' => 'John Doe (Golden Hospitality)',
|
|
||||||
'email' => 'hiring@golden.hotel',
|
|
||||||
'mobile' => '+971 50 345 6789',
|
|
||||||
'password' => \Illuminate\Support\Facades\Hash::make('password'),
|
|
||||||
'country_code' => 'AE',
|
|
||||||
'subscription_status' => 'none',
|
|
||||||
'subscription_plan' => null,
|
|
||||||
'subscription_start_date' => null,
|
|
||||||
'subscription_end_date' => null,
|
|
||||||
'payment_status' => 'unpaid',
|
|
||||||
'is_verified' => false,
|
|
||||||
'otp_verified_at' => null,
|
|
||||||
'profile_image' => null,
|
|
||||||
'address' => 'Al Majaz 3, Corniche St',
|
|
||||||
'city' => 'Sharjah',
|
|
||||||
'nationality' => 'American',
|
|
||||||
'status' => 'inactive',
|
|
||||||
'last_login_at' => null,
|
|
||||||
'created_at' => now()->subDays(2),
|
|
||||||
'updated_at' => now(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'full_name' => 'Mike Ross (Emirates Logistics)',
|
|
||||||
'email' => 'jobs@emirateslogistics.com',
|
|
||||||
'mobile' => '+971 50 456 7890',
|
|
||||||
'password' => \Illuminate\Support\Facades\Hash::make('password'),
|
|
||||||
'country_code' => 'AE',
|
|
||||||
'subscription_status' => 'active',
|
|
||||||
'subscription_plan' => 'premium',
|
|
||||||
'subscription_start_date' => now()->subDays(30),
|
|
||||||
'subscription_end_date' => now()->addDays(335),
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'is_verified' => true,
|
|
||||||
'otp_verified_at' => now()->subDays(30),
|
|
||||||
'profile_image' => null,
|
|
||||||
'address' => 'Logistics City, Zone 4',
|
|
||||||
'city' => 'Dubai',
|
|
||||||
'nationality' => 'Canadian',
|
|
||||||
'status' => 'active',
|
|
||||||
'last_login_at' => now()->subDays(1),
|
|
||||||
'created_at' => now()->subDays(30),
|
|
||||||
'updated_at' => now(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'full_name' => 'Jane Foster (Dubai Mall Services)',
|
|
||||||
'email' => 'support@dubaimall.ae',
|
|
||||||
'mobile' => '+971 50 567 8901',
|
|
||||||
'password' => \Illuminate\Support\Facades\Hash::make('password'),
|
|
||||||
'country_code' => 'AE',
|
|
||||||
'subscription_status' => 'expired',
|
|
||||||
'subscription_plan' => 'vip',
|
|
||||||
'subscription_start_date' => now()->subDays(400),
|
|
||||||
'subscription_end_date' => now()->subDays(35),
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'is_verified' => true,
|
|
||||||
'otp_verified_at' => now()->subDays(400),
|
|
||||||
'profile_image' => null,
|
|
||||||
'address' => 'Downtown Dubai Mall Management',
|
|
||||||
'city' => 'Dubai',
|
|
||||||
'nationality' => 'Australian',
|
|
||||||
'status' => 'suspended',
|
|
||||||
'last_login_at' => now()->subDays(40),
|
|
||||||
'created_at' => now()->subDays(400),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Seed Announcements
|
|
||||||
\Illuminate\Support\Facades\DB::table('announcements')->insert([
|
|
||||||
[
|
|
||||||
'title' => 'New Visa Regulations Update',
|
|
||||||
'body' => 'The Ministry of Human Resources and Emiratisation has announced updated guidelines for domestic worker sponsorship starting this month.',
|
|
||||||
'type' => 'info',
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'title' => 'Enhanced OCR Verification Active',
|
|
||||||
'body' => 'We have deployed upgraded OCR passport verification to ensure 100% legal compliance for all worker profiles on the platform.',
|
|
||||||
'type' => 'info',
|
|
||||||
'created_at' => now()->subDays(5),
|
|
||||||
'updated_at' => now()->subDays(5),
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -135,8 +135,61 @@ public function run(): void
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Dynamic Category Resolution
|
||||||
|
$categoryMapping = [
|
||||||
|
1 => 'Electrician',
|
||||||
|
2 => 'Mason',
|
||||||
|
3 => 'Plumber',
|
||||||
|
4 => 'Cleaner',
|
||||||
|
5 => 'Site Supervisor',
|
||||||
|
6 => 'Driver',
|
||||||
|
7 => 'General Helper',
|
||||||
|
8 => 'Childcare',
|
||||||
|
9 => 'Housekeeping',
|
||||||
|
10 => 'Cooking',
|
||||||
|
11 => 'Elderly Care',
|
||||||
|
];
|
||||||
|
|
||||||
|
$categoryIds = [];
|
||||||
|
foreach ($categoryMapping as $oldId => $name) {
|
||||||
|
$cat = \Illuminate\Support\Facades\DB::table('worker_categories')->where('name', $name)->first();
|
||||||
|
if (!$cat) {
|
||||||
|
$catId = \Illuminate\Support\Facades\DB::table('worker_categories')->insertGetId([
|
||||||
|
'name' => $name,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$catId = $cat->id;
|
||||||
|
}
|
||||||
|
$categoryIds[$oldId] = $catId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic Skill Resolution
|
||||||
|
$skillIds = \Illuminate\Support\Facades\DB::table('skills')->pluck('id')->toArray();
|
||||||
|
if (empty($skillIds)) {
|
||||||
|
foreach (['cooking', 'driving', 'childcare', 'cleaning', 'elderly care', 'gardening'] as $skillName) {
|
||||||
|
$skillIds[] = \Illuminate\Support\Facades\DB::table('skills')->insertGetId([
|
||||||
|
'name' => $skillName,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sId1 = $skillIds[0] ?? 1;
|
||||||
|
$sId2 = $skillIds[1] ?? 2;
|
||||||
|
|
||||||
foreach ($workers as $workerData) {
|
foreach ($workers as $workerData) {
|
||||||
|
$resolvedCategoryId = $categoryIds[$workerData['category_id']] ?? $workerData['category_id'];
|
||||||
|
|
||||||
|
// Exclude email duplicates to avoid unique constraint violations
|
||||||
|
if (\Illuminate\Support\Facades\DB::table('workers')->where('email', $workerData['email'])->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$workerId = \Illuminate\Support\Facades\DB::table('workers')->insertGetId(array_merge($workerData, [
|
$workerId = \Illuminate\Support\Facades\DB::table('workers')->insertGetId(array_merge($workerData, [
|
||||||
|
'category_id' => $resolvedCategoryId,
|
||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
]));
|
]));
|
||||||
@ -165,17 +218,17 @@ public function run(): void
|
|||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Seed Skills (assuming skill IDs 1 and 2 exist)
|
// Seed Skills using resolved IDs
|
||||||
\Illuminate\Support\Facades\DB::table('worker_skills')->insert([
|
\Illuminate\Support\Facades\DB::table('worker_skills')->insert([
|
||||||
[
|
[
|
||||||
'worker_id' => $workerId,
|
'worker_id' => $workerId,
|
||||||
'skill_id' => 1,
|
'skill_id' => $sId1,
|
||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'worker_id' => $workerId,
|
'worker_id' => $workerId,
|
||||||
'skill_id' => 2,
|
'skill_id' => $sId2,
|
||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -318,6 +318,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/workers/skills": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Worker/Auth"
|
||||||
|
],
|
||||||
|
"summary": "Get Master Skills List",
|
||||||
|
"description": "Returns a list of all available master skills for helper onboarding profile setup.",
|
||||||
|
"security": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Master skills list retrieved successfully.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"skills": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Cooking"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/workers/send-otp": {
|
"/workers/send-otp": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@ -2083,7 +2127,12 @@
|
|||||||
},
|
},
|
||||||
"emirates_id_status": {
|
"emirates_id_status": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Emirates ID Verified"
|
"example": "Passport Verified",
|
||||||
|
"description": "Deprecated - Use passport_status instead"
|
||||||
|
},
|
||||||
|
"passport_status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Passport Verified"
|
||||||
},
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -2369,7 +2418,7 @@
|
|||||||
"Employer/Billing"
|
"Employer/Billing"
|
||||||
],
|
],
|
||||||
"summary": "Get Payment History",
|
"summary": "Get Payment History",
|
||||||
"description": "Retrieves the complete transaction history for the authenticated sponsor/employer, including subscription passes and document vetting receipts.",
|
"description": "Retrieves the complete transaction history for the authenticated sponsor/employer, including subscription passes and document verification receipts.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "page",
|
"name": "page",
|
||||||
|
|||||||
BIN
public/uploads/documents/1780461756_passport_1000416131.jpg
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
public/uploads/documents/1780462083_passport_1000416131.jpg
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
public/uploads/documents/1780465309_passport_1000416581.jpg
Normal file
|
After Width: | Height: | Size: 417 KiB |
BIN
public/uploads/documents/1780465309_visa_1000416581.jpg
Normal file
|
After Width: | Height: | Size: 417 KiB |
BIN
public/uploads/documents/1780465587_passport_1000416581.jpg
Normal file
|
After Width: | Height: | Size: 417 KiB |
BIN
public/uploads/documents/1780465587_visa_1000416581.jpg
Normal file
|
After Width: | Height: | Size: 417 KiB |
BIN
public/uploads/documents/1780471725_passport_1000416581.jpg
Normal file
|
After Width: | Height: | Size: 417 KiB |
BIN
public/uploads/documents/1780471725_visa_1000416581.jpg
Normal file
|
After Width: | Height: | Size: 417 KiB |
BIN
public/uploads/documents/1780473071_passport_1000100136.jpg
Normal file
|
After Width: | Height: | Size: 529 KiB |
BIN
public/uploads/documents/1780473071_visa_1000100136.jpg
Normal file
|
After Width: | Height: | Size: 529 KiB |
@ -55,11 +55,11 @@ export default function EmployerLayout({ children, title, fullPage = false }) {
|
|||||||
name: 'Guest',
|
name: 'Guest',
|
||||||
company_name: '',
|
company_name: '',
|
||||||
email: '',
|
email: '',
|
||||||
subscription_status: 'inactive',
|
subscription_status: 'active',
|
||||||
subscription_expires_at: '',
|
subscription_expires_at: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSubActive = user.subscription_status === 'active';
|
const isSubActive = true;
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ name: 'Dashboard', translationKey: 'dashboard', href: '/employer/dashboard', icon: LayoutDashboard },
|
{ name: 'Dashboard', translationKey: 'dashboard', href: '/employer/dashboard', icon: LayoutDashboard },
|
||||||
|
|||||||
@ -28,18 +28,23 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
const [activeTrendTab, setActiveTrendTab] = useState('All');
|
const [activeTrendTab, setActiveTrendTab] = useState('All');
|
||||||
const [selectedSlice, setSelectedSlice] = useState(null);
|
const [selectedSlice, setSelectedSlice] = useState(null);
|
||||||
|
|
||||||
// Mock trend monthly data for custom SVG Line chart
|
// Dynamic trend monthly data for custom SVG Line chart
|
||||||
const trendData = [
|
const trendData = stats?.trend_data || [
|
||||||
{ month: 'Dec', basic: 110, premium: 60, vip: 15 },
|
{ month: 'Dec', basic: 0, premium: 0, vip: 0 },
|
||||||
{ month: 'Jan', basic: 130, premium: 70, vip: 18 },
|
{ month: 'Jan', basic: 0, premium: 0, vip: 0 },
|
||||||
{ month: 'Feb', basic: 145, premium: 82, vip: 22 },
|
{ month: 'Feb', basic: 0, premium: 0, vip: 0 },
|
||||||
{ month: 'Mar', basic: 160, premium: 88, vip: 24 },
|
{ month: 'Mar', basic: 0, premium: 0, vip: 0 },
|
||||||
{ month: 'Apr', basic: 172, premium: 92, vip: 28 },
|
{ month: 'Apr', basic: 0, premium: 0, vip: 0 },
|
||||||
{ month: 'May', basic: 184, premium: 98, vip: 30 }
|
{ month: 'May', basic: 0, premium: 0, vip: 0 }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const maxVal = Math.max(
|
||||||
|
10,
|
||||||
|
...trendData.map(d => Math.max(d.basic || 0, d.premium || 0, d.vip || 0))
|
||||||
|
);
|
||||||
|
|
||||||
// Compute lines points for SVG Spline
|
// Compute lines points for SVG Spline
|
||||||
const getCoordinatesForLine = (key, height, width, maxVal) => {
|
const getCoordinatesForLine = (key, height, width, max) => {
|
||||||
const points = [];
|
const points = [];
|
||||||
const paddingLeft = 35;
|
const paddingLeft = 35;
|
||||||
const paddingRight = 15;
|
const paddingRight = 15;
|
||||||
@ -50,9 +55,9 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
const graphWidth = width - paddingLeft - paddingRight;
|
const graphWidth = width - paddingLeft - paddingRight;
|
||||||
|
|
||||||
trendData.forEach((d, idx) => {
|
trendData.forEach((d, idx) => {
|
||||||
const val = d[key];
|
const val = d[key] || 0;
|
||||||
const x = paddingLeft + (idx / (trendData.length - 1)) * graphWidth;
|
const x = paddingLeft + (idx / (trendData.length - 1)) * graphWidth;
|
||||||
const y = paddingTop + graphHeight - (val / maxVal) * graphHeight;
|
const y = paddingTop + graphHeight - (val / max) * graphHeight;
|
||||||
points.push(`${x},${y}`);
|
points.push(`${x},${y}`);
|
||||||
});
|
});
|
||||||
return points.join(' ');
|
return points.join(' ');
|
||||||
@ -73,7 +78,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
<span>System Status: Online</span>
|
<span>System Status: Online</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-black tracking-tight mt-2">Welcome back, Administrator</h2>
|
<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>
|
<p className="text-sm text-slate-400 font-medium">UAE domestic workers platform is operating optimally. {stats?.verifications_today || 0} automated verifications completed today.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
@ -99,12 +104,12 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="text-3xl font-black text-slate-900 tracking-tight">
|
<h3 className="text-3xl font-black text-slate-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">
|
<div className="flex items-center space-x-2 mt-2 text-xs font-bold">
|
||||||
<span className="text-emerald-600">{stats?.active_workers || 980} Active</span>
|
<span className="text-emerald-600">{stats?.active_workers || 0} Active</span>
|
||||||
<span className="text-slate-300">•</span>
|
<span className="text-slate-300">•</span>
|
||||||
<span className="text-slate-400">{stats?.inactive_workers || 440} Inactive</span>
|
<span className="text-slate-400">{stats?.inactive_workers || 0} Inactive</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -112,18 +117,18 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
{/* Verification Stats */}
|
{/* Verification Stats */}
|
||||||
<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-2xl p-6 border border-slate-100 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-black text-slate-400 uppercase tracking-wider">Vetting & OCR Rate</span>
|
<span className="text-xs font-black text-slate-400 uppercase tracking-wider">Verification & OCR Rate</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">
|
||||||
<ShieldCheck className="w-5 h-5 text-emerald-600" />
|
<ShieldCheck 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-3xl font-black text-slate-900 tracking-tight">
|
||||||
{stats?.verification_rate || 88.5}%
|
{stats?.verification_rate || 0}%
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-slate-500 font-medium mt-2 flex items-center">
|
<p className="text-xs text-slate-500 font-medium mt-2 flex items-center">
|
||||||
<CheckCircle className="w-3.5 h-3.5 text-emerald-500 mr-1 inline" />
|
<CheckCircle className="w-3.5 h-3.5 text-emerald-500 mr-1 inline" />
|
||||||
<span>1,256 Profiles Auto-OCR Verified</span>
|
<span>{stats?.verified_workers_count?.toLocaleString() || 0} Profiles Auto-OCR Verified</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -138,11 +143,11 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="text-3xl font-black text-slate-900 tracking-tight">
|
<h3 className="text-3xl font-black text-slate-900 tracking-tight">
|
||||||
{stats?.hiring_conversion_rate || 14.8}%
|
{stats?.hiring_conversion_rate || 0}%
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center space-x-2 mt-2 text-xs font-bold text-blue-600">
|
<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" />
|
<MessageSquare className="w-3.5 h-3.5" />
|
||||||
<span>{stats?.chat_to_hire_rate || 12.6}% Chat-to-Hire</span>
|
<span>{stats?.chat_to_hire_rate || 0}% Chat-to-Hire</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -157,11 +162,11 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h3 className="text-3xl font-black text-slate-900 tracking-tight">
|
<h3 className="text-3xl font-black text-slate-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-bold mt-2 flex items-center">
|
||||||
<ArrowUpRight className="w-3.5 h-3.5 mr-0.5" />
|
<ArrowUpRight className="w-3.5 h-3.5 mr-0.5" />
|
||||||
<span>+{stats?.new_employers_this_week || 28} Sponsors Added This Week</span>
|
<span>+{stats?.new_employers_this_week || 0} Sponsors Added This Week</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -199,9 +204,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
<line x1="35" y1="20" x2="435" y2="20" stroke="#f1f5f9" strokeWidth="1" />
|
<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="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="130" x2="435" y2="130" stroke="#f1f5f9" strokeWidth="1" />
|
||||||
<line x1="35" y1="180" x2="435" y2="180" stroke="#cbd5e1" strokeWidth="1" />
|
<line x1="35" y1="180" x2="435" y2="180" stroke="#cbd5e1" strokeWidth="1" /> {/* Line 1: Basic */}
|
||||||
|
|
||||||
{/* Line 1: Basic */}
|
|
||||||
{(activeTrendTab === 'All') && (
|
{(activeTrendTab === 'All') && (
|
||||||
<polyline
|
<polyline
|
||||||
fill="none"
|
fill="none"
|
||||||
@ -209,7 +212,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
strokeWidth="3.5"
|
strokeWidth="3.5"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
points={getCoordinatesForLine('basic', 200, 450, 200)}
|
points={getCoordinatesForLine('basic', 200, 450, maxVal)}
|
||||||
className="transition-all duration-500 opacity-60"
|
className="transition-all duration-500 opacity-60"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -222,7 +225,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
strokeWidth="4"
|
strokeWidth="4"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
points={getCoordinatesForLine('premium', 200, 450, 200)}
|
points={getCoordinatesForLine('premium', 200, 450, maxVal)}
|
||||||
className="transition-all duration-500"
|
className="transition-all duration-500"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -235,7 +238,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
strokeWidth="4.5"
|
strokeWidth="4.5"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
points={getCoordinatesForLine('vip', 200, 450, 200)}
|
points={getCoordinatesForLine('vip', 200, 450, maxVal)}
|
||||||
className="transition-all duration-500"
|
className="transition-all duration-500"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -251,9 +254,9 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Y-Axis Labels */}
|
{/* Y-Axis Labels */}
|
||||||
<text x="25" y="25" textAnchor="end" className="text-[10px] font-black fill-slate-300">200</text>
|
<text x="25" y="25" textAnchor="end" className="text-[10px] font-black fill-slate-300">{maxVal}</text>
|
||||||
<text x="25" y="80" textAnchor="end" className="text-[10px] font-black fill-slate-300">100</text>
|
<text x="25" y="80" textAnchor="end" className="text-[10px] font-black fill-slate-300">{Math.round(maxVal / 2)}</text>
|
||||||
<text x="25" y="135" textAnchor="end" className="text-[10px] font-black fill-slate-300">50</text>
|
<text x="25" y="135" textAnchor="end" className="text-[10px] font-black fill-slate-300">{Math.round(maxVal / 4)}</text>
|
||||||
<text x="25" y="185" textAnchor="end" className="text-[10px] font-black fill-slate-300">0</text>
|
<text x="25" y="185" textAnchor="end" className="text-[10px] font-black fill-slate-300">0</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
@ -261,15 +264,15 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
<div className="flex items-center justify-center space-x-6 mt-4">
|
<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="flex items-center space-x-2 text-xs font-bold text-slate-600">
|
||||||
<div className="w-3 h-1 bg-slate-400 rounded" />
|
<div className="w-3 h-1 bg-slate-400 rounded" />
|
||||||
<span>Basic Search ({trendData[5].basic})</span>
|
<span>Basic Search ({trendData[trendData.length - 1]?.basic || 0})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
|
<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" />
|
<div className="w-3 h-1 bg-blue-500 rounded" />
|
||||||
<span>Premium Pass ({trendData[5].premium})</span>
|
<span>Premium Pass ({trendData[trendData.length - 1]?.premium || 0})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
|
<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" />
|
<div className="w-3 h-1 bg-emerald-500 rounded" />
|
||||||
<span>VIP Concierge ({trendData[5].vip})</span>
|
<span>VIP Concierge ({trendData[trendData.length - 1]?.vip || 0})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -285,46 +288,72 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
<p className="text-xs text-slate-500 font-medium uppercase tracking-widest mt-1">Real-time availability of candidates pool</p>
|
<p className="text-xs text-slate-500 font-medium uppercase tracking-widest mt-1">Real-time availability of candidates pool</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
const activeVal = stats?.worker_availability?.['Active'] || 0;
|
||||||
|
const hiddenVal = stats?.worker_availability?.['Hidden'] || 0;
|
||||||
|
const hiredVal = stats?.worker_availability?.['Hired'] || 0;
|
||||||
|
const totalVal = activeVal + hiddenVal + hiredVal;
|
||||||
|
|
||||||
|
const activePct = totalVal > 0 ? Math.round((activeVal / totalVal) * 100) : 0;
|
||||||
|
const hiddenPct = totalVal > 0 ? Math.round((hiddenVal / totalVal) * 100) : 0;
|
||||||
|
const hiredPct = totalVal > 0 ? Math.round((hiredVal / totalVal) * 100) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 items-center my-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 items-center my-4">
|
||||||
{/* Custom Donut Chart */}
|
{/* Custom Donut Chart */}
|
||||||
<div className="flex justify-center relative">
|
<div className="flex justify-center relative">
|
||||||
<svg className="w-40 h-40" viewBox="0 0 100 100">
|
<svg className="w-40 h-40" viewBox="0 0 100 100">
|
||||||
{/* Segment 1: Available Now (640 / 1420 = 45%) -> Stroke-dasharray: 45, 55 */}
|
{totalVal > 0 ? (
|
||||||
|
<g className="transform -rotate-90 origin-center">
|
||||||
|
{/* Segment 1: Active */}
|
||||||
<circle
|
<circle
|
||||||
cx="50" cy="50" r="40"
|
cx="50" cy="50" r="40"
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
stroke="#10b981"
|
stroke="#10b981"
|
||||||
strokeWidth="12"
|
strokeWidth="12"
|
||||||
strokeDasharray="45 55"
|
pathLength="100"
|
||||||
strokeDashoffset="25"
|
strokeDasharray={`${activePct} ${100 - activePct}`}
|
||||||
onMouseEnter={() => setSelectedSlice('Available')}
|
strokeDashoffset="0"
|
||||||
|
onMouseEnter={() => setSelectedSlice('Active')}
|
||||||
onMouseLeave={() => setSelectedSlice(null)}
|
onMouseLeave={() => setSelectedSlice(null)}
|
||||||
className="cursor-pointer transition-all duration-300 hover:stroke-[14]"
|
className="cursor-pointer transition-all duration-300 hover:stroke-[14]"
|
||||||
/>
|
/>
|
||||||
{/* Segment 2: Contracted (480 / 1420 = 34%) -> Stroke-dasharray: 34, 66 */}
|
{/* Segment 2: Hidden */}
|
||||||
<circle
|
<circle
|
||||||
cx="50" cy="50" r="40"
|
cx="50" cy="50" r="40"
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
stroke="#94a3b8"
|
stroke="#94a3b8"
|
||||||
strokeWidth="12"
|
strokeWidth="12"
|
||||||
strokeDasharray="34 66"
|
pathLength="100"
|
||||||
strokeDashoffset="-20"
|
strokeDasharray={`${hiddenPct} ${100 - hiddenPct}`}
|
||||||
onMouseEnter={() => setSelectedSlice('Contracted')}
|
strokeDashoffset={`-${activePct}`}
|
||||||
|
onMouseEnter={() => setSelectedSlice('Hidden')}
|
||||||
onMouseLeave={() => setSelectedSlice(null)}
|
onMouseLeave={() => setSelectedSlice(null)}
|
||||||
className="cursor-pointer transition-all duration-300 hover:stroke-[14]"
|
className="cursor-pointer transition-all duration-300 hover:stroke-[14]"
|
||||||
/>
|
/>
|
||||||
{/* Segment 3: In Interview (300 / 1420 = 21%) -> Stroke-dasharray: 21, 79 */}
|
{/* Segment 3: Hired */}
|
||||||
<circle
|
<circle
|
||||||
cx="50" cy="50" r="40"
|
cx="50" cy="50" r="40"
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
stroke="#3b82f6"
|
stroke="#3b82f6"
|
||||||
strokeWidth="12"
|
strokeWidth="12"
|
||||||
strokeDasharray="21 79"
|
pathLength="100"
|
||||||
strokeDashoffset="-54"
|
strokeDasharray={`${hiredPct} ${100 - hiredPct}`}
|
||||||
onMouseEnter={() => setSelectedSlice('Interviewing')}
|
strokeDashoffset={`-${activePct + hiddenPct}`}
|
||||||
|
onMouseEnter={() => setSelectedSlice('Hired')}
|
||||||
onMouseLeave={() => setSelectedSlice(null)}
|
onMouseLeave={() => setSelectedSlice(null)}
|
||||||
className="cursor-pointer transition-all duration-300 hover:stroke-[14]"
|
className="cursor-pointer transition-all duration-300 hover:stroke-[14]"
|
||||||
/>
|
/>
|
||||||
|
</g>
|
||||||
|
) : (
|
||||||
|
/* Empty State Grey Circle */
|
||||||
|
<circle
|
||||||
|
cx="50" cy="50" r="40"
|
||||||
|
fill="transparent"
|
||||||
|
stroke="#e2e8f0"
|
||||||
|
strokeWidth="12"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Inner Text */}
|
{/* Inner Text */}
|
||||||
<circle cx="50" cy="50" r="28" fill="white" />
|
<circle cx="50" cy="50" r="28" fill="white" />
|
||||||
@ -332,7 +361,7 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
{selectedSlice || 'TOTAL'}
|
{selectedSlice || 'TOTAL'}
|
||||||
</text>
|
</text>
|
||||||
<text x="50" y="60" textAnchor="middle" className="text-[8px] font-black fill-slate-400 tracking-wider">
|
<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'}
|
{selectedSlice === 'Active' ? `${activeVal} Workers` : selectedSlice === 'Hidden' ? `${hiddenVal} Workers` : selectedSlice === 'Hired' ? `${hiredVal} Workers` : `${totalVal} Total`}
|
||||||
</text>
|
</text>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@ -342,26 +371,28 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
<div className="p-3 bg-emerald-50/50 border border-emerald-100 rounded-xl flex items-center justify-between">
|
<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="flex items-center space-x-2">
|
||||||
<div className="w-2.5 h-2.5 bg-emerald-500 rounded-full" />
|
<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>
|
<span className="text-xs font-bold text-slate-700">Active</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-black text-slate-900">640 (45%)</span>
|
<span className="text-xs font-black text-slate-900">{activeVal} ({activePct}%)</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>
|
||||||
<div className="p-3 bg-slate-50 border border-slate-100 rounded-xl flex items-center justify-between">
|
<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="flex items-center space-x-2">
|
||||||
<div className="w-2.5 h-2.5 bg-slate-400 rounded-full" />
|
<div className="w-2.5 h-2.5 bg-slate-400 rounded-full" />
|
||||||
<span className="text-xs font-bold text-slate-700">Engaged</span>
|
<span className="text-xs font-bold text-slate-700">Hidden</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-black text-slate-900">480 (34%)</span>
|
<span className="text-xs font-black text-slate-900">{hiddenVal} ({hiddenPct}%)</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">Hired</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-black text-slate-900">{hiredVal} ({hiredPct}%)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -372,16 +403,30 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
<p className="text-xs text-slate-500 font-medium uppercase tracking-widest mt-1">Platform-wide worker placement workflow stats</p>
|
<p className="text-xs text-slate-500 font-medium uppercase tracking-widest mt-1">Platform-wide worker placement workflow stats</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
{[
|
{(() => {
|
||||||
{ stage: 'Profiles Browsed', val: '12,500', pct: '100%', bg: 'bg-slate-100 border-slate-200 text-slate-600' },
|
const funnelData = stats?.funnel || {
|
||||||
{ stage: 'Chats Initiated', val: '4,820', pct: '38.5%', bg: 'bg-teal-50 border-teal-100 text-teal-700' },
|
profiles_browsed: 0,
|
||||||
{ stage: 'Interviews Held', val: '1,860', pct: '14.8%', bg: 'bg-blue-50 border-blue-100 text-blue-700' },
|
chats_initiated: 0,
|
||||||
{ stage: 'Offers Sent', val: '920', pct: '7.3%', bg: 'bg-purple-50 border-purple-100 text-purple-700' },
|
candidates_shortlisted: 0,
|
||||||
{ stage: 'Workers Hired', val: '610', pct: '4.8%', bg: 'bg-emerald-50 border-emerald-100 text-emerald-700' }
|
workers_hired: 0
|
||||||
].map((step, i) => (
|
};
|
||||||
|
|
||||||
|
const browsed = funnelData.profiles_browsed || 0;
|
||||||
|
const chats = funnelData.chats_initiated || 0;
|
||||||
|
const shortlisted = funnelData.candidates_shortlisted || 0;
|
||||||
|
const hired = funnelData.workers_hired || 0;
|
||||||
|
|
||||||
|
const funnelSteps = [
|
||||||
|
{ stage: 'Profiles Browsed', val: browsed.toLocaleString(), pct: '100%', bg: 'bg-slate-100 border-slate-200 text-slate-600' },
|
||||||
|
{ stage: 'Chats Initiated', val: chats.toLocaleString(), pct: browsed > 0 ? `${((chats / browsed) * 100).toFixed(1)}%` : '0%', bg: 'bg-teal-50 border-teal-100 text-teal-700' },
|
||||||
|
{ stage: 'Candidates Shortlisted', val: shortlisted.toLocaleString(), pct: browsed > 0 ? `${((shortlisted / browsed) * 100).toFixed(1)}%` : '0%', bg: 'bg-blue-50 border-blue-100 text-blue-700' },
|
||||||
|
{ stage: 'Workers Hired', val: hired.toLocaleString(), pct: browsed > 0 ? `${((hired / browsed) * 100).toFixed(1)}%` : '0%', bg: 'bg-emerald-50 border-emerald-100 text-emerald-700' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return funnelSteps.map((step, i) => (
|
||||||
<div key={i} className={`p-4 border rounded-2xl ${step.bg} flex flex-col justify-between relative`}>
|
<div key={i} className={`p-4 border rounded-2xl ${step.bg} flex flex-col justify-between relative`}>
|
||||||
{i < 4 && (
|
{i < 3 && (
|
||||||
<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">
|
<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" />
|
<ArrowRight className="w-3.5 h-3.5 text-slate-400" />
|
||||||
</div>
|
</div>
|
||||||
@ -395,116 +440,11 @@ export default function Dashboard({ stats, recent_verifications, recent_subscrip
|
|||||||
<div className="text-[10px] font-bold mt-0.5">Ratio: {step.pct}</div>
|
<div className="text-[10px] font-bold mt-0.5">Ratio: {step.pct}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
));
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recent Verifications Section */}
|
|
||||||
<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>
|
|
||||||
<h2 className="text-base font-semibold text-gray-800 tracking-tight">Recent Verifications</h2>
|
|
||||||
<p className="text-xs text-gray-500 mt-0.5">Profiles automatically verified via document proof</p>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href="/admin/workers/verifications"
|
|
||||||
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" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{recent_verifications && recent_verifications.length > 0 ? (
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
|
|
||||||
<TableHead className="font-semibold text-slate-600 text-xs h-10">Name</TableHead>
|
|
||||||
<TableHead className="font-semibold text-slate-600 text-xs h-10">Type</TableHead>
|
|
||||||
<TableHead className="font-semibold text-slate-600 text-xs h-10">Document</TableHead>
|
|
||||||
<TableHead className="font-semibold text-slate-600 text-xs h-10">Processed</TableHead>
|
|
||||||
<TableHead className="font-semibold text-slate-600 text-xs h-10 text-right pr-6">Status</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{recent_verifications.map((item) => (
|
|
||||||
<TableRow key={item.id} className="hover:bg-slate-50/80 transition-colors">
|
|
||||||
<TableCell className="font-medium text-gray-900 text-sm">{item.name}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
||||||
item.type === 'Worker' ? 'bg-teal-50 text-[#0F6E56]' : 'bg-indigo-50 text-indigo-700'
|
|
||||||
}`}>
|
|
||||||
{item.type}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-sm text-gray-600">{item.document_type}</TableCell>
|
|
||||||
<TableCell className="text-xs text-gray-500">{item.processed_at}</TableCell>
|
|
||||||
<TableCell className="text-right pr-6">
|
|
||||||
<span className="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold bg-emerald-100 text-emerald-800">
|
|
||||||
<CheckCircle className="w-3 h-3 mr-1" />
|
|
||||||
{item.status || 'Verified'}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
) : (
|
|
||||||
<div className="p-12 text-center flex flex-col items-center justify-center">
|
|
||||||
<CheckCircle className="w-12 h-12 text-emerald-500 mb-3" />
|
|
||||||
<h3 className="text-base font-semibold text-gray-800 tracking-tight">No recent activity</h3>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">Automatic verification system is monitoring new registrations.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Recent Subscriptions Section */}
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden font-sans">
|
|
||||||
<div className="p-5 border-b border-gray-100 flex items-center justify-between bg-white">
|
|
||||||
<div>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href="/admin/payments"
|
|
||||||
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" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{recent_subscriptions && recent_subscriptions.length > 0 ? (
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<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">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 text-right pr-6">Date</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{recent_subscriptions.map((item) => (
|
|
||||||
<TableRow key={item.id} className="hover:bg-slate-50/80 transition-colors">
|
|
||||||
<TableCell className="font-medium text-gray-900 text-sm">{item.employer_name}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700">
|
|
||||||
{item.plan_name}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-sm font-medium text-gray-900">
|
|
||||||
AED {item.amount_aed}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-xs text-gray-500 text-right pr-6">{item.subscribed_at}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
) : (
|
|
||||||
<div className="p-12 text-center text-gray-500 text-xs font-medium">
|
|
||||||
No recent subscriptions found.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -418,7 +418,7 @@ export default function DisputesHub({ tickets }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-[10px] bg-slate-50 p-2.5 rounded-xl border border-slate-100 text-slate-400 font-bold text-center uppercase tracking-wider">
|
<div className="text-[10px] bg-slate-50 p-2.5 rounded-xl border border-slate-100 text-slate-400 font-bold text-center uppercase tracking-wider">
|
||||||
Logs Digitally Secured by UAE Vetting Compliance
|
Logs Digitally Secured by UAE Verification Compliance
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -497,7 +497,7 @@ export default function SponsorsIndex({ sponsors, filters = {} }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 space-y-6 overflow-y-auto flex-1 bg-slate-50/50">
|
<div className="p-6 space-y-6 overflow-y-auto flex-1 bg-slate-50/50">
|
||||||
{/* Vetting info */}
|
{/* Verification info */}
|
||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-4">
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-4">
|
||||||
<div className="flex items-center justify-between border-b border-slate-100 pb-2">
|
<div className="flex items-center justify-between border-b border-slate-100 pb-2">
|
||||||
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest flex items-center gap-1.5">
|
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest flex items-center gap-1.5">
|
||||||
|
|||||||
@ -41,19 +41,13 @@ export default function PaymentsIndex({ stats, payments: initialPayments }) {
|
|||||||
const [selectedPayment, setSelectedPayment] = React.useState(null);
|
const [selectedPayment, setSelectedPayment] = React.useState(null);
|
||||||
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
||||||
|
|
||||||
const payments = initialPayments || [
|
const payments = initialPayments || [];
|
||||||
{ id: 'PAY-001', employer: 'Al Barari Real Estate', plan: 'Premium Pass', amount: 199, date: '2026-05-15', status: 'Completed', method: 'Visa •••• 4242', period: 'May 2026 - June 2026' },
|
|
||||||
{ id: 'PAY-002', employer: 'Marina Cleaners', plan: 'Basic Search', amount: 99, date: '2026-05-14', status: 'Completed', method: 'Mastercard •••• 8812', period: 'May 2026 - June 2026' },
|
|
||||||
{ id: 'PAY-003', employer: 'Golden Hospitality', plan: 'VIP Concierge', amount: 499, date: '2026-05-14', status: 'Pending', method: 'Bank Transfer', period: 'May 2026 - June 2026' },
|
|
||||||
{ id: 'PAY-004', employer: 'Emirates Logistics', plan: 'Premium Pass', amount: 199, date: '2026-05-13', status: 'Completed', method: 'Visa •••• 1002', period: 'May 2026 - June 2026' },
|
|
||||||
{ id: 'PAY-005', employer: 'Dubai Mall Services', plan: 'Premium Pass', amount: 199, date: '2026-05-12', status: 'Failed', method: 'Visa •••• 4242', period: 'May 2026 - June 2026' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const analytics = [
|
const analytics = [
|
||||||
{ title: 'Total Revenue', value: '42,850', icon: BadgeDollarSign, trend: '+15.2%', positive: true, color: 'emerald' },
|
{ title: 'Total Revenue', value: stats?.total_revenue ?? '0.00', icon: BadgeDollarSign, trend: 'Live', positive: true, color: 'emerald' },
|
||||||
{ title: 'Active Subscriptions', value: '184', icon: Users, trend: '+4.3%', positive: true, color: 'blue' },
|
{ title: 'Active Subscriptions', value: String(stats?.active_subscriptions ?? 0), icon: Users, trend: 'Live', positive: true, color: 'blue' },
|
||||||
{ title: 'Failed Payments', value: '3', icon: XCircle, trend: '-2.1%', positive: true, color: 'rose' },
|
{ title: 'Failed Payments', value: String(stats?.failed_payments ?? 0), icon: XCircle, trend: 'Live', positive: true, color: 'rose' },
|
||||||
{ title: 'Average Ticket', value: '232', icon: TrendingUp, trend: '+0.8%', positive: true, color: 'amber' },
|
{ title: 'Average Ticket', value: stats?.average_ticket ?? '0.00', icon: TrendingUp, trend: 'Live', positive: true, color: 'amber' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const openDetails = (pay) => {
|
const openDetails = (pay) => {
|
||||||
@ -127,7 +121,14 @@ export default function PaymentsIndex({ stats, payments: initialPayments }) {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{payments.map((pay) => (
|
{payments.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="py-10 text-center font-bold text-slate-400 uppercase pl-8">
|
||||||
|
No transactions found.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
payments.map((pay) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={pay.id}
|
key={pay.id}
|
||||||
className="hover:bg-slate-50 transition-colors cursor-pointer group"
|
className="hover:bg-slate-50 transition-colors cursor-pointer group"
|
||||||
@ -165,12 +166,13 @@ export default function PaymentsIndex({ stats, payments: initialPayments }) {
|
|||||||
<span className="text-[10px] font-mono font-bold text-slate-300 uppercase">{pay.id}</span>
|
<span className="text-[10px] font-mono font-bold text-slate-300 uppercase">{pay.id}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<div className="p-6 border-t border-slate-100 flex items-center justify-between bg-slate-50/30">
|
<div className="p-6 border-t border-slate-100 flex items-center justify-between bg-slate-50/30">
|
||||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Showing 5 of 1,284 transactions</p>
|
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Showing {payments.length} transactions</p>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<button className="px-4 py-2 bg-white border border-slate-200 rounded-lg text-[10px] font-black uppercase tracking-widest text-slate-500 hover:bg-slate-50 transition-colors">Previous</button>
|
<button className="px-4 py-2 bg-white border border-slate-200 rounded-lg text-[10px] font-black uppercase tracking-widest text-slate-500 hover:bg-slate-50 transition-colors">Previous</button>
|
||||||
<button className="px-4 py-2 bg-white border border-slate-200 rounded-lg text-[10px] font-black uppercase tracking-widest text-slate-500 hover:bg-slate-50 transition-colors">Next</button>
|
<button className="px-4 py-2 bg-white border border-slate-200 rounded-lg text-[10px] font-black uppercase tracking-widest text-slate-500 hover:bg-slate-50 transition-colors">Next</button>
|
||||||
|
|||||||
@ -124,9 +124,9 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
|
|||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm flex items-center justify-between">
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm flex items-center justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-xs text-slate-500 font-semibold block">Total Reports</span>
|
<span className="text-xs text-slate-500 font-semibold block">Total Reports</span>
|
||||||
<span className="text-2xl font-extrabold text-slate-900 block">128</span>
|
<span className="text-2xl font-extrabold text-slate-900 block">{reports ? reports.length : 0}</span>
|
||||||
<span className="inline-flex items-center gap-0.5 px-2 py-0.5 bg-emerald-50 text-emerald-700 rounded-full text-[10px] font-bold">
|
<span className="inline-flex items-center gap-0.5 px-2 py-0.5 bg-emerald-50 text-emerald-700 rounded-full text-[10px] font-bold">
|
||||||
↑ 12% <span className="font-normal text-slate-400 ml-1">from last 30 days</span>
|
Live <span className="font-normal text-slate-400 ml-1">database count</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-[#0F6E56]/10 text-[#0F6E56] rounded-2xl">
|
<div className="p-3 bg-[#0F6E56]/10 text-[#0F6E56] rounded-2xl">
|
||||||
@ -138,7 +138,7 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
|
|||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm flex items-center justify-between">
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm flex items-center justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-xs text-slate-500 font-semibold block">Pending Review</span>
|
<span className="text-xs text-slate-500 font-semibold block">Pending Review</span>
|
||||||
<span className="text-2xl font-extrabold text-slate-900 block">42</span>
|
<span className="text-2xl font-extrabold text-slate-900 block">{reports ? reports.filter(r => r.status === 'Pending').length : 0}</span>
|
||||||
<span className="text-[11px] text-amber-600 font-bold block">Requires attention</span>
|
<span className="text-[11px] text-amber-600 font-bold block">Requires attention</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-amber-50 text-amber-600 rounded-2xl">
|
<div className="p-3 bg-amber-50 text-amber-600 rounded-2xl">
|
||||||
@ -150,7 +150,7 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
|
|||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm flex items-center justify-between">
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm flex items-center justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-xs text-slate-500 font-semibold block">Resolved</span>
|
<span className="text-xs text-slate-500 font-semibold block">Resolved</span>
|
||||||
<span className="text-2xl font-extrabold text-slate-900 block">78</span>
|
<span className="text-2xl font-extrabold text-slate-900 block">{reports ? reports.filter(r => r.status === 'Resolved').length : 0}</span>
|
||||||
<span className="text-[11px] text-emerald-600 font-bold block">This period</span>
|
<span className="text-[11px] text-emerald-600 font-bold block">This period</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-emerald-50 text-emerald-600 rounded-2xl">
|
<div className="p-3 bg-emerald-50 text-emerald-600 rounded-2xl">
|
||||||
@ -162,7 +162,7 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
|
|||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm flex items-center justify-between">
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm flex items-center justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-xs text-slate-500 font-semibold block">Actions Taken</span>
|
<span className="text-xs text-slate-500 font-semibold block">Actions Taken</span>
|
||||||
<span className="text-2xl font-extrabold text-slate-900 block">56</span>
|
<span className="text-2xl font-extrabold text-slate-900 block">{reports ? reports.filter(r => r.status === 'Resolved').length : 0}</span>
|
||||||
<span className="text-[11px] text-blue-600 font-bold block">Warnings/Suspensions</span>
|
<span className="text-[11px] text-blue-600 font-bold block">Warnings/Suspensions</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-blue-50 text-blue-600 rounded-2xl">
|
<div className="p-3 bg-blue-50 text-blue-600 rounded-2xl">
|
||||||
@ -339,13 +339,13 @@ export default function SafetyHub({ reports, rules, moderation_queue }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subsections: Vetting Queue & Auto-Detection Rules */}
|
{/* Subsections: Verification Queue & Auto-Detection Rules */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
{/* Content Vetting Queue */}
|
{/* Content Verification Queue */}
|
||||||
<div className="lg:col-span-2 bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
<div className="lg:col-span-2 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 className="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/20">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest">Profile Content Vetting Queue</h3>
|
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest">Profile Content Verification Queue</h3>
|
||||||
<p className="text-[10px] text-slate-400 mt-0.5">Bio updates or pictures flagged for manual review</p>
|
<p className="text-[10px] text-slate-400 mt-0.5">Bio updates or pictures flagged for manual review</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge className="bg-amber-50 text-amber-700 border-none font-bold uppercase tracking-wider text-[8px]">Action Required</Badge>
|
<Badge className="bg-amber-50 text-amber-700 border-none font-bold uppercase tracking-wider text-[8px]">Action Required</Badge>
|
||||||
|
|||||||
@ -150,8 +150,8 @@ export default function WorkerManagement({ workers }) {
|
|||||||
|
|
||||||
const filteredWorkers = workers.map(w => ({
|
const filteredWorkers = workers.map(w => ({
|
||||||
...w,
|
...w,
|
||||||
availability: w.id === 101 ? 'Available Now' : w.id === 102 ? 'In Interview' : 'Engaged',
|
availability: w.availability || 'Available Now',
|
||||||
verified: w.status === 'active'
|
verified: w.verified !== undefined ? w.verified : (w.status === 'active')
|
||||||
})).filter(worker => {
|
})).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());
|
||||||
@ -179,7 +179,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
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.5 bg-[#0F6E56] text-white rounded-xl text-xs font-bold hover:bg-[#085041] transition-colors shadow-sm uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Vetting OCR Queue
|
Verification OCR Queue
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -229,7 +229,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<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-bold text-slate-500 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-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Placement Status</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Vetting Status</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Verification Status</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 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-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Lifecycle</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4">Lifecycle</TableHead>
|
||||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4 text-right pr-8">Actions</TableHead>
|
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest py-4 text-right pr-8">Actions</TableHead>
|
||||||
@ -271,7 +271,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
<FileText className={`w-3.5 h-3.5 text-${worker.verified ? 'emerald' : 'amber'}-500`} />
|
<FileText className={`w-3.5 h-3.5 text-${worker.verified ? 'emerald' : 'amber'}-500`} />
|
||||||
</div>
|
</div>
|
||||||
<span className={`text-[10px] font-black uppercase tracking-widest text-${worker.verified ? 'emerald' : 'amber'}-600`}>
|
<span className={`text-[10px] font-black uppercase tracking-widest text-${worker.verified ? 'emerald' : 'amber'}-600`}>
|
||||||
{worker.verified ? 'OCR Verified' : 'Pending Vetting'}
|
{worker.verified ? 'OCR Verified' : 'Pending Verification'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -360,7 +360,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
|
|
||||||
{/* Worker Management & Moderation Dialog */}
|
{/* Worker Management & Moderation Dialog */}
|
||||||
<Dialog open={isDetailDialogOpen} onOpenChange={setIsDetailDialogOpen}>
|
<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">
|
<DialogContent className="sm:max-w-4xl w-full bg-white p-0 overflow-hidden rounded-[24px] border-none shadow-2xl font-sans max-h-[90vh] flex flex-col">
|
||||||
{/* Top Header Banner */}
|
{/* Top Header Banner */}
|
||||||
<div className="bg-[#0F6E56] p-6 text-white relative flex-shrink-0">
|
<div className="bg-[#0F6E56] p-6 text-white relative flex-shrink-0">
|
||||||
{selectedWorker?.is_fraud && (
|
{selectedWorker?.is_fraud && (
|
||||||
@ -382,78 +382,8 @@ export default function WorkerManagement({ workers }) {
|
|||||||
{/* Scrollable details tab */}
|
{/* Scrollable details tab */}
|
||||||
<div className="p-6 space-y-6 overflow-y-auto flex-1">
|
<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="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="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 className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Experience</label>
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Experience</label>
|
||||||
@ -484,7 +414,6 @@ export default function WorkerManagement({ workers }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Management Controls: Verification, Availability, Fraud flags */}
|
{/* Management Controls: Verification, Availability, Fraud flags */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
@ -583,7 +512,7 @@ export default function WorkerManagement({ workers }) {
|
|||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows="2"
|
rows="2"
|
||||||
placeholder="Write admin logs regarding candidate vetting reviews or user complaints..."
|
placeholder="Write admin logs regarding candidate verification 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"
|
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}
|
value={adminNotes}
|
||||||
onChange={e => setAdminNotes(e.target.value)}
|
onChange={e => setAdminNotes(e.target.value)}
|
||||||
|
|||||||
@ -20,82 +20,17 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
|
|
||||||
export default function Verifications({ verifications, status }) {
|
export default function Verifications({ verifications, status, summary, history = [] }) {
|
||||||
const [selectedItem, setSelectedItem] = useState(null);
|
const [selectedItem, setSelectedItem] = useState(null);
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
// Vetting form overrides
|
// Verification form overrides
|
||||||
const [ocrOverrides, setOcrOverrides] = useState({});
|
const [ocrOverrides, setOcrOverrides] = useState({});
|
||||||
const [isEditingOcr, setIsEditingOcr] = useState(false);
|
const [isEditingOcr, setIsEditingOcr] = useState(false);
|
||||||
const [rejectionReason, setRejectionReason] = useState('');
|
const [rejectionReason, setRejectionReason] = useState('');
|
||||||
const [showRejectionForm, setShowRejectionForm] = useState(false);
|
const [showRejectionForm, setShowRejectionForm] = useState(false);
|
||||||
|
|
||||||
// Local queue of workers to verify
|
const activeList = verifications?.data || [];
|
||||||
const localQueue = [
|
|
||||||
{
|
|
||||||
id: 101,
|
|
||||||
worker_name: 'Fatima Zahra',
|
|
||||||
nationality: 'Morocco',
|
|
||||||
passport_number: 'MA9823471',
|
|
||||||
processed_at: '2026-05-23 14:15',
|
|
||||||
status: 'approved',
|
|
||||||
confidence: 98,
|
|
||||||
verification_method: 'Auto',
|
|
||||||
document_type: 'Passport',
|
|
||||||
warnings: [],
|
|
||||||
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',
|
|
||||||
document_type: 'Passport',
|
|
||||||
warnings: ['Blurry text region', 'Signature discrepancy flagged'],
|
|
||||||
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',
|
|
||||||
document_type: 'Visa scan details',
|
|
||||||
warnings: ['Document expiration alert (Expires in 5 days)'],
|
|
||||||
ocr_data: {
|
|
||||||
'Name': 'Siti Aminah',
|
|
||||||
'DOB': '1988-03-25',
|
|
||||||
'Nationality': 'Indonesia',
|
|
||||||
'Passport No.': 'B76543210',
|
|
||||||
'Expiry': '2026-05-28',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const activeList = localQueue.filter(item => {
|
|
||||||
if (status === 'approved') return item.status === 'approved';
|
|
||||||
if (status === 'rejected') return item.status === 'flagged';
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const openReview = (item) => {
|
const openReview = (item) => {
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
@ -159,7 +94,7 @@ export default function Verifications({ verifications, status }) {
|
|||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">Auto-Approve Rate</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">Auto-Approve Rate</span>
|
||||||
<span className="text-2xl font-black text-slate-800 mt-1">92.4%</span>
|
<span className="text-2xl font-black text-slate-800 mt-1">{summary?.auto_approve_rate || 0}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-10 h-10 bg-emerald-50 rounded-xl flex items-center justify-center text-emerald-600">
|
<div className="w-10 h-10 bg-emerald-50 rounded-xl flex items-center justify-center text-emerald-600">
|
||||||
<ShieldCheck className="w-5 h-5" />
|
<ShieldCheck className="w-5 h-5" />
|
||||||
@ -168,7 +103,7 @@ export default function Verifications({ verifications, status }) {
|
|||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">Average Match Score</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">Average Match Score</span>
|
||||||
<span className="text-2xl font-black text-blue-600 mt-1">91.8%</span>
|
<span className="text-2xl font-black text-blue-600 mt-1">{summary?.average_match_score || 0}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center text-blue-600">
|
<div className="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center text-blue-600">
|
||||||
<Sparkles className="w-5 h-5" />
|
<Sparkles className="w-5 h-5" />
|
||||||
@ -177,7 +112,7 @@ export default function Verifications({ verifications, status }) {
|
|||||||
<div className="bg-white p-5 border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">Flagged Workers</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">Flagged Workers</span>
|
||||||
<span className="text-2xl font-black text-amber-600 mt-1">2 Pending</span>
|
<span className="text-2xl font-black text-amber-600 mt-1">{summary?.flagged_count || 0} Pending</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-10 h-10 bg-amber-50 rounded-xl flex items-center justify-center text-amber-600">
|
<div className="w-10 h-10 bg-amber-50 rounded-xl flex items-center justify-center text-amber-600">
|
||||||
<AlertTriangle className="w-5 h-5" />
|
<AlertTriangle className="w-5 h-5" />
|
||||||
@ -200,7 +135,8 @@ export default function Verifications({ verifications, status }) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100 text-xs font-bold text-slate-700">
|
<tbody className="divide-y divide-slate-100 text-xs font-bold text-slate-700">
|
||||||
{activeList.map((item) => (
|
{activeList.length > 0 ? (
|
||||||
|
activeList.map((item) => (
|
||||||
<tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
|
<tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<div>
|
<div>
|
||||||
@ -254,7 +190,14 @@ export default function Verifications({ verifications, status }) {
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="6" className="py-8 text-center text-slate-400 font-medium italic">
|
||||||
|
No verification records found in this queue.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -267,11 +210,7 @@ export default function Verifications({ verifications, status }) {
|
|||||||
<span>Verification History</span>
|
<span>Verification History</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{[
|
{history.length > 0 ? history.map((log, idx) => (
|
||||||
{ time: '2026-05-23 15:20', text: 'Admin manual approval completed for Leila Bekri passport verification', ip: '192.168.1.5' },
|
|
||||||
{ time: '2026-05-23 14:15', text: 'Auto passport verification completed successfully for Fatima Zahra', ip: 'Auto-System' },
|
|
||||||
{ time: '2026-05-23 13:42', text: 'Auto verification flagged passport scan of Grace Omondi due to blurred signature scan', ip: 'Auto-System' },
|
|
||||||
].map((log, idx) => (
|
|
||||||
<div key={idx} className="bg-slate-50 p-3.5 rounded-xl border border-slate-100 flex items-center justify-between text-xs font-bold text-slate-700 shadow-inner">
|
<div key={idx} className="bg-slate-50 p-3.5 rounded-xl border border-slate-100 flex items-center justify-between text-xs font-bold text-slate-700 shadow-inner">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Clock className="w-4 h-4 text-slate-400" />
|
<Clock className="w-4 h-4 text-slate-400" />
|
||||||
@ -279,7 +218,11 @@ export default function Verifications({ verifications, status }) {
|
|||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] text-slate-400 font-semibold">{log.time} • IP: {log.ip}</span>
|
<span className="text-[10px] text-slate-400 font-semibold">{log.time} • IP: {log.ip}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)) : (
|
||||||
|
<div className="text-center py-6 text-slate-400 text-xs font-medium italic">
|
||||||
|
No verification logs recorded.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -352,13 +352,6 @@ export default function Dashboard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-2 border-t border-slate-200/80 flex items-center justify-between text-xs">
|
|
||||||
<span className="text-[10px] text-slate-400 font-bold uppercase">{t('availability', 'Availability')}:</span>
|
|
||||||
<span className="font-bold text-[#185FA5] bg-blue-50 px-2 py-0.5 rounded text-[10px] border border-blue-100">
|
|
||||||
{worker.availability}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={`/employer/workers/${worker.id}`}
|
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"
|
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"
|
||||||
|
|||||||
@ -26,6 +26,13 @@ export default function PaymentHistory({ payments = [], currentPlan, expiresAt,
|
|||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
const [sortBy, setSortBy] = useState('date_desc');
|
const [sortBy, setSortBy] = useState('date_desc');
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const itemsPerPage = 5;
|
||||||
|
|
||||||
|
// Reset to page 1 when filter/sorting parameters change
|
||||||
|
React.useEffect(() => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [searchTerm, statusFilter, sortBy]);
|
||||||
|
|
||||||
// Calculate dynamic stats
|
// Calculate dynamic stats
|
||||||
const totalSpent = payments.reduce((acc, curr) => acc + (curr.status === 'success' ? curr.amount : 0), 0);
|
const totalSpent = payments.reduce((acc, curr) => acc + (curr.status === 'success' ? curr.amount : 0), 0);
|
||||||
@ -59,6 +66,12 @@ export default function PaymentHistory({ payments = [], currentPlan, expiresAt,
|
|||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(filteredPayments.length / itemsPerPage);
|
||||||
|
const paginatedPayments = filteredPayments.slice(
|
||||||
|
(currentPage - 1) * itemsPerPage,
|
||||||
|
currentPage * itemsPerPage
|
||||||
|
);
|
||||||
|
|
||||||
const downloadReceipt = (invoice) => {
|
const downloadReceipt = (invoice) => {
|
||||||
// Calculate UAE VAT (5%)
|
// Calculate UAE VAT (5%)
|
||||||
const total = parseFloat(invoice.amount);
|
const total = parseFloat(invoice.amount);
|
||||||
@ -408,16 +421,240 @@ export default function PaymentHistory({ payments = [], currentPlan, expiresAt,
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleExportCSV = () => {
|
const handleExportCSV = () => {
|
||||||
|
if (filteredPayments.length === 0) {
|
||||||
|
toast.error(t('no_data_to_export', 'No data to export'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = ['Invoice ID', 'Date & Time', 'Description', 'Status', 'Amount', 'Currency'];
|
||||||
|
const rows = filteredPayments.map(p => [
|
||||||
|
p.invoice_no,
|
||||||
|
`${p.date} ${p.time}`,
|
||||||
|
p.description,
|
||||||
|
p.status.toUpperCase(),
|
||||||
|
p.amount.toFixed(2),
|
||||||
|
p.currency
|
||||||
|
]);
|
||||||
|
|
||||||
|
const csvContent = "data:text/csv;charset=utf-8,"
|
||||||
|
+ [headers.join(','), ...rows.map(e => e.map(val => `"${String(val).replace(/"/g, '""')}"`).join(','))].join('\n');
|
||||||
|
|
||||||
|
const encodedUri = encodeURI(csvContent);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.setAttribute("href", encodedUri);
|
||||||
|
link.setAttribute("download", `payment_history_${new Date().toISOString().slice(0,10)}.csv`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
toast.success(`📊 ${t('csv_exported', 'CSV Export Complete!')}`, {
|
toast.success(`📊 ${t('csv_exported', 'CSV Export Complete!')}`, {
|
||||||
description: `${filteredPayments.length} billing items formatted and saved as spreadsheet.`
|
description: `${filteredPayments.length} billing items downloaded successfully.`
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
navigator.clipboard.writeText(JSON.stringify(filteredPayments, null, 2));
|
if (filteredPayments.length === 0) {
|
||||||
|
toast.error(t('no_data_to_copy', 'No data to copy'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const headers = ['Invoice ID', 'Date & Time', 'Description', 'Status', 'Amount', 'Currency'];
|
||||||
|
const rows = filteredPayments.map(p => [
|
||||||
|
p.invoice_no,
|
||||||
|
`${p.date} ${p.time}`,
|
||||||
|
p.description,
|
||||||
|
p.status.toUpperCase(),
|
||||||
|
p.amount.toFixed(2),
|
||||||
|
p.currency
|
||||||
|
]);
|
||||||
|
const text = [headers.join('\t'), ...rows.map(r => r.join('\t'))].join('\n');
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
toast.success(`📋 ${t('copied_to_clipboard', 'Copied to Clipboard!')}`);
|
toast.success(`📋 ${t('copied_to_clipboard', 'Copied to Clipboard!')}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExportPDF = () => {
|
||||||
|
if (filteredPayments.length === 0) {
|
||||||
|
toast.error(t('no_data_to_export', 'No data to export'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const printWindow = window.open('', '_blank', 'width=900,height=900');
|
||||||
|
if (!printWindow) {
|
||||||
|
toast.error(t('popup_blocked', 'Popup blocked! Please allow popups.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalAmount = filteredPayments.reduce((sum, p) => sum + p.amount, 0);
|
||||||
|
|
||||||
|
printWindow.document.write(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Payment History Statement</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
color: #1e293b;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 50px;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.statement-container {
|
||||||
|
max-width: 850px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
border-bottom: 2px solid #e2e8f0;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #185FA5;
|
||||||
|
}
|
||||||
|
.meta-info {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #185FA5;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.summary-card {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.summary-item h4 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.summary-item p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 850;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
.btn-print {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
right: 30px;
|
||||||
|
background: #185FA5;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 12px rgba(24, 95, 165, 0.3);
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
.btn-print {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="statement-container">
|
||||||
|
<div class="header">
|
||||||
|
<div>
|
||||||
|
<div class="title">PAYMENT HISTORY STATEMENT</div>
|
||||||
|
<div style="font-size: 12px; margin-top: 5px; font-weight: 500;">Employer Account: ${currentPlan}</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta-info">
|
||||||
|
<div><strong>Statement Date:</strong> ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</div>
|
||||||
|
<div><strong>Status:</strong> ${subscriptionStatus}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="summary-item">
|
||||||
|
<h4>Total Invoices</h4>
|
||||||
|
<p>${filteredPayments.length}</p>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<h4>Total Amount</h4>
|
||||||
|
<p>${totalAmount.toFixed(2)} AED</p>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<h4>Active Until</h4>
|
||||||
|
<p>${expiresAt}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Invoice ID</th>
|
||||||
|
<th>Date & Time</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th class="text-right">Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${filteredPayments.map(p => `
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: monospace; font-weight: bold; color: #185FA5;">${p.invoice_no}</td>
|
||||||
|
<td>${p.date} - ${p.time}</td>
|
||||||
|
<td>${p.description}</td>
|
||||||
|
<td style="font-weight: 700; color: ${p.status === 'success' ? '#10b981' : '#f43f5e'};">${p.status.toUpperCase()}</td>
|
||||||
|
<td class="text-right" style="font-weight: bold;">${p.amount.toFixed(2)} AED</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<button class="btn-print" onclick="window.print()">Print / Save PDF</button>
|
||||||
|
<script>
|
||||||
|
window.onload = function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
window.print();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
printWindow.document.close();
|
||||||
|
|
||||||
|
toast.success(`📄 ${t('pdf_exported', 'Statement PDF Generated')}`, {
|
||||||
|
description: `${filteredPayments.length} billing items compiled into PDF statement.`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmployerLayout title={t('payment_history', 'Payment History')}>
|
<EmployerLayout title={t('payment_history', 'Payment History')}>
|
||||||
<Head title={`${t('payment_history', 'Payment History')} - ${t('employer_portal', 'Employer Portal')}`} />
|
<Head title={`${t('payment_history', 'Payment History')} - ${t('employer_portal', 'Employer Portal')}`} />
|
||||||
@ -429,7 +666,7 @@ export default function PaymentHistory({ payments = [], currentPlan, expiresAt,
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-black text-slate-900 tracking-tight">{t('payment_history', 'Payment History')}</h1>
|
<h1 className="text-2xl font-black text-slate-900 tracking-tight">{t('payment_history', 'Payment History')}</h1>
|
||||||
<p className="text-xs font-medium text-slate-500 mt-1">
|
<p className="text-xs font-medium text-slate-500 mt-1">
|
||||||
{t('payment_history_desc', 'Track subscription plans, document vetting, and direct hiring invoice details.')}
|
{t('payment_history_desc', 'Track subscription plans, document verification, and direct hiring invoice details.')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -563,6 +800,12 @@ export default function PaymentHistory({ payments = [], currentPlan, expiresAt,
|
|||||||
>
|
>
|
||||||
CSV
|
CSV
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleExportPDF}
|
||||||
|
className="px-3 py-1.5 text-[10px] font-black text-slate-500 hover:text-[#185FA5] hover:bg-blue-50 rounded-lg transition-all"
|
||||||
|
>
|
||||||
|
PDF
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handlePrint}
|
onClick={handlePrint}
|
||||||
className="px-3 py-1.5 text-[10px] font-black text-slate-500 hover:text-[#185FA5] hover:bg-blue-50 rounded-lg transition-all"
|
className="px-3 py-1.5 text-[10px] font-black text-slate-500 hover:text-[#185FA5] hover:bg-blue-50 rounded-lg transition-all"
|
||||||
@ -587,8 +830,8 @@ export default function PaymentHistory({ payments = [], currentPlan, expiresAt,
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-50">
|
<tbody className="divide-y divide-slate-50">
|
||||||
{filteredPayments.length > 0 ? (
|
{paginatedPayments.length > 0 ? (
|
||||||
filteredPayments.map((payment) => (
|
paginatedPayments.map((payment) => (
|
||||||
<tr key={payment.id} className="group hover:bg-slate-50/50 transition-colors">
|
<tr key={payment.id} className="group hover:bg-slate-50/50 transition-colors">
|
||||||
<td className="px-6 py-5">
|
<td className="px-6 py-5">
|
||||||
<div className="flex items-center space-x-2 font-mono text-xs font-black text-[#185FA5]">
|
<div className="flex items-center space-x-2 font-mono text-xs font-black text-[#185FA5]">
|
||||||
@ -654,14 +897,45 @@ export default function PaymentHistory({ payments = [], currentPlan, expiresAt,
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer Summary Info */}
|
{/* Footer Summary Info & Pagination */}
|
||||||
<div className="px-6 py-4 bg-slate-50/50 border-t border-slate-100 flex items-center justify-between text-[10px] text-slate-500 font-bold uppercase tracking-wider">
|
<div className="px-6 py-4 bg-slate-50/50 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-slate-500 font-bold">
|
||||||
<div>
|
<div className="uppercase tracking-wider text-[10px]">
|
||||||
{t('showing_invoices', 'Showing {start} to {end} of {total} billing items')
|
{t('showing_invoices', 'Showing {start} to {end} of {total} billing items')
|
||||||
.replace('{start}', filteredPayments.length > 0 ? 1 : 0)
|
.replace('{start}', filteredPayments.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0)
|
||||||
.replace('{end}', filteredPayments.length)
|
.replace('{end}', Math.min(currentPage * itemsPerPage, filteredPayments.length))
|
||||||
.replace('{total}', filteredPayments.length)}
|
.replace('{total}', filteredPayments.length)}
|
||||||
</div>
|
</div>
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="px-2.5 py-1.5 border border-slate-200 rounded-lg bg-white hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||||
|
>
|
||||||
|
{t('prev', 'Prev')}
|
||||||
|
</button>
|
||||||
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
|
||||||
|
<button
|
||||||
|
key={page}
|
||||||
|
onClick={() => setCurrentPage(page)}
|
||||||
|
className={`px-3 py-1.5 rounded-lg border transition-all ${
|
||||||
|
currentPage === page
|
||||||
|
? 'bg-[#185FA5] border-[#185FA5] text-white'
|
||||||
|
: 'bg-white border-slate-200 hover:bg-slate-50 text-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="px-2.5 py-1.5 border border-slate-200 rounded-lg bg-white hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||||
|
>
|
||||||
|
{t('next', 'Next')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -119,11 +119,19 @@ export default function Shortlist({ shortlistedWorkers }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Emirates ID Status Verification Badge & Visa Status */}
|
{/* Passport Status Verification Badge & Visa Status */}
|
||||||
<div className="flex flex-wrap gap-1 mt-0.5">
|
<div className="flex flex-wrap gap-1 mt-0.5">
|
||||||
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-emerald-700 bg-emerald-50 px-1.5 py-0.5 rounded border border-emerald-100">
|
<div className={`flex items-center space-x-1 text-[9px] font-black uppercase px-1.5 py-0.5 rounded border ${
|
||||||
<ShieldCheck className="w-3 h-3 text-emerald-600 flex-shrink-0" />
|
worker.passport_status.toLowerCase().includes('pending')
|
||||||
<span>{worker.emirates_id_status}</span>
|
? 'text-amber-700 bg-amber-50 border-amber-200'
|
||||||
|
: 'text-emerald-700 bg-emerald-50 border-emerald-100'
|
||||||
|
}`}>
|
||||||
|
<ShieldCheck className={`w-3 h-3 flex-shrink-0 ${
|
||||||
|
worker.passport_status.toLowerCase().includes('pending')
|
||||||
|
? 'text-amber-600'
|
||||||
|
: 'text-emerald-600'
|
||||||
|
}`} />
|
||||||
|
<span>{worker.passport_status}</span>
|
||||||
</div>
|
</div>
|
||||||
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
|
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
|
||||||
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
||||||
@ -153,14 +161,6 @@ export default function Shortlist({ shortlistedWorkers }) {
|
|||||||
|
|
||||||
{/* Top Right Availability status and trash remove button */}
|
{/* Top Right Availability status and trash remove button */}
|
||||||
<div className="flex flex-col items-end space-y-2">
|
<div className="flex flex-col items-end space-y-2">
|
||||||
<span className={`px-2 py-0.5 text-[8px] font-black rounded-lg uppercase tracking-wider border ${
|
|
||||||
worker.availability_status === 'Active' ? 'bg-emerald-50 text-emerald-700 border-emerald-200' :
|
|
||||||
worker.availability_status === 'Hired' ? 'bg-blue-50 text-blue-700 border-blue-200' :
|
|
||||||
'bg-slate-100 text-slate-600 border-slate-200'
|
|
||||||
}`}>
|
|
||||||
{worker.availability_status}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeWorker(worker.id)}
|
onClick={() => removeWorker(worker.id)}
|
||||||
@ -383,9 +383,17 @@ export default function Shortlist({ shortlistedWorkers }) {
|
|||||||
{previewWorker.verified && <CheckCircle2 className="w-4 h-4 text-emerald-600" />}
|
{previewWorker.verified && <CheckCircle2 className="w-4 h-4 text-emerald-600" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-1 text-emerald-700 font-black text-[9px] uppercase tracking-wider">
|
<div className={`flex items-center space-x-1 font-black text-[9px] uppercase tracking-wider ${
|
||||||
<ShieldCheck className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" />
|
previewWorker.passport_status?.toLowerCase().includes('pending')
|
||||||
<span>{previewWorker.emirates_id_status}</span>
|
? 'text-amber-700 bg-amber-50 border border-amber-200 px-1.5 py-0.5 rounded'
|
||||||
|
: 'text-emerald-700 bg-emerald-50 border border-emerald-100 px-1.5 py-0.5 rounded'
|
||||||
|
}`}>
|
||||||
|
<ShieldCheck className={`w-3.5 h-3.5 flex-shrink-0 ${
|
||||||
|
previewWorker.passport_status?.toLowerCase().includes('pending')
|
||||||
|
? 'text-amber-600'
|
||||||
|
: 'text-emerald-600'
|
||||||
|
}`} />
|
||||||
|
<span>{previewWorker.passport_status}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-500 font-bold">{previewWorker.nationality} • {previewWorker.category}</div>
|
<div className="text-xs text-slate-500 font-bold">{previewWorker.nationality} • {previewWorker.category}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -407,6 +415,10 @@ export default function Shortlist({ shortlistedWorkers }) {
|
|||||||
<div className="text-slate-800">{previewWorker.salary} AED / mo</div>
|
<div className="text-slate-800">{previewWorker.salary} AED / mo</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-slate-50/50 rounded-xl">
|
<div className="p-3 bg-slate-50/50 rounded-xl">
|
||||||
|
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('job_preference', 'Job Preference')}</div>
|
||||||
|
<div className="text-slate-800 uppercase text-[10px] mt-1">{previewWorker.preferred_job_type}</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-slate-50/50 rounded-xl col-span-2">
|
||||||
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('languages', 'Languages')}</div>
|
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('languages', 'Languages')}</div>
|
||||||
<div className="flex flex-wrap gap-1.5 mt-1">
|
<div className="flex flex-wrap gap-1.5 mt-1">
|
||||||
{previewWorker.languages?.map(lang => (
|
{previewWorker.languages?.map(lang => (
|
||||||
@ -417,20 +429,6 @@ export default function Shortlist({ shortlistedWorkers }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-slate-50/50 rounded-xl">
|
|
||||||
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('availability_status_label', 'Availability Status')}</div>
|
|
||||||
<span className={`inline-block px-2 py-0.5 text-[8px] font-black rounded-lg uppercase tracking-wider mt-1 border ${
|
|
||||||
previewWorker.availability_status === 'Active' ? 'bg-emerald-50 text-emerald-700 border-emerald-200' :
|
|
||||||
previewWorker.availability_status === 'Hired' ? 'bg-blue-50 text-blue-700 border-blue-200' :
|
|
||||||
'bg-slate-100 text-slate-600 border-slate-200'
|
|
||||||
}`}>
|
|
||||||
{previewWorker.availability_status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-slate-50/50 rounded-xl">
|
|
||||||
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('job_preference', 'Job Preference')}</div>
|
|
||||||
<div className="text-slate-800 uppercase text-[10px] mt-1">{previewWorker.preferred_job_type}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Core Platform Skills inside Quick Preview */}
|
{/* Core Platform Skills inside Quick Preview */}
|
||||||
|
|||||||
@ -48,7 +48,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedCategory, setSelectedCategory] = useState('All Categories');
|
const [selectedCategory, setSelectedCategory] = useState('All Categories');
|
||||||
const [selectedNationality, setSelectedNationality] = useState('All Nationalities');
|
const [selectedNationality, setSelectedNationality] = useState('All Nationalities');
|
||||||
const [selectedAvailability, setSelectedAvailability] = useState('All Availabilities');
|
|
||||||
const [selectedExperience, setSelectedExperience] = useState('All Experience');
|
const [selectedExperience, setSelectedExperience] = useState('All Experience');
|
||||||
const [selectedReligion, setSelectedReligion] = useState('All Religions');
|
const [selectedReligion, setSelectedReligion] = useState('All Religions');
|
||||||
const [maxSalary, setMaxSalary] = useState(3000);
|
const [maxSalary, setMaxSalary] = useState(3000);
|
||||||
@ -89,12 +88,10 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const cat = params.get('category');
|
const cat = params.get('category');
|
||||||
const nat = params.get('nationality');
|
const nat = params.get('nationality');
|
||||||
const avail = params.get('availability');
|
|
||||||
const sal = params.get('max_salary');
|
const sal = params.get('max_salary');
|
||||||
|
|
||||||
if (cat) setSelectedCategory(cat);
|
if (cat) setSelectedCategory(cat);
|
||||||
if (nat) setSelectedNationality(nat);
|
if (nat) setSelectedNationality(nat);
|
||||||
if (avail) setSelectedAvailability(avail);
|
|
||||||
if (sal) setMaxSalary(Number(sal));
|
if (sal) setMaxSalary(Number(sal));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -151,7 +148,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
setSelectedCategory('All Categories');
|
setSelectedCategory('All Categories');
|
||||||
setSelectedNationality('All Nationalities');
|
setSelectedNationality('All Nationalities');
|
||||||
setSelectedAvailability('All Availabilities');
|
|
||||||
setSelectedExperience('All Experience');
|
setSelectedExperience('All Experience');
|
||||||
setSelectedReligion('All Religions');
|
setSelectedReligion('All Religions');
|
||||||
setMaxSalary(3000);
|
setMaxSalary(3000);
|
||||||
@ -178,7 +174,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
// Dropdown filters
|
// Dropdown filters
|
||||||
if (selectedCategory !== 'All Categories' && worker.category !== selectedCategory) return false;
|
if (selectedCategory !== 'All Categories' && worker.category !== selectedCategory) return false;
|
||||||
if (selectedNationality !== 'All Nationalities' && worker.nationality !== selectedNationality) return false;
|
if (selectedNationality !== 'All Nationalities' && worker.nationality !== selectedNationality) return false;
|
||||||
if (selectedAvailability !== 'All Availabilities' && worker.availability_status !== selectedAvailability) return false;
|
|
||||||
if (selectedExperience !== 'All Experience' && worker.experience !== selectedExperience) return false;
|
if (selectedExperience !== 'All Experience' && worker.experience !== selectedExperience) return false;
|
||||||
if (selectedReligion !== 'All Religions' && worker.religion !== selectedReligion) return false;
|
if (selectedReligion !== 'All Religions' && worker.religion !== selectedReligion) return false;
|
||||||
if (worker.salary > maxSalary) return false;
|
if (worker.salary > maxSalary) return false;
|
||||||
@ -217,7 +212,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
searchQuery,
|
searchQuery,
|
||||||
selectedCategory,
|
selectedCategory,
|
||||||
selectedNationality,
|
selectedNationality,
|
||||||
selectedAvailability,
|
|
||||||
selectedExperience,
|
selectedExperience,
|
||||||
selectedReligion,
|
selectedReligion,
|
||||||
maxSalary,
|
maxSalary,
|
||||||
@ -316,7 +310,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Primary filters grid */}
|
{/* Primary filters grid */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3 pt-4 border-t border-slate-100">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 pt-4 border-t border-slate-100">
|
||||||
{/* Nationality */}
|
{/* Nationality */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">{t('nationality')}</label>
|
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">{t('nationality')}</label>
|
||||||
@ -403,20 +397,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Availability */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">{t('availability_status', 'Availability')}</label>
|
|
||||||
<select
|
|
||||||
value={selectedAvailability}
|
|
||||||
onChange={(e) => setSelectedAvailability(e.target.value)}
|
|
||||||
className="w-full px-3 py-2.5 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5] font-semibold text-slate-700 cursor-pointer"
|
|
||||||
>
|
|
||||||
{filtersMetadata.availabilities?.map(av => (
|
|
||||||
<option key={av} value={av}>{av}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Visa Status */}
|
{/* Visa Status */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">{t('visa_status', 'Visa Status')}</label>
|
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">{t('visa_status', 'Visa Status')}</label>
|
||||||
@ -501,11 +481,19 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Emirates ID Status Verification Badge & Visa Status */}
|
{/* Passport Status Verification Badge & Visa Status */}
|
||||||
<div className="flex flex-wrap gap-1 mt-0.5">
|
<div className="flex flex-wrap gap-1 mt-0.5">
|
||||||
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-emerald-700 bg-emerald-50 px-1.5 py-0.5 rounded border border-emerald-100">
|
<div className={`flex items-center space-x-1 text-[9px] font-black uppercase px-1.5 py-0.5 rounded border ${
|
||||||
<ShieldCheck className="w-3 h-3 text-emerald-600 flex-shrink-0" />
|
worker.passport_status.toLowerCase().includes('pending')
|
||||||
<span>{worker.emirates_id_status}</span>
|
? 'text-amber-700 bg-amber-50 border-amber-200'
|
||||||
|
: 'text-emerald-700 bg-emerald-50 border-emerald-100'
|
||||||
|
}`}>
|
||||||
|
<ShieldCheck className={`w-3 h-3 flex-shrink-0 ${
|
||||||
|
worker.passport_status.toLowerCase().includes('pending')
|
||||||
|
? 'text-amber-600'
|
||||||
|
: 'text-emerald-600'
|
||||||
|
}`} />
|
||||||
|
<span>{worker.passport_status}</span>
|
||||||
</div>
|
</div>
|
||||||
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
|
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
|
||||||
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
||||||
@ -535,14 +523,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
|
|
||||||
{/* Top Right Availability status and bookmark */}
|
{/* Top Right Availability status and bookmark */}
|
||||||
<div className="flex flex-col items-end space-y-2">
|
<div className="flex flex-col items-end space-y-2">
|
||||||
<span className={`px-2 py-0.5 text-[8px] font-black rounded-lg uppercase tracking-wider border ${
|
|
||||||
worker.availability_status === 'Active' ? 'bg-emerald-50 text-emerald-700 border-emerald-200' :
|
|
||||||
worker.availability_status === 'Hired' ? 'bg-blue-50 text-blue-700 border-blue-200' :
|
|
||||||
'bg-slate-100 text-slate-600 border-slate-200'
|
|
||||||
}`}>
|
|
||||||
{worker.availability_status}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleShortlist(worker.id)}
|
onClick={() => toggleShortlist(worker.id)}
|
||||||
@ -773,9 +753,17 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
{previewWorker.verified && <CheckCircle2 className="w-4 h-4 text-emerald-600" />}
|
{previewWorker.verified && <CheckCircle2 className="w-4 h-4 text-emerald-600" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-1 text-emerald-700 font-black text-[9px] uppercase tracking-wider">
|
<div className={`flex items-center space-x-1 font-black text-[9px] uppercase tracking-wider ${
|
||||||
<ShieldCheck className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" />
|
previewWorker.passport_status?.toLowerCase().includes('pending')
|
||||||
<span>{previewWorker.emirates_id_status}</span>
|
? 'text-amber-700 bg-amber-50 border border-amber-200 px-1.5 py-0.5 rounded'
|
||||||
|
: 'text-emerald-700 bg-emerald-50 border border-emerald-100 px-1.5 py-0.5 rounded'
|
||||||
|
}`}>
|
||||||
|
<ShieldCheck className={`w-3.5 h-3.5 flex-shrink-0 ${
|
||||||
|
previewWorker.passport_status?.toLowerCase().includes('pending')
|
||||||
|
? 'text-amber-600'
|
||||||
|
: 'text-emerald-600'
|
||||||
|
}`} />
|
||||||
|
<span>{previewWorker.passport_status}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-500 font-bold">{previewWorker.nationality} • {previewWorker.category}</div>
|
<div className="text-xs text-slate-500 font-bold">{previewWorker.nationality} • {previewWorker.category}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -791,6 +779,10 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
<div className="text-slate-800">{previewWorker.salary} {t('aed', 'AED')} / {t('mo', 'mo')}</div>
|
<div className="text-slate-800">{previewWorker.salary} {t('aed', 'AED')} / {t('mo', 'mo')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-slate-50/50 rounded-xl">
|
<div className="p-3 bg-slate-50/50 rounded-xl">
|
||||||
|
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('job_preference', 'Job Preference')}</div>
|
||||||
|
<div className="text-slate-800 uppercase text-[10px] mt-1">{previewWorker.preferred_job_type}</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-slate-50/50 rounded-xl col-span-2">
|
||||||
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('languages_spoken', 'Languages')}</div>
|
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('languages_spoken', 'Languages')}</div>
|
||||||
<div className="flex flex-wrap gap-1.5 mt-1">
|
<div className="flex flex-wrap gap-1.5 mt-1">
|
||||||
{previewWorker.languages?.map(lang => (
|
{previewWorker.languages?.map(lang => (
|
||||||
@ -801,20 +793,6 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-slate-50/50 rounded-xl">
|
|
||||||
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('availability_status', 'Availability Status')}</div>
|
|
||||||
<span className={`inline-block px-2 py-0.5 text-[8px] font-black rounded-lg uppercase tracking-wider mt-1 border ${
|
|
||||||
previewWorker.availability_status === 'Active' ? 'bg-emerald-50 text-emerald-700 border-emerald-200' :
|
|
||||||
previewWorker.availability_status === 'Hired' ? 'bg-blue-50 text-blue-700 border-blue-200' :
|
|
||||||
'bg-slate-100 text-slate-600 border-slate-200'
|
|
||||||
}`}>
|
|
||||||
{previewWorker.availability_status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-slate-50/50 rounded-xl">
|
|
||||||
<div className="text-[10px] text-slate-400 uppercase tracking-widest mb-0.5">{t('job_preference', 'Job Preference')}</div>
|
|
||||||
<div className="text-slate-800 uppercase text-[10px] mt-1">{previewWorker.preferred_job_type}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Core Platform Skills inside Quick Preview */}
|
{/* Core Platform Skills inside Quick Preview */}
|
||||||
|
|||||||
@ -42,6 +42,9 @@ const getLanguageFlag = (lang) => {
|
|||||||
|
|
||||||
export default function Show({ worker }) {
|
export default function Show({ worker }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const passportDoc = worker.documents?.find(d => d.type === 'passport');
|
||||||
|
const visaDoc = worker.documents?.find(d => d.type === 'visa');
|
||||||
|
|
||||||
const [showReportModal, setShowReportModal] = useState(false);
|
const [showReportModal, setShowReportModal] = useState(false);
|
||||||
const [reportReason, setReportReason] = useState('');
|
const [reportReason, setReportReason] = useState('');
|
||||||
const [reportDetails, setReportDetails] = useState('');
|
const [reportDetails, setReportDetails] = useState('');
|
||||||
@ -185,22 +188,21 @@ export default function Show({ worker }) {
|
|||||||
<span>{t('verified')}</span>
|
<span>{t('verified')}</span>
|
||||||
</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>
|
||||||
|
|
||||||
{/* Emirates ID verification status bar & Visa Status */}
|
{/* Passport verification status bar & Visa Status */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<div className="flex items-center space-x-1.5 bg-emerald-50 px-2.5 py-1 rounded-lg text-emerald-700 border border-emerald-100 text-[9px] font-black uppercase tracking-wider w-fit">
|
<div className={`flex items-center space-x-1.5 px-2.5 py-1 rounded-lg border text-[9px] font-black uppercase tracking-wider w-fit ${
|
||||||
<ShieldCheck className="w-3.5 h-3.5 text-emerald-600" />
|
worker.passport_status.toLowerCase().includes('pending')
|
||||||
<span>{worker.emirates_id_status}</span>
|
? 'bg-amber-50 text-amber-700 border-amber-200'
|
||||||
|
: 'bg-emerald-50 text-emerald-700 border-emerald-100'
|
||||||
|
}`}>
|
||||||
|
<ShieldCheck className={`w-3.5 h-3.5 ${
|
||||||
|
worker.passport_status.toLowerCase().includes('pending')
|
||||||
|
? 'text-amber-600'
|
||||||
|
: 'text-emerald-600'
|
||||||
|
}`} />
|
||||||
|
<span>{worker.passport_status}</span>
|
||||||
</div>
|
</div>
|
||||||
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
|
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
|
||||||
<div className="flex items-center space-x-1.5 bg-amber-50 px-2.5 py-1 rounded-lg text-amber-800 border border-amber-200 text-[9px] font-black uppercase tracking-wider w-fit shadow-xs">
|
<div className="flex items-center space-x-1.5 bg-amber-50 px-2.5 py-1 rounded-lg text-amber-800 border border-amber-200 text-[9px] font-black uppercase tracking-wider w-fit shadow-xs">
|
||||||
@ -289,15 +291,7 @@ export default function Show({ worker }) {
|
|||||||
<span className="font-extrabold text-slate-900 text-sm">{worker.salary} {t('aed', 'AED')} / {t('mo', 'mo')}</span>
|
<span className="font-extrabold text-slate-900 text-sm">{worker.salary} {t('aed', 'AED')} / {t('mo', 'mo')}</span>
|
||||||
</div>
|
</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">
|
|
||||||
<Calendar className="w-4 h-4 text-[#185FA5]" />
|
|
||||||
<span>{t('availability_status', 'Availability Status')}</span>
|
|
||||||
</div>
|
|
||||||
<span className="font-black text-[#185FA5] bg-blue-50 px-2 py-0.5 rounded text-xs border border-blue-100">
|
|
||||||
{availabilityStatus}
|
|
||||||
</span>
|
|
||||||
</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-bold">
|
||||||
@ -316,11 +310,11 @@ export default function Show({ worker }) {
|
|||||||
</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-655 font-bold">
|
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
||||||
<ShieldCheck className="w-4 h-4 text-emerald-600" />
|
<ShieldCheck className={`w-4 h-4 ${worker.passport_status.toLowerCase().includes('pending') ? 'text-amber-600' : 'text-emerald-600'}`} />
|
||||||
<span>{t('emirates_id_vetting', 'Emirates ID Vetting')}</span>
|
<span>{t('passport_verification', 'Passport Verification')}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-bold text-emerald-700 text-xs uppercase">{worker.emirates_id_status}</span>
|
<span className={`font-bold text-xs uppercase ${worker.passport_status.toLowerCase().includes('pending') ? 'text-amber-700' : 'text-emerald-700'}`}>{worker.passport_status}</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">
|
||||||
@ -389,51 +383,11 @@ export default function Show({ worker }) {
|
|||||||
<span>{t('verified_legal_docs', 'Verified Legal Documents (UAE MOHRE Vetted)')}</span>
|
<span>{t('verified_legal_docs', 'Verified Legal Documents (UAE MOHRE Vetted)')}</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg: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-xs hover:shadow-sm">
|
||||||
<div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center justify-between">
|
<div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center justify-between">
|
||||||
<span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">{t('passport_verified', 'Passport (Verified)')}</span>
|
<span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">{t('passport_verified', 'Passport (PDPL Compliant)')}</span>
|
||||||
<FileText className="w-4 h-4 text-slate-400" />
|
|
||||||
</div>
|
|
||||||
<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">
|
|
||||||
<Search className="w-5 h-5 text-slate-300" />
|
|
||||||
<span className="absolute bottom-1 text-[7px] font-bold text-slate-400 uppercase">{t('click_zoom', 'Click zoom')}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 space-y-2">
|
|
||||||
<div>
|
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_id', 'Document ID')}</div>
|
|
||||||
<div className="text-xs font-bold text-slate-800">L82739102-UAE</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
<div>
|
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue')}</div>
|
|
||||||
<div className="text-[10px] font-bold text-slate-800">12 Jan 2021</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Expiry')}</div>
|
|
||||||
<div className="text-[10px] font-bold text-emerald-600">11 Jan 2031</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('ocr_match', 'OCR Match')}</div>
|
|
||||||
<div className="flex items-center space-x-2 mt-0.5">
|
|
||||||
<div className="flex-1 h-1 bg-slate-100 rounded-full overflow-hidden">
|
|
||||||
<div className="h-full bg-emerald-500 w-[95%]" />
|
|
||||||
</div>
|
|
||||||
<span className="text-[9px] font-bold text-emerald-600">95%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Document Card: Emirates ID */}
|
|
||||||
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm">
|
|
||||||
<div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center justify-between">
|
|
||||||
<span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">{t('emirates_id_pdpl', 'Emirates ID (PDPL Compliant)')}</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">
|
||||||
@ -445,14 +399,26 @@ export default function Show({ worker }) {
|
|||||||
|
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('verification_type', 'Verification Type')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_id', 'Document ID')}</div>
|
||||||
<div className="text-xs font-black text-slate-800">{t('emirates_id_ocr', 'Emirates ID OCR')}</div>
|
<div className="text-xs font-bold text-slate-800">{passportDoc?.number || 'L82739102-UAE'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-1">
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue')}</div>
|
||||||
|
<div className="text-[10px] font-bold text-slate-800">{passportDoc?.issue_date || '12 Jan 2021'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('status', 'Status')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Expiry')}</div>
|
||||||
<div className="flex items-center space-x-1 mt-0.5">
|
<div className="text-[10px] font-bold text-emerald-600">{passportDoc?.expiry_date || '11 Jan 2031'}</div>
|
||||||
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" />
|
</div>
|
||||||
<span className="text-[9px] font-black text-emerald-700 uppercase tracking-wider">{worker.emirates_id_status}</span>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('ocr_match', 'OCR Match')}</div>
|
||||||
|
<div className="flex items-center space-x-2 mt-0.5">
|
||||||
|
<div className="flex-1 h-1 bg-slate-100 rounded-full overflow-hidden">
|
||||||
|
<div className="h-full bg-emerald-500" style={{ width: `${passportDoc?.ocr_accuracy || 95}%` }} />
|
||||||
|
</div>
|
||||||
|
<span className="text-[9px] font-bold text-emerald-600">{passportDoc?.ocr_accuracy || 95}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -466,7 +432,7 @@ export default function Show({ worker }) {
|
|||||||
{/* 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-xs hover:shadow-sm">
|
||||||
<div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center justify-between">
|
<div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center justify-between">
|
||||||
<span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">{t('entry_visa_verified', 'Entry Visa (Verified)')}</span>
|
<span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">{t('entry_visa_verified', 'Entry Visa (PDPL Compliant)')}</span>
|
||||||
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit') || worker.visa_status.toLowerCase().includes('cancelled')) ? (
|
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit') || worker.visa_status.toLowerCase().includes('cancelled')) ? (
|
||||||
<AlertTriangle className="w-4 h-4 text-amber-500 animate-pulse" />
|
<AlertTriangle className="w-4 h-4 text-amber-500 animate-pulse" />
|
||||||
) : (
|
) : (
|
||||||
@ -474,18 +440,33 @@ export default function Show({ worker }) {
|
|||||||
)}
|
)}
|
||||||
</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">
|
<div className="w-20 h-28 bg-slate-950 rounded-lg flex-shrink-0 border border-slate-800 flex flex-col items-center justify-center p-1.5 text-center relative shadow-inner">
|
||||||
<Search className="w-5 h-5 text-slate-300" />
|
<ShieldCheck className="w-6 h-6 text-emerald-500 mb-1 animate-pulse" />
|
||||||
<span className="absolute bottom-1 text-[7px] font-bold text-slate-400 uppercase">{t('click_zoom', 'Click zoom')}</span>
|
<span className="text-[6px] font-black text-emerald-500 uppercase tracking-tighter block">{t('scan_purged', 'Scan Purged')}</span>
|
||||||
|
<span className="text-[5px] font-medium text-slate-400 mt-1 leading-tight block">{t('uae_pdpl_compliant', 'UAE PDPL compliant')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_id', 'Document ID')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{visaDoc?.number || 'V2839102-DXB'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-1">
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue')}</div>
|
||||||
|
<div className="text-[10px] font-bold text-slate-800">{visaDoc?.issue_date || '12 Jan 2023'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Expiry')}</div>
|
||||||
|
<div className="text-[10px] font-bold text-emerald-600">{visaDoc?.expiry_date || '11 Jan 2026'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('visa_category', 'Visa Category')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('visa_category', 'Visa Category')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{worker.visa_status}</div>
|
<div className="text-xs font-bold text-slate-800">{worker.visa_status}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('vetting_status', 'Vetting status')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('verification_status', 'Verification status')}</div>
|
||||||
<div className="flex items-center space-x-1.5 mt-0.5">
|
<div className="flex items-center space-x-1.5 mt-0.5">
|
||||||
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
|
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
|
||||||
<span className="inline-flex items-center bg-amber-50 border border-amber-200 text-[8px] font-black uppercase text-amber-700 px-1.5 py-0.5 rounded tracking-wide">
|
<span className="inline-flex items-center bg-amber-50 border border-amber-200 text-[8px] font-black uppercase text-amber-700 px-1.5 py-0.5 rounded tracking-wide">
|
||||||
@ -508,7 +489,6 @@ export default function Show({ worker }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -565,9 +545,11 @@ export default function Show({ worker }) {
|
|||||||
<h4 className="font-extrabold text-sm text-slate-900">{sim.name}</h4>
|
<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>
|
<p className="text-[10px] text-slate-500 font-bold">{sim.nationality} • {sim.category}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="bg-blue-50 text-[#185FA5] px-2 py-0.5 border border-blue-100 rounded text-[9px] font-black uppercase">
|
{sim.verified && (
|
||||||
{sim.availability_status}
|
<span className="bg-emerald-50 text-emerald-700 px-2 py-0.5 border border-emerald-100 rounded text-[9px] font-black uppercase">
|
||||||
|
{t('verified', 'Verified')}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between pt-2 border-t border-slate-100">
|
<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} {t('aed', 'AED')}/{t('mo', 'mo')}</span>
|
<span className="font-black text-slate-800 text-xs">{sim.salary} {t('aed', 'AED')}/{t('mo', 'mo')}</span>
|
||||||
@ -643,7 +625,7 @@ export default function Show({ worker }) {
|
|||||||
|
|
||||||
<div className="flex items-center space-x-2 text-rose-600">
|
<div className="flex items-center space-x-2 text-rose-600">
|
||||||
<AlertTriangle className="w-5 h-5" />
|
<AlertTriangle className="w-5 h-5" />
|
||||||
<h4 className="font-black text-sm uppercase">{t('report_abuse_violation', 'Report Vetting Abuse / Terms Violation')}</h4>
|
<h4 className="font-black text-sm uppercase">{t('report_abuse_violation', 'Report Verification Abuse / Terms Violation')}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-slate-500 leading-relaxed">
|
<p className="text-xs text-slate-500 leading-relaxed">
|
||||||
|
|||||||
@ -327,7 +327,7 @@
|
|||||||
"sponsorship_details": "Sponsorship Details",
|
"sponsorship_details": "Sponsorship Details",
|
||||||
"salary_expectations": "Salary Expectations",
|
"salary_expectations": "Salary Expectations",
|
||||||
"work_experience": "Work Experience",
|
"work_experience": "Work Experience",
|
||||||
"emirates_id_vetting": "Emirates ID Vetting",
|
"emirates_id_verification": "Emirates ID Verification",
|
||||||
"medical_health_test": "Medical Health Test",
|
"medical_health_test": "Medical Health Test",
|
||||||
"passed_certified": "PASSED CERTIFIED",
|
"passed_certified": "PASSED CERTIFIED",
|
||||||
"professional_summary": "Professional Summary",
|
"professional_summary": "Professional Summary",
|
||||||
@ -347,7 +347,7 @@
|
|||||||
"permanently_purged": "Permanently Purged",
|
"permanently_purged": "Permanently Purged",
|
||||||
"entry_visa_verified": "Entry Visa (Verified)",
|
"entry_visa_verified": "Entry Visa (Verified)",
|
||||||
"visa_category": "Visa Category",
|
"visa_category": "Visa Category",
|
||||||
"vetting_status": "Vetting status",
|
"verification_status": "Verification status",
|
||||||
"transfer_required": "Transfer Required",
|
"transfer_required": "Transfer Required",
|
||||||
"needs_regularization": "Needs Regularization",
|
"needs_regularization": "Needs Regularization",
|
||||||
"clear_records": "Clear Records",
|
"clear_records": "Clear Records",
|
||||||
@ -361,7 +361,7 @@
|
|||||||
"post_hire_review_desc": "Help the UAE community hire safely by posting an anonymous review describing {name}'s childcare, cooking, driving, or domestic skills.",
|
"post_hire_review_desc": "Help the UAE community hire safely by posting an anonymous review describing {name}'s childcare, cooking, driving, or domestic skills.",
|
||||||
"leave_trust_review": "Leave a Trust Review",
|
"leave_trust_review": "Leave a Trust Review",
|
||||||
"back_to_directory": "Back to Worker Directory",
|
"back_to_directory": "Back to Worker Directory",
|
||||||
"report_abuse_title": "Report Vetting Abuse / Terms Violation",
|
"report_abuse_title": "Report Verification Abuse / Terms Violation",
|
||||||
"report_abuse_desc": "We maintain zero-tolerance for fake documentation, independent recruiters trading cash, or incorrect contact details.",
|
"report_abuse_desc": "We maintain zero-tolerance for fake documentation, independent recruiters trading cash, or incorrect contact details.",
|
||||||
"reason": "Reason",
|
"reason": "Reason",
|
||||||
"select_reason": "Select a reason...",
|
"select_reason": "Select a reason...",
|
||||||
@ -413,7 +413,7 @@
|
|||||||
"next_step_post_hire_review": "Next Step: Stage 5 Post-Hire Review",
|
"next_step_post_hire_review": "Next Step: Stage 5 Post-Hire Review",
|
||||||
"help_community_post_review": "Help the UAE community hire safely by posting an anonymous review describing {name}'s childcare, cooking, driving, or domestic skills.",
|
"help_community_post_review": "Help the UAE community hire safely by posting an anonymous review describing {name}'s childcare, cooking, driving, or domestic skills.",
|
||||||
"back_to_worker_directory": "Back to Worker Directory",
|
"back_to_worker_directory": "Back to Worker Directory",
|
||||||
"report_abuse_violation": "Report Vetting Abuse / Terms Violation",
|
"report_abuse_violation": "Report Verification Abuse / Terms Violation",
|
||||||
"zero_tolerance_abuse_desc": "We maintain zero-tolerance for fake documentation, independent recruiters trading cash, or incorrect contact details.",
|
"zero_tolerance_abuse_desc": "We maintain zero-tolerance for fake documentation, independent recruiters trading cash, or incorrect contact details.",
|
||||||
"additional_context_details": "Additional context details",
|
"additional_context_details": "Additional context details",
|
||||||
"provide_specifics_placeholder": "Provide detailed specifics of what happened...",
|
"provide_specifics_placeholder": "Provide detailed specifics of what happened...",
|
||||||
|
|||||||
@ -327,7 +327,7 @@
|
|||||||
"sponsorship_details": "प्रायोजन विवरण",
|
"sponsorship_details": "प्रायोजन विवरण",
|
||||||
"salary_expectations": "वेतन अपेक्षाएं",
|
"salary_expectations": "वेतन अपेक्षाएं",
|
||||||
"work_experience": "कार्य अनुभव",
|
"work_experience": "कार्य अनुभव",
|
||||||
"emirates_id_vetting": "अमीरात आईडी जांच",
|
"emirates_id_verification": "अमीरात आईडी जांच",
|
||||||
"medical_health_test": "चिकित्सा स्वास्थ्य परीक्षण",
|
"medical_health_test": "चिकित्सा स्वास्थ्य परीक्षण",
|
||||||
"passed_certified": "उत्तीर्ण प्रमाणित",
|
"passed_certified": "उत्तीर्ण प्रमाणित",
|
||||||
"professional_summary": "पेशेवर सारांश",
|
"professional_summary": "पेशेवर सारांश",
|
||||||
@ -347,7 +347,7 @@
|
|||||||
"permanently_purged": "स्थायी रूप से मिटा दिया गया",
|
"permanently_purged": "स्थायी रूप से मिटा दिया गया",
|
||||||
"entry_visa_verified": "प्रवेश वीज़ा (सत्यापित)",
|
"entry_visa_verified": "प्रवेश वीज़ा (सत्यापित)",
|
||||||
"visa_category": "वीज़ा श्रेणी",
|
"visa_category": "वीज़ा श्रेणी",
|
||||||
"vetting_status": "जांच की स्थिति",
|
"verification_status": "जांच की स्थिति",
|
||||||
"transfer_required": "स्थानांतरण आवश्यक",
|
"transfer_required": "स्थानांतरण आवश्यक",
|
||||||
"needs_regularization": "नियमितीकरण की आवश्यकता है",
|
"needs_regularization": "नियमितीकरण की आवश्यकता है",
|
||||||
"clear_records": "स्पष्ट रिकॉर्ड",
|
"clear_records": "स्पष्ट रिकॉर्ड",
|
||||||
|
|||||||
@ -327,7 +327,7 @@
|
|||||||
"sponsorship_details": "Maelezo ya Ufadhili",
|
"sponsorship_details": "Maelezo ya Ufadhili",
|
||||||
"salary_expectations": "Matarajio ya Mshahara",
|
"salary_expectations": "Matarajio ya Mshahara",
|
||||||
"work_experience": "Uzoefu wa Kazi",
|
"work_experience": "Uzoefu wa Kazi",
|
||||||
"emirates_id_vetting": "Uhakiki wa Emirates ID",
|
"emirates_id_verification": "Uhakiki wa Emirates ID",
|
||||||
"medical_health_test": "Kipimo cha Afya ya Matibabu",
|
"medical_health_test": "Kipimo cha Afya ya Matibabu",
|
||||||
"passed_certified": "PASSED CERTIFIED",
|
"passed_certified": "PASSED CERTIFIED",
|
||||||
"professional_summary": "Muhtasari wa Kitaalamu",
|
"professional_summary": "Muhtasari wa Kitaalamu",
|
||||||
@ -347,7 +347,7 @@
|
|||||||
"permanently_purged": "Imefutwa Kabisa",
|
"permanently_purged": "Imefutwa Kabisa",
|
||||||
"entry_visa_verified": "Visa ya Kuingia (Imethibitishwa)",
|
"entry_visa_verified": "Visa ya Kuingia (Imethibitishwa)",
|
||||||
"visa_category": "Aina ya Visa",
|
"visa_category": "Aina ya Visa",
|
||||||
"vetting_status": "Hali ya Uhakiki",
|
"verification_status": "Hali ya Uhakiki",
|
||||||
"transfer_required": "Uhamisho Unahitajika",
|
"transfer_required": "Uhamisho Unahitajika",
|
||||||
"needs_regularization": "Inahitaji Udhibiti",
|
"needs_regularization": "Inahitaji Udhibiti",
|
||||||
"clear_records": "Nyaraka Ziko Wazi",
|
"clear_records": "Nyaraka Ziko Wazi",
|
||||||
|
|||||||
@ -327,7 +327,7 @@
|
|||||||
"sponsorship_details": "ஸ்பான்சர்ஷிப் விவரங்கள்",
|
"sponsorship_details": "ஸ்பான்சர்ஷிப் விவரங்கள்",
|
||||||
"salary_expectations": "சம்பள எதிர்பார்ப்புகள்",
|
"salary_expectations": "சம்பள எதிர்பார்ப்புகள்",
|
||||||
"work_experience": "வேலை அனுபவம்",
|
"work_experience": "வேலை அனுபவம்",
|
||||||
"emirates_id_vetting": "எமிரேட்ஸ் ஐடி தணிக்கை",
|
"emirates_id_verification": "எமிரேட்ஸ் ஐடி தணிக்கை",
|
||||||
"medical_health_test": "மருத்துவ சுகாதார சோதனை",
|
"medical_health_test": "மருத்துவ சுகாதார சோதனை",
|
||||||
"passed_certified": "தேர்ச்சி பெற்றது சரிபார்க்கப்பட்டது",
|
"passed_certified": "தேர்ச்சி பெற்றது சரிபார்க்கப்பட்டது",
|
||||||
"professional_summary": "தொழில்முறை சுருக்கம்",
|
"professional_summary": "தொழில்முறை சுருக்கம்",
|
||||||
@ -347,7 +347,7 @@
|
|||||||
"permanently_purged": "நிரந்தரமாக நீக்கப்பட்டது",
|
"permanently_purged": "நிரந்தரமாக நீக்கப்பட்டது",
|
||||||
"entry_visa_verified": "நுழைவு விசா (சரிபார்க்கப்பட்டது)",
|
"entry_visa_verified": "நுழைவு விசா (சரிபார்க்கப்பட்டது)",
|
||||||
"visa_category": "விசா வகை",
|
"visa_category": "விசா வகை",
|
||||||
"vetting_status": "சரிபார்ப்பு நிலை",
|
"verification_status": "சரிபார்ப்பு நிலை",
|
||||||
"transfer_required": "பரிமாற்றம் தேவை",
|
"transfer_required": "பரிமாற்றம் தேவை",
|
||||||
"needs_regularization": "விசா சரிசெய்தல் தேவை",
|
"needs_regularization": "விசா சரிசெய்தல் தேவை",
|
||||||
"clear_records": "தெளிவான பதிவுகள்",
|
"clear_records": "தெளிவான பதிவுகள்",
|
||||||
|
|||||||
@ -327,7 +327,7 @@
|
|||||||
"sponsorship_details": "Mga Detalye ng Sponsorship",
|
"sponsorship_details": "Mga Detalye ng Sponsorship",
|
||||||
"salary_expectations": "Inaasahang Sahod",
|
"salary_expectations": "Inaasahang Sahod",
|
||||||
"work_experience": "Karanasan sa Trabaho",
|
"work_experience": "Karanasan sa Trabaho",
|
||||||
"emirates_id_vetting": "Pagsusuri ng Emirates ID",
|
"emirates_id_verification": "Pagsusuri ng Emirates ID",
|
||||||
"medical_health_test": "Medikal at Kalusugan na Pagsusuri",
|
"medical_health_test": "Medikal at Kalusugan na Pagsusuri",
|
||||||
"passed_certified": "PASADO AT VERIFIED",
|
"passed_certified": "PASADO AT VERIFIED",
|
||||||
"professional_summary": "Propesyonal na Buod",
|
"professional_summary": "Propesyonal na Buod",
|
||||||
@ -347,7 +347,7 @@
|
|||||||
"permanently_purged": "Permanenteng Tinanggal",
|
"permanently_purged": "Permanenteng Tinanggal",
|
||||||
"entry_visa_verified": "Entry Visa (Na-verify)",
|
"entry_visa_verified": "Entry Visa (Na-verify)",
|
||||||
"visa_category": "Kategorya ng Visa",
|
"visa_category": "Kategorya ng Visa",
|
||||||
"vetting_status": "Katayuan ng Pagsusuri",
|
"verification_status": "Katayuan ng Pagsusuri",
|
||||||
"transfer_required": "Kailangang Ilipat",
|
"transfer_required": "Kailangang Ilipat",
|
||||||
"needs_regularization": "Kailangang Ayusin ang Visa",
|
"needs_regularization": "Kailangang Ayusin ang Visa",
|
||||||
"clear_records": "Malinis ang Rekord",
|
"clear_records": "Malinis ang Rekord",
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
// Unprotected Worker Mobile Auth Endpoints
|
// Unprotected Worker Mobile Auth Endpoints
|
||||||
Route::get('/workers/config', [WorkerAuthController::class, 'config']);
|
Route::get('/workers/config', [WorkerAuthController::class, 'config']);
|
||||||
|
Route::get('/workers/skills', [WorkerAuthController::class, 'skills']);
|
||||||
Route::post('/workers/send-otp', [WorkerAuthController::class, 'sendOtp']);
|
Route::post('/workers/send-otp', [WorkerAuthController::class, 'sendOtp']);
|
||||||
Route::post('/workers/verify-otp', [WorkerAuthController::class, 'verifyOtp']);
|
Route::post('/workers/verify-otp', [WorkerAuthController::class, 'verifyOtp']);
|
||||||
Route::post('/workers/setup-profile', [WorkerAuthController::class, 'setupProfile']);
|
Route::post('/workers/setup-profile', [WorkerAuthController::class, 'setupProfile']);
|
||||||
|
|||||||
@ -46,7 +46,40 @@
|
|||||||
})->name('admin.subscriptions');
|
})->name('admin.subscriptions');
|
||||||
|
|
||||||
Route::get('/payments', function () {
|
Route::get('/payments', function () {
|
||||||
return Inertia::render('Admin/Payments/Index');
|
$payments = \Illuminate\Support\Facades\DB::table('payments')
|
||||||
|
->leftJoin('users', 'payments.user_id', '=', 'users.id')
|
||||||
|
->select('payments.*', 'users.name as user_name', 'users.email as user_email')
|
||||||
|
->orderBy('payments.created_at', 'desc')
|
||||||
|
->get()
|
||||||
|
->map(function($payment) {
|
||||||
|
return [
|
||||||
|
'id' => 'PAY-' . str_pad($payment->id, 3, '0', STR_PAD_LEFT),
|
||||||
|
'employer' => $payment->user_name ?: 'System Guest / Sponsor',
|
||||||
|
'plan' => $payment->description ?: 'Subscription Plan',
|
||||||
|
'amount' => (float)$payment->amount,
|
||||||
|
'method' => 'Stripe Gateway',
|
||||||
|
'date' => date('Y-m-d', strtotime($payment->created_at)),
|
||||||
|
'status' => $payment->status === 'success' ? 'Completed' : (ucfirst($payment->status) ?: 'Completed'),
|
||||||
|
'period' => date('M Y', strtotime($payment->created_at)) . ' - ' . date('M Y', strtotime($payment->created_at . ' +1 month')),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$totalRevenue = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'success')->sum('amount');
|
||||||
|
$activeSubs = \Illuminate\Support\Facades\DB::table('sponsors')->where('subscription_status', 'active')->count();
|
||||||
|
$failedPayments = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'failed')->count();
|
||||||
|
$avgTicket = \Illuminate\Support\Facades\DB::table('payments')->where('status', 'success')->avg('amount') ?: 0;
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'total_revenue' => number_format($totalRevenue, 2),
|
||||||
|
'active_subscriptions' => $activeSubs,
|
||||||
|
'failed_payments' => $failedPayments,
|
||||||
|
'average_ticket' => number_format($avgTicket, 2),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Inertia::render('Admin/Payments/Index', [
|
||||||
|
'payments' => $payments,
|
||||||
|
'stats' => $stats
|
||||||
|
]);
|
||||||
})->name('admin.payments');
|
})->name('admin.payments');
|
||||||
|
|
||||||
Route::post('/payments/{id}/refund', [\App\Http\Controllers\Admin\AdminExtraController::class, 'refundPayment'])->name('admin.payments.refund');
|
Route::post('/payments/{id}/refund', [\App\Http\Controllers\Admin\AdminExtraController::class, 'refundPayment'])->name('admin.payments.refund');
|
||||||
@ -184,7 +217,7 @@
|
|||||||
'name' => 'Basic Search',
|
'name' => 'Basic Search',
|
||||||
'price' => '99 AED',
|
'price' => '99 AED',
|
||||||
'period' => 'month',
|
'period' => 'month',
|
||||||
'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR vetting'],
|
'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR verification'],
|
||||||
'popular' => false,
|
'popular' => false,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@ -107,10 +107,7 @@ public function test_employer_can_register()
|
|||||||
->assertJsonStructure([
|
->assertJsonStructure([
|
||||||
'success',
|
'success',
|
||||||
'message',
|
'message',
|
||||||
'data' => [
|
'email'
|
||||||
'employer',
|
|
||||||
'token'
|
|
||||||
]
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('users', [
|
$this->assertDatabaseHas('users', [
|
||||||
@ -119,7 +116,7 @@ public function test_employer_can_register()
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('employer_profiles', [
|
$this->assertDatabaseHas('employer_profiles', [
|
||||||
'company_name' => 'New Company Corp',
|
'company_name' => 'Alice Employer Household',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,6 +51,31 @@ public function test_s1_get_languages_config()
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Get Skills Master API.
|
||||||
|
*/
|
||||||
|
public function test_get_skills_master_api()
|
||||||
|
{
|
||||||
|
// Seed some master skills
|
||||||
|
\Illuminate\Support\Facades\DB::table('skills')->insert([
|
||||||
|
['name' => 'Cooking', 'created_at' => now(), 'updated_at' => now()],
|
||||||
|
['name' => 'Cleaning', 'created_at' => now(), 'updated_at' => now()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/workers/skills');
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
])
|
||||||
|
->assertJsonStructure([
|
||||||
|
'success',
|
||||||
|
'skills' => [
|
||||||
|
'*' => ['id', 'name']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test S1 Onboard: Send and Verify OTP.
|
* Test S1 Onboard: Send and Verify OTP.
|
||||||
*/
|
*/
|
||||||
|
|||||||
1
toArray())
Normal file
@ -0,0 +1 @@
|
|||||||
|
PARSE ERROR PHP Parse error: Syntax error, unexpected ';' in vendor\psy\psysh\src\Exception\ParseErrorException.php on line 44.
|
||||||
@ -19,7 +19,12 @@ export default defineConfig({
|
|||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
host: '192.168.29.131',
|
host: '0.0.0.0',
|
||||||
|
port: 5173,
|
||||||
|
cors: true,
|
||||||
|
hmr: {
|
||||||
|
host: '192.168.0.249',
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
ignored: ['**/storage/framework/views/**'],
|
ignored: ['**/storage/framework/views/**'],
|
||||||
},
|
},
|
||||||
|
|||||||