291 lines
18 KiB
JavaScript
291 lines
18 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { Head } from '@inertiajs/react';
|
|
import AdminLayout from '@/Layouts/AdminLayout';
|
|
import {
|
|
FileText,
|
|
Users,
|
|
ShieldAlert,
|
|
CreditCard,
|
|
Search,
|
|
RefreshCw,
|
|
Download,
|
|
Building2,
|
|
DollarSign
|
|
} from 'lucide-react';
|
|
|
|
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
|
|
}) {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
|
|
// 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 matchesStatus = statusFilter === 'all' || payment.status.toLowerCase() === statusFilter.toLowerCase();
|
|
|
|
return matchesSearch && matchesStatus;
|
|
});
|
|
|
|
const handleExportCSV = () => {
|
|
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="Reports">
|
|
<Head title="Reports" />
|
|
|
|
<div className="font-sans max-w-7xl mx-auto space-y-8 pb-12">
|
|
|
|
{/* 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="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>
|
|
|
|
{/* Simplified Summary Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
|
|
{/* 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>
|
|
<span className="text-[10px] bg-slate-105 text-[#0F6E56] font-extrabold uppercase px-2 py-0.5 rounded tracking-wide">
|
|
Workers
|
|
</span>
|
|
</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>
|
|
|
|
{/* 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 text-left border-collapse">
|
|
<thead>
|
|
<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-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>
|
|
))}
|
|
|
|
{filteredPayments.length === 0 && (
|
|
<tr>
|
|
<td colSpan="6" className="p-12 text-center text-slate-400 font-bold uppercase">
|
|
No payments found.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</AdminLayout>
|
|
);
|
|
}
|