306 lines
17 KiB
JavaScript
306 lines
17 KiB
JavaScript
import React, { useState } from 'react';
|
||
import { Head, router } from '@inertiajs/react';
|
||
import AdminLayout from '@/Layouts/AdminLayout';
|
||
import {
|
||
Search,
|
||
FileText,
|
||
Eye,
|
||
CheckCircle,
|
||
XCircle,
|
||
Clock,
|
||
Edit2,
|
||
ZoomIn,
|
||
X,
|
||
Loader2
|
||
} from 'lucide-react';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from '@/components/ui/dialog';
|
||
import {
|
||
Pagination,
|
||
PaginationContent,
|
||
PaginationItem,
|
||
PaginationLink,
|
||
} from '@/components/ui/pagination';
|
||
|
||
export default function Verifications({ verifications, status }) {
|
||
const [selectedItem, setSelectedItem] = useState(null);
|
||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||
const [lightboxImage, setLightboxImage] = useState(null);
|
||
|
||
// Dialog state
|
||
const [ocrOverrides, setOcrOverrides] = useState({});
|
||
const [editingField, setEditingField] = useState(null);
|
||
const [rejectionReason, setRejectionReason] = useState('');
|
||
const [showRejectionInput, setShowRejectionInput] = useState(false);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
|
||
const tabs = [
|
||
{ label: 'All', value: 'all' },
|
||
{ label: 'Auto-Approved', value: 'approved' },
|
||
{ label: 'Flagged/Manual', value: 'rejected' },
|
||
];
|
||
|
||
const handleTabClick = (tabValue) => {
|
||
router.get('/admin/workers/verifications', { status: tabValue }, { preserveState: true });
|
||
};
|
||
|
||
const openReview = (item) => {
|
||
setSelectedItem(item);
|
||
setOcrOverrides(item.ocr_data || {});
|
||
setEditingField(null);
|
||
setShowRejectionInput(false);
|
||
setRejectionReason('');
|
||
setIsDialogOpen(true);
|
||
};
|
||
|
||
return (
|
||
<AdminLayout title="Worker Identity Verifications">
|
||
<Head title="Worker Verifications - Admin Portal" />
|
||
|
||
<div className="font-sans max-w-7xl mx-auto space-y-6">
|
||
{/* Header / Sub-title */}
|
||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||
<div>
|
||
<h1 className="text-xl font-bold text-gray-900 tracking-tight">Identity Audit Logs</h1>
|
||
<p className="text-xs text-gray-500 mt-0.5">Automated document verification system logs and OCR extractions.</p>
|
||
</div>
|
||
|
||
{/* Filter Tabs */}
|
||
<div className="flex bg-slate-200/60 p-1 rounded-xl w-fit border border-slate-200">
|
||
{tabs.map((tab) => (
|
||
<button
|
||
key={tab.value}
|
||
type="button"
|
||
onClick={() => handleTabClick(tab.value)}
|
||
className={`px-4 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||
status === tab.value
|
||
? 'bg-white text-[#0F6E56] shadow-sm'
|
||
: 'text-slate-600 hover:text-slate-900'
|
||
}`}
|
||
>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Desktop Table / Mobile Card List */}
|
||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
||
<div className="hidden sm:block overflow-x-auto">
|
||
<table className="w-full text-left border-collapse">
|
||
<thead>
|
||
<tr className="bg-slate-50/70 border-b border-gray-100 text-xs font-semibold text-slate-500">
|
||
<th className="py-3.5 px-5">Worker Name</th>
|
||
<th className="py-3.5 px-5">Nationality</th>
|
||
<th className="py-3.5 px-5">Passport No.</th>
|
||
<th className="py-3.5 px-5">Processed</th>
|
||
<th className="py-3.5 px-5">Status</th>
|
||
<th className="py-3.5 px-5 text-right">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-100 text-sm">
|
||
{verifications?.data?.map((item) => (
|
||
<tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
|
||
<td className="py-4 px-5 font-medium text-gray-900">{item.worker_name}</td>
|
||
<td className="py-4 px-5 text-gray-600">{item.nationality}</td>
|
||
<td className="py-4 px-5 font-mono text-xs text-gray-600">{item.passport_number}</td>
|
||
<td className="py-4 px-5 text-xs text-gray-500">{item.processed_at}</td>
|
||
<td className="py-4 px-5">
|
||
<Badge variant="outline" className="bg-emerald-50 text-emerald-700 border-emerald-200 px-2.5 py-0.5 rounded-full font-medium text-xs">
|
||
<CheckCircle className="w-3 h-3 mr-1 inline" /> Auto-Verified
|
||
</Badge>
|
||
</td>
|
||
<td className="py-4 px-5 text-right">
|
||
<button
|
||
type="button"
|
||
onClick={() => openReview(item)}
|
||
className="inline-flex items-center px-3 py-1.5 border border-slate-200 hover:border-[#0F6E56] text-xs font-semibold text-slate-700 hover:text-[#0F6E56] bg-white hover:bg-teal-50/30 rounded-lg transition-colors focus:outline-none"
|
||
>
|
||
<Eye className="w-3.5 h-3.5 mr-1.5" /> View Log
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Mobile Card List */}
|
||
<div className="block sm:hidden divide-y divide-gray-100">
|
||
{verifications?.data?.map((item) => (
|
||
<div key={item.id} className="p-4 space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<span className="font-semibold text-gray-900">{item.worker_name}</span>
|
||
<Badge className="bg-emerald-50 text-emerald-700 border-emerald-200">Auto-Verified</Badge>
|
||
</div>
|
||
<div className="grid grid-cols-2 text-xs text-gray-600 gap-1">
|
||
<div><span className="text-gray-400">Nationality:</span> {item.nationality}</div>
|
||
<div><span className="text-gray-400">Passport:</span> {item.passport_number}</div>
|
||
<div className="col-span-2 text-gray-400 text-[10px] mt-1">Processed: {item.processed_at}</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => openReview(item)}
|
||
className="w-full inline-flex items-center justify-center px-3 py-2 border border-slate-200 text-xs font-semibold text-slate-700 hover:text-[#0F6E56] bg-slate-50 hover:bg-teal-50 rounded-lg transition-colors"
|
||
>
|
||
<Eye className="w-3.5 h-3.5 mr-1.5" /> View Audit Details
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{verifications?.data?.length === 0 && (
|
||
<div className="p-12 text-center text-gray-500 text-sm font-medium">
|
||
No verification logs found for the selected filter.
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Pagination Component */}
|
||
{verifications?.links && verifications.links.length > 3 && (
|
||
<div className="flex justify-center pt-4">
|
||
<Pagination>
|
||
<PaginationContent>
|
||
{verifications.links.map((link, idx) => {
|
||
const isPrev = link.label.includes('Previous');
|
||
const isNext = link.label.includes('Next');
|
||
const label = isPrev ? '‹' : isNext ? '›' : link.label;
|
||
|
||
if (!link.url) {
|
||
return (
|
||
<PaginationItem key={idx}>
|
||
<span className="px-3 py-2 text-xs text-slate-300 cursor-not-allowed select-none">
|
||
{label}
|
||
</span>
|
||
</PaginationItem>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<PaginationItem key={idx}>
|
||
<PaginationLink
|
||
href={link.url}
|
||
isActive={link.active}
|
||
className={`text-xs px-3 py-2 rounded-lg transition-colors ${
|
||
link.active ? 'bg-[#0F6E56] text-white hover:bg-[#085041] hover:text-white font-bold' : 'text-slate-600 hover:bg-slate-100'
|
||
}`}
|
||
>
|
||
{label}
|
||
</PaginationLink>
|
||
</PaginationItem>
|
||
);
|
||
})}
|
||
</PaginationContent>
|
||
</Pagination>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Review Dialog */}
|
||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||
<DialogContent className="max-w-4xl bg-white p-0 overflow-hidden font-sans rounded-2xl">
|
||
<DialogHeader className="p-6 pb-4 border-b border-slate-100 bg-slate-50/50">
|
||
<DialogTitle className="text-lg font-semibold text-gray-800 flex items-center justify-between tracking-tight">
|
||
<span>Verification Log: <span className="text-[#0F6E56]">{selectedItem?.worker_name}</span></span>
|
||
<Badge className="bg-emerald-100 text-emerald-800 border-none px-3 py-1">System Verified</Badge>
|
||
</DialogTitle>
|
||
</DialogHeader>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6 max-h-[80vh] overflow-y-auto">
|
||
{/* Left: Document Images */}
|
||
<div className="space-y-4">
|
||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider flex items-center">
|
||
<FileText className="w-3.5 h-3.5 mr-1.5" /> Scanned Proof Documents ({selectedItem?.document_images?.length || 0})
|
||
</h3>
|
||
<div className="grid grid-cols-1 gap-4">
|
||
{selectedItem?.document_images?.map((imgUrl, i) => (
|
||
<div
|
||
key={i}
|
||
onClick={() => setLightboxImage(imgUrl)}
|
||
className="relative group rounded-xl overflow-hidden border border-slate-200 bg-slate-100 cursor-pointer shadow-sm hover:ring-2 hover:ring-[#0F6E56]/40 transition-all aspect-[4/3]"
|
||
>
|
||
<img
|
||
src={imgUrl}
|
||
alt="Document scan"
|
||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||
/>
|
||
<div className="absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center text-white text-xs font-semibold backdrop-blur-[1px]">
|
||
<ZoomIn className="w-5 h-5 mr-1.5" /> Click to View
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right: OCR Data & Status */}
|
||
<div className="space-y-6 flex flex-col justify-between">
|
||
<div className="space-y-4">
|
||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider flex items-center justify-between">
|
||
<span>System Extracted Data</span>
|
||
<span className="text-[10px] text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-full font-medium">Confidence: 98%</span>
|
||
</h3>
|
||
|
||
<div className="space-y-2.5">
|
||
{Object.entries(ocrOverrides).map(([key, value]) => (
|
||
<div key={key} className="flex items-center justify-between p-3 bg-slate-50 rounded-xl text-sm border border-slate-100 hover:border-slate-200 transition-colors">
|
||
<span className="font-medium text-slate-600 text-xs">{key}:</span>
|
||
<span className="text-gray-900 font-semibold text-xs font-mono">{value}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Status Info */}
|
||
<div className="space-y-4 pt-4 border-t border-slate-100">
|
||
<div className="bg-emerald-50 p-4 rounded-xl border border-emerald-100 flex items-start">
|
||
<CheckCircle className="w-5 h-5 text-emerald-600 mr-3 mt-0.5" />
|
||
<div>
|
||
<p className="text-xs font-bold text-emerald-800">Verified Automatically</p>
|
||
<p className="text-[11px] text-emerald-700 mt-1">
|
||
This profile was verified by the system on {selectedItem?.processed_at}. No further action is required from administrators.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsDialogOpen(false)}
|
||
className="w-full bg-slate-900 hover:bg-slate-800 text-white py-3 px-4 rounded-xl font-semibold text-xs shadow-sm flex items-center justify-center transition-colors"
|
||
>
|
||
Close Audit Log
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Lightbox Zoom */}
|
||
{lightboxImage && (
|
||
<div
|
||
onClick={() => setLightboxImage(null)}
|
||
className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 animate-in fade-in duration-200"
|
||
>
|
||
<button
|
||
type="button"
|
||
className="absolute top-6 right-6 text-white/80 hover:text-white p-2 bg-white/10 hover:bg-white/20 rounded-full transition-all focus:outline-none"
|
||
onClick={() => setLightboxImage(null)}
|
||
>
|
||
<X className="w-6 h-6" />
|
||
</button>
|
||
<img
|
||
src={lightboxImage}
|
||
alt="Zoomed document"
|
||
className="max-w-full max-h-[90vh] object-contain rounded-xl shadow-2xl border border-white/20 animate-in zoom-in-95 duration-200"
|
||
/>
|
||
</div>
|
||
)}
|
||
</AdminLayout>
|
||
);
|
||
}
|