507 lines
26 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { Head, router, Link } from '@inertiajs/react';
import AdminLayout from '@/Layouts/AdminLayout';
import {
Users,
Building2,
ShieldAlert,
DollarSign,
LifeBuoy,
Search,
RefreshCw,
Download,
Calendar,
Filter,
FileSpreadsheet,
Globe,
X,
FileText
} from 'lucide-react';
export default function ReportsHub({
reportType = 'workers',
startDate = '',
endDate = '',
status = 'all',
search = '',
headers = [],
reportData = {},
nationalities = [],
perPage = 10,
nationality = 'all'
}) {
// 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(nationality || 'all');
const [localPerPage, setLocalPerPage] = useState(perPage || 10);
// Sync with props when props change (e.g. after tab switch)
useEffect(() => {
setLocalStartDate(startDate);
setLocalEndDate(endDate);
setLocalStatus(status);
setLocalSearch(search);
setLocalNationality(nationality || 'all');
setLocalPerPage(perPage || 10);
}, [reportType, startDate, endDate, status, search, nationality, perPage]);
const isPaginated = reportData && typeof reportData === 'object' && !Array.isArray(reportData);
const dataList = isPaginated ? (reportData.data || []) : (reportData || []);
const totalRecords = isPaginated ? (reportData.total || 0) : dataList.length;
const handleTabChange = (type) => {
router.get('/admin/analytics', {
report_type: type,
start_date: localStartDate,
end_date: localEndDate,
status: 'all',
search: '',
per_page: localPerPage
}, {
preserveState: false
});
};
const applyFilters = () => {
router.get('/admin/analytics', {
report_type: reportType,
start_date: localStartDate,
end_date: localEndDate,
status: localStatus,
search: localSearch,
nationality: reportType === 'workers' ? localNationality : undefined,
per_page: localPerPage
}, {
preserveState: true,
replace: true
});
};
const resetFilters = () => {
setLocalStartDate('');
setLocalEndDate('');
setLocalStatus('all');
setLocalSearch('');
setLocalNationality('all');
setLocalPerPage(10);
router.get('/admin/analytics', {
report_type: reportType,
start_date: '',
end_date: '',
status: 'all',
search: '',
per_page: 10
});
};
const handlePerPageChange = (val) => {
setLocalPerPage(val);
router.get('/admin/analytics', {
report_type: reportType,
start_date: localStartDate,
end_date: localEndDate,
status: localStatus,
search: localSearch,
nationality: reportType === 'workers' ? localNationality : undefined,
per_page: val,
page: 1
}, {
preserveState: true,
replace: true
});
};
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>;
};
return (
<AdminLayout title="Operational Reports Portal">
<Head title="Operational Reports Portal" />
<div className="max-w-7xl mx-auto space-y-6 pb-16 px-4 sm:px-6 lg:px-8">
{/* 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" />
</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>
</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>
{/* 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>
</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: {totalRecords} records
</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>
</div>
{/* Report Table View */}
<div className="overflow-x-auto">
<table className="w-full border-collapse text-left">
<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>
</thead>
<tbody className="divide-y divide-gray-100">
{dataList.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>
))}
</tr>
))}
{dataList.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>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="p-5 border-t border-gray-150 flex flex-col sm:flex-row items-center justify-between gap-4 bg-gray-50/50">
<div className="flex items-center gap-4">
<span className="text-xs text-gray-500 font-bold">
{isPaginated && reportData.total > 0 ? (
`Showing ${reportData.from || 0} to ${reportData.to || 0} of ${reportData.total} records`
) : (
`Showing ${dataList.length} records`
)}
</span>
{/* Per Page Select */}
<div className="flex items-center gap-2">
<span className="text-[10px] text-gray-400 font-extrabold uppercase tracking-wider">Per Page:</span>
<select
value={localPerPage}
onChange={(e) => handlePerPageChange(Number(e.target.value))}
className="bg-white border border-gray-200 rounded-xl px-2 py-1 text-xs font-bold focus:ring-2 focus:ring-[#0F6E56]/10 outline-none cursor-pointer"
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
{isPaginated && reportData.last_page > 1 && (
<div className="flex items-center space-x-2">
<Link
href={reportData.prev_page_url || '#'}
preserveScroll
className={`px-4 py-2 bg-white border border-gray-200 rounded-xl text-xs font-bold text-gray-700 shadow-xs hover:bg-gray-50 transition-colors ${
!reportData.prev_page_url && 'opacity-50 pointer-events-none'
}`}
>
Previous
</Link>
<span className="text-xs text-gray-500 font-bold px-2">
Page {reportData.current_page} of {reportData.last_page}
</span>
<Link
href={reportData.next_page_url || '#'}
preserveScroll
className={`px-4 py-2 bg-white border border-gray-200 rounded-xl text-xs font-bold text-gray-700 shadow-xs hover:bg-gray-50 transition-colors ${
!reportData.next_page_url && 'opacity-50 pointer-events-none'
}`}
>
Next
</Link>
</div>
)}
</div>
</div>
</div>
</AdminLayout>
);
}