diff --git a/app/Http/Controllers/Employer/WorkerController.php b/app/Http/Controllers/Employer/WorkerController.php index 41abe6e..13a6b7d 100644 --- a/app/Http/Controllers/Employer/WorkerController.php +++ b/app/Http/Controllers/Employer/WorkerController.php @@ -409,14 +409,6 @@ public function show($id) 'in_country' => (bool) $w->in_country, 'preferred_location' => $w->preferred_location, '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 [ 'id' => $doc->id, 'type' => $doc->type, @@ -425,7 +417,7 @@ public function show($id) 'expiry_date' => $doc->expiry_date, 'ocr_accuracy' => $doc->ocr_accuracy, 'file_path' => $doc->file_path ? url($doc->file_path) : null, - 'ocr_data' => $ocrData, + 'ocr_data' => $doc->ocr_data, ]; })->toArray(), ]; diff --git a/check_doc.php b/check_doc.php new file mode 100644 index 0000000..fdeaac5 --- /dev/null +++ b/check_doc.php @@ -0,0 +1,12 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +$doc = \App\Models\WorkerDocument::where('number', 'P666382')->first(); +if ($doc) { + print_r($doc->getAttributes()); +} else { + echo "NOT FOUND\n"; +} diff --git a/resources/js/Pages/Employer/Auth/UploadEmiratesId.jsx b/resources/js/Pages/Employer/Auth/UploadEmiratesId.jsx index 56a1a7c..b611549 100644 --- a/resources/js/Pages/Employer/Auth/UploadEmiratesId.jsx +++ b/resources/js/Pages/Employer/Auth/UploadEmiratesId.jsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { Head, Link, router } from '@inertiajs/react'; import axios from 'axios'; import { toast } from 'sonner'; +import { compressImage } from '../../../lib/utils'; import { Loader2, ShieldAlert, @@ -70,7 +71,7 @@ export default function UploadEmiratesId({ email }) { } }; - const handleFrontFileChange = (e) => { + const handleFrontFileChange = async (e) => { const selectedFile = e.target.files[0]; if (selectedFile) { if (selectedFile.size > 10240 * 1024) { @@ -78,12 +79,17 @@ export default function UploadEmiratesId({ email }) { toast.error('The document must not exceed 10MB.'); return; } - setFrontFile(selectedFile); + try { + const compressedFile = await compressImage(selectedFile); + setFrontFile(compressedFile); + } catch (err) { + setFrontFile(selectedFile); + } setError(null); } }; - const handleBackFileChange = (e) => { + const handleBackFileChange = async (e) => { const selectedFile = e.target.files[0]; if (selectedFile) { if (selectedFile.size > 10240 * 1024) { @@ -91,7 +97,12 @@ export default function UploadEmiratesId({ email }) { toast.error('The document must not exceed 10MB.'); return; } - setBackFile(selectedFile); + try { + const compressedFile = await compressImage(selectedFile); + setBackFile(compressedFile); + } catch (err) { + setBackFile(selectedFile); + } setError(null); } }; diff --git a/resources/js/Pages/Employer/Profile.jsx b/resources/js/Pages/Employer/Profile.jsx index ba7d0cb..7fbc7a4 100644 --- a/resources/js/Pages/Employer/Profile.jsx +++ b/resources/js/Pages/Employer/Profile.jsx @@ -29,6 +29,7 @@ import { XCircle } from 'lucide-react'; import { toast } from 'sonner'; +import { compressImage } from '../../lib/utils'; export default function Profile({ employerProfile }) { const { t } = useTranslation(); @@ -484,11 +485,17 @@ export default function Profile({ employerProfile }) { { + onChange={async (e) => { const file = e.target.files[0]; if (file) { - setFrontFile(file); - setFrontPreview(URL.createObjectURL(file)); + try { + const compressed = await compressImage(file); + setFrontFile(compressed); + setFrontPreview(URL.createObjectURL(compressed)); + } catch (err) { + setFrontFile(file); + setFrontPreview(URL.createObjectURL(file)); + } } }} className="absolute inset-0 opacity-0 cursor-pointer" @@ -517,11 +524,17 @@ export default function Profile({ employerProfile }) { { + onChange={async (e) => { const file = e.target.files[0]; if (file) { - setBackFile(file); - setBackPreview(URL.createObjectURL(file)); + try { + const compressed = await compressImage(file); + setBackFile(compressed); + setBackPreview(URL.createObjectURL(compressed)); + } catch (err) { + setBackFile(file); + setBackPreview(URL.createObjectURL(file)); + } } }} className="absolute inset-0 opacity-0 cursor-pointer" diff --git a/resources/js/Pages/Employer/Workers/Show.jsx b/resources/js/Pages/Employer/Workers/Show.jsx index 42148d3..1abf6d7 100644 --- a/resources/js/Pages/Employer/Workers/Show.jsx +++ b/resources/js/Pages/Employer/Workers/Show.jsx @@ -71,8 +71,8 @@ const getExpiryStatus = (expiryDate) => { } }; -const safeRender = (value) => { - if (value === null || value === undefined) return 'N/A'; +const safeRender = (value, fallback = 'Not Extracted') => { + if (value === null || value === undefined || String(value).trim() === '') return fallback; if (typeof value === 'object') { if (value.name) return String(value.name); if (value.full_name) return String(value.full_name); @@ -87,6 +87,52 @@ export default function Show({ worker }) { const emiratesIdDoc = worker.documents?.find(d => d.type === 'emirates_id' || d.type === 'eid'); const visaDoc = worker.documents?.find(d => d.type === 'visa'); + // Robust passport document fallbacks + const passportOcr = passportDoc?.ocr_data || {}; + const passportGivenNames = passportOcr.given_names || worker.name || 'Not Extracted'; + const passportSurname = passportOcr.surname || ''; + const passportNumber = passportOcr.passport_number || passportDoc?.number || 'Not Extracted'; + const passportSex = passportOcr.sex || worker.gender || 'Not Extracted'; + const passportDob = passportOcr.date_of_birth || (worker.age ? `Age ${worker.age}` : 'Not Extracted'); + const passportPlaceOfBirth = passportOcr.place_of_birth || 'Not Extracted'; + const passportNationality = passportOcr.nationality || worker.nationality || 'Not Extracted'; + const passportIssuingCountry = passportOcr.issuing_country || 'Not Extracted'; + const passportIssueDate = passportOcr.date_of_issue || passportDoc?.issue_date || 'Not Extracted'; + const passportExpiryDate = passportOcr.date_of_expiry || passportDoc?.expiry_date || 'Not Extracted'; + const passportAuthority = passportOcr.authority || 'Not Extracted'; + const passportDocType = passportOcr.document_type || passportDoc?.type || 'Passport'; + + // Robust visa document fallbacks + const visaOcr = visaDoc?.ocr_data || {}; + const sponsorObj = visaOcr.sponsor; + const isSponsorObj = sponsorObj && typeof sponsorObj === 'object' && !Array.isArray(sponsorObj); + const sponsorName = isSponsorObj + ? (sponsorObj.name || sponsorObj.sponsor_name) + : (visaOcr.sponsor_name || (typeof sponsorObj === 'string' ? sponsorObj : '')); + const sponsorPhone = isSponsorObj + ? (sponsorObj.phone || sponsorObj.mobile || sponsorObj.mobile_number) + : ''; + const sponsorAddress = isSponsorObj + ? (sponsorObj.address || '') + : ''; + + const visaFullName = visaOcr.full_name || worker.name || 'Not Extracted'; + const visaEntryPermitNo = visaOcr.entry_permit_no || visaDoc?.number || 'Not Extracted'; + const visaUidNo = visaOcr.uid_no || 'Not Extracted'; + const visaType = visaOcr.visa_type || worker.visa_status || 'Entry Permit'; + const visaPassportNo = visaOcr.passport_no || visaOcr.passport_number || passportDoc?.number || 'Not Extracted'; + const visaProfession = visaOcr.profession || worker.main_profession || 'Domestic Worker'; + const visaSponsorName = sponsorName || visaOcr.sponsor_name || 'Not Extracted'; + const visaSponsorPhone = sponsorPhone || 'Not Extracted'; + const visaSponsorAddress = sponsorAddress || 'Not Extracted'; + const visaNationality = visaOcr.nationality || worker.nationality || 'Not Extracted'; + const visaCountry = visaOcr.country || 'United Arab Emirates'; + const visaDob = visaOcr.date_of_birth || visaOcr.dob || 'Not Extracted'; + const visaPlaceOfBirth = visaOcr.place_of_birth || 'Not Extracted'; + const visaIssueDate = visaOcr.issue_date || visaDoc?.issue_date || 'Not Extracted'; + const visaValidUntil = visaOcr.valid_until || visaOcr.expiry_date || visaDoc?.expiry_date || 'Not Extracted'; + const visaDocType = visaOcr.document_type || visaDoc?.type || 'uae_visa'; + const [showReportModal, setShowReportModal] = useState(false); const [reportReason, setReportReason] = useState(''); const [reportDetails, setReportDetails] = useState(''); @@ -460,73 +506,56 @@ export default function Show({ worker }) {
- {passportDoc.ocr_data ? ( -
-
-
{t('given_names', 'Given Names')}
-
{safeRender(passportDoc.ocr_data.given_names)}
-
-
-
{t('surname', 'Surname')}
-
{safeRender(passportDoc.ocr_data.surname)}
-
-
-
{t('passport_number', 'Passport Number')}
-
{safeRender(passportDoc.ocr_data.passport_number || passportDoc.number)}
-
-
-
{t('sex', 'Sex')}
-
{safeRender(passportDoc.ocr_data.sex)}
-
-
-
{t('date_of_birth', 'Date of Birth')}
-
{safeRender(passportDoc.ocr_data.date_of_birth)}
-
-
-
{t('place_of_birth', 'Place of Birth')}
-
{safeRender(passportDoc.ocr_data.place_of_birth)}
-
-
-
{t('nationality', 'Nationality')}
-
{safeRender(passportDoc.ocr_data.nationality || worker.nationality)}
-
-
-
{t('issuing_country', 'Issuing Country')}
-
{safeRender(passportDoc.ocr_data.issuing_country)}
-
-
-
{t('issue_date', 'Date of Issue')}
-
{safeRender(passportDoc.ocr_data.date_of_issue || passportDoc.issue_date)}
-
-
-
{t('expiry_date', 'Date of Expiry')}
-
{safeRender(passportDoc.ocr_data.date_of_expiry || passportDoc.expiry_date)}
-
-
-
{t('authority', 'Authority')}
-
{safeRender(passportDoc.ocr_data.authority)}
-
-
-
{t('document_type', 'Document Type')}
-
{safeRender(passportDoc.ocr_data.document_type || passportDoc.type || 'passport')}
-
+
+
+
{t('given_names', 'Given Names')}
+
{safeRender(passportGivenNames)}
- ) : ( -
-
-
{t('document_id', 'Document ID')}
-
{passportDoc.number}
-
-
-
{t('issue_date', 'Issue')}
-
{passportDoc.issue_date || 'N/A'}
-
-
-
{t('expiry_date', 'Expiry')}
-
{passportDoc.expiry_date || 'N/A'}
-
+
+
{t('surname', 'Surname')}
+
{safeRender(passportSurname)}
- )} +
+
{t('passport_number', 'Passport Number')}
+
{safeRender(passportNumber)}
+
+
+
{t('sex', 'Sex')}
+
{safeRender(passportSex)}
+
+
+
{t('date_of_birth', 'Date of Birth')}
+
{safeRender(passportDob)}
+
+
+
{t('place_of_birth', 'Place of Birth')}
+
{safeRender(passportPlaceOfBirth)}
+
+
+
{t('nationality', 'Nationality')}
+
{safeRender(passportNationality)}
+
+
+
{t('issuing_country', 'Issuing Country')}
+
{safeRender(passportIssuingCountry)}
+
+
+
{t('issue_date', 'Date of Issue')}
+
{safeRender(passportIssueDate)}
+
+
+
{t('expiry_date', 'Date of Expiry')}
+
{safeRender(passportExpiryDate)}
+
+
+
{t('authority', 'Authority')}
+
{safeRender(passportAuthority)}
+
+
+
{t('document_type', 'Document Type')}
+
{safeRender(passportDocType)}
+
+
@@ -686,109 +715,77 @@ export default function Show({ worker }) { )}
-
- {visaDoc.ocr_data ? (() => { - const sponsorObj = visaDoc.ocr_data?.sponsor; - const isSponsorObj = sponsorObj && typeof sponsorObj === 'object' && !Array.isArray(sponsorObj); - const sponsorName = isSponsorObj - ? (sponsorObj.name || sponsorObj.sponsor_name) - : (visaDoc.ocr_data?.sponsor_name || (typeof sponsorObj === 'string' ? sponsorObj : '')); - const sponsorPhone = isSponsorObj - ? (sponsorObj.phone || sponsorObj.mobile || sponsorObj.mobile_number) - : ''; - const sponsorAddress = isSponsorObj - ? (sponsorObj.address || '') - : ''; - - return ( -
-
-
{t('full_name', 'Full Name')}
-
{safeRender(visaDoc.ocr_data.full_name || worker.name)}
-
-
-
{t('entry_permit_no', 'Entry Permit No')}
-
{safeRender(visaDoc.ocr_data.entry_permit_no || visaDoc.number)}
-
-
-
{t('uid_no', 'UID No')}
-
{safeRender(visaDoc.ocr_data.uid_no)}
-
-
-
{t('visa_type', 'Visa Type')}
-
{safeRender(visaDoc.ocr_data.visa_type || 'ENTRY PERMIT')}
-
-
-
{t('passport_no', 'Passport No')}
-
{safeRender(visaDoc.ocr_data.passport_no || visaDoc.ocr_data.passport_number)}
-
-
-
{t('profession', 'Profession')}
-
{safeRender(visaDoc.ocr_data.profession || worker.visa_status)}
-
-
-
{t('sponsor_name', 'Sponsor Name')}
-
{safeRender(sponsorName || visaDoc.ocr_data.sponsor_name)}
-
-
-
{t('sponsor_phone', 'Sponsor Phone')}
-
{safeRender(sponsorPhone)}
-
-
-
{t('sponsor_address', 'Sponsor Address')}
-
{safeRender(sponsorAddress)}
-
-
-
{t('nationality', 'Nationality')}
-
{safeRender(visaDoc.ocr_data.nationality || worker.nationality)}
-
-
-
{t('country', 'Country')}
-
{safeRender(visaDoc.ocr_data.country || 'United Arab Emirates')}
-
-
-
{t('date_of_birth', 'Date of Birth')}
-
{safeRender(visaDoc.ocr_data.date_of_birth || visaDoc.ocr_data.dob)}
-
-
-
{t('place_of_birth', 'Place of Birth')}
-
{safeRender(visaDoc.ocr_data.place_of_birth)}
-
-
-
{t('issue_date', 'Issue Date')}
-
{safeRender(visaDoc.ocr_data.issue_date || visaDoc.issue_date)}
-
-
-
{t('valid_until', 'Valid Until')}
-
{safeRender(visaDoc.ocr_data.valid_until || visaDoc.ocr_data.expiry_date || visaDoc.expiry_date)}
-
-
-
{t('document_type', 'Document Type')}
-
{safeRender(visaDoc.ocr_data.document_type || visaDoc.type || 'uae_visa')}
-
-
-
{t('ocr_accuracy', 'OCR Accuracy')}
-
{(visaDoc.ocr_accuracy || visaDoc.ocr_data?.ocr_accuracy || 99)}%
-
-
- ); - })() : ( -
-
-
{t('document_id', 'Document ID')}
-
{visaDoc.number}
-
-
-
{t('issue_date', 'Issue')}
-
{visaDoc.issue_date || 'N/A'}
-
-
-
{t('expiry_date', 'Expiry')}
-
{visaDoc.expiry_date || 'N/A'}
-
+
+
+
{t('full_name', 'Full Name')}
+
{safeRender(visaFullName)}
- )} +
+
{t('entry_permit_no', 'Entry Permit No')}
+
{safeRender(visaEntryPermitNo)}
+
+
+
{t('uid_no', 'UID No')}
+
{safeRender(visaUidNo)}
+
+
+
{t('visa_type', 'Visa Type')}
+
{safeRender(visaType)}
+
+
+
{t('passport_no', 'Passport No')}
+
{safeRender(visaPassportNo)}
+
+
+
{t('profession', 'Profession')}
+
{safeRender(visaProfession)}
+
+
+
{t('sponsor_name', 'Sponsor Name')}
+
{safeRender(visaSponsorName)}
+
+
+
{t('sponsor_phone', 'Sponsor Phone')}
+
{safeRender(visaSponsorPhone)}
+
+
+
{t('sponsor_address', 'Sponsor Address')}
+
{safeRender(visaSponsorAddress)}
+
+
+
{t('nationality', 'Nationality')}
+
{safeRender(visaNationality)}
+
+
+
{t('country', 'Country')}
+
{safeRender(visaCountry)}
+
+
+
{t('date_of_birth', 'Date of Birth')}
+
{safeRender(visaDob)}
+
+
+
{t('place_of_birth', 'Place of Birth')}
+
{safeRender(visaPlaceOfBirth)}
+
+
+
{t('issue_date', 'Issue Date')}
+
{safeRender(visaIssueDate)}
+
+
+
{t('valid_until', 'Valid Until')}
+
{safeRender(visaValidUntil)}
+
+
+
{t('document_type', 'Document Type')}
+
{safeRender(visaDocType)}
+
+
+
{t('ocr_accuracy', 'OCR Accuracy')}
+
{(visaDoc.ocr_accuracy || visaOcr.ocr_accuracy || 99)}%
+
+
diff --git a/resources/js/lib/utils.js b/resources/js/lib/utils.js index b20bf01..9da77a1 100644 --- a/resources/js/lib/utils.js +++ b/resources/js/lib/utils.js @@ -4,3 +4,83 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs) { return twMerge(clsx(inputs)); } + +/** + * Compresses and resizes an image file on the client side. + * If the file is not an image (e.g. PDF), it resolves with the original file. + * + * @param {File} file - The file to compress. + * @param {number} maxWidth - Max width of compressed image. + * @param {number} maxHeight - Max height of compressed image. + * @param {number} quality - Compression quality between 0 and 1. + * @returns {Promise} - A promise that resolves to the compressed File object. + */ +export function compressImage(file, maxWidth = 1200, maxHeight = 1200, quality = 0.8) { + return new Promise((resolve) => { + if (!file || !file.type.startsWith('image/')) { + resolve(file); + return; + } + + const reader = new FileReader(); + reader.onload = (event) => { + const img = new Image(); + img.onload = () => { + let width = img.width; + let height = img.height; + + // Calculate aspect ratio + if (width > height) { + if (width > maxWidth) { + height = Math.round((height * maxWidth) / width); + width = maxWidth; + } + } else { + if (height > maxHeight) { + width = Math.round((width * maxHeight) / height); + height = maxHeight; + } + } + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + resolve(file); + return; + } + + // Draw image + ctx.drawImage(img, 0, 0, width, height); + + // Convert to blob + if (canvas.toBlob) { + canvas.toBlob( + (blob) => { + if (blob) { + // Keep original extension or use jpeg, but File constructor needs a name + const compressedFile = new File([blob], file.name.substring(0, file.name.lastIndexOf('.')) + '.jpg', { + type: 'image/jpeg', + lastModified: Date.now() + }); + resolve(compressedFile); + } else { + resolve(file); + } + }, + 'image/jpeg', + quality + ); + } else { + resolve(file); + } + }; + img.onerror = () => resolve(file); + img.src = event.target.result; + }; + reader.onerror = () => resolve(file); + reader.readAsDataURL(file); + }); +} + diff --git a/scratch/fix_show.php b/scratch/fix_show.php new file mode 100644 index 0000000..e610095 --- /dev/null +++ b/scratch/fix_show.php @@ -0,0 +1,19 @@ +" and line 717 is whitespace. +if (strpos($lines[715], '
') !== false && trim($lines[716]) === '') { + $lines[715] = str_repeat(' ', 48) . ')}'; + $lines[716] = str_repeat(' ', 44) . '
'; + echo "Successfully replaced lines!\n"; +} else { + echo "Line content mismatch!\n"; + echo "Line 716: [" . $lines[715] . "]\n"; + echo "Line 717: [" . $lines[716] . "]\n"; +} + +file_put_contents($file, implode("\r\n", $lines));