498 lines
30 KiB
JavaScript
498 lines
30 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,
|
|
AlertTriangle,
|
|
ShieldCheck,
|
|
Edit3,
|
|
History,
|
|
Sparkles,
|
|
CheckSquare
|
|
} from 'lucide-react';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
|
|
export default function Verifications({ verifications, status }) {
|
|
const [selectedItem, setSelectedItem] = useState(null);
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [lightboxImage, setLightboxImage] = useState(null);
|
|
|
|
// Vetting form overrides
|
|
const [ocrOverrides, setOcrOverrides] = useState({});
|
|
const [isEditingOcr, setIsEditingOcr] = useState(false);
|
|
const [rejectionReason, setRejectionReason] = useState('');
|
|
const [showRejectionForm, setShowRejectionForm] = useState(false);
|
|
|
|
// Local mock verification queue containing OCR score mismatches
|
|
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-OCR',
|
|
document_type: 'Passport & Visa Scan',
|
|
warnings: [],
|
|
document_images: [
|
|
'https://images.unsplash.com/photo-1544717305-2782549b5136?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: 103,
|
|
worker_name: 'Amina Diop',
|
|
nationality: 'Senegal',
|
|
passport_number: 'SN8765432',
|
|
processed_at: '2026-05-23 11:30',
|
|
status: 'flagged',
|
|
confidence: 58,
|
|
verification_method: 'Auto-OCR',
|
|
document_type: 'Passport Scan',
|
|
warnings: ['Blurry text region', 'Signature discrepancy flagged'],
|
|
document_images: [
|
|
'https://images.unsplash.com/photo-1580489944761-15a19d654956?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-22 16:40',
|
|
status: 'flagged',
|
|
confidence: 62,
|
|
verification_method: 'Auto-OCR',
|
|
document_type: 'Visa scan details',
|
|
warnings: ['Document expiration alert (Expires in 5 days)'],
|
|
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': '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) => {
|
|
setSelectedItem(item);
|
|
setOcrOverrides(item.ocr_data || {});
|
|
setIsEditingOcr(false);
|
|
setShowRejectionForm(false);
|
|
setRejectionReason('');
|
|
setIsDialogOpen(true);
|
|
};
|
|
|
|
const handleVerifySubmit = (action) => {
|
|
router.post(`/admin/workers/${selectedItem.id}/verify`, {
|
|
action,
|
|
rejection_reason: action === 'reject' ? rejectionReason : null,
|
|
ocr_overrides: ocrOverrides
|
|
}, {
|
|
onSuccess: () => {
|
|
setIsDialogOpen(false);
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<AdminLayout title="Vetting & OCR Review Hub">
|
|
<Head title="OCR Verifications" />
|
|
|
|
<div className="font-sans max-w-7xl mx-auto space-y-6">
|
|
|
|
{/* Header overview and status */}
|
|
<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">OCR Vetting Queue & Verification System</h1>
|
|
<p className="text-xs text-gray-500 mt-0.5">High-confidence auto-approvals, low-confidence warning audits, and inline OCR override triggers.</p>
|
|
</div>
|
|
|
|
{/* Filter Tabs */}
|
|
<div className="flex bg-slate-200/60 p-1 rounded-xl w-fit border border-slate-200 font-sans">
|
|
{[
|
|
{ label: 'All Audits', value: 'all' },
|
|
{ label: 'Auto-Approved (High Confidence)', value: 'approved' },
|
|
{ label: 'Vetting Flags (Low Confidence)', value: 'rejected' },
|
|
].map((tab) => (
|
|
<button
|
|
key={tab.value}
|
|
type="button"
|
|
onClick={() => router.get('/admin/workers/verifications', { status: tab.value })}
|
|
className={`px-4 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all ${
|
|
(status || 'all') === tab.value
|
|
? 'bg-white text-[#0F6E56] shadow-sm'
|
|
: 'text-slate-600 hover:text-slate-900'
|
|
}`}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Queue Summary widgets */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
|
|
<div>
|
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">System Auto-Approve Rate</span>
|
|
<span className="text-2xl font-black text-slate-800">92.4%</span>
|
|
</div>
|
|
<div className="w-10 h-10 bg-emerald-50 rounded-xl flex items-center justify-center">
|
|
<ShieldCheck className="w-5 h-5 text-emerald-600" />
|
|
</div>
|
|
</div>
|
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
|
|
<div>
|
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">OCR Average Confidence Score</span>
|
|
<span className="text-2xl font-black text-blue-600">91.8%</span>
|
|
</div>
|
|
<div className="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center">
|
|
<Sparkles className="w-5 h-5 text-blue-600" />
|
|
</div>
|
|
</div>
|
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
|
|
<div>
|
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">Vetting Queue Flags</span>
|
|
<span className="text-2xl font-black text-amber-600">2 Pending review</span>
|
|
</div>
|
|
<div className="w-10 h-10 bg-amber-50 rounded-xl flex items-center justify-center">
|
|
<AlertTriangle className="w-5 h-5 text-amber-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Verification Queue Datagrid */}
|
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr className="bg-slate-50/70 border-b border-slate-100 text-[10px] font-black text-slate-500 uppercase tracking-widest">
|
|
<th className="py-4 px-6">Worker Name</th>
|
|
<th className="py-4 px-6">Passport ID</th>
|
|
<th className="py-4 px-6">OCR Confidence Score</th>
|
|
<th className="py-4 px-6">Vetting Warnings</th>
|
|
<th className="py-4 px-6">Processed Date</th>
|
|
<th className="py-4 px-6 text-right">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100 text-xs font-bold text-slate-700">
|
|
{activeList.map((item) => (
|
|
<tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
|
|
<td className="py-4 px-6">
|
|
<div>
|
|
<div className="text-sm font-black text-slate-900">{item.worker_name}</div>
|
|
<div className="text-[10px] text-slate-400 font-semibold">{item.nationality} • {item.document_type}</div>
|
|
</div>
|
|
</td>
|
|
<td className="py-4 px-6 font-mono font-medium">{item.passport_number}</td>
|
|
<td className="py-4 px-6">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-24 bg-slate-100 rounded-full h-2.5 overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full ${
|
|
item.confidence > 90 ? 'bg-emerald-500' :
|
|
item.confidence > 70 ? 'bg-blue-500' : 'bg-amber-500'
|
|
}`}
|
|
style={{ width: `${item.confidence}%` }}
|
|
/>
|
|
</div>
|
|
<span className={`text-[10px] font-black ${
|
|
item.confidence > 90 ? 'text-emerald-600' :
|
|
item.confidence > 70 ? 'text-blue-600' : 'text-amber-600'
|
|
}`}>{item.confidence}%</span>
|
|
</div>
|
|
</td>
|
|
<td className="py-4 px-6">
|
|
{item.warnings.length > 0 ? (
|
|
<div className="space-y-1">
|
|
{item.warnings.map((w, idx) => (
|
|
<span key={idx} className="inline-flex items-center bg-red-50 text-red-700 text-[9px] px-2 py-0.5 rounded-full uppercase">
|
|
<AlertTriangle className="w-2.5 h-2.5 mr-1" /> {w}
|
|
</span>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<span className="text-[10px] font-black text-emerald-600 uppercase">None (Safe)</span>
|
|
)}
|
|
</td>
|
|
<td className="py-4 px-6 text-slate-400 font-semibold">{item.processed_at}</td>
|
|
<td className="py-4 px-6 text-right">
|
|
<button
|
|
type="button"
|
|
onClick={() => openReview(item)}
|
|
className={`inline-flex items-center px-4 py-2 border rounded-xl text-[10px] font-black uppercase tracking-wider transition-colors focus:outline-none ${
|
|
item.status === 'flagged'
|
|
? 'bg-amber-500 text-white border-none shadow-md shadow-amber-500/20 hover:bg-amber-600'
|
|
: 'bg-white hover:bg-slate-50 text-slate-700 border-slate-200'
|
|
}`}
|
|
>
|
|
{item.status === 'flagged' ? 'Verify OCR' : 'Audit Log'}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Audit logs details grid */}
|
|
<div className="bg-slate-50 rounded-2xl p-5 border border-slate-200">
|
|
<h3 className="text-xs font-black text-slate-800 uppercase tracking-widest flex items-center gap-1.5 mb-3">
|
|
<History className="w-4 h-4 text-slate-600" />
|
|
<span>OCR Vetting Action Audit Logs</span>
|
|
</h3>
|
|
<div className="space-y-2">
|
|
{[
|
|
{ time: '2026-05-23 15:20', text: 'Admin Vetting Officer manually approved passport scan verification for Leila Bekri', ip: '192.168.1.5' },
|
|
{ time: '2026-05-23 14:15', text: 'System OCR Passport auto-verification completed successfully for Fatima Zahra', ip: 'Auto-System' },
|
|
{ time: '2026-05-23 13:42', text: 'System flagged passport scan of Grace Omondi due to blurred signature signature check failure', ip: 'Auto-System' },
|
|
].map((log, idx) => (
|
|
<div key={idx} className="bg-white p-3 rounded-xl border border-slate-100 flex items-center justify-between text-xs font-bold text-slate-700 shadow-sm">
|
|
<div className="flex items-center space-x-3">
|
|
<Clock className="w-4 h-4 text-slate-400" />
|
|
<span>{log.text}</span>
|
|
</div>
|
|
<span className="text-[10px] text-slate-400">{log.time} • IP: {log.ip}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Vetting Vetting Audit Dialog */}
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogContent className="sm:max-w-4xl w-full bg-white p-0 overflow-hidden font-sans rounded-[24px] border-none shadow-2xl max-h-[90vh] flex flex-col">
|
|
<DialogHeader className="p-6 border-b border-slate-100 bg-[#0f6e56]/5 flex-shrink-0">
|
|
<DialogTitle className="text-lg font-black text-slate-800 flex items-center justify-between tracking-tight">
|
|
<span className="flex items-center gap-2">
|
|
<ShieldCheck className="w-5 h-5 text-[#0F6E56]" />
|
|
<span>Vetting Manual Audit: <span className="text-[#0F6E56]">{selectedItem?.worker_name}</span></span>
|
|
</span>
|
|
<div className="flex items-center space-x-2">
|
|
<Badge className={`border-none px-3.5 py-1.5 font-black text-[9px] uppercase tracking-wider rounded-lg shadow-sm flex items-center gap-1 ${
|
|
selectedItem?.confidence > 80
|
|
? 'bg-emerald-500 text-white shadow-emerald-500/20'
|
|
: 'bg-amber-500 text-white shadow-amber-500/20'
|
|
}`}>
|
|
<Sparkles className="w-3.5 h-3.5 animate-pulse" />
|
|
<span>OCR Confidence: {selectedItem?.confidence}%</span>
|
|
</Badge>
|
|
</div>
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 p-6 overflow-y-auto flex-1 bg-slate-50/30">
|
|
|
|
{/* Scanned proof & Warnings Column */}
|
|
<div className="space-y-5">
|
|
<div className="bg-white p-4 rounded-2xl border border-slate-200 shadow-sm space-y-3">
|
|
<h3 className="text-xs font-black text-slate-700 uppercase tracking-widest flex items-center gap-1.5">
|
|
<FileText className="w-4 h-4 text-[#0F6E56]" />
|
|
<span>Scanned Passport Scan Document</span>
|
|
</h3>
|
|
{selectedItem?.document_type?.toLowerCase().includes('emirates') || selectedItem?.document_type?.toLowerCase().includes('eid') || selectedItem?.document_type?.toLowerCase().includes('id') ? (
|
|
<div className="relative rounded-xl overflow-hidden border border-slate-200 bg-slate-950 flex flex-col items-center justify-center p-6 text-center aspect-[4/3] max-h-72 shadow-inner">
|
|
<ShieldCheck className="w-12 h-12 text-emerald-500 mb-3 animate-pulse" />
|
|
<div className="text-xs font-black text-white uppercase tracking-wider">Raw Scan Deleted (PDPL Law)</div>
|
|
<div className="text-[10px] text-slate-400 mt-2 font-medium leading-relaxed">
|
|
To comply strictly with UAE Personal Data Protection Law (PDPL), raw Emirates ID images are immediately destroyed post-OCR. Vetted metadata has been securely preserved.
|
|
</div>
|
|
<div className="mt-4 bg-emerald-600 text-white font-black text-[8px] tracking-wider uppercase border-none px-2.5 py-1 rounded">
|
|
[DELETED_FOR_PDPL_COMPLIANCE]
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div
|
|
onClick={() => setLightboxImage(selectedItem?.document_images[0])}
|
|
className="relative rounded-xl overflow-hidden border border-slate-200 bg-slate-100 cursor-zoom-in shadow-inner aspect-[4/3] max-h-72 group"
|
|
>
|
|
<img
|
|
src={selectedItem?.document_images[0]}
|
|
alt="Verification Scan"
|
|
className="w-full h-full object-cover group-hover:scale-102 transition-transform duration-300"
|
|
/>
|
|
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center text-white text-xs font-black uppercase tracking-wider backdrop-blur-[1px]">
|
|
<ZoomIn className="w-4.5 h-4.5 mr-1" /> Click to Zoom Document
|
|
</div>
|
|
</div>
|
|
)}
|
|
<p className="text-[10px] text-slate-400 font-bold text-center uppercase tracking-wide">
|
|
Type: {selectedItem?.document_type}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Warnings Box */}
|
|
{selectedItem?.warnings?.length > 0 ? (
|
|
<div className="bg-rose-50/50 border border-rose-100 p-5 rounded-2xl space-y-3 shadow-sm">
|
|
<span className="text-[10px] font-black text-rose-700 uppercase tracking-widest flex items-center gap-1.5">
|
|
<AlertTriangle className="w-4 h-4 text-rose-600 animate-bounce" />
|
|
<span>Suspicious Document Vetting Warning</span>
|
|
</span>
|
|
<ul className="space-y-2 text-xs font-bold text-rose-950">
|
|
{selectedItem.warnings.map((w, idx) => (
|
|
<li key={idx} className="flex items-center gap-2 bg-white px-3 py-2 rounded-xl border border-rose-100 shadow-xs">
|
|
<div className="w-1.5 h-1.5 bg-rose-500 rounded-full flex-shrink-0" />
|
|
<span>{w}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
) : (
|
|
<div className="bg-emerald-50/30 border border-emerald-100 p-4 rounded-2xl flex items-center space-x-2 text-emerald-800 text-xs font-bold">
|
|
<CheckCircle className="w-4 h-4 text-emerald-600" />
|
|
<span>Vetting passed checks. System matches signature algorithms perfectly.</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* OCR Data Extraction Form Column - Supports editing */}
|
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-6 flex flex-col justify-between">
|
|
<div className="space-y-4">
|
|
<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">Extracted Text Vitals</h3>
|
|
<button
|
|
onClick={() => setIsEditingOcr(!isEditingOcr)}
|
|
className="text-xs text-[#0F6E56] hover:text-[#085041] font-black flex items-center gap-1 uppercase tracking-wider text-[10px]"
|
|
>
|
|
<Edit3 className="w-3.5 h-3.5" />
|
|
<span>{isEditingOcr ? 'Cancel Edit' : 'Edit OCR Values'}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{Object.entries(ocrOverrides).map(([key, value]) => (
|
|
<div key={key} className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest block">{key}</label>
|
|
{isEditingOcr ? (
|
|
<input
|
|
type="text"
|
|
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs font-bold focus:bg-white outline-none focus:ring-4 focus:ring-[#0F6E56]/10 focus:border-[#0F6E56] transition-all"
|
|
value={value}
|
|
onChange={e => {
|
|
const newVal = e.target.value;
|
|
setOcrOverrides(prev => ({ ...prev, [key]: newVal }));
|
|
}}
|
|
/>
|
|
) : (
|
|
<div className="px-3.5 py-2.5 bg-slate-50 rounded-xl text-xs font-bold text-slate-800 border border-slate-100/50 font-mono tracking-wide">
|
|
{value}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Manual Rejection reason input */}
|
|
{showRejectionForm ? (
|
|
<div className="bg-rose-50/30 p-4 border border-rose-100 rounded-xl space-y-3">
|
|
<label className="text-[10px] font-black text-rose-700 uppercase tracking-widest block">Rejection compliance reason</label>
|
|
<textarea
|
|
rows="2"
|
|
className="w-full bg-white border border-rose-200 rounded-xl p-3 text-xs font-bold focus:ring-4 focus:ring-rose-500/10 outline-none transition-all"
|
|
placeholder="Explain to worker what document error occurred (e.g., expiry date mismatch, blurred image)..."
|
|
value={rejectionReason}
|
|
onChange={e => setRejectionReason(e.target.value)}
|
|
/>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => handleVerifySubmit('reject')}
|
|
className="px-4 py-2.5 bg-red-600 hover:bg-red-700 text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md transition-colors"
|
|
>
|
|
Confirm Rejection
|
|
</button>
|
|
<button
|
|
onClick={() => setShowRejectionForm(false)}
|
|
className="px-4 py-2.5 bg-slate-200 text-slate-600 hover:bg-slate-300 rounded-xl text-[10px] font-black uppercase tracking-widest transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex gap-3 pt-4 border-t border-slate-100 flex-shrink-0">
|
|
<button
|
|
onClick={() => handleVerifySubmit('approve')}
|
|
className="flex-1 py-3 bg-[#0F6E56] text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-lg shadow-teal-700/20 hover:bg-[#085041] transition-all flex items-center justify-center gap-1"
|
|
>
|
|
<CheckCircle className="w-4 h-4" />
|
|
<span>Approve Verification</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setShowRejectionForm(true)}
|
|
className="flex-1 py-3 bg-red-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-lg shadow-red-600/10 hover:bg-red-700 transition-all flex items-center justify-center gap-1"
|
|
>
|
|
<XCircle className="w-4 h-4" />
|
|
<span>Flag Rejection</span>
|
|
</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"
|
|
>
|
|
<button
|
|
type="button"
|
|
className="absolute top-6 right-6 text-white/80 hover:text-white p-2 bg-white/10 rounded-full transition-all focus:outline-none"
|
|
>
|
|
<X className="w-6 h-6" />
|
|
</button>
|
|
<img
|
|
src={lightboxImage}
|
|
alt="Zoomed scan"
|
|
className="max-w-full max-h-[90vh] object-contain rounded-xl shadow-2xl border border-white/20"
|
|
/>
|
|
</div>
|
|
)}
|
|
</AdminLayout>
|
|
);
|
|
}
|