diff --git a/.env.example b/.env.example index 566bca8..35836ad 100644 --- a/.env.example +++ b/.env.example @@ -62,6 +62,10 @@ AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false -OCR_API_URL=http://192.168.0.220:5000/passport +PASSPORT_API_URL= +VISA_API_URL= +EMIRATES_FRONT_API_URL= +EMIRATES_BACK_API_URL= +SPONSOR_LICENSE_API_URL= VITE_APP_NAME="${APP_NAME}" diff --git a/app/Http/Controllers/Admin/WorkerController.php b/app/Http/Controllers/Admin/WorkerController.php index 0fd0bdd..bcb8a78 100644 --- a/app/Http/Controllers/Admin/WorkerController.php +++ b/app/Http/Controllers/Admin/WorkerController.php @@ -13,7 +13,7 @@ class WorkerController extends Controller */ public function index() { - $workers = \App\Models\Worker::with(['skills']) + $workers = \App\Models\Worker::with(['skills', 'documents']) ->latest() ->get() ->map(function ($worker) { @@ -56,6 +56,7 @@ public function index() 'availability' => $worker->availability, 'verified' => (bool)$worker->verified, 'bio' => $worker->bio, + 'documents' => $worker->documents, 'joined_at' => $worker->created_at ? $worker->created_at->format('Y-m-d') : 'N/A', ]; }); @@ -126,20 +127,16 @@ public function updateProfile(Request $request, $id) 'phone' => 'required|string', 'gender' => 'nullable|string', 'language' => 'nullable|string', - 'country' => 'nullable|string', - 'city' => 'nullable|string', - 'area' => 'nullable|string', 'preferred_location' => 'nullable|string', 'live_in_out' => 'nullable|string', 'experience' => 'required|string', 'salary' => 'nullable|numeric', - 'bio' => 'nullable|string', 'nationality' => 'nullable|string', 'visa_status' => 'nullable|string', - 'religion' => 'nullable|string', 'age' => 'nullable|integer', - 'in_country' => 'nullable|boolean', + 'in_country' => 'nullable', 'preferred_job_type' => 'nullable|string', + 'skills' => 'nullable|string', ]); $worker = \App\Models\Worker::findOrFail($id); @@ -148,24 +145,31 @@ public function updateProfile(Request $request, $id) $worker->phone = $validated['phone']; $worker->gender = $validated['gender'] ?? $worker->gender; $worker->language = $validated['language'] ?? $worker->language; - $worker->country = $validated['country'] ?? $worker->country; - $worker->city = $validated['city'] ?? $worker->city; - $worker->area = $validated['area'] ?? $worker->area; $worker->preferred_location = $validated['preferred_location'] ?? $worker->preferred_location; $worker->live_in_out = $validated['live_in_out'] ?? $worker->live_in_out; $worker->experience = $validated['experience']; $worker->salary = $validated['salary'] ?? $worker->salary; - $worker->bio = $validated['bio'] ?? $worker->bio; $worker->nationality = $validated['nationality'] ?? $worker->nationality; $worker->visa_status = $validated['visa_status'] ?? $worker->visa_status; - $worker->religion = $validated['religion'] ?? $worker->religion; $worker->age = $validated['age'] ?? $worker->age; + if (isset($validated['in_country'])) { - $worker->in_country = (bool)$validated['in_country']; + $worker->in_country = filter_var($validated['in_country'], FILTER_VALIDATE_BOOLEAN); } + $worker->preferred_job_type = $validated['preferred_job_type'] ?? $worker->preferred_job_type; $worker->save(); + if ($request->has('skills')) { + $skillsArray = array_filter(array_map('trim', explode(',', $validated['skills'] ?? ''))); + $skillIds = []; + foreach ($skillsArray as $name) { + $skill = \App\Models\Skill::firstOrCreate(['name' => $name]); + $skillIds[] = $skill->id; + } + $worker->skills()->sync($skillIds); + } + return back()->with('success', "Worker #{$id} profile details moderated and updated successfully."); } diff --git a/app/Http/Controllers/Api/WorkerAuthController.php b/app/Http/Controllers/Api/WorkerAuthController.php index 6be5edc..37b71dc 100644 --- a/app/Http/Controllers/Api/WorkerAuthController.php +++ b/app/Http/Controllers/Api/WorkerAuthController.php @@ -292,8 +292,8 @@ public function setupProfile(Request $request) } } - /** - * Unified registration — supports backward compatibility and multipart file registration. + /** + * Unified registration — supports direct JSON payloads for passport and visa. */ public function register(Request $request) { @@ -314,6 +314,8 @@ public function register(Request $request) 'live_in_out' => 'nullable|string|in:live_in,live_out', 'preferred_location' => 'nullable|string|max:255', 'fcm_token' => 'nullable|string|max:255', + 'passport' => 'nullable|array', + 'visa' => 'nullable|array', ]); if ($validator->fails()) { @@ -348,27 +350,75 @@ public function register(Request $request) $apiToken = Str::random(80); $result = DB::transaction(function () use ($request, $email, $skillsArray, $apiToken, $inCountry) { + // Initialize fields + $name = $request->name; + $nationality = $request->nationality ?? 'Not Specified'; + $age = $request->age ?? 25; + $visaStatus = $inCountry ? $request->visa_status : null; + + $passportDataInput = $request->input('passport'); + $visaDataInput = $request->input('visa'); + + // Process passport if provided + if ($passportDataInput) { + $givenNames = trim($passportDataInput['given_names'] ?? ''); + $surname = trim($passportDataInput['surname'] ?? ''); + $fullName = trim($givenNames . ' ' . $surname); + if (empty($fullName)) { + $fullName = $passportDataInput['name'] ?? null; + } + if ($fullName) { + $name = $fullName; + } + + if (!empty($passportDataInput['nationality'])) { + $codeUpper = strtoupper($passportDataInput['nationality']); + $matchedNationality = \App\Models\Nationality::where('iso3', $codeUpper) + ->orWhere('code', $codeUpper) + ->first(); + $nationality = $matchedNationality ? $matchedNationality->name : $passportDataInput['nationality']; + } + + if (!empty($passportDataInput['date_of_birth'])) { + try { + $dob = new \DateTime($this->normaliseDateForController($passportDataInput['date_of_birth'])); + $age = (new \DateTime())->diff($dob)->y; + } catch (\Exception $dateEx) { + logger()->error('Passport DOB parse error in register: ' . $dateEx->getMessage()); + } + } + } + + // Process visa if provided + if ($visaDataInput) { + if (!empty($visaDataInput['profession'])) { + if (empty($visaStatus)) { + $visaStatus = $visaDataInput['profession']; + } + } + } + $worker = Worker::create([ - 'name' => $request->name, + 'name' => $name, 'email' => $email, 'phone' => $request->phone, 'language' => $request->language ?? 'HI', 'password' => Hash::make($request->password), - 'nationality' => $request->nationality ?? 'Not Specified', + 'nationality' => $nationality, 'gender' => $request->gender, 'live_in_out' => $request->live_in_out, 'preferred_location' => $request->preferred_location, - 'age' => $request->age ?? 25, + 'age' => $age, 'salary' => $request->salary, 'availability' => 'Immediate', 'experience' => $request->experience ?? 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Hardworking and reliable General Helper available for immediate hire.', - 'verified' => false, + 'verified' => ($passportDataInput || $visaDataInput) ? true : false, 'status' => 'active', 'api_token' => $apiToken, 'in_country' => $inCountry, - 'visa_status' => $inCountry ? $request->visa_status : null, + 'visa_status' => $visaStatus, 'preferred_job_type' => $request->preferred_job_type, 'fcm_token' => $request->fcm_token, ]); @@ -380,6 +430,46 @@ public function register(Request $request) } } + // Create passport document if provided + if ($passportDataInput) { + $passportNum = $passportDataInput['passport_number'] ?? ('P' . rand(1000000, 9999999)); + $expiryDate = $passportDataInput['date_of_expiry'] ?? null; + $issueDate = $passportDataInput['date_of_issue'] ?? null; + + $expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(8)->toDateString(); + $issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : now()->subYears(3)->toDateString(); + + $worker->documents()->create([ + 'type' => 'passport', + 'number' => $passportNum, + 'issue_date' => $issueDateFormatted, + 'expiry_date' => $expiryDateFormatted, + 'ocr_accuracy' => 99.0, + 'file_path' => null, + 'ocr_data' => $passportDataInput, + ]); + } + + // Create visa document if provided + if ($visaDataInput) { + $visaNum = $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999)); + $expiryDate = $visaDataInput['expiry_date'] ?? null; + $issueDate = $visaDataInput['issue_date'] ?? null; + + $expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(2)->toDateString(); + $issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : now()->subYears(2)->toDateString(); + + $worker->documents()->create([ + 'type' => 'visa', + 'number' => $visaNum, + 'issue_date' => $issueDateFormatted, + 'expiry_date' => $expiryDateFormatted, + 'ocr_accuracy' => 99.0, + 'file_path' => null, + 'ocr_data' => $visaDataInput, + ]); + } + return $worker; }); @@ -387,7 +477,7 @@ public function register(Request $request) \App\Services\FCMService::sendPushNotification( $request->fcm_token, 'Welcome to Migrant', - 'Worker registered successfully. Please upload your passport and visa documents.' + 'Worker registered successfully.' ); } @@ -395,7 +485,7 @@ public function register(Request $request) return response()->json([ 'success' => true, - 'message' => 'Worker registered successfully. Please upload your passport and visa documents.', + 'message' => 'Worker registered successfully.', 'data' => ['worker' => $result, 'token' => $apiToken] ], 201); @@ -403,223 +493,15 @@ public function register(Request $request) logger()->error('Worker Registration Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, - 'message' => 'An error occurred during worker registration. Please try again.', - 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' - ], 500); - } - } - - // ------------------------------------------------------------------------- - // Document Upload Endpoints (protected — require Bearer token) - // ------------------------------------------------------------------------- - - /** - * Upload & OCR-process the worker's passport document. - * POST /workers/register/passport (auth.worker) - * - * Calls the dedicated Passport OCR API (http://192.168.0.252:5000/passport). - * Creates or updates the worker's passport document record. - * Optionally syncs name, nationality, and age extracted from OCR onto the worker profile. - */ - public function registerPassport(Request $request) - { - $validator = Validator::make($request->all(), [ - 'passport_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240', - 'sync_profile' => 'nullable|in:true,false,1,0,TRUE,FALSE', - ]); - - if ($validator->fails()) { - return response()->json([ - 'success' => false, - 'message' => 'Validation error.', - 'errors' => $validator->errors() - ], 422); - } - - /** @var \App\Models\Worker $worker */ - $worker = $request->attributes->get('worker'); - - try { - $file = $request->file('passport_file'); - $ocrRes = \App\Services\OcrDocumentService::extractPassportData($file); - - if (empty($ocrRes['success'])) { - return response()->json([ - 'success' => false, - 'message' => 'Passport OCR extraction failed. Please upload a clear image and try again.', - 'errors' => [ - 'passport_file' => ['Passport OCR API could not read the document. Please try again.'] - ] - ], 422); - } - - // Build document data from OCR - $number = $ocrRes['document_number'] ?? ('P' . rand(1000000, 9999999)); - $expiryDate = $ocrRes['expiry_date'] ?? now()->addYears(8)->toDateString(); - $issueDate = !empty($ocrRes['expiry_date']) - ? date('Y-m-d', strtotime($ocrRes['expiry_date'] . ' -5 years')) - : now()->subYears(3)->toDateString(); - - // Upsert passport document (update if already exists, create if not) - $worker->documents()->updateOrCreate( - ['type' => 'passport'], - [ - 'number' => $number, - 'issue_date' => $issueDate, - 'expiry_date' => $expiryDate, - 'ocr_accuracy' => 98.5, - 'file_path' => null, - ] - ); - - // Optionally sync OCR-extracted profile fields - $profileUpdates = ['verified' => true]; - $syncProfile = filter_var($request->input('sync_profile', true), FILTER_VALIDATE_BOOLEAN); - if ($syncProfile) { - if (!empty($ocrRes['name'])) { - $profileUpdates['name'] = $ocrRes['name']; - } - - if (!empty($ocrRes['nationality'])) { - $codeUpper = strtoupper($ocrRes['nationality']); - $matchedNationality = \App\Models\Nationality::where('iso3', $codeUpper) - ->orWhere('code', $codeUpper) - ->first(); - $profileUpdates['nationality'] = $matchedNationality ? $matchedNationality->name : $ocrRes['nationality']; - } - - if (!empty($ocrRes['date_of_birth'])) { - try { - $dob = new \DateTime($ocrRes['date_of_birth']); - $profileUpdates['age'] = (new \DateTime())->diff($dob)->y; - } catch (\Exception $dateEx) { - logger()->error('Passport DOB parse error: ' . $dateEx->getMessage()); - } - } - } - - if (!empty($profileUpdates)) { - $worker->update($profileUpdates); - } - - $worker->load(['skills', 'documents']); - - $passport = $worker->documents->where('type', 'passport')->first(); - $passportData = null; - if ($passport) { - $passportData = array_merge($passport->toArray(), [ - 'document_number' => $ocrRes['document_number'], - 'expiry_date' => $ocrRes['expiry_date'], - 'date_of_birth' => $ocrRes['date_of_birth'] ?? null, - 'nationality' => $ocrRes['nationality'] ?? null, - 'name' => $ocrRes['name'] ?? null, - ]); - } - - - return response()->json([ - 'success' => true, - 'message' => 'Passport uploaded and processed successfully.', - 'data' => [ - 'worker' => $worker, - 'passport' => $passportData, - ] - ], 200); - - } catch (\Exception $e) { - logger()->error('Worker Passport Upload Failure: ' . $e->getMessage()); - return response()->json([ - 'success' => false, - 'message' => 'An error occurred while processing the passport. Please try again.', - 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' - ], 500); - } - } - - /** - * Upload & OCR-process the worker's visa document. - * POST /workers/register/visa (auth.worker) - * - * Calls the dedicated Visa OCR API (http://192.168.0.252:5000/visa). - * Creates or updates the worker's visa document record. - */ - public function registerVisa(Request $request) - { - $validator = Validator::make($request->all(), [ - 'visa_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240', - ]); - - if ($validator->fails()) { - return response()->json([ - 'success' => false, - 'message' => 'Validation error.', - 'errors' => $validator->errors() - ], 422); - } - - /** @var \App\Models\Worker $worker */ - $worker = $request->attributes->get('worker'); - - try { - $file = $request->file('visa_file'); - $ocrRes = \App\Services\OcrDocumentService::extractVisaData($file); - - $number = $ocrRes['document_number'] ?? ('V' . rand(1000000, 9999999)); - $expiryDate = $ocrRes['expiry_date'] ?? now()->addYears(2)->toDateString(); - $issueDate = $ocrRes['issue_date'] ?? date('Y-m-d', strtotime($expiryDate . ' -2 years')); - $accuracy = $ocrRes['ocr_accuracy'] ?? ($ocrRes['success'] ? 98.5 : 0.0); - - // Upsert visa document - $worker->documents()->updateOrCreate( - ['type' => 'visa'], - [ - 'number' => $number, - 'issue_date' => $issueDate, - 'expiry_date' => $expiryDate, - 'ocr_accuracy' => $accuracy, - 'file_path' => null, - ] - ); - - $worker->update(['verified' => true]); - - $worker->load(['skills', 'documents']); - - $visa = $worker->documents->where('type', 'visa')->first(); - $visaData = null; - if ($visa) { - $visaData = array_merge($visa->toArray(), [ - 'document_number' => $ocrRes['document_number'] ?? null, - 'expiry_date' => $ocrRes['expiry_date'] ?? null, - 'issue_date' => $ocrRes['issue_date'] ?? null, - 'visa_type' => $ocrRes['visa_type'] ?? null, - ]); - } - - - return response()->json([ - 'success' => true, - 'message' => 'Visa uploaded and processed successfully.', - 'data' => [ - 'worker' => $worker, - 'visa' => $visaData, - ] - ], 200); - - } catch (\Exception $e) { - logger()->error('Worker Visa Upload Failure: ' . $e->getMessage()); - return response()->json([ - 'success' => false, - 'message' => 'An error occurred while processing the visa. Please try again.', - 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + 'message' => 'Registration failed.', + 'error' => app()->environment('local', 'testing') ? $e->getMessage() : 'Internal Server Error' ], 500); } } /** * Authenticate a worker and return a secure Bearer token. - */ - public function login(Request $request) + */public function login(Request $request) { $validator = Validator::make($request->all(), [ 'phone' => 'required_without:email|nullable|string', @@ -1057,4 +939,35 @@ public function languages(Request $request) ], ], 200); } + + /** + * E.g. "14/05/1990" -> "1990-05-14" + */ + private function normaliseDateForController(?string $raw): ?string + { + if (empty($raw)) { + return null; + } + $raw = trim($raw); + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw)) { + return $raw; + } + try { + if (preg_match('/^(\d{1,2})[\/\- ]([A-Za-z0-9]+)[\/\- ](\d{4})$/', $raw, $m)) { + $day = str_pad($m[1], 2, '0', STR_PAD_LEFT); + $month = $m[2]; + $year = $m[3]; + if (is_numeric($month)) { + $month = str_pad($month, 2, '0', STR_PAD_LEFT); + return "{$year}-{$month}-{$day}"; + } + $ts = strtotime("{$day} {$month} {$year}"); + return $ts ? date('Y-m-d', $ts) : $raw; + } + $dt = new \DateTime($raw); + return $dt->format('Y-m-d'); + } catch (\Exception $e) { + return $raw; + } + } } diff --git a/app/Models/WorkerDocument.php b/app/Models/WorkerDocument.php index a35135c..8bf38f6 100644 --- a/app/Models/WorkerDocument.php +++ b/app/Models/WorkerDocument.php @@ -17,6 +17,11 @@ class WorkerDocument extends Model 'expiry_date', 'ocr_accuracy', 'file_path', + 'ocr_data', + ]; + + protected $casts = [ + 'ocr_data' => 'array', ]; public function worker() diff --git a/app/Services/OcrDocumentService.php b/app/Services/OcrDocumentService.php index ecea293..de9eba3 100644 --- a/app/Services/OcrDocumentService.php +++ b/app/Services/OcrDocumentService.php @@ -21,7 +21,7 @@ public static function extractData(UploadedFile $file): array } // ------------------------------------------------------------------------- - // Dedicated Passport OCR → http://192.168.0.252:5000/passport + // Dedicated Passport OCR API // ------------------------------------------------------------------------- /** @@ -52,7 +52,10 @@ public static function extractPassportData(UploadedFile $file): array ]; try { - $apiUrl = config('services.ocr.passport_url', 'http://192.168.0.252:5000/passport'); + $apiUrl = config('services.ocr.passport_url'); + if (empty($apiUrl)) { + throw new \Exception('Passport OCR API URL is not configured.'); + } $contents = $file->getContent(); if ($contents === '' || $contents === false) { $contents = ' '; @@ -191,7 +194,7 @@ private static function normaliseDate(string $raw): string // ------------------------------------------------------------------------- - // Dedicated Visa OCR → http://192.168.0.252:5000/visa + // Dedicated Visa OCR API // ------------------------------------------------------------------------- /** @@ -220,7 +223,10 @@ public static function extractVisaData(UploadedFile $file): array ]; try { - $apiUrl = config('services.ocr.visa_url', 'http://192.168.0.252:5000/visa'); + $apiUrl = config('services.ocr.visa_url'); + if (empty($apiUrl)) { + throw new \Exception('Visa OCR API URL is not configured.'); + } $contents = $file->getContent(); if ($contents === '' || $contents === false) { $contents = ' '; @@ -293,7 +299,7 @@ public static function extractVisaData(UploadedFile $file): array } // ------------------------------------------------------------------------- - // Dedicated Emirates ID Front OCR → http://192.168.0.231:5000/emirates_front + // Dedicated Emirates ID Front OCR API // ------------------------------------------------------------------------- /** @@ -315,7 +321,10 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array ]; try { - $apiUrl = config('services.ocr.emirates_front_url', 'http://192.168.0.231:5000/emirates_front'); + $apiUrl = config('services.ocr.emirates_front_url'); + if (empty($apiUrl)) { + throw new \Exception('Emirates ID Front OCR API URL is not configured.'); + } $contents = $file->getContent(); if ($contents === '' || $contents === false) { $contents = ' '; @@ -396,7 +405,7 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array } // ------------------------------------------------------------------------- - // Dedicated Emirates ID Back OCR → http://192.168.0.231:5000/emirates_back + // Dedicated Emirates ID Back OCR API // ------------------------------------------------------------------------- /** @@ -415,7 +424,10 @@ public static function extractEmiratesIdBackData(UploadedFile $file): array ]; try { - $apiUrl = config('services.ocr.emirates_back_url', 'http://192.168.0.231:5000/emirates_back'); + $apiUrl = config('services.ocr.emirates_back_url'); + if (empty($apiUrl)) { + throw new \Exception('Emirates ID Back OCR API URL is not configured.'); + } $contents = $file->getContent(); if ($contents === '' || $contents === false) { $contents = ' '; @@ -525,7 +537,7 @@ private static function emiratesIdBackTestFallback(): array /** * Extract data from a Sponsor License document. - * Calls http://192.168.0.252:5000/sponsor_license + * Calls the Sponsor License OCR API * * @param UploadedFile $file * @return array{success: bool, expiry_date: string|null, license_number: string|null, organization_name: string|null} @@ -540,7 +552,10 @@ public static function extractSponsorLicenseData(UploadedFile $file): array ]; try { - $apiUrl = config('services.ocr.sponsor_license_url', 'http://192.168.0.252:5000/sponsor_license'); + $apiUrl = config('services.ocr.sponsor_license_url'); + if (empty($apiUrl)) { + throw new \Exception('Sponsor License OCR API URL is not configured.'); + } if (app()->environment('testing')) { return self::sponsorLicenseTestFallback(); } diff --git a/config/services.php b/config/services.php index f79fbf6..808553f 100644 --- a/config/services.php +++ b/config/services.php @@ -36,11 +36,11 @@ ], 'ocr' => [ - 'passport_url' => env('PASSPORT_API_URL', 'http://192.168.0.252:5000/passport'), - 'visa_url' => env('VISA_API_URL', 'http://192.168.0.252:5000/visa'), - 'emirates_front_url' => env('EMIRATES_FRONT_API_URL', 'http://192.168.0.231:5000/emirates_front'), - 'emirates_back_url' => env('EMIRATES_BACK_API_URL', 'http://192.168.0.231:5000/emirates_back'), - 'sponsor_license_url' => env('SPONSOR_LICENSE_API_URL', 'http://192.168.0.252:5000/sponsor_license'), + 'passport_url' => env('PASSPORT_API_URL'), + 'visa_url' => env('VISA_API_URL'), + 'emirates_front_url' => env('EMIRATES_FRONT_API_URL'), + 'emirates_back_url' => env('EMIRATES_BACK_API_URL'), + 'sponsor_license_url' => env('SPONSOR_LICENSE_API_URL'), ], ]; diff --git a/database/migrations/2026_06_18_143000_add_ocr_data_to_worker_documents_table.php b/database/migrations/2026_06_18_143000_add_ocr_data_to_worker_documents_table.php new file mode 100644 index 0000000..264ba24 --- /dev/null +++ b/database/migrations/2026_06_18_143000_add_ocr_data_to_worker_documents_table.php @@ -0,0 +1,28 @@ +json('ocr_data')->nullable()->after('file_path'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('worker_documents', function (Blueprint $table) { + $table->dropColumn('ocr_data'); + }); + } +}; diff --git a/public/swagger.json b/public/swagger.json index d4953cc..acd5b93 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -19,9 +19,11 @@ "paths": { "/sponsors/register": { "post": { - "tags": ["Sponsor/Auth"], + "tags": [ + "Sponsor/Auth" + ], "summary": "Register Sponsor Account", - "description": "Registers a new sponsor with basic information and uploads their organization/trade license. Sponsors have restricted access — dashboard and charity events only. No payment required.", + "description": "Registers a new sponsor with basic information and uploads their organization/trade license. Sponsors have restricted access \u2014 dashboard and charity events only. No payment required.", "security": [], "requestBody": { "required": true, @@ -49,7 +51,7 @@ "mobile": { "type": "string", "example": "+971501112233", - "description": "Mobile phone number — must be unique. (REQUIRED)" + "description": "Mobile phone number \u2014 must be unique. (REQUIRED)" }, "password": { "type": "string", @@ -122,13 +124,24 @@ "schema": { "type": "object", "properties": { - "success": {"type": "boolean", "example": true}, - "message": {"type": "string", "example": "Sponsor account registered successfully. Your license is pending admin review."}, + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Sponsor account registered successfully. Your license is pending admin review." + }, "data": { "type": "object", "properties": { - "sponsor": {"$ref": "#/components/schemas/Sponsor"}, - "token": {"type": "string", "example": "abc123...bearerToken..."} + "sponsor": { + "$ref": "#/components/schemas/Sponsor" + }, + "token": { + "type": "string", + "example": "abc123...bearerToken..." + } } } } @@ -136,7 +149,9 @@ } } }, - "422": {"description": "Validation error (duplicate mobile/email, missing required fields)."} + "422": { + "description": "Validation error (duplicate mobile/email, missing required fields)." + } } } }, @@ -183,27 +198,58 @@ "schema": { "type": "object", "properties": { - "success": {"type": "boolean", "example": true}, - "message": {"type": "string", "example": "Emirates ID front uploaded and processed successfully."}, + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Emirates ID front uploaded and processed successfully." + }, "ocr_extracted_data": { "type": "object", "properties": { - "success": {"type": "boolean", "example": true}, - "emirates_id_number": {"type": "string", "example": "784-1988-5310327-2"}, - "name": {"type": "string", "example": "KRISHNA PRASAD"}, - "nationality": {"type": "string", "example": "NPL"}, - "date_of_birth": {"type": "string", "example": "1988-03-22"}, - "ocr_accuracy": {"type": "number", "example": 98.5} + "success": { + "type": "boolean", + "example": true + }, + "emirates_id_number": { + "type": "string", + "example": "784-1988-5310327-2" + }, + "name": { + "type": "string", + "example": "KRISHNA PRASAD" + }, + "nationality": { + "type": "string", + "example": "NPL" + }, + "date_of_birth": { + "type": "string", + "example": "1988-03-22" + }, + "ocr_accuracy": { + "type": "number", + "example": 98.5 + } } }, - "email": {"type": "string", "example": "info@alrashidi.ae"} + "email": { + "type": "string", + "example": "info@alrashidi.ae" + } } } } } }, - "404": {"description": "Sponsor account not found."}, - "422": {"description": "Validation error."} + "404": { + "description": "Sponsor account not found." + }, + "422": { + "description": "Validation error." + } } } }, @@ -250,25 +296,50 @@ "schema": { "type": "object", "properties": { - "success": {"type": "boolean", "example": true}, - "message": {"type": "string", "example": "Emirates ID back uploaded and processed successfully."}, + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Emirates ID back uploaded and processed successfully." + }, "ocr_extracted_data": { "type": "object", "properties": { - "success": {"type": "boolean", "example": true}, - "expiry_date": {"type": "string", "example": "2028-04-11"}, - "issue_date": {"type": "string", "example": "2023-04-11"}, - "ocr_accuracy": {"type": "number", "example": 98.5} + "success": { + "type": "boolean", + "example": true + }, + "expiry_date": { + "type": "string", + "example": "2028-04-11" + }, + "issue_date": { + "type": "string", + "example": "2023-04-11" + }, + "ocr_accuracy": { + "type": "number", + "example": 98.5 + } } }, - "email": {"type": "string", "example": "info@alrashidi.ae"} + "email": { + "type": "string", + "example": "info@alrashidi.ae" + } } } } } }, - "404": {"description": "Sponsor account not found."}, - "422": {"description": "Validation error."} + "404": { + "description": "Sponsor account not found." + }, + "422": { + "description": "Validation error." + } } } }, @@ -315,25 +386,50 @@ "schema": { "type": "object", "properties": { - "success": {"type": "boolean", "example": true}, - "message": {"type": "string", "example": "License uploaded and processed successfully."}, + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "License uploaded and processed successfully." + }, "ocr_extracted_data": { "type": "object", "properties": { - "success": {"type": "boolean", "example": true}, - "expiry_date": {"type": "string", "example": "2028-06-12"}, - "license_number": {"type": "string", "example": "123456"}, - "organization_name": {"type": "string", "example": "Charity Co"} + "success": { + "type": "boolean", + "example": true + }, + "expiry_date": { + "type": "string", + "example": "2028-06-12" + }, + "license_number": { + "type": "string", + "example": "123456" + }, + "organization_name": { + "type": "string", + "example": "Charity Co" + } } }, - "email": {"type": "string", "example": "info@alrashidi.ae"} + "email": { + "type": "string", + "example": "info@alrashidi.ae" + } } } } } }, - "404": {"description": "Sponsor account not found."}, - "422": {"description": "Validation error."} + "404": { + "description": "Sponsor account not found." + }, + "422": { + "description": "Validation error." + } } } }, @@ -398,7 +494,10 @@ "role": { "type": "string", "example": "sponsor", - "enum": ["employer", "sponsor"] + "enum": [ + "employer", + "sponsor" + ] }, "data": { "type": "object", @@ -419,7 +518,9 @@ }, "/sponsors/dashboard": { "get": { - "tags": ["Sponsor"], + "tags": [ + "Sponsor" + ], "summary": "Sponsor Dashboard", "description": "Returns the sponsor profile summary, latest charity events, and total & active counts for employers and workers.", "responses": { @@ -481,23 +582,52 @@ } } }, - "401": {"description": "Unauthenticated."} + "401": { + "description": "Unauthenticated." + } } } }, "/sponsors/charity-events": { "get": { - "tags": ["Sponsor"], + "tags": [ + "Sponsor" + ], "summary": "List Charity Events", "description": "Returns a paginated list of all charity and announcement events. Optionally filter by type.", "parameters": [ - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 15}}, - {"name": "type", "in": "query", "schema": {"type": "string"}, "description": "Optional event type filter (e.g. 'charity', 'update')."} + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 15 + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Optional event type filter (e.g. 'charity', 'update')." + } ], "responses": { - "200": {"description": "Events retrieved successfully."}, - "401": {"description": "Unauthenticated."} + "200": { + "description": "Events retrieved successfully." + }, + "401": { + "description": "Unauthenticated." + } } }, "post": { @@ -587,12 +717,18 @@ }, "/sponsors/profile": { "get": { - "tags": ["Sponsor"], + "tags": [ + "Sponsor" + ], "summary": "Get Sponsor Profile", "description": "Returns the full profile of the authenticated sponsor.", "responses": { - "200": {"description": "Profile retrieved successfully."}, - "401": {"description": "Unauthenticated."} + "200": { + "description": "Profile retrieved successfully." + }, + "401": { + "description": "Unauthenticated." + } } } }, @@ -602,7 +738,7 @@ "Worker/Auth" ], "summary": "Register Worker Account (Step 1)", - "description": "Creates the worker account and returns a secure Bearer token.\n\nDocument upload is a **separate step** after registration:\n- `POST /workers/register/passport` — upload passport (OCR via `http://192.168.0.252:5000/passport`)\n- `POST /workers/register/visa` — upload visa (OCR via `http://192.168.0.252:5000/visa`)\n\nBoth document endpoints require the Bearer token returned here.", + "description": "Creates the worker account and returns a secure Bearer token.\n\nDocument upload is a **separate step** after registration:\n- `POST /workers/register/passport` \u2014 upload passport (OCR-processed)\n- `POST /workers/register/visa` \u2014 upload visa (OCR-processed)\n\nBoth document endpoints require the Bearer token returned here.", "security": [], "requestBody": { "required": true, @@ -687,13 +823,20 @@ }, "live_in_out": { "type": "string", - "enum": ["live_in", "live_out"], + "enum": [ + "live_in", + "live_out" + ], "example": "live_out", "description": "Accommodation preference (live_in, live_out)." }, "gender": { "type": "string", - "enum": ["male", "female", "other"], + "enum": [ + "male", + "female", + "other" + ], "example": "male", "description": "Gender of the worker (male, female, other)." }, @@ -706,6 +849,92 @@ "type": "string", "example": "fcm_token_example", "description": "Firebase Cloud Messaging token for push notifications." + }, + "passport": { + "type": "object", + "description": "Optional JSON object containing extracted passport details.", + "properties": { + "authority": { + "type": "string" + }, + "date_of_birth": { + "type": "string", + "example": "14/05/1990" + }, + "date_of_expiry": { + "type": "string", + "example": "05/07/2022" + }, + "date_of_issue": { + "type": "string", + "example": "05/07/2017" + }, + "document_type": { + "type": "string" + }, + "given_names": { + "type": "string" + }, + "issuing_country": { + "type": "string" + }, + "nationality": { + "type": "string" + }, + "passport_number": { + "type": "string" + }, + "personal_number": { + "type": "string" + }, + "place_of_birth": { + "type": "string" + }, + "sex": { + "type": "string" + }, + "surname": { + "type": "string" + } + } + }, + "visa": { + "type": "object", + "description": "Optional JSON object containing extracted visa details.", + "properties": { + "accompanied_by": { + "type": "string" + }, + "expiry_date": { + "type": "string", + "example": "2024-02-15" + }, + "file_number": { + "type": "string" + }, + "id_number": { + "type": "string" + }, + "issue_date": { + "type": "string", + "example": "2022-02-16" + }, + "name": { + "type": "string" + }, + "passport_number": { + "type": "string" + }, + "place_of_issue": { + "type": "string" + }, + "profession": { + "type": "string" + }, + "sponsor": { + "type": "string" + } + } } } } @@ -746,7 +975,7 @@ } }, "422": { - "description": "Validation error — field validation failed or Passport OCR extraction failed.", + "description": "Validation error \u2014 field validation failed or Passport OCR extraction failed.", "content": { "application/json": { "schema": { @@ -765,13 +994,21 @@ "properties": { "phone": { "type": "array", - "items": {"type": "string"}, - "example": ["This mobile number is already registered."] + "items": { + "type": "string" + }, + "example": [ + "This mobile number is already registered." + ] }, "name": { "type": "array", - "items": {"type": "string"}, - "example": ["The name field is required."] + "items": { + "type": "string" + }, + "example": [ + "The name field is required." + ] } } } @@ -787,9 +1024,18 @@ "schema": { "type": "object", "properties": { - "success": {"type": "boolean", "example": false}, - "message": {"type": "string", "example": "An error occurred during worker registration. Please try again."}, - "error": {"type": "string", "example": "Internal Server Error"} + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "An error occurred during worker registration. Please try again." + }, + "error": { + "type": "string", + "example": "Internal Server Error" + } } } } @@ -798,179 +1044,6 @@ } } }, - "/workers/register/passport": { - "post": { - "tags": ["Worker/Auth"], - "summary": "Upload Worker Passport (Step 2)", - "description": "**Authenticated — requires Bearer token from `POST /workers/register`.**\n\nUploads the worker's passport and forwards it to the Passport OCR API (`http://192.168.0.252:5000/passport`) to extract: passport number, expiry date, date of birth (→ age), nationality (ISO-3 → demonym), and full name.\n\nThe extracted data is saved as a `passport` document record (`updateOrCreate`). When `sync_profile` is `true` (default), the worker's `name`, `nationality`, and `age` are also updated from OCR values.", - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": ["passport_file"], - "properties": { - "passport_file": { - "type": "string", - "format": "binary", - "description": "Passport scan or photo (jpg/png/pdf, max 10MB). Forwarded to http://192.168.0.252:5000/passport for OCR. (REQUIRED)" - }, - "sync_profile": { - "type": "boolean", - "example": true, - "description": "If true (default), updates worker name, nationality, and age from OCR results." - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Passport uploaded and OCR-processed successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": {"type": "boolean", "example": true}, - "message": {"type": "string", "example": "Passport uploaded and processed successfully."}, - "data": { - "type": "object", - "properties": { - "worker": {"$ref": "#/components/schemas/Worker"}, - "passport": { - "type": "object", - "properties": { - "type": {"type": "string", "example": "passport"}, - "number": {"type": "string", "example": "A12345678"}, - "issue_date": {"type": "string", "format": "date", "example": "2018-06-01"}, - "expiry_date": {"type": "string", "format": "date", "example": "2028-06-01"}, - "ocr_accuracy": {"type": "number", "example": 98.5}, - "document_number": {"type": "string", "example": "A12345678"}, - "date_of_birth": {"type": "string", "example": "1995-03-14"}, - "nationality": {"type": "string", "example": "IND"}, - "name": {"type": "string", "example": "AMINA AL-MASRY"} - } - } - - - } - } - } - } - } - } - }, - "422": { - "description": "Validation error or Passport OCR API failure.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": {"type": "boolean", "example": false}, - "message": {"type": "string", "example": "Passport OCR extraction failed. Please upload a clear image and try again."}, - "errors": { - "type": "object", - "properties": { - "passport_file": {"type": "array", "items": {"type": "string"}, "example": ["Passport OCR API could not read the document. Please try again."]} - } - } - } - } - } - } - }, - "401": {"description": "Unauthenticated — missing or invalid Bearer token."}, - "500": {"description": "Server error during passport processing."} - } - } - }, - "/workers/register/visa": { - "post": { - "tags": ["Worker/Auth"], - "summary": "Upload Worker Visa (Step 3)", - "description": "**Authenticated — requires Bearer token from `POST /workers/register`.**\n\nUploads the worker's visa and forwards it to the Visa OCR API (`http://192.168.0.252:5000/visa`) to extract: visa number, issue date, expiry date, and visa type.\n\nThe extracted data is saved as a `visa` document record (`updateOrCreate` — safe to call multiple times).", - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": ["visa_file"], - "properties": { - "visa_file": { - "type": "string", - "format": "binary", - "description": "Visa scan or photo (jpg/png/pdf, max 10MB). Forwarded to http://192.168.0.252:5000/visa for OCR. (REQUIRED)" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Visa uploaded and OCR-processed successfully.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": {"type": "boolean", "example": true}, - "message": {"type": "string", "example": "Visa uploaded and processed successfully."}, - "data": { - "type": "object", - "properties": { - "worker": {"$ref": "#/components/schemas/Worker"}, - "visa": { - "type": "object", - "properties": { - "type": {"type": "string", "example": "visa"}, - "number": {"type": "string", "example": "V98765432"}, - "issue_date": {"type": "string", "format": "date", "example": "2023-01-15"}, - "expiry_date": {"type": "string", "format": "date", "example": "2025-01-14"}, - "ocr_accuracy": {"type": "number", "example": 98.5}, - "document_number": {"type": "string", "example": "V98765432"}, - "visa_type": {"type": "string", "example": "Residence"} - } - } - - - } - } - } - } - } - } - }, - "422": { - "description": "Validation error.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": {"type": "boolean", "example": false}, - "message": {"type": "string", "example": "Validation error."}, - "errors": { - "type": "object", - "properties": { - "visa_file": {"type": "array", "items": {"type": "string"}, "example": ["The visa file field is required."]} - } - } - } - } - } - } - }, - "401": {"description": "Unauthenticated — missing or invalid Bearer token."}, - "500": {"description": "Server error during visa processing."} - } - } - }, "/workers/login": { "post": { "tags": [ @@ -1098,7 +1171,7 @@ }, "name": { "type": "string", - "example": "Hindi (हिन्दी)" + "example": "Hindi (\u0939\u093f\u0928\u094d\u0926\u0940)" } } } @@ -1941,13 +2014,20 @@ }, "gender": { "type": "string", - "enum": ["male", "female", "other"], + "enum": [ + "male", + "female", + "other" + ], "example": "male", "description": "Gender of the worker (male, female, other)." }, "live_in_out": { "type": "string", - "enum": ["live_in", "live_out"], + "enum": [ + "live_in", + "live_out" + ], "example": "live_out", "description": "Accommodation preference (live_in, live_out)." }, @@ -2083,7 +2163,6 @@ "type": "string", "example": "Highly certified housekeeper and babysitter with cooking experience." }, - "skills": { "type": "array", "items": { @@ -2840,7 +2919,6 @@ } } }, - "/employers/register/emirates-front": { "post": { "tags": [ @@ -3159,7 +3237,10 @@ "role": { "type": "string", "example": "employer", - "enum": ["employer", "sponsor"] + "enum": [ + "employer", + "sponsor" + ] }, "data": { "type": "object", @@ -5021,21 +5102,66 @@ "Sponsor": { "type": "object", "properties": { - "id": {"type": "integer", "example": 1}, - "full_name": {"type": "string", "example": "Mohammed Al-Rashidi"}, - "organization_name": {"type": "string", "example": "Al-Rashidi Charitable Foundation"}, - "email": {"type": "string", "example": "sponsor.971501112233@migrant.ae"}, - "mobile": {"type": "string", "example": "+971501112233"}, - "nationality": {"type": "string", "example": "UAE"}, - "city": {"type": "string", "example": "Dubai"}, - "address": {"type": "string", "example": "Villa 12, Jumeirah 2"}, - "country_code": {"type": "string", "example": "+971"}, - "is_verified": {"type": "boolean", "example": false}, - "status": {"type": "string", "example": "active"}, - "license_file": {"type": "string", "example": "uploads/licenses/1234567890_license_trade.pdf"}, - "emirates_id_file": {"type": "string", "example": "uploads/licenses/1234567890_emirates_id.pdf"}, - "fcm_token": {"type": "string", "example": "fcm_token_example"}, - "created_at": {"type": "string", "format": "date-time"} + "id": { + "type": "integer", + "example": 1 + }, + "full_name": { + "type": "string", + "example": "Mohammed Al-Rashidi" + }, + "organization_name": { + "type": "string", + "example": "Al-Rashidi Charitable Foundation" + }, + "email": { + "type": "string", + "example": "sponsor.971501112233@migrant.ae" + }, + "mobile": { + "type": "string", + "example": "+971501112233" + }, + "nationality": { + "type": "string", + "example": "UAE" + }, + "city": { + "type": "string", + "example": "Dubai" + }, + "address": { + "type": "string", + "example": "Villa 12, Jumeirah 2" + }, + "country_code": { + "type": "string", + "example": "+971" + }, + "is_verified": { + "type": "boolean", + "example": false + }, + "status": { + "type": "string", + "example": "active" + }, + "license_file": { + "type": "string", + "example": "uploads/licenses/1234567890_license_trade.pdf" + }, + "emirates_id_file": { + "type": "string", + "example": "uploads/licenses/1234567890_emirates_id.pdf" + }, + "fcm_token": { + "type": "string", + "example": "fcm_token_example" + }, + "created_at": { + "type": "string", + "format": "date-time" + } } }, "Worker": { @@ -5093,7 +5219,6 @@ "type": "string", "example": "Certified infant caregiver and housekeeper with excellent cooking skills." }, - "verified": { "type": "boolean", "example": false @@ -5128,7 +5253,11 @@ }, "gender": { "type": "string", - "enum": ["male", "female", "other"], + "enum": [ + "male", + "female", + "other" + ], "example": "male" }, "preferred_location": { diff --git a/resources/js/Pages/Admin/Workers/Index.jsx b/resources/js/Pages/Admin/Workers/Index.jsx index 7b8f8f8..e52102d 100644 --- a/resources/js/Pages/Admin/Workers/Index.jsx +++ b/resources/js/Pages/Admin/Workers/Index.jsx @@ -428,7 +428,7 @@ export default function WorkerManagement({ workers }) { {selectedWorker?.verified ? 'Verified' : 'Pending'} -

{selectedWorker?.nationality} • ID #{selectedWorker?.id}

+

{selectedWorker?.nationality}

@@ -714,6 +714,134 @@ export default function WorkerManagement({ workers }) { + {/* Passport Details */} +
+

Passport Details

+ {(() => { + const passportDoc = selectedWorker?.documents?.find(d => d.type === 'passport'); + if (!passportDoc) { + return ( +
+ No passport details available. +
+ ); + } + return ( +
+
+
+ Passport Number + {passportDoc.ocr_data?.passport_number || passportDoc.number || 'N/A'} +
+
+ Given Names + {passportDoc.ocr_data?.given_names || 'N/A'} +
+
+ Date of Birth + {passportDoc.ocr_data?.date_of_birth || 'N/A'} +
+
+ Nationality + {passportDoc.ocr_data?.nationality || 'N/A'} +
+
+ Issuing Country + {passportDoc.ocr_data?.issuing_country || 'N/A'} +
+
+ Place of Birth + {passportDoc.ocr_data?.place_of_birth || 'N/A'} +
+
+ Date of Issue + {passportDoc.ocr_data?.date_of_issue || passportDoc.issue_date || 'N/A'} +
+
+ Date of Expiry + {passportDoc.ocr_data?.date_of_expiry || passportDoc.expiry_date || 'N/A'} +
+
+ {passportDoc.file_path && ( +
+ + + View Passport File + +
+ )} +
+ ); + })()} +
+ + {/* Visa Details */} +
+

Visa Details

+ {(() => { + const visaDoc = selectedWorker?.documents?.find(d => d.type === 'visa'); + if (!visaDoc) { + return ( +
+ No visa details available. +
+ ); + } + return ( +
+
+
+ File Number + {visaDoc.ocr_data?.file_number || visaDoc.number || 'N/A'} +
+
+ ID Number + {visaDoc.ocr_data?.id_number || 'N/A'} +
+
+ Issue Date + {visaDoc.ocr_data?.issue_date || visaDoc.issue_date || 'N/A'} +
+
+ Expiry Date + {visaDoc.ocr_data?.expiry_date || visaDoc.expiry_date || 'N/A'} +
+
+ Passport Number + {visaDoc.ocr_data?.passport_number || 'N/A'} +
+
+ Place of Issue + {visaDoc.ocr_data?.place_of_issue || 'N/A'} +
+
+ Sponsor + {visaDoc.ocr_data?.sponsor || 'N/A'} +
+
+ {visaDoc.file_path && ( +
+ + + View Visa File + +
+ )} +
+ ); + })()} +
+ {/* Compliance Notes */}