dashboard api for employer
This commit is contained in:
parent
b2719888a2
commit
3d97fb2e29
@ -92,7 +92,10 @@ private function formatWorker(Worker $w)
|
|||||||
'documents' => $w->documents->map(function ($doc) {
|
'documents' => $w->documents->map(function ($doc) {
|
||||||
$ocrData = $doc->ocr_data;
|
$ocrData = $doc->ocr_data;
|
||||||
if ($doc->type === 'visa' && is_array($ocrData)) {
|
if ($doc->type === 'visa' && is_array($ocrData)) {
|
||||||
unset($ocrData['sponsor']);
|
unset($ocrData['file_number']);
|
||||||
|
unset($ocrData['name']);
|
||||||
|
unset($ocrData['passport_number']);
|
||||||
|
unset($ocrData['accompanied_by']);
|
||||||
unset($ocrData['valid_until']);
|
unset($ocrData['valid_until']);
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
@ -1000,7 +1003,10 @@ public function getWorkerDetail(Request $request, $id)
|
|||||||
'documents' => $w->documents->map(function ($doc) {
|
'documents' => $w->documents->map(function ($doc) {
|
||||||
$ocrData = $doc->ocr_data;
|
$ocrData = $doc->ocr_data;
|
||||||
if ($doc->type === 'visa' && is_array($ocrData)) {
|
if ($doc->type === 'visa' && is_array($ocrData)) {
|
||||||
unset($ocrData['sponsor']);
|
unset($ocrData['file_number']);
|
||||||
|
unset($ocrData['name']);
|
||||||
|
unset($ocrData['passport_number']);
|
||||||
|
unset($ocrData['accompanied_by']);
|
||||||
unset($ocrData['valid_until']);
|
unset($ocrData['valid_until']);
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -390,6 +390,14 @@ public function show($id)
|
|||||||
'in_country' => (bool) $w->in_country,
|
'in_country' => (bool) $w->in_country,
|
||||||
'preferred_location' => $w->preferred_location,
|
'preferred_location' => $w->preferred_location,
|
||||||
'documents' => $w->documents->map(function ($doc) {
|
'documents' => $w->documents->map(function ($doc) {
|
||||||
|
$ocrData = $doc->ocr_data;
|
||||||
|
if ($doc->type === 'visa' && is_array($ocrData)) {
|
||||||
|
unset($ocrData['file_number']);
|
||||||
|
unset($ocrData['name']);
|
||||||
|
unset($ocrData['passport_number']);
|
||||||
|
unset($ocrData['accompanied_by']);
|
||||||
|
unset($ocrData['valid_until']);
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
'id' => $doc->id,
|
'id' => $doc->id,
|
||||||
'type' => $doc->type,
|
'type' => $doc->type,
|
||||||
@ -398,7 +406,7 @@ public function show($id)
|
|||||||
'expiry_date' => $doc->expiry_date,
|
'expiry_date' => $doc->expiry_date,
|
||||||
'ocr_accuracy' => $doc->ocr_accuracy,
|
'ocr_accuracy' => $doc->ocr_accuracy,
|
||||||
'file_path' => $doc->file_path ? url($doc->file_path) : null,
|
'file_path' => $doc->file_path ? url($doc->file_path) : null,
|
||||||
'ocr_data' => $doc->ocr_data,
|
'ocr_data' => $ocrData,
|
||||||
];
|
];
|
||||||
})->toArray(),
|
})->toArray(),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -123,23 +123,7 @@ export default function Shortlist({ shortlistedWorkers }) {
|
|||||||
|
|
||||||
{/* Passport Status Verification Badge & Visa Status */}
|
{/* Passport Status Verification Badge & Visa Status */}
|
||||||
<div className="flex flex-wrap gap-1 mt-0.5">
|
<div className="flex flex-wrap gap-1 mt-0.5">
|
||||||
{worker.visa_expiry_date ? (
|
{/* Removed visa expiry warning badge for cleaner UI */}
|
||||||
<div className={`flex items-center space-x-1 text-[9px] font-black uppercase px-1.5 py-0.5 rounded border ${
|
|
||||||
worker.document_expiry_days !== null && worker.document_expiry_days <= 30
|
|
||||||
? 'text-rose-700 bg-rose-50 border-rose-200 animate-pulse'
|
|
||||||
: worker.document_expiry_days !== null && worker.document_expiry_days <= 90
|
|
||||||
? 'text-amber-700 bg-amber-50 border-amber-200'
|
|
||||||
: 'text-[#185FA5] bg-blue-50 border-blue-100'
|
|
||||||
}`}>
|
|
||||||
<Calendar className="w-3 h-3 flex-shrink-0" />
|
|
||||||
<span>Visa Exp: {worker.visa_expiry_date}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
|
||||||
<Calendar className="w-3 h-3 flex-shrink-0 text-amber-600" />
|
|
||||||
<span>Visa Expiry Pending</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
|
{worker.visa_status && (worker.visa_status.toLowerCase().includes('tourist') || worker.visa_status.toLowerCase().includes('visit')) ? (
|
||||||
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
<div className="flex items-center space-x-1 text-[9px] font-black uppercase text-amber-700 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
||||||
<AlertTriangle className="w-3 h-3 text-amber-600 flex-shrink-0 animate-bounce" />
|
<AlertTriangle className="w-3 h-3 text-amber-600 flex-shrink-0 animate-bounce" />
|
||||||
|
|||||||
@ -704,17 +704,7 @@ export default function Index({ initialWorkers = [], initialShortlistedIds = [],
|
|||||||
<span>{worker.visa_status}</span>
|
<span>{worker.visa_status}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{worker.document_expiry_status && (
|
{/* Removed visa expiry status badge for cleaner UI */}
|
||||||
<div className={`flex items-center space-x-1 text-[9px] font-black uppercase px-1.5 py-0.5 rounded border ${
|
|
||||||
worker.document_expiry_days !== null && worker.document_expiry_days <= 30
|
|
||||||
? 'text-rose-700 bg-rose-50 border-rose-200 animate-pulse'
|
|
||||||
: worker.document_expiry_days !== null && worker.document_expiry_days <= 90
|
|
||||||
? 'text-amber-700 bg-amber-50 border-amber-200'
|
|
||||||
: 'text-[#185FA5] bg-blue-50 border-blue-100'
|
|
||||||
}`}>
|
|
||||||
<span>📅 {worker.document_expiry_status}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-1.5 text-xs text-slate-500 mt-1">
|
<div className="flex items-center space-x-1.5 text-xs text-slate-500 mt-1">
|
||||||
|
|||||||
@ -54,7 +54,7 @@ const getExpiryStatus = (expiryDate) => {
|
|||||||
|
|
||||||
if (diffDays < 0) {
|
if (diffDays < 0) {
|
||||||
return {
|
return {
|
||||||
text: `Expired \${Math.abs(diffDays)} days ago`,
|
text: 'Expired',
|
||||||
color: 'text-rose-700 bg-rose-50 border-rose-200 font-black'
|
color: 'text-rose-700 bg-rose-50 border-rose-200 font-black'
|
||||||
};
|
};
|
||||||
} else if (diffDays === 0) {
|
} else if (diffDays === 0) {
|
||||||
@ -64,7 +64,7 @@ const getExpiryStatus = (expiryDate) => {
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
text: `Valid until \${expiryDate} (Expires in \${diffDays} days)`,
|
text: `Valid until ${expiryDate}`,
|
||||||
color: 'text-emerald-700 bg-emerald-50 border-emerald-150 font-black'
|
color: 'text-emerald-700 bg-emerald-50 border-emerald-150 font-black'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -83,6 +83,7 @@ const safeRender = (value) => {
|
|||||||
export default function Show({ worker }) {
|
export default function Show({ worker }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const passportDoc = worker.documents?.find(d => d.type === 'passport');
|
const passportDoc = worker.documents?.find(d => d.type === 'passport');
|
||||||
|
const emiratesIdDoc = worker.documents?.find(d => d.type === 'emirates_id' || d.type === 'eid');
|
||||||
const visaDoc = worker.documents?.find(d => d.type === 'visa');
|
const visaDoc = worker.documents?.find(d => d.type === 'visa');
|
||||||
|
|
||||||
const [showReportModal, setShowReportModal] = useState(false);
|
const [showReportModal, setShowReportModal] = useState(false);
|
||||||
@ -434,8 +435,8 @@ export default function Show({ worker }) {
|
|||||||
{(() => {
|
{(() => {
|
||||||
const expiryStatus = getExpiryStatus(passportDoc.expiry_date);
|
const expiryStatus = getExpiryStatus(passportDoc.expiry_date);
|
||||||
return (
|
return (
|
||||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider \${expiryStatus.color}`}>
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider ${expiryStatus.color}`}>
|
||||||
\${expiryStatus.text}
|
{expiryStatus.text}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@ -474,6 +475,14 @@ export default function Show({ worker }) {
|
|||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_birth', 'Place of Birth')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_birth', 'Place of Birth')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.place_of_birth)}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.place_of_birth)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('nationality', 'Nationality')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.nationality || worker.nationality)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issuing_country', 'Issuing Country')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.issuing_country)}</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Date of Issue')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Date of Issue')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.date_of_issue || passportDoc.issue_date)}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.date_of_issue || passportDoc.issue_date)}</div>
|
||||||
@ -483,8 +492,12 @@ export default function Show({ worker }) {
|
|||||||
<div className="text-xs font-bold text-emerald-600">{safeRender(passportDoc.ocr_data.date_of_expiry || passportDoc.expiry_date)}</div>
|
<div className="text-xs font-bold text-emerald-600">{safeRender(passportDoc.ocr_data.date_of_expiry || passportDoc.expiry_date)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issuing_country', 'Issuing Country')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('authority', 'Authority')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.issuing_country)}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.authority)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_type', 'Document Type')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(passportDoc.ocr_data.document_type || passportDoc.type || 'passport')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -537,6 +550,106 @@ export default function Show({ worker }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Document Card: Emirates ID */}
|
||||||
|
{emiratesIdDoc && (
|
||||||
|
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm lg:col-span-2">
|
||||||
|
<div className="bg-slate-50 px-5 py-3.5 border-b border-slate-100 flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-[11px] font-black text-slate-800 uppercase tracking-wider">{t('emirates_id_details', 'Emirates ID Details')}</span>
|
||||||
|
{(() => {
|
||||||
|
const expiryStatus = getExpiryStatus(emiratesIdDoc.expiry_date);
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider ${expiryStatus.color}`}>
|
||||||
|
{expiryStatus.text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
{emiratesIdDoc.ocr_accuracy && (
|
||||||
|
<span className="text-[9px] font-bold text-emerald-600 bg-emerald-50/50 border border-emerald-100 px-2 py-0.5 rounded-full">
|
||||||
|
{emiratesIdDoc.ocr_accuracy}% Match
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
{emiratesIdDoc.ocr_data ? (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('emirates_id_number', 'Emirates ID Number')}</div>
|
||||||
|
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.emirates_id_number || emiratesIdDoc.number)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('full_name', 'Full Name')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.name || emiratesIdDoc.ocr_data.full_name)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('nationality', 'Nationality')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.nationality)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('date_of_birth', 'Date of Birth')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.date_of_birth || emiratesIdDoc.ocr_data.dob)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('gender', 'Gender')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800 uppercase">{safeRender(emiratesIdDoc.ocr_data.gender || emiratesIdDoc.ocr_data.sex)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('occupation', 'Occupation')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.occupation || emiratesIdDoc.ocr_data.profession)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('employer', 'Employer')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.employer)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue Date')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(emiratesIdDoc.ocr_data.issue_date || emiratesIdDoc.issue_date)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Expiry Date')}</div>
|
||||||
|
<div className="text-xs font-bold text-emerald-600">{safeRender(emiratesIdDoc.ocr_data.expiry_date || emiratesIdDoc.expiry_date)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_id', 'Document ID')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{emiratesIdDoc.number}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{emiratesIdDoc.issue_date || 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Expiry')}</div>
|
||||||
|
<div className="text-xs font-bold text-emerald-600">{emiratesIdDoc.expiry_date || 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-6 pt-4 border-t border-slate-100 flex items-center justify-between text-xs text-slate-500 font-medium">
|
||||||
|
<span className="flex items-center space-x-1.5">
|
||||||
|
<ShieldCheck className="w-4 h-4 text-emerald-600" />
|
||||||
|
<span>UAE PDPL Compliant Scan Storage (Permanently Purged)</span>
|
||||||
|
</span>
|
||||||
|
{emiratesIdDoc.file_path && (
|
||||||
|
<a
|
||||||
|
href={emiratesIdDoc.file_path}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="inline-flex items-center space-x-1 text-[9px] font-black text-blue-600 hover:text-blue-700 bg-blue-50 px-2.5 py-1.5 rounded-lg border border-blue-155 transition-colors uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
<span>View Emirates ID Scan</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Document Card: Visa */}
|
{/* Document Card: Visa */}
|
||||||
{visaDoc ? (
|
{visaDoc ? (
|
||||||
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm lg:col-span-2">
|
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-xs hover:shadow-sm lg:col-span-2">
|
||||||
@ -544,10 +657,10 @@ export default function Show({ worker }) {
|
|||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-[11px] font-black text-slate-800 uppercase tracking-wider">{t('visa_details', 'Entry Visa Details')}</span>
|
<span className="text-[11px] font-black text-slate-800 uppercase tracking-wider">{t('visa_details', 'Entry Visa Details')}</span>
|
||||||
{(() => {
|
{(() => {
|
||||||
const expiryStatus = getExpiryStatus(passportDoc.expiry_date);
|
const expiryStatus = getExpiryStatus(visaDoc.expiry_date || worker.visa_expiry_date);
|
||||||
return (
|
return (
|
||||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider \${expiryStatus.color}`}>
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[8px] font-black border uppercase tracking-wider ${expiryStatus.color}`}>
|
||||||
\${expiryStatus.text}
|
{expiryStatus.text}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@ -576,13 +689,29 @@ export default function Show({ worker }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-4 gap-x-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('entry_permit_no', 'Entry Permit No')}</div>
|
||||||
|
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(visaDoc.ocr_data.entry_permit_no)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('uid_no', 'UID No')}</div>
|
||||||
|
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(visaDoc.ocr_data.uid_no)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('id_number', 'ID Number')}</div>
|
||||||
|
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(visaDoc.ocr_data.id_number || visaDoc.ocr_data.emirates_id_number)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('visa_type', 'Visa Type')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800 uppercase">{safeRender(visaDoc.ocr_data.visa_type || 'ENTRY PERMIT')}</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('profession', 'Profession')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('profession', 'Profession')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.profession || worker.visa_status)}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.profession || worker.visa_status)}</div>
|
||||||
</div>
|
</div>
|
||||||
{sponsorName && (
|
{sponsorName && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sponsor', 'Sponsor')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('sponsor_name', 'Sponsor Name')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{safeRender(sponsorName)}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(sponsorName)}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -592,18 +721,42 @@ export default function Show({ worker }) {
|
|||||||
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(sponsorMobile)}</div>
|
<div className="text-xs font-mono font-bold text-slate-800">{safeRender(sponsorMobile)}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('nationality', 'Nationality')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.nationality || worker.nationality)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('country', 'Country')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.country || 'United Arab Emirates')}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('date_of_birth', 'Date of Birth')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.date_of_birth || visaDoc.ocr_data.dob)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_birth', 'Place of Birth')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.place_of_birth)}</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_issue', 'Place of Issue')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('place_of_issue', 'Place of Issue')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.place_of_issue)}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.place_of_issue)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Date of Issue')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('issue_date', 'Issue Date')}</div>
|
||||||
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.issue_date || visaDoc.issue_date)}</div>
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.issue_date || visaDoc.issue_date)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Date of Expiry')}</div>
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('expiry_date', 'Expiry Date')}</div>
|
||||||
<div className="text-xs font-bold text-emerald-600">{safeRender(visaDoc.ocr_data.expiry_date || visaDoc.expiry_date)}</div>
|
<div className="text-xs font-bold text-emerald-600">{safeRender(visaDoc.ocr_data.expiry_date || visaDoc.expiry_date)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('document_type', 'Document Type')}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-800">{safeRender(visaDoc.ocr_data.document_type || visaDoc.type || 'uae_visa')}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{t('ocr_accuracy', 'OCR Accuracy')}</div>
|
||||||
|
<div className="text-xs font-bold text-emerald-600">{(visaDoc.ocr_accuracy || visaDoc.ocr_data?.ocr_accuracy || 99)}%</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})() : (
|
})() : (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from 'react';
|
import * as React from 'react';
|
||||||
import { X, RotateCcw, SlidersHorizontal } from 'lucide-react';
|
import { X, RotateCcw, SlidersHorizontal } from 'lucide-react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -14,7 +14,7 @@ import { X, RotateCcw, SlidersHorizontal } from 'lucide-react';
|
|||||||
*/
|
*/
|
||||||
export default function FilterDrawer({ open, onClose, onReset, activeCount = 0, title = 'Filters', children }) {
|
export default function FilterDrawer({ open, onClose, onReset, activeCount = 0, title = 'Filters', children }) {
|
||||||
// Lock body scroll when drawer is open
|
// Lock body scroll when drawer is open
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
} else {
|
} else {
|
||||||
@ -24,8 +24,8 @@ export default function FilterDrawer({ open, onClose, onReset, activeCount = 0,
|
|||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
// Close on Escape key
|
// Close on Escape key
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handler = (e) => { if (e.key === 'Escape') onClose(); };
|
const handler = (e) => { if (e.key === 'Escape' && typeof onClose === 'function') onClose(); };
|
||||||
window.addEventListener('keydown', handler);
|
window.addEventListener('keydown', handler);
|
||||||
return () => window.removeEventListener('keydown', handler);
|
return () => window.removeEventListener('keydown', handler);
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
@ -164,14 +164,15 @@ export function FilterInput({ id, value, onChange, placeholder }) {
|
|||||||
* FilterChips — a row of selectable chip buttons for single-select options.
|
* FilterChips — a row of selectable chip buttons for single-select options.
|
||||||
* options: [{ value, label }]
|
* options: [{ value, label }]
|
||||||
*/
|
*/
|
||||||
export function FilterChips({ options, selected, onChange }) {
|
export function FilterChips({ options = [], selected, onChange }) {
|
||||||
|
const opts = options || [];
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{options.map(opt => (
|
{opts.map(opt => (
|
||||||
<button
|
<button
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onChange(opt.value)}
|
onClick={() => onChange && onChange(opt.value)}
|
||||||
className={`px-3 py-1.5 rounded-full text-xs font-bold transition-all border ${
|
className={`px-3 py-1.5 rounded-full text-xs font-bold transition-all border ${
|
||||||
selected === opt.value
|
selected === opt.value
|
||||||
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-sm'
|
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-sm'
|
||||||
@ -185,16 +186,19 @@ export function FilterChips({ options, selected, onChange }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilterCheckboxList({ items, selected, onToggle, placeholder = 'Search...' }) {
|
export function FilterCheckboxList({ items = [], selected = [], onToggle, placeholder = 'Search...' }) {
|
||||||
const [search, setSearch] = React.useState('');
|
const [search, setSearch] = React.useState('');
|
||||||
|
|
||||||
const filteredItems = items.filter(item =>
|
const itemsList = items || [];
|
||||||
item.toLowerCase().includes(search.toLowerCase())
|
const selectedList = selected || [];
|
||||||
|
|
||||||
|
const filteredItems = itemsList.filter(item =>
|
||||||
|
item && item.toLowerCase().includes((search || '').toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{items.length > 5 && (
|
{itemsList.length > 5 && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -216,12 +220,12 @@ export function FilterCheckboxList({ items, selected, onToggle, placeholder = 'S
|
|||||||
)}
|
)}
|
||||||
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
|
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
|
||||||
{filteredItems.map(item => {
|
{filteredItems.map(item => {
|
||||||
const isChecked = selected.includes(item);
|
const isChecked = selectedList.includes(item);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={item}
|
key={item}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onToggle(item)}
|
onClick={() => onToggle && onToggle(item)}
|
||||||
className="w-full flex items-center space-x-3 text-left focus:outline-none group cursor-pointer"
|
className="w-full flex items-center space-x-3 text-left focus:outline-none group cursor-pointer"
|
||||||
>
|
>
|
||||||
<div className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 transition-all ${
|
<div className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 transition-all ${
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user