Compare commits
No commits in common. "3fcd8d54a0441716e86a60873478937d33fe3706" and "1a037567abba21f4e55a911c513920ef42848cb4" have entirely different histories.
3fcd8d54a0
...
1a037567ab
@ -360,242 +360,66 @@ public function refundPayment(Request $request, $id)
|
||||
/**
|
||||
* Interactive Reports Hub
|
||||
*/
|
||||
/**
|
||||
* Interactive Reports Hub
|
||||
*/
|
||||
public function analytics(Request $request)
|
||||
public function analytics()
|
||||
{
|
||||
$reportType = $request->input('report_type', 'workers');
|
||||
$startDate = $request->input('start_date');
|
||||
$endDate = $request->input('end_date');
|
||||
$status = $request->input('status', 'all');
|
||||
$search = $request->input('search');
|
||||
$export = $request->input('export');
|
||||
// 1. Worker Stats
|
||||
$workerStats = [
|
||||
'total' => DB::table('workers')->count(),
|
||||
'verified' => DB::table('workers')->where('verified', 1)->count(),
|
||||
'active' => DB::table('workers')->where('status', 'active')->count(),
|
||||
'inactive' => DB::table('workers')->where('status', 'inactive')->count(),
|
||||
];
|
||||
|
||||
$headers = [];
|
||||
$data = collect();
|
||||
// 2. Sponsor Stats
|
||||
$sponsorStats = [
|
||||
'total' => DB::table('sponsors')->count(),
|
||||
'verified' => DB::table('sponsors')->where('is_verified', 1)->count(),
|
||||
'active_subscribers' => DB::table('sponsors')->where('subscription_status', 'active')->count(),
|
||||
'unpaid' => DB::table('sponsors')->where('payment_status', 'unpaid')->count(),
|
||||
];
|
||||
|
||||
if ($reportType === 'workers') {
|
||||
$query = DB::table('workers');
|
||||
if ($startDate) {
|
||||
$query->whereDate('created_at', '>=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$query->whereDate('created_at', '<=', $endDate);
|
||||
}
|
||||
if ($status && $status !== 'all') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
if ($search) {
|
||||
$query->where(function($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%")
|
||||
->orWhere('nationality', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
// 3. Dispute & Safety Stats
|
||||
$disputeStats = [
|
||||
'total' => DB::table('disputes')->count(),
|
||||
'open' => DB::table('disputes')->where('status', 'Open')->count(),
|
||||
'under_review' => DB::table('disputes')->where('status', 'Under Review')->count(),
|
||||
'resolved' => DB::table('disputes')->where('status', 'Resolved')->count(),
|
||||
];
|
||||
|
||||
$headers = ['Worker ID', 'Name', 'Email', 'Phone', 'Nationality', 'Status', 'Salary (AED)', 'Verified', 'Joined Date'];
|
||||
$data = $query->orderBy('created_at', 'desc')->get()->map(function($w) {
|
||||
$safetyStats = [
|
||||
'total' => DB::table('moderation_reports')->count(),
|
||||
'pending' => DB::table('moderation_reports')->where('status', 'Pending')->count(),
|
||||
'resolved' => DB::table('moderation_reports')->where('status', 'Resolved')->count(),
|
||||
];
|
||||
|
||||
// 4. Financial Payments & Ledger
|
||||
$payments = 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' => 'WRK-' . str_pad($w->id, 4, '0', STR_PAD_LEFT),
|
||||
'name' => $w->name,
|
||||
'email' => $w->email,
|
||||
'phone' => $w->phone ?: 'N/A',
|
||||
'nationality' => $w->nationality ?: 'N/A',
|
||||
'status' => $w->status,
|
||||
'salary' => (int)$w->salary,
|
||||
'verified' => $w->verified ? 'Yes' : 'No',
|
||||
'joined_at' => date('Y-m-d', strtotime($w->created_at)),
|
||||
'id' => $payment->id,
|
||||
'user_name' => $payment->user_name ?: 'System Guest / Sponsor',
|
||||
'user_email' => $payment->user_email ?: 'no-email@marketplace.com',
|
||||
'amount' => (float)$payment->amount,
|
||||
'currency' => $payment->currency,
|
||||
'description' => $payment->description,
|
||||
'status' => $payment->status,
|
||||
'created_at' => date('M d, Y h:i A', strtotime($payment->created_at)),
|
||||
];
|
||||
});
|
||||
|
||||
} elseif ($reportType === 'employers') {
|
||||
$query = DB::table('users')
|
||||
->leftJoin('employer_profiles', 'users.id', '=', 'employer_profiles.user_id')
|
||||
->leftJoin('sponsors', 'users.email', '=', 'sponsors.email')
|
||||
->where('users.role', 'employer')
|
||||
->select(
|
||||
'users.id',
|
||||
'users.name',
|
||||
'users.email',
|
||||
'sponsors.status as sponsor_status',
|
||||
'sponsors.subscription_status',
|
||||
'users.created_at'
|
||||
);
|
||||
|
||||
if ($startDate) {
|
||||
$query->whereDate('users.created_at', '>=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$query->whereDate('users.created_at', '<=', $endDate);
|
||||
}
|
||||
if ($status && $status !== 'all') {
|
||||
$query->where('sponsors.subscription_status', $status);
|
||||
}
|
||||
if ($search) {
|
||||
$query->where(function($q) use ($search) {
|
||||
$q->where('users.name', 'like', "%{$search}%")
|
||||
->orWhere('users.email', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$headers = ['Employer ID', 'Name', 'Email', 'Status', 'Subscription Status', 'Total Spent', 'Joined Date'];
|
||||
$data = $query->orderBy('users.created_at', 'desc')->get()->map(function($emp) {
|
||||
$totalPaid = DB::table('payments')->where('user_id', $emp->id)->where('status', 'success')->sum('amount');
|
||||
return [
|
||||
'id' => 'EMP-' . str_pad($emp->id, 4, '0', STR_PAD_LEFT),
|
||||
'name' => $emp->name,
|
||||
'email' => $emp->email,
|
||||
'status' => ucfirst($emp->sponsor_status ?? 'Active'),
|
||||
'subscription' => ucfirst($emp->subscription_status ?? 'Inactive'),
|
||||
'total_spent' => number_format($totalPaid, 2) . ' AED',
|
||||
'joined_at' => date('Y-m-d', strtotime($emp->created_at)),
|
||||
];
|
||||
});
|
||||
|
||||
} elseif ($reportType === 'payments') {
|
||||
$query = DB::table('payments')
|
||||
->leftJoin('users', 'payments.user_id', '=', 'users.id')
|
||||
->select('payments.*', 'users.name as user_name', 'users.email as user_email');
|
||||
|
||||
if ($startDate) {
|
||||
$query->whereDate('payments.created_at', '>=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$query->whereDate('payments.created_at', '<=', $endDate);
|
||||
}
|
||||
if ($status && $status !== 'all') {
|
||||
$query->where('payments.status', $status);
|
||||
}
|
||||
if ($search) {
|
||||
$query->where(function($q) use ($search) {
|
||||
$q->where('users.name', 'like', "%{$search}%")
|
||||
->orWhere('payments.description', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$headers = ['Payment ID', 'Employer Name', 'Employer Email', 'Amount', 'Description', 'Status', 'Date'];
|
||||
$data = $query->orderBy('payments.created_at', 'desc')->get()->map(function($pay) {
|
||||
return [
|
||||
'id' => 'PAY-' . str_pad($pay->id, 4, '0', STR_PAD_LEFT),
|
||||
'employer_name' => $pay->user_name ?: 'System Guest / Sponsor',
|
||||
'employer_email' => $pay->user_email ?: 'no-email@marketplace.com',
|
||||
'amount' => number_format((float)$pay->amount, 2) . ' AED',
|
||||
'description' => $pay->description ?: 'Subscription Plan',
|
||||
'status' => ucfirst($pay->status),
|
||||
'date' => date('Y-m-d H:i', strtotime($pay->created_at)),
|
||||
];
|
||||
});
|
||||
|
||||
} elseif ($reportType === 'tickets') {
|
||||
$query = DB::table('support_tickets')
|
||||
->leftJoin('users', 'support_tickets.user_id', '=', 'users.id')
|
||||
->leftJoin('report_reasons', 'support_tickets.reason_id', '=', 'report_reasons.id')
|
||||
->select('support_tickets.*', 'users.name as user_name', 'users.email as user_email', 'report_reasons.reason as reason_name');
|
||||
|
||||
if ($startDate) {
|
||||
$query->whereDate('support_tickets.created_at', '>=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$query->whereDate('support_tickets.created_at', '<=', $endDate);
|
||||
}
|
||||
if ($status && $status !== 'all') {
|
||||
$query->where('support_tickets.status', $status);
|
||||
}
|
||||
if ($search) {
|
||||
$query->where(function($q) use ($search) {
|
||||
$q->where('users.name', 'like', "%{$search}%")
|
||||
->orWhere('support_tickets.subject', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$headers = ['Ticket ID', 'User Name', 'User Email', 'Subject', 'Reason', 'Status', 'Voice Note Attached', 'Created Date'];
|
||||
$data = $query->orderBy('support_tickets.created_at', 'desc')->get()->map(function($tick) {
|
||||
return [
|
||||
'id' => 'TCK-' . str_pad($tick->id, 4, '0', STR_PAD_LEFT),
|
||||
'user_name' => $tick->user_name ?: 'System User',
|
||||
'user_email' => $tick->user_email ?: 'N/A',
|
||||
'subject' => $tick->subject,
|
||||
'reason' => $tick->reason_name ?: 'General Support',
|
||||
'status' => ucfirst($tick->status),
|
||||
'has_voice_note' => $tick->voice_note_path ? 'Yes' : 'No',
|
||||
'created_at' => date('Y-m-d H:i', strtotime($tick->created_at)),
|
||||
];
|
||||
});
|
||||
|
||||
} elseif ($reportType === 'safety') {
|
||||
$query = DB::table('moderation_reports');
|
||||
|
||||
if ($startDate) {
|
||||
$query->whereDate('reported_at', '>=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$query->whereDate('reported_at', '<=', $endDate);
|
||||
}
|
||||
if ($status && $status !== 'all') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
if ($search) {
|
||||
$query->where(function($q) use ($search) {
|
||||
$q->where('reported_user_name', 'like', "%{$search}%")
|
||||
->orWhere('reported_by_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$headers = ['Report ID', 'Reported User', 'Reported By', 'Reason', 'Status', 'Reported At'];
|
||||
$data = $query->orderBy('reported_at', 'desc')->get()->map(function($rep) {
|
||||
return [
|
||||
'id' => $rep->id,
|
||||
'reported_user' => $rep->reported_user_name . ' (' . $rep->reported_user_role . ')',
|
||||
'reported_by' => $rep->reported_by_name . ' (' . $rep->reported_by_role . ')',
|
||||
'reason' => $rep->reason,
|
||||
'status' => ucfirst($rep->status),
|
||||
'reported_at' => date('Y-m-d H:i', strtotime($rep->reported_at)),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if ($export === 'csv') {
|
||||
$filename = "report_" . $reportType . "_" . date('Ymd_His') . ".csv";
|
||||
$callback = function() use ($data, $headers) {
|
||||
$file = fopen('php://output', 'w');
|
||||
|
||||
// UTF-8 BOM
|
||||
fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF));
|
||||
|
||||
fputcsv($file, $headers);
|
||||
foreach ($data as $row) {
|
||||
fputcsv($file, array_values((array)$row));
|
||||
}
|
||||
fclose($file);
|
||||
};
|
||||
|
||||
return response()->stream($callback, 200, [
|
||||
"Content-type" => "text/csv; charset=UTF-8",
|
||||
"Content-Disposition" => "attachment; filename={$filename}",
|
||||
"Pragma" => "no-cache",
|
||||
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
|
||||
"Expires" => "0"
|
||||
]);
|
||||
}
|
||||
|
||||
// Fetch distinct nationalities for filter
|
||||
$nationalities = DB::table('workers')
|
||||
->whereNotNull('nationality')
|
||||
->where('nationality', '!=', '')
|
||||
->distinct()
|
||||
->pluck('nationality')
|
||||
->toArray();
|
||||
$totalRevenue = DB::table('payments')->where('status', 'success')->sum('amount');
|
||||
|
||||
return Inertia::render('Admin/Analytics/Index', [
|
||||
'reportType' => $reportType,
|
||||
'startDate' => $startDate ?: '',
|
||||
'endDate' => $endDate ?: '',
|
||||
'status' => $status,
|
||||
'search' => $search ?: '',
|
||||
'headers' => $headers,
|
||||
'reportData' => $data->toArray(),
|
||||
'nationalities' => $nationalities,
|
||||
'worker_stats' => $workerStats,
|
||||
'sponsor_stats' => $sponsorStats,
|
||||
'dispute_stats' => $disputeStats,
|
||||
'safety_stats' => $safetyStats,
|
||||
'payments' => $payments,
|
||||
'total_revenue' => (float)$totalRevenue,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -101,7 +101,7 @@ public function getConversations(Request $request)
|
||||
'worker_name' => $conv->worker->name ?? 'Candidate',
|
||||
'nationality' => $conv->worker->nationality ?? 'Unknown',
|
||||
'worker_status' => strtolower($conv->worker->status ?? 'active'),
|
||||
'salary' => ($conv->worker->salary ?? 2000) . ' AED',
|
||||
'salary' => ($conv->worker->salary ?? $conv->worker->expected_salary ?? 2000) . ' AED',
|
||||
'last_message' => $lastMsg->text ?? 'No messages yet.',
|
||||
'unread_count' => $unreadCount,
|
||||
'sent_at' => $lastMsg ? $lastMsg->created_at->diffForHumans() : 'Just now',
|
||||
@ -176,7 +176,7 @@ public function getMessages(Request $request, $id)
|
||||
'worker_name' => $conv->worker->name ?? 'Candidate',
|
||||
'nationality' => $conv->worker->nationality ?? 'Unknown',
|
||||
'worker_status' => strtolower($conv->worker->status ?? 'active'),
|
||||
'salary' => ($conv->worker->salary ?? 2000) . ' AED',
|
||||
'salary' => ($conv->worker->salary ?? $conv->worker->expected_salary ?? 2000) . ' AED',
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
|
||||
@ -123,7 +123,7 @@ public function applicants($id)
|
||||
'id' => $worker->id,
|
||||
'name' => $worker->name,
|
||||
'nationality' => $worker->nationality,
|
||||
'salary' => (int) $worker->salary,
|
||||
'salary' => (int) $worker->expected_salary,
|
||||
'experience' => ($worker->experience_years ?? 3) . ' Years',
|
||||
'status' => ucfirst($app->status), // Applied, Shortlisted, Hired, Rejected
|
||||
'match_score' => $worker->match_score ?? 90,
|
||||
|
||||
@ -167,7 +167,7 @@ public function show($id)
|
||||
'worker_name' => $activeConv->worker->name ?? 'Candidate',
|
||||
'worker_status' => strtolower($activeConv->worker->status ?? 'active'),
|
||||
'online' => true,
|
||||
'salary' => ($activeConv->worker->salary ?? 2000) . ' AED',
|
||||
'salary' => ($activeConv->worker->expected_salary ?? 2000) . ' AED',
|
||||
'nationality' => $activeConv->worker->nationality ?? 'Unknown',
|
||||
];
|
||||
|
||||
|
||||
@ -1,412 +1,281 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import React, { useState } from 'react';
|
||||
import { Head } from '@inertiajs/react';
|
||||
import AdminLayout from '@/Layouts/AdminLayout';
|
||||
import {
|
||||
FileText,
|
||||
Users,
|
||||
Building2,
|
||||
ShieldAlert,
|
||||
DollarSign,
|
||||
LifeBuoy,
|
||||
CreditCard,
|
||||
Search,
|
||||
RefreshCw,
|
||||
Download,
|
||||
Calendar,
|
||||
Filter,
|
||||
FileSpreadsheet,
|
||||
Globe,
|
||||
X,
|
||||
FileText
|
||||
Building2,
|
||||
DollarSign
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function ReportsHub({
|
||||
reportType = 'workers',
|
||||
startDate = '',
|
||||
endDate = '',
|
||||
status = 'all',
|
||||
search = '',
|
||||
headers = [],
|
||||
reportData = [],
|
||||
nationalities = []
|
||||
export default function AnalyticsHub({
|
||||
worker_stats = { total: 0, verified: 0, active: 0, inactive: 0 },
|
||||
sponsor_stats = { total: 0, verified: 0, active_subscribers: 0, unpaid: 0 },
|
||||
dispute_stats = { total: 0, open: 0, under_review: 0, resolved: 0 },
|
||||
safety_stats = { total: 0, pending: 0, resolved: 0 },
|
||||
payments = [],
|
||||
total_revenue = 0
|
||||
}) {
|
||||
// Local filter state
|
||||
const [localStartDate, setLocalStartDate] = useState(startDate);
|
||||
const [localEndDate, setLocalEndDate] = useState(endDate);
|
||||
const [localStatus, setLocalStatus] = useState(status);
|
||||
const [localSearch, setLocalSearch] = useState(search);
|
||||
const [localNationality, setLocalNationality] = useState('all');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
|
||||
// Sync with props when props change (e.g. after tab switch)
|
||||
useEffect(() => {
|
||||
setLocalStartDate(startDate);
|
||||
setLocalEndDate(endDate);
|
||||
setLocalStatus(status);
|
||||
setLocalSearch(search);
|
||||
}, [reportType, startDate, endDate, status, search]);
|
||||
// Filter payments dynamically
|
||||
const filteredPayments = payments.filter(payment => {
|
||||
const matchesSearch =
|
||||
payment.id.toString().toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
payment.user_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
payment.user_email.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
payment.description.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const handleTabChange = (type) => {
|
||||
router.get('/admin/analytics', {
|
||||
report_type: type,
|
||||
start_date: localStartDate,
|
||||
end_date: localEndDate,
|
||||
status: 'all',
|
||||
search: ''
|
||||
}, {
|
||||
preserveState: false
|
||||
});
|
||||
};
|
||||
const matchesStatus = statusFilter === 'all' || payment.status.toLowerCase() === statusFilter.toLowerCase();
|
||||
|
||||
const applyFilters = () => {
|
||||
router.get('/admin/analytics', {
|
||||
report_type: reportType,
|
||||
start_date: localStartDate,
|
||||
end_date: localEndDate,
|
||||
status: localStatus,
|
||||
search: localSearch,
|
||||
nationality: reportType === 'workers' ? localNationality : undefined
|
||||
}, {
|
||||
preserveState: true,
|
||||
replace: true
|
||||
});
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setLocalStartDate('');
|
||||
setLocalEndDate('');
|
||||
setLocalStatus('all');
|
||||
setLocalSearch('');
|
||||
setLocalNationality('all');
|
||||
|
||||
router.get('/admin/analytics', {
|
||||
report_type: reportType,
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
status: 'all',
|
||||
search: ''
|
||||
});
|
||||
};
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const handleExportCSV = () => {
|
||||
const params = new URLSearchParams({
|
||||
report_type: reportType,
|
||||
start_date: localStartDate,
|
||||
end_date: localEndDate,
|
||||
status: localStatus,
|
||||
search: localSearch,
|
||||
export: 'csv'
|
||||
});
|
||||
if (reportType === 'workers' && localNationality !== 'all') {
|
||||
params.append('nationality', localNationality);
|
||||
}
|
||||
window.location.href = `/admin/analytics?${params.toString()}`;
|
||||
};
|
||||
|
||||
// Context-aware status list for dropdown
|
||||
const getStatusOptions = () => {
|
||||
switch (reportType) {
|
||||
case 'workers':
|
||||
return [
|
||||
{ value: 'all', label: 'All Statuses' },
|
||||
{ value: 'Available', label: 'Available' },
|
||||
{ value: 'hired', label: 'Hired' },
|
||||
{ value: 'inactive', label: 'Inactive' },
|
||||
{ value: 'suspended', label: 'Suspended' },
|
||||
{ value: 'banned', label: 'Banned' }
|
||||
];
|
||||
case 'employers':
|
||||
return [
|
||||
{ value: 'all', label: 'All Subscriptions' },
|
||||
{ value: 'active', label: 'Active Pass' },
|
||||
{ value: 'inactive', label: 'Inactive Pass' }
|
||||
];
|
||||
case 'payments':
|
||||
return [
|
||||
{ value: 'all', label: 'All Payments' },
|
||||
{ value: 'success', label: 'Success' },
|
||||
{ value: 'failed', label: 'Failed' },
|
||||
{ value: 'refunded', label: 'Refunded' }
|
||||
];
|
||||
case 'tickets':
|
||||
return [
|
||||
{ value: 'all', label: 'All Statuses' },
|
||||
{ value: 'Open', label: 'Open' },
|
||||
{ value: 'In-Progress', label: 'In-Progress' },
|
||||
{ value: 'Resolved', label: 'Resolved' },
|
||||
{ value: 'Closed', label: 'Closed' }
|
||||
];
|
||||
case 'safety':
|
||||
return [
|
||||
{ value: 'all', label: 'All Statuses' },
|
||||
{ value: 'Pending', label: 'Pending' },
|
||||
{ value: 'Resolved', label: 'Resolved' }
|
||||
];
|
||||
default:
|
||||
return [{ value: 'all', label: 'All Statuses' }];
|
||||
}
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'workers', label: 'Workers List', icon: Users, desc: 'Registration & status report' },
|
||||
{ id: 'employers', label: 'Employers List', icon: Building2, desc: 'Subscription & spending report' },
|
||||
{ id: 'payments', label: 'Payments Ledger', icon: DollarSign, desc: 'Revenue & transactions report' },
|
||||
{ id: 'tickets', label: 'Support Inquiries', icon: LifeBuoy, desc: 'Inquiries & voice notes report' },
|
||||
{ id: 'safety', label: 'Safety Moderation', icon: ShieldAlert, desc: 'Moderation & flagged user reports' },
|
||||
];
|
||||
|
||||
const renderCell = (val, cellIdx, type) => {
|
||||
const lowerVal = String(val).toLowerCase();
|
||||
|
||||
// Status Badges
|
||||
if (['active', 'available', 'success', 'yes', 'approved', 'resolved'].includes(lowerVal)) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider bg-emerald-50 text-emerald-700 border border-emerald-200">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
|
||||
{val}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (['pending', 'in-progress', 'under review', 'no'].includes(lowerVal)) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider bg-amber-50 text-amber-700 border border-amber-200">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-amber-500"></span>
|
||||
{val}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (['failed', 'inactive', 'suspended', 'banned', 'closed', 'rejected'].includes(lowerVal)) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider bg-rose-50 text-rose-700 border border-rose-200">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-rose-500"></span>
|
||||
{val}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ID Formats
|
||||
if (typeof val === 'string' && (val.startsWith('WRK-') || val.startsWith('EMP-') || val.startsWith('PAY-') || val.startsWith('TCK-') || val.startsWith('REP-'))) {
|
||||
return (
|
||||
<span className="font-mono font-bold text-[#0F6E56] tracking-wider bg-teal-50 px-2.5 py-1 rounded-lg border border-teal-100">
|
||||
{val}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Email style
|
||||
if (typeof val === 'string' && val.includes('@')) {
|
||||
return <span className="font-medium text-gray-500 text-xs">{val}</span>;
|
||||
}
|
||||
|
||||
return <span className="font-bold text-gray-800">{val}</span>;
|
||||
const headers = ['Payment ID,Customer,Email,Details,Amount,Status,Date\n'];
|
||||
const rows = filteredPayments.map(p =>
|
||||
`"${p.id}","${p.user_name}","${p.user_email}","${p.description}",${p.amount},"${p.status}","${p.created_at}"`
|
||||
);
|
||||
const blob = new Blob([headers.concat(rows.join('\n'))], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.setAttribute('download', `financial_report_${new Date().toISOString().split('T')[0]}.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout title="Operational Reports Portal">
|
||||
<Head title="Operational Reports Portal" />
|
||||
<AdminLayout title="Reports">
|
||||
<Head title="Reports" />
|
||||
|
||||
<div className="max-w-7xl mx-auto space-y-6 pb-16 px-4 sm:px-6 lg:px-8">
|
||||
<div className="font-sans max-w-7xl mx-auto space-y-8 pb-12">
|
||||
|
||||
{/* Header Title & Description */}
|
||||
<div className="bg-gradient-to-r from-teal-900 via-[#0F6E56] to-emerald-800 rounded-3xl p-6 md:p-8 text-white shadow-xl relative overflow-hidden">
|
||||
<div className="absolute right-0 bottom-0 opacity-10 pointer-events-none transform translate-y-6 translate-x-6">
|
||||
<FileText className="w-80 h-80" />
|
||||
{/* Simplified Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-slate-200 pb-5">
|
||||
<div>
|
||||
<h1 className="text-xl font-black text-slate-800 uppercase tracking-tight">Platform Reports</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5 font-medium">Summary of workers, employers, disputes, and payments.</p>
|
||||
</div>
|
||||
<div className="relative z-10 space-y-2">
|
||||
<span className="text-[10px] bg-teal-500/30 border border-teal-400/20 px-3 py-1.5 rounded-full font-bold uppercase tracking-widest text-teal-200">
|
||||
Management Portal
|
||||
</span>
|
||||
<h1 className="text-2xl md:text-3xl font-black tracking-tight mt-1">Platform Reports Portal</h1>
|
||||
<p className="text-sm text-teal-100 max-w-2xl font-medium">
|
||||
Gain direct access to database operational reports, track registrations, monitor logs, analyze payment histories, and export filtered tabular datasets.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="inline-flex items-center px-3.5 py-2 bg-white border border-slate-200 text-slate-700 rounded-xl text-xs font-black uppercase tracking-wider hover:bg-slate-50 transition-colors shadow-sm gap-1.5"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportCSV}
|
||||
className="inline-flex items-center px-4 py-2 bg-[#0F6E56] hover:bg-[#085041] text-white rounded-xl text-xs font-black uppercase tracking-wider transition-all shadow-md gap-1.5"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
<span>Export CSV</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Report Module Navigation Tabs */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = reportType === tab.id;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => handleTabChange(tab.id)}
|
||||
className={`flex flex-col items-start text-left p-4 rounded-2xl border transition-all duration-300 ${
|
||||
isActive
|
||||
? 'bg-white border-[#0F6E56] shadow-lg scale-[1.01] ring-2 ring-[#0F6E56]/10'
|
||||
: 'bg-white text-gray-600 border-gray-200 hover:border-gray-300 hover:bg-gray-50/80 shadow-sm'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className={`p-2 rounded-xl ${isActive ? 'bg-teal-50 text-[#0F6E56]' : 'bg-gray-100 text-gray-500'}`}>
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<span className={`text-xs font-black uppercase tracking-wider ${isActive ? 'text-gray-900' : 'text-gray-700'}`}>
|
||||
{tab.label}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-gray-400 font-semibold mt-2 line-clamp-1">
|
||||
{tab.desc}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* Simplified Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
|
||||
{/* Filter & Table Block */}
|
||||
<div className="bg-white border border-gray-200 rounded-3xl shadow-sm overflow-hidden">
|
||||
|
||||
{/* Filter Bar */}
|
||||
<div className="p-6 border-b border-gray-100 bg-gray-50/50 space-y-4">
|
||||
<div className="flex items-center gap-2 text-xs font-bold text-gray-800 uppercase tracking-wider">
|
||||
<Filter className="w-4 h-4 text-teal-600" />
|
||||
<span>Filters & Query Parameters</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
{/* Start Date */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] text-gray-400 font-extrabold uppercase tracking-widest block">Start Date</label>
|
||||
<div className="relative">
|
||||
<Calendar className="w-3.5 h-3.5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="date"
|
||||
className="w-full bg-white border border-gray-250 rounded-xl pl-9 pr-3 py-2 text-xs font-bold focus:ring-2 focus:ring-[#0F6E56]/10 outline-none transition-all"
|
||||
value={localStartDate}
|
||||
onChange={e => setLocalStartDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Workers Card */}
|
||||
<div className="bg-white border border-slate-200 rounded-2xl p-5 shadow-sm space-y-4 hover:border-[#0F6E56]/30 transition-all">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="p-2.5 bg-teal-50 rounded-xl text-[#0F6E56]">
|
||||
<Users className="w-5 h-5" />
|
||||
</div>
|
||||
|
||||
{/* End Date */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] text-gray-400 font-extrabold uppercase tracking-widest block">End Date</label>
|
||||
<div className="relative">
|
||||
<Calendar className="w-3.5 h-3.5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="date"
|
||||
className="w-full bg-white border border-gray-250 rounded-xl pl-9 pr-3 py-2 text-xs font-bold focus:ring-2 focus:ring-[#0F6E56]/10 outline-none transition-all"
|
||||
value={localEndDate}
|
||||
onChange={e => setLocalEndDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status filter */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] text-gray-400 font-extrabold uppercase tracking-widest block">Status</label>
|
||||
<select
|
||||
className="w-full bg-white border border-gray-250 rounded-xl px-3 py-2 text-xs font-bold focus:ring-2 focus:ring-[#0F6E56]/10 outline-none cursor-pointer transition-all"
|
||||
value={localStatus}
|
||||
onChange={e => setLocalStatus(e.target.value)}
|
||||
>
|
||||
{getStatusOptions().map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Workers Tab specific filter: Nationality */}
|
||||
{reportType === 'workers' ? (
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] text-gray-400 font-extrabold uppercase tracking-widest block">Nationality</label>
|
||||
<div className="relative">
|
||||
<Globe className="w-3.5 h-3.5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<select
|
||||
className="w-full bg-white border border-gray-250 rounded-xl pl-9 pr-3 py-2 text-xs font-bold focus:ring-2 focus:ring-[#0F6E56]/10 outline-none cursor-pointer transition-all"
|
||||
value={localNationality}
|
||||
onChange={e => setLocalNationality(e.target.value)}
|
||||
>
|
||||
<option value="all">All Nationalities</option>
|
||||
{nationalities.map(n => (
|
||||
<option key={n} value={n}>{n}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="hidden lg:block"></div>
|
||||
)}
|
||||
|
||||
{/* Search Box */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] text-gray-400 font-extrabold uppercase tracking-widest block">Search Query</label>
|
||||
<div className="relative">
|
||||
<Search className="w-3.5 h-3.5 text-gray-400 absolute left-3.5 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by keywords..."
|
||||
className="w-full bg-white border border-gray-250 rounded-xl pl-9 pr-3 py-2 text-xs font-bold focus:ring-2 focus:ring-[#0F6E56]/10 outline-none transition-all placeholder-gray-400"
|
||||
value={localSearch}
|
||||
onChange={e => setLocalSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-2">
|
||||
<span className="text-[11px] text-gray-400 font-bold uppercase tracking-wider bg-gray-100 px-3 py-1.5 rounded-lg border border-gray-200/50 self-start">
|
||||
Matches found: {reportData.length} records
|
||||
<span className="text-[10px] bg-slate-105 text-[#0F6E56] font-extrabold uppercase px-2 py-0.5 rounded tracking-wide">
|
||||
Workers
|
||||
</span>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={resetFilters}
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 border border-gray-200 bg-white hover:bg-gray-50 text-gray-600 rounded-xl text-xs font-bold uppercase tracking-wider transition-colors"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={applyFilters}
|
||||
className="inline-flex items-center gap-1.5 px-5 py-2 bg-[#0F6E56] hover:bg-[#0c5845] text-white rounded-xl text-xs font-bold uppercase tracking-wider transition-colors shadow-sm shadow-teal-900/10"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
<span>Apply Filters</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleExportCSV}
|
||||
className="inline-flex items-center gap-1.5 px-5 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl text-xs font-bold uppercase tracking-wider transition-colors shadow-sm shadow-emerald-900/10"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
<span>Export CSV</span>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider block">Total Workers</span>
|
||||
<span className="text-3xl font-black text-slate-800 block mt-1 tracking-tight">{worker_stats.total}</span>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-slate-100 grid grid-cols-2 gap-2 text-[10px] font-bold text-slate-500">
|
||||
<div>
|
||||
<span className="text-slate-400 uppercase tracking-widest text-[8px] block">Verified</span>
|
||||
<span className="text-slate-700 font-extrabold text-xs">{worker_stats.verified}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 uppercase tracking-widest text-[8px] block">Active</span>
|
||||
<span className="text-slate-700 font-extrabold text-xs">{worker_stats.active}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Report Table View */}
|
||||
{/* Sponsors Card */}
|
||||
<div className="bg-white border border-slate-200 rounded-2xl p-5 shadow-sm space-y-4 hover:border-[#0F6E56]/30 transition-all">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="p-2.5 bg-blue-50 rounded-xl text-blue-600">
|
||||
<Building2 className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="text-[10px] bg-blue-50 text-blue-700 font-extrabold uppercase px-2 py-0.5 rounded tracking-wide">
|
||||
Employers
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider block">Total Employers</span>
|
||||
<span className="text-3xl font-black text-slate-800 block mt-1 tracking-tight">{sponsor_stats.total}</span>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-slate-100 grid grid-cols-2 gap-2 text-[10px] font-bold text-slate-500">
|
||||
<div>
|
||||
<span className="text-slate-400 uppercase tracking-widest text-[8px] block">Active Plan</span>
|
||||
<span className="text-slate-700 font-extrabold text-xs">{sponsor_stats.active_subscribers}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 uppercase tracking-widest text-[8px] block">Unpaid</span>
|
||||
<span className="text-slate-700 font-extrabold text-xs">{sponsor_stats.unpaid}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disputes Card */}
|
||||
<div className="bg-white border border-slate-200 rounded-2xl p-5 shadow-sm space-y-4 hover:border-[#0F6E56]/30 transition-all">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="p-2.5 bg-orange-50 rounded-xl text-orange-600">
|
||||
<ShieldAlert className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="text-[10px] bg-orange-50 text-orange-700 font-extrabold uppercase px-2 py-0.5 rounded tracking-wide">
|
||||
Disputes
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider block">Total Disputes</span>
|
||||
<span className="text-3xl font-black text-slate-800 block mt-1 tracking-tight">{dispute_stats.open + dispute_stats.under_review}</span>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-slate-100 grid grid-cols-2 gap-2 text-[10px] font-bold text-slate-500">
|
||||
<div>
|
||||
<span className="text-slate-400 uppercase tracking-widest text-[8px] block">Resolved</span>
|
||||
<span className="text-slate-700 font-extrabold text-xs">{dispute_stats.resolved}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 uppercase tracking-widest text-[8px] block">Pending Flags</span>
|
||||
<span className="text-slate-700 font-extrabold text-xs">{safety_stats.pending}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Revenue Card */}
|
||||
<div className="bg-white border border-slate-200 rounded-2xl p-5 shadow-sm space-y-4 hover:border-[#0F6E56]/30 transition-all">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="p-2.5 bg-emerald-50 rounded-xl text-emerald-600">
|
||||
<DollarSign className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="text-[10px] bg-emerald-50 text-emerald-700 font-extrabold uppercase px-2 py-0.5 rounded tracking-wide">
|
||||
Revenue
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-wider block">Total Revenue</span>
|
||||
<span className="text-3xl font-black text-slate-800 block mt-1 tracking-tight">
|
||||
AED {total_revenue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-slate-100 grid grid-cols-2 gap-2 text-[10px] font-bold text-slate-500">
|
||||
<div>
|
||||
<span className="text-slate-400 uppercase tracking-widest text-[8px] block">Total Paid</span>
|
||||
<span className="text-slate-700 font-extrabold text-xs">{payments.length}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 uppercase tracking-widest text-[8px] block">Average Amount</span>
|
||||
<span className="text-slate-700 font-extrabold text-xs">
|
||||
AED {payments.length > 0 ? (total_revenue / payments.length).toFixed(0) : '0'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Simplified Payment Ledger */}
|
||||
<div className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm space-y-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-slate-100 pb-5">
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest flex items-center gap-2">
|
||||
<CreditCard className="w-4.5 h-4.5 text-[#0F6E56]" />
|
||||
<span>Payment List</span>
|
||||
</h3>
|
||||
<p className="text-[10px] text-slate-400 font-semibold mt-0.5">Search and check payments.</p>
|
||||
</div>
|
||||
|
||||
{/* Search & Status Filters */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search payments..."
|
||||
className="bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-4 py-2 text-xs font-semibold focus:bg-white outline-none focus:ring-2 focus:ring-[#0F6E56]/10 w-64 shadow-sm"
|
||||
value={searchTerm}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
className="bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 text-xs font-semibold outline-none cursor-pointer focus:bg-white focus:ring-2 focus:ring-[#0F6E56]/10 shadow-sm"
|
||||
value={statusFilter}
|
||||
onChange={e => setStatusFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">All Payments</option>
|
||||
<option value="success">Success</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="failed">Failed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payments Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse text-left">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 bg-gray-50 text-[10px] text-gray-400 font-black uppercase tracking-widest">
|
||||
{headers.map((hdr, idx) => (
|
||||
<th key={idx} className="px-6 py-4">{hdr}</th>
|
||||
))}
|
||||
<tr className="border-b border-slate-150 text-[10px] text-slate-400 font-black uppercase tracking-widest">
|
||||
<th className="px-5 py-3">Payment ID</th>
|
||||
<th className="px-5 py-3">Customer</th>
|
||||
<th className="px-5 py-3">Details</th>
|
||||
<th className="px-5 py-3">Amount</th>
|
||||
<th className="px-5 py-3">Status</th>
|
||||
<th className="px-5 py-3">Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{reportData.map((row, idx) => (
|
||||
<tr key={idx} className="hover:bg-teal-50/10 transition-colors">
|
||||
{Object.values(row).map((val, cellIdx) => (
|
||||
<td key={cellIdx} className="px-6 py-4 text-xs">
|
||||
{renderCell(val, cellIdx, reportType)}
|
||||
</td>
|
||||
))}
|
||||
<tbody className="divide-y divide-slate-100 text-xs font-bold text-slate-700">
|
||||
{filteredPayments.map(p => (
|
||||
<tr key={p.id} className="hover:bg-slate-50/50 transition-colors">
|
||||
<td className="px-5 py-4 font-mono text-[#0F6E56]">TXN-{p.id}</td>
|
||||
<td className="px-5 py-4">
|
||||
<div>
|
||||
<span className="font-extrabold text-slate-800 block">{p.user_name}</span>
|
||||
<span className="text-[10px] text-slate-400 font-semibold block">{p.user_email}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-4 font-semibold text-slate-650">{p.description}</td>
|
||||
<td className="px-5 py-4 font-extrabold text-slate-800">
|
||||
{p.amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {p.currency}
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
<span className={`px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wide ${
|
||||
p.status === 'success' ? 'bg-emerald-50 text-emerald-700 border border-emerald-100' :
|
||||
p.status === 'pending' ? 'bg-amber-50 text-amber-700 border border-amber-100' :
|
||||
'bg-red-50 text-red-700 border border-red-100'
|
||||
}`}>
|
||||
{p.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-slate-400 font-semibold">{p.created_at}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
{reportData.length === 0 && (
|
||||
{filteredPayments.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={headers.length || 1} className="px-6 py-16 text-center">
|
||||
<div className="flex flex-col items-center justify-center space-y-2 text-gray-400">
|
||||
<FileSpreadsheet className="w-12 h-12 stroke-[1.5]" />
|
||||
<span className="font-black uppercase tracking-wider text-xs">No Records Found</span>
|
||||
<span className="text-[10px] text-gray-400 font-semibold max-w-xs leading-relaxed">
|
||||
There are no database records matching your current filter configuration.
|
||||
</span>
|
||||
</div>
|
||||
<td colSpan="6" className="p-12 text-center text-slate-400 font-bold uppercase">
|
||||
No payments found.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@ -414,6 +283,7 @@ export default function ReportsHub({
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
|
||||
@ -252,13 +252,13 @@
|
||||
Route::get('/workers', [\App\Http\Controllers\Employer\WorkerController::class, 'index'])->name('employer.workers');
|
||||
Route::get('/workers/{id}', [\App\Http\Controllers\Employer\WorkerController::class, 'show'])->name('employer.workers.show');
|
||||
Route::get('/workers/{id}/hire', function ($id) {
|
||||
$worker = \App\Models\Worker::findOrFail($id);
|
||||
$worker = \App\Models\Worker::with('category')->findOrFail($id);
|
||||
$workerData = [
|
||||
'id' => $worker->id,
|
||||
'name' => $worker->name,
|
||||
'nationality' => $worker->nationality,
|
||||
'category' => $worker->preferred_job_type ?? 'Domestic Worker',
|
||||
'salary' => (int) $worker->salary,
|
||||
'category' => $worker->category->name ?? 'General Worker',
|
||||
'salary' => (int) $worker->expected_salary,
|
||||
];
|
||||
return Inertia::render('Employer/Hiring/Confirm', ['worker' => $workerData]);
|
||||
})->name('employer.hiring.confirm');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user