admin panel, worker, employer field changes
This commit is contained in:
parent
8444347fb1
commit
8fb9b9dc26
@ -330,7 +330,34 @@ public function register(Request $request)
|
||||
return $query->where('type', 'passport');
|
||||
}),
|
||||
],
|
||||
'visa' => 'nullable|array',
|
||||
'passport.document_type' => 'nullable|string|max:10',
|
||||
'passport.issuing_country' => 'nullable|string|max:10',
|
||||
'passport.surname' => 'nullable|string|max:255',
|
||||
'passport.given_names' => 'nullable|string|max:255',
|
||||
'passport.nationality' => 'nullable|string|max:100',
|
||||
'passport.date_of_birth' => 'nullable|string',
|
||||
'passport.date_of_expiry' => 'nullable|string',
|
||||
'passport.place_of_birth' => 'nullable|string|max:255',
|
||||
'passport.authority' => 'nullable|string|max:255',
|
||||
'passport.sex' => 'nullable|string|max:10',
|
||||
'passport.date_of_issue' => 'nullable|string',
|
||||
'passport.ocr_accuracy' => 'nullable|numeric',
|
||||
'visa' => 'nullable|array',
|
||||
'visa.document_type' => 'nullable|string|max:100',
|
||||
'visa.country' => 'nullable|string|max:100',
|
||||
'visa.visa_type' => 'nullable|string|max:100',
|
||||
'visa.entry_permit_no' => 'nullable|string|max:100',
|
||||
'visa.issue_date' => 'nullable|string',
|
||||
'visa.valid_until' => 'nullable|string',
|
||||
'visa.uid_no' => 'nullable|string|max:100',
|
||||
'visa.full_name' => 'nullable|string|max:255',
|
||||
'visa.nationality' => 'nullable|string|max:100',
|
||||
'visa.place_of_birth' => 'nullable|string|max:255',
|
||||
'visa.date_of_birth' => 'nullable|string',
|
||||
'visa.passport_no' => 'nullable|string|max:100',
|
||||
'visa.profession' => 'nullable|string|max:255',
|
||||
'visa.sponsor_name' => 'nullable|string|max:255',
|
||||
'visa.ocr_accuracy' => 'nullable|numeric',
|
||||
], [
|
||||
'phone.unique' => 'This mobile number is already registered.',
|
||||
'passport.passport_number.unique' => 'This passport number is already registered.',
|
||||
@ -462,7 +489,7 @@ public function register(Request $request)
|
||||
'number' => $passportNum,
|
||||
'issue_date' => $issueDateFormatted,
|
||||
'expiry_date' => $expiryDateFormatted,
|
||||
'ocr_accuracy' => 99.0,
|
||||
'ocr_accuracy' => isset($passportDataInput['ocr_accuracy']) ? (float)$passportDataInput['ocr_accuracy'] : 99.0,
|
||||
'file_path' => null,
|
||||
'ocr_data' => $passportDataInput,
|
||||
]);
|
||||
@ -470,8 +497,8 @@ public function register(Request $request)
|
||||
|
||||
// Create visa document if provided
|
||||
if ($visaDataInput) {
|
||||
$visaNum = $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999));
|
||||
$expiryDate = $visaDataInput['expiry_date'] ?? null;
|
||||
$visaNum = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? ('V' . rand(1000000, 9999999));
|
||||
$expiryDate = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null;
|
||||
$issueDate = $visaDataInput['issue_date'] ?? null;
|
||||
|
||||
$expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(2)->toDateString();
|
||||
@ -482,7 +509,7 @@ public function register(Request $request)
|
||||
'number' => $visaNum,
|
||||
'issue_date' => $issueDateFormatted,
|
||||
'expiry_date' => $expiryDateFormatted,
|
||||
'ocr_accuracy' => 99.0,
|
||||
'ocr_accuracy' => isset($visaDataInput['ocr_accuracy']) ? (float)$visaDataInput['ocr_accuracy'] : 99.0,
|
||||
'file_path' => null,
|
||||
'ocr_data' => $visaDataInput,
|
||||
]);
|
||||
|
||||
@ -290,17 +290,35 @@ public function uploadEmiratesId(Request $request)
|
||||
|
||||
$extractedIdNumber = null;
|
||||
$extractedExpiry = null;
|
||||
$extractedName = null;
|
||||
$extractedDob = null;
|
||||
$extractedNationality = null;
|
||||
$extractedIssueDate = null;
|
||||
$extractedEmployer = null;
|
||||
$extractedIssuePlace = null;
|
||||
$extractedOccupation = null;
|
||||
$extractedCardNumber = null;
|
||||
$extractedGender = null;
|
||||
|
||||
if ($request->hasFile('emirates_id_front')) {
|
||||
$frontFile = $request->file('emirates_id_front');
|
||||
$frontOcr = \App\Services\OcrDocumentService::extractEmiratesIdFrontData($frontFile);
|
||||
$extractedIdNumber = $frontOcr['emirates_id_number'];
|
||||
$extractedName = $frontOcr['name'] ?? null;
|
||||
$extractedDob = $frontOcr['date_of_birth'] ?? null;
|
||||
$extractedNationality = $frontOcr['nationality'] ?? null;
|
||||
$extractedCardNumber = $frontOcr['card_number'] ?? null;
|
||||
$extractedGender = $frontOcr['gender'] ?? null;
|
||||
$extractedOccupation = $frontOcr['occupation'] ?? null;
|
||||
$extractedEmployer = $frontOcr['employer'] ?? null;
|
||||
$extractedIssuePlace = $frontOcr['issue_place'] ?? null;
|
||||
}
|
||||
|
||||
if ($request->hasFile('emirates_id_back')) {
|
||||
$backFile = $request->file('emirates_id_back');
|
||||
$backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($backFile);
|
||||
$extractedExpiry = $backOcr['expiry_date'];
|
||||
$extractedIssueDate = $backOcr['issue_date'] ?? null;
|
||||
}
|
||||
|
||||
if ($request->hasFile('emirates_id_file')) {
|
||||
@ -309,12 +327,30 @@ public function uploadEmiratesId(Request $request)
|
||||
$backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($file);
|
||||
$extractedIdNumber = $frontOcr['emirates_id_number'];
|
||||
$extractedExpiry = $backOcr['expiry_date'];
|
||||
$extractedName = $frontOcr['name'] ?? null;
|
||||
$extractedDob = $frontOcr['date_of_birth'] ?? null;
|
||||
$extractedNationality = $frontOcr['nationality'] ?? null;
|
||||
$extractedCardNumber = $frontOcr['card_number'] ?? null;
|
||||
$extractedGender = $frontOcr['gender'] ?? null;
|
||||
$extractedOccupation = $frontOcr['occupation'] ?? null;
|
||||
$extractedEmployer = $frontOcr['employer'] ?? null;
|
||||
$extractedIssuePlace = $frontOcr['issue_place'] ?? null;
|
||||
$extractedIssueDate = $backOcr['issue_date'] ?? null;
|
||||
}
|
||||
|
||||
$pending = session('pending_employer_registration');
|
||||
$pending['emirates_id_file'] = null;
|
||||
$pending['emirates_id_number'] = $extractedIdNumber;
|
||||
$pending['emirates_id_expiry'] = $extractedExpiry;
|
||||
$pending['emirates_id_name'] = $extractedName;
|
||||
$pending['emirates_id_dob'] = $extractedDob;
|
||||
$pending['emirates_id_nationality'] = $extractedNationality;
|
||||
$pending['emirates_id_card_number'] = $extractedCardNumber;
|
||||
$pending['emirates_id_gender'] = $extractedGender;
|
||||
$pending['emirates_id_occupation'] = $extractedOccupation;
|
||||
$pending['emirates_id_employer'] = $extractedEmployer;
|
||||
$pending['emirates_id_issue_place'] = $extractedIssuePlace;
|
||||
$pending['emirates_id_issue_date'] = $extractedIssueDate;
|
||||
session(['pending_employer_registration' => $pending]);
|
||||
session(['employer_emirates_id_uploaded' => true]);
|
||||
|
||||
@ -438,6 +474,13 @@ public function createPassword(Request $request)
|
||||
'emirates_id_front' => null, // We did not store the file
|
||||
'emirates_id_number' => $pending['emirates_id_number'] ?? null,
|
||||
'emirates_id_expiry' => $pending['emirates_id_expiry'] ?? null,
|
||||
'emirates_id_name' => $pending['emirates_id_name'] ?? null,
|
||||
'emirates_id_dob' => $pending['emirates_id_dob'] ?? null,
|
||||
'emirates_id_issue_date' => $pending['emirates_id_issue_date'] ?? null,
|
||||
'emirates_id_employer' => $pending['emirates_id_employer'] ?? null,
|
||||
'emirates_id_issue_place' => $pending['emirates_id_issue_place'] ?? null,
|
||||
'emirates_id_occupation' => $pending['emirates_id_occupation'] ?? null,
|
||||
'nationality' => $pending['emirates_id_nationality'] ?? null,
|
||||
'address' => $pending['address'] ?? null,
|
||||
]);
|
||||
|
||||
@ -459,6 +502,15 @@ public function createPassword(Request $request)
|
||||
'last_login_at' => now(),
|
||||
'emirates_id_file' => null, // We did not store the file
|
||||
'address' => $pending['address'] ?? null,
|
||||
'emirates_id' => $pending['emirates_id_number'] ?? null,
|
||||
'emirates_id_name' => $pending['emirates_id_name'] ?? null,
|
||||
'emirates_id_dob' => $pending['emirates_id_dob'] ?? null,
|
||||
'emirates_id_issue_date' => $pending['emirates_id_issue_date'] ?? null,
|
||||
'emirates_id_expiry' => $pending['emirates_id_expiry'] ?? null,
|
||||
'emirates_id_employer' => $pending['emirates_id_employer'] ?? null,
|
||||
'emirates_id_issue_place' => $pending['emirates_id_issue_place'] ?? null,
|
||||
'emirates_id_occupation' => $pending['emirates_id_occupation'] ?? null,
|
||||
'nationality' => $pending['emirates_id_nationality'] ?? null,
|
||||
]);
|
||||
|
||||
// Create active subscription in database
|
||||
|
||||
@ -42,15 +42,15 @@ private function buildProfileData($user, $profile)
|
||||
'email' => $user->email,
|
||||
'phone' => $profile->phone ?? '+971509990001',
|
||||
'emirates_id' => [
|
||||
'emirates_id_number' => $profile->emirates_id_number ?? '784-1988-5310327-2',
|
||||
'emirates_id_number' => $profile->emirates_id_number ?? '784-1987-5493842-5',
|
||||
'name' => $profile->emirates_id_name ?? $user->name,
|
||||
'date_of_birth' => $profile->emirates_id_dob ?? '1990-01-01',
|
||||
'nationality' => $profile->nationality ?? 'Emirati',
|
||||
'issue_date' => $profile->emirates_id_issue_date ?? '2023-04-11',
|
||||
'expiry_date' => $profile->emirates_id_expiry ?? '2028-04-11',
|
||||
'employer' => $profile->emirates_id_employer ?? 'Federal Authority',
|
||||
'issue_place' => $profile->emirates_id_issue_place ?? 'Abu Dhabi',
|
||||
'occupation' => $profile->emirates_id_occupation ?? 'Manager',
|
||||
'date_of_birth' => $profile->emirates_id_dob ?? '1987-04-14',
|
||||
'nationality' => $profile->nationality ?? 'Bangladesh',
|
||||
'issue_date' => $profile->emirates_id_issue_date ?? '2025-12-12',
|
||||
'expiry_date' => $profile->emirates_id_expiry ?? '2027-12-11',
|
||||
'employer' => $profile->emirates_id_employer ?? 'Msj International Technical Services L.L.C UAE',
|
||||
'issue_place' => $profile->emirates_id_issue_place ?? 'Dubai',
|
||||
'occupation' => $profile->emirates_id_occupation ?? 'Electrician',
|
||||
],
|
||||
'fcm_token' => $user->fcm_token ?? 'fcm_token_example',
|
||||
'address' => $profile->address ?? 'Villa 45, Street 12',
|
||||
@ -74,15 +74,15 @@ public function index(Request $request)
|
||||
$profile = EmployerProfile::create([
|
||||
'user_id' => $user->id,
|
||||
'phone' => '+971509990001',
|
||||
'emirates_id_number' => '784-1988-5310327-2',
|
||||
'emirates_id_name' => 'Ahmad Bin Ahmed',
|
||||
'emirates_id_dob' => '1990-01-01',
|
||||
'nationality' => 'Emirati',
|
||||
'emirates_id_issue_date' => '2023-04-11',
|
||||
'emirates_id_expiry' => '2028-04-11',
|
||||
'emirates_id_employer' => 'Federal Authority',
|
||||
'emirates_id_issue_place' => 'Abu Dhabi',
|
||||
'emirates_id_occupation' => 'Manager',
|
||||
'emirates_id_number' => '784-1987-5493842-5',
|
||||
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||
'emirates_id_dob' => '1987-04-14',
|
||||
'nationality' => 'Bangladesh',
|
||||
'emirates_id_issue_date' => '2025-12-12',
|
||||
'emirates_id_expiry' => '2027-12-11',
|
||||
'emirates_id_employer' => 'Msj International Technical Services L.L.C UAE',
|
||||
'emirates_id_issue_place' => 'Dubai',
|
||||
'emirates_id_occupation' => 'Electrician',
|
||||
'address' => 'Villa 45, Street 12',
|
||||
]);
|
||||
}
|
||||
@ -106,15 +106,15 @@ public function edit(Request $request)
|
||||
$profile = EmployerProfile::create([
|
||||
'user_id' => $user->id,
|
||||
'phone' => '+971509990001',
|
||||
'emirates_id_number' => '784-1988-5310327-2',
|
||||
'emirates_id_name' => 'Ahmad Bin Ahmed',
|
||||
'emirates_id_dob' => '1990-01-01',
|
||||
'nationality' => 'Emirati',
|
||||
'emirates_id_issue_date' => '2023-04-11',
|
||||
'emirates_id_expiry' => '2028-04-11',
|
||||
'emirates_id_employer' => 'Federal Authority',
|
||||
'emirates_id_issue_place' => 'Abu Dhabi',
|
||||
'emirates_id_occupation' => 'Manager',
|
||||
'emirates_id_number' => '784-1987-5493842-5',
|
||||
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||
'emirates_id_dob' => '1987-04-14',
|
||||
'nationality' => 'Bangladesh',
|
||||
'emirates_id_issue_date' => '2025-12-12',
|
||||
'emirates_id_expiry' => '2027-12-11',
|
||||
'emirates_id_employer' => 'Msj International Technical Services L.L.C UAE',
|
||||
'emirates_id_issue_place' => 'Dubai',
|
||||
'emirates_id_occupation' => 'Electrician',
|
||||
'address' => 'Villa 45, Street 12',
|
||||
]);
|
||||
}
|
||||
|
||||
@ -316,6 +316,11 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
|
||||
'name' => null,
|
||||
'nationality' => null,
|
||||
'date_of_birth' => null,
|
||||
'card_number' => null,
|
||||
'gender' => null,
|
||||
'occupation' => null,
|
||||
'employer' => null,
|
||||
'issue_place' => null,
|
||||
'ocr_accuracy' => 0.0,
|
||||
'success' => false,
|
||||
];
|
||||
@ -376,6 +381,12 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
|
||||
if (!empty($data['date_of_birth'])) {
|
||||
$extracted['date_of_birth'] = self::normaliseDate($data['date_of_birth']);
|
||||
}
|
||||
|
||||
$extracted['card_number'] = $data['card_number'] ?? null;
|
||||
$extracted['gender'] = $data['gender'] ?? $data['sex'] ?? null;
|
||||
$extracted['occupation'] = $data['occupation'] ?? null;
|
||||
$extracted['employer'] = $data['employer'] ?? $data['company_name'] ?? null;
|
||||
$extracted['issue_place'] = $data['issue_place'] ?? $data['issuing_place'] ?? $data['place_of_issue'] ?? null;
|
||||
} else {
|
||||
Log::warning('Emirates ID Front OCR API success=false', ['response' => $json]);
|
||||
}
|
||||
@ -392,13 +403,31 @@ public static function extractEmiratesIdFrontData(UploadedFile $file): array
|
||||
|
||||
// Fallbacks
|
||||
if (empty($extracted['emirates_id_number'])) {
|
||||
$extracted['emirates_id_number'] = '784-' . rand(1975, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
|
||||
$extracted['emirates_id_number'] = '784-1987-5493842-5';
|
||||
}
|
||||
if (empty($extracted['name'])) {
|
||||
$extracted['name'] = 'KRISHNA PRASAD';
|
||||
$extracted['name'] = 'Mohammad Jobaier Mohammad Abul Kalam';
|
||||
}
|
||||
if (empty($extracted['nationality'])) {
|
||||
$extracted['nationality'] = 'NPL';
|
||||
$extracted['nationality'] = 'Bangladesh';
|
||||
}
|
||||
if (empty($extracted['date_of_birth'])) {
|
||||
$extracted['date_of_birth'] = '1987-04-14';
|
||||
}
|
||||
if (empty($extracted['card_number'])) {
|
||||
$extracted['card_number'] = '151023946';
|
||||
}
|
||||
if (empty($extracted['gender'])) {
|
||||
$extracted['gender'] = 'M';
|
||||
}
|
||||
if (empty($extracted['occupation'])) {
|
||||
$extracted['occupation'] = 'Electrician';
|
||||
}
|
||||
if (empty($extracted['employer'])) {
|
||||
$extracted['employer'] = 'Msj International Technical Services L.L.C UAE';
|
||||
}
|
||||
if (empty($extracted['issue_place'])) {
|
||||
$extracted['issue_place'] = 'Dubai';
|
||||
}
|
||||
|
||||
return $extracted;
|
||||
@ -517,10 +546,15 @@ private static function emiratesIdFrontTestFallback(): array
|
||||
{
|
||||
return [
|
||||
'success' => true,
|
||||
'emirates_id_number' => '784-1988-5310327-2',
|
||||
'name' => 'KRISHNA PRASAD',
|
||||
'nationality' => 'NPL',
|
||||
'date_of_birth' => '1988-03-22',
|
||||
'emirates_id_number' => '784-1987-5493842-5',
|
||||
'name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||
'nationality' => 'Bangladesh',
|
||||
'date_of_birth' => '1987-04-14',
|
||||
'card_number' => '151023946',
|
||||
'gender' => 'M',
|
||||
'occupation' => 'Electrician',
|
||||
'employer' => 'Msj International Technical Services L.L.C UAE',
|
||||
'issue_place' => 'Dubai',
|
||||
'ocr_accuracy' => 98.5,
|
||||
];
|
||||
}
|
||||
@ -529,8 +563,8 @@ private static function emiratesIdBackTestFallback(): array
|
||||
{
|
||||
return [
|
||||
'success' => true,
|
||||
'expiry_date' => '2028-04-11',
|
||||
'issue_date' => '2023-04-11',
|
||||
'expiry_date' => '2027-12-11',
|
||||
'issue_date' => '2025-12-12',
|
||||
'ocr_accuracy' => 98.5,
|
||||
];
|
||||
}
|
||||
|
||||
@ -743,6 +743,11 @@
|
||||
"nationality": {
|
||||
"type": "string"
|
||||
},
|
||||
"ocr_accuracy": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
"example": 99.0
|
||||
},
|
||||
"passport_number": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -767,6 +772,22 @@
|
||||
"accompanied_by": {
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
"type": "string",
|
||||
"example": "United Arab Emirates"
|
||||
},
|
||||
"date_of_birth": {
|
||||
"type": "string",
|
||||
"example": "25-JUN-1999"
|
||||
},
|
||||
"document_type": {
|
||||
"type": "string",
|
||||
"example": "uae_visa"
|
||||
},
|
||||
"entry_permit_no": {
|
||||
"type": "string",
|
||||
"example": "77003098 / 2019 / 204"
|
||||
},
|
||||
"expiry_date": {
|
||||
"type": "string",
|
||||
"example": "2024-02-15"
|
||||
@ -774,6 +795,10 @@
|
||||
"file_number": {
|
||||
"type": "string"
|
||||
},
|
||||
"full_name": {
|
||||
"type": "string",
|
||||
"example": "Mr.MUHAMMAD NADEEM RASHEED"
|
||||
},
|
||||
"id_number": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -784,17 +809,50 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"nationality": {
|
||||
"type": "string",
|
||||
"example": "PAKISTAN"
|
||||
},
|
||||
"ocr_accuracy": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
"example": 99.0
|
||||
},
|
||||
"passport_no": {
|
||||
"type": "string",
|
||||
"example": "EN9458281"
|
||||
},
|
||||
"passport_number": {
|
||||
"type": "string"
|
||||
},
|
||||
"place_of_birth": {
|
||||
"type": "string",
|
||||
"example": "SIALKOT PAK"
|
||||
},
|
||||
"place_of_issue": {
|
||||
"type": "string"
|
||||
},
|
||||
"profession": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "SALES REPRESENTATIVE"
|
||||
},
|
||||
"sponsor": {
|
||||
"type": "string"
|
||||
},
|
||||
"sponsor_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"uid_no": {
|
||||
"type": "string",
|
||||
"example": "207404887"
|
||||
},
|
||||
"valid_until": {
|
||||
"type": "string",
|
||||
"example": "04-MAR-2019"
|
||||
},
|
||||
"visa_type": {
|
||||
"type": "string",
|
||||
"example": "ENTRY PERMIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,6 +40,7 @@ export default function AdminLayout({ children, title = 'Dashboard' }) {
|
||||
{ name: 'Support Tickets', href: '/admin/tickets', icon: LifeBuoy },
|
||||
{ name: 'Frequently Asked Questions', href: '/admin/faqs', icon: HelpCircle },
|
||||
{ name: 'Payments', href: '/admin/payments', icon: BadgeDollarSign },
|
||||
{ name: 'Plan Configuration', href: '/admin/subscriptions', icon: CreditCard },
|
||||
{ name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 },
|
||||
{ name: 'System Audit Logs', href: '/admin/audit-logs', icon: History },
|
||||
{ name: 'Charity Events', href: '/admin/events', icon: Heart },
|
||||
|
||||
@ -43,19 +43,78 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [editingPlan, setEditingPlan] = useState(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Form states
|
||||
const [name, setName] = useState('');
|
||||
const [price, setPrice] = useState('');
|
||||
const [duration, setDuration] = useState('Monthly');
|
||||
const [features, setFeatures] = useState('');
|
||||
|
||||
const handleCreateClick = () => {
|
||||
setEditingPlan(null);
|
||||
setName('');
|
||||
setPrice('');
|
||||
setDuration('Monthly');
|
||||
setFeatures('');
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (plan) => {
|
||||
setEditingPlan(plan);
|
||||
setName(plan.name);
|
||||
setPrice(plan.price);
|
||||
setDuration(plan.duration);
|
||||
setFeatures(plan.features.join('\n'));
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
if (confirm('Are you sure you want to delete this plan?')) {
|
||||
// router.delete(`/admin/subscriptions/plans/${id}`);
|
||||
setPlans(plans.filter(p => p.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = (e) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim() || !price) {
|
||||
alert('Please fill out Name and Price.');
|
||||
return;
|
||||
}
|
||||
|
||||
const featuresArray = features
|
||||
.split('\n')
|
||||
.map(f => f.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (editingPlan) {
|
||||
setPlans(plans.map(p => p.id === editingPlan.id ? {
|
||||
...p,
|
||||
name,
|
||||
price: Number(price),
|
||||
duration,
|
||||
features: featuresArray
|
||||
} : p));
|
||||
} else {
|
||||
const newId = name.toLowerCase().replace(/\s+/g, '-');
|
||||
setPlans([...plans, {
|
||||
id: newId || `plan-${Date.now()}`,
|
||||
name,
|
||||
price: Number(price),
|
||||
duration,
|
||||
features: featuresArray,
|
||||
status: 'Active'
|
||||
}]);
|
||||
}
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
const filteredPlans = plans.filter(plan =>
|
||||
plan.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
plan.duration.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
plan.features.some(f => f.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
return (
|
||||
<AdminLayout title="Subscription Management">
|
||||
<Head title="Plans Management" />
|
||||
@ -68,11 +127,13 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search plans..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500/10 focus:border-teal-500 outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { setEditingPlan(null); setIsDialogOpen(true); }}
|
||||
onClick={handleCreateClick}
|
||||
className="bg-[#0F6E56] text-white px-4 py-2 rounded-lg text-sm font-semibold flex items-center justify-center space-x-2 hover:bg-[#085041] transition-colors shadow-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
@ -94,64 +155,72 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{plans.map((plan) => (
|
||||
<TableRow key={plan.id} className="hover:bg-slate-50/50 transition-colors">
|
||||
<TableCell className="py-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
|
||||
plan.id === 'basic' ? 'bg-blue-50 text-blue-600' :
|
||||
plan.id === 'premium' ? 'bg-teal-50 text-[#0F6E56]' :
|
||||
'bg-amber-50 text-amber-600'
|
||||
}`}>
|
||||
{plan.id === 'basic' ? <Zap className="w-4 h-4" /> :
|
||||
plan.id === 'premium' ? <Shield className="w-4 h-4" /> :
|
||||
<Trophy className="w-4 h-4" />}
|
||||
</div>
|
||||
<span className="font-bold text-slate-900 text-sm">{plan.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-bold text-slate-900">{plan.price}</TableCell>
|
||||
<TableCell className="text-sm text-slate-500 font-medium">{plan.duration}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plan.features.map((f, i) => (
|
||||
<span key={i} className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-[10px] font-bold uppercase tracking-tight">
|
||||
{f}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest ${
|
||||
plan.status === 'Active' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-500'
|
||||
}`}>
|
||||
{plan.status}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right pr-6">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
onClick={() => handleEdit(plan)}
|
||||
className="p-2 text-slate-400 hover:text-[#0F6E56] hover:bg-teal-50 rounded-lg transition-all"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(plan.id)}
|
||||
className="p-2 text-slate-400 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{filteredPlans.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-slate-400 text-sm">
|
||||
No plans found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
) : (
|
||||
filteredPlans.map((plan) => (
|
||||
<TableRow key={plan.id} className="hover:bg-slate-50/50 transition-colors">
|
||||
<TableCell className="py-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
|
||||
plan.id === 'basic' ? 'bg-blue-50 text-blue-600' :
|
||||
plan.id === 'premium' ? 'bg-teal-50 text-[#0F6E56]' :
|
||||
'bg-amber-50 text-amber-600'
|
||||
}`}>
|
||||
{plan.id === 'basic' ? <Zap className="w-4 h-4" /> :
|
||||
plan.id === 'premium' ? <Shield className="w-4 h-4" /> :
|
||||
<Trophy className="w-4 h-4" />}
|
||||
</div>
|
||||
<span className="font-bold text-slate-900 text-sm">{plan.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-bold text-slate-900">{plan.price}</TableCell>
|
||||
<TableCell className="text-sm text-slate-500 font-medium">{plan.duration}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plan.features.map((f, i) => (
|
||||
<span key={i} className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-[10px] font-bold uppercase tracking-tight">
|
||||
{f}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest ${
|
||||
plan.status === 'Active' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-500'
|
||||
}`}>
|
||||
{plan.status}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right pr-6">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
onClick={() => handleEdit(plan)}
|
||||
className="p-2 text-slate-400 hover:text-[#0F6E56] hover:bg-teal-50 rounded-lg transition-all"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(plan.id)}
|
||||
className="p-2 text-slate-400 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan Modal (Placeholder UI) */}
|
||||
{/* Plan Modal */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[500px] rounded-[24px]">
|
||||
<DialogHeader>
|
||||
@ -163,44 +232,70 @@ export default function SubscriptionsIndex({ plans: initialPlans }) {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-6 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Plan Name</label>
|
||||
<input className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none" placeholder="e.g. Pro Search" />
|
||||
<form onSubmit={handleSave}>
|
||||
<div className="grid gap-6 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Plan Name</label>
|
||||
<input
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||
placeholder="e.g. Pro Search"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Price (AED)</label>
|
||||
<input
|
||||
required
|
||||
type="number"
|
||||
value={price}
|
||||
onChange={(e) => setPrice(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||
placeholder="299"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Price (AED)</label>
|
||||
<input type="number" className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none" placeholder="299" />
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label>
|
||||
<select
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
||||
>
|
||||
<option value="Monthly">Monthly</option>
|
||||
<option value="Quarterly">Quarterly</option>
|
||||
<option value="Yearly">Yearly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Features (one per line)</label>
|
||||
<textarea
|
||||
value={features}
|
||||
onChange={(e) => setFeatures(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none min-h-[100px]"
|
||||
placeholder="Unlimited search Verified badges"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label>
|
||||
<select className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none">
|
||||
<option>Monthly</option>
|
||||
<option>Quarterly</option>
|
||||
<option>Yearly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Features (one per line)</label>
|
||||
<textarea className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none min-h-[100px]" placeholder="Unlimited search Verified badges" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="sm:justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
className="px-6 py-2.5 rounded-xl text-sm font-bold text-slate-400 uppercase tracking-widest hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="bg-[#0F6E56] text-white px-8 py-2.5 rounded-xl text-sm font-bold uppercase tracking-widest shadow-lg shadow-teal-500/20 active:scale-95 transition-all"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</DialogFooter>
|
||||
<DialogFooter className="sm:justify-end gap-3 pt-4 border-t border-slate-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
className="px-6 py-2.5 rounded-xl text-sm font-bold text-slate-400 uppercase tracking-widest hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-[#0F6E56] text-white px-8 py-2.5 rounded-xl text-sm font-bold uppercase tracking-widest shadow-lg shadow-teal-500/20 active:scale-95 transition-all"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AdminLayout>
|
||||
|
||||
@ -208,7 +208,7 @@ export default function WorkerManagement({ workers }) {
|
||||
})).filter(worker => {
|
||||
const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
worker.email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = statusFilter === 'all' || worker.status === statusFilter;
|
||||
const matchesStatus = statusFilter === 'all' || worker.status?.toLowerCase() === statusFilter.toLowerCase();
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
@ -258,7 +258,7 @@ export default function WorkerManagement({ workers }) {
|
||||
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mr-2">Lifecycle Filter:</span>
|
||||
<div className="flex bg-slate-100 p-1 rounded-xl">
|
||||
{['all', 'active', 'inactive', 'suspended'].map((f) => (
|
||||
{['all', 'active', 'hired', 'suspended'].map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setStatusFilter(f)}
|
||||
@ -377,9 +377,13 @@ export default function WorkerManagement({ workers }) {
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`px-2 py-0.5 rounded-full font-black text-[9px] uppercase tracking-wider block w-fit ${
|
||||
worker.status === 'active'
|
||||
worker.status?.toLowerCase() === 'active'
|
||||
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
||||
: worker.status === 'suspended' ? 'bg-red-100 text-red-800 border-none' : 'bg-slate-50 text-slate-700 border-slate-200'
|
||||
: worker.status?.toLowerCase() === 'hired'
|
||||
? 'bg-blue-50 text-blue-700 border-blue-200'
|
||||
: worker.status?.toLowerCase() === 'suspended'
|
||||
? 'bg-red-100 text-red-800 border-none'
|
||||
: 'bg-slate-50 text-slate-700 border-slate-200'
|
||||
}`}
|
||||
>
|
||||
{worker.status}
|
||||
@ -526,15 +530,7 @@ export default function WorkerManagement({ workers }) {
|
||||
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-bold text-slate-500">Religion</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.religion}
|
||||
onChange={e => setEditForm({ ...editForm, religion: e.target.value })}
|
||||
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-xs font-bold text-slate-700 outline-none focus:ring-2 focus:ring-[#0F6E56]/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -716,10 +712,7 @@ export default function WorkerManagement({ workers }) {
|
||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Visa Status</label>
|
||||
<span className="text-slate-800 font-extrabold">{selectedWorker?.visa_status || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Religion</label>
|
||||
<span className="text-slate-800 font-extrabold">{selectedWorker?.religion || 'N/A'}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Age</label>
|
||||
<span className="text-slate-800 font-extrabold">{selectedWorker?.age || 'N/A'}</span>
|
||||
@ -760,14 +753,26 @@ export default function WorkerManagement({ workers }) {
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
|
||||
<span className="font-mono font-bold text-slate-800">{passportDoc.ocr_data?.passport_number || passportDoc.number || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Surname</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.surname || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Given Names</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.given_names || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sex</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.sex || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Birth</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_birth || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Birth</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.place_of_birth || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Nationality</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.nationality || 'N/A'}</span>
|
||||
@ -776,10 +781,6 @@ export default function WorkerManagement({ workers }) {
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issuing Country</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.issuing_country || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Birth</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.place_of_birth || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Issue</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_issue || passportDoc.issue_date || 'N/A'}</span>
|
||||
@ -788,6 +789,18 @@ export default function WorkerManagement({ workers }) {
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Expiry</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.date_of_expiry || passportDoc.expiry_date || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Authority</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.authority || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Document Type</span>
|
||||
<span className="font-bold text-slate-800">{passportDoc.ocr_data?.document_type || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Personal Number</span>
|
||||
<span className="font-mono font-bold text-slate-800">{passportDoc.ocr_data?.personal_number || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{passportDoc.file_path && (
|
||||
<div className="pt-2 border-t border-slate-200/60 flex justify-end">
|
||||
@ -822,6 +835,14 @@ export default function WorkerManagement({ workers }) {
|
||||
return (
|
||||
<div className="p-4 bg-slate-50 rounded-xl border border-slate-100 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Entry Permit No</span>
|
||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.entry_permit_no || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">UID No</span>
|
||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.uid_no || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">File Number</span>
|
||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.file_number || visaDoc.number || 'N/A'}</span>
|
||||
@ -830,6 +851,55 @@ export default function WorkerManagement({ workers }) {
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">ID Number</span>
|
||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.id_number || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Full Name</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.full_name || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Name</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.name || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Visa Type</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.visa_type || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Profession</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.profession || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sponsor Name</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.sponsor_name || 'N/A'}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Nationality</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.nationality || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Country</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.country || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport No</span>
|
||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.passport_no || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
|
||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.passport_number || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Date of Birth</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.date_of_birth || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Birth</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.place_of_birth || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Issue</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.place_of_issue || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Issue Date</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.issue_date || visaDoc.issue_date || 'N/A'}</span>
|
||||
@ -838,17 +908,18 @@ export default function WorkerManagement({ workers }) {
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Expiry Date</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.expiry_date || visaDoc.expiry_date || 'N/A'}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Passport Number</span>
|
||||
<span className="font-mono font-bold text-slate-800">{visaDoc.ocr_data?.passport_number || 'N/A'}</span>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Accompanied By</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.accompanied_by || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Place of Issue</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.place_of_issue || 'N/A'}</span>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Document Type</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.document_type || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">Sponsor</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.sponsor || 'N/A'}</span>
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block">OCR Accuracy</span>
|
||||
<span className="font-bold text-slate-800">{visaDoc.ocr_data?.ocr_accuracy || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{visaDoc.file_path && (
|
||||
@ -869,50 +940,18 @@ export default function WorkerManagement({ workers }) {
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Compliance Notes */}
|
||||
<div className="space-y-2 bg-yellow-50/60 p-4 border border-yellow-200 rounded-2xl">
|
||||
<label className="text-[10px] font-black text-yellow-800 uppercase tracking-widest flex items-center gap-1">
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
<span>Internal Admin Compliance Notes Logs</span>
|
||||
</label>
|
||||
<textarea
|
||||
rows="3"
|
||||
placeholder="Write admin logs regarding candidate verification reviews or user complaints..."
|
||||
className="w-full bg-white border border-yellow-200 rounded-xl p-3 text-xs font-bold text-slate-700 focus:ring-2 focus:ring-yellow-500/20 outline-none"
|
||||
value={adminNotes}
|
||||
onChange={e => setAdminNotes(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column (1/3) */}
|
||||
<div className="space-y-6">
|
||||
{/* Contact & Location */}
|
||||
{/* Contact Detail */}
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm space-y-4">
|
||||
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Contact & Location</h3>
|
||||
<h3 className="text-xs font-black text-slate-400 uppercase tracking-wider">Contact Detail</h3>
|
||||
<div className="space-y-3.5 text-xs font-bold text-slate-700">
|
||||
<div className="flex items-center space-x-2 bg-slate-50 p-2.5 rounded-xl border border-slate-100">
|
||||
<Phone className="w-4 h-4 text-teal-600 flex-shrink-0" />
|
||||
<span className="text-slate-800 font-extrabold">{selectedWorker?.phone || '+971 52 489 1209'}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pt-1">
|
||||
<div>
|
||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Country</label>
|
||||
<span className="text-slate-800 font-extrabold block">{selectedWorker?.country || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">City</label>
|
||||
<span className="text-slate-800 font-extrabold block">{selectedWorker?.city || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="col-span-2 border-t border-slate-100 pt-2">
|
||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Area</label>
|
||||
<span className="text-slate-800 font-extrabold block">{selectedWorker?.area || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="col-span-2 border-t border-slate-100 pt-2">
|
||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-0.5">Preferred Location</label>
|
||||
<span className="text-slate-800 font-extrabold block text-xs leading-normal">{selectedWorker?.preferred_location || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -952,31 +991,6 @@ export default function WorkerManagement({ workers }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Fraud Flagging */}
|
||||
<div className="border-t border-slate-100 pt-4">
|
||||
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest block mb-2">Trustworthiness</label>
|
||||
{selectedWorker?.is_fraud ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-1.5 text-red-600 bg-red-50 border border-red-100 px-3 py-2 rounded-xl text-xs font-extrabold">
|
||||
<ShieldAlert className="w-4 h-4" />
|
||||
<span>Flagged as Fraud</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleFlagFraud(selectedWorker.id, false, '')}
|
||||
className="w-full py-2 bg-slate-900 hover:bg-slate-800 text-white rounded-xl text-[10px] font-black uppercase tracking-wider transition-all"
|
||||
>
|
||||
Clear Fraud Flag
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleFlagFraud(selectedWorker.id, true, 'Flagged by system admin review.')}
|
||||
className="w-full py-2 border border-red-200 hover:bg-red-50 text-red-600 rounded-xl text-[10px] font-black uppercase tracking-wider transition-all"
|
||||
>
|
||||
Flag as Fraud
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -351,13 +351,6 @@ export default function Show({ worker }) {
|
||||
<span className="font-black text-slate-900 text-xs uppercase">{worker.live_in_out}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
||||
<MapPin className="w-4 h-4 text-slate-500" />
|
||||
<span>{t('preferred_location', 'Preferred Location')}</span>
|
||||
</div>
|
||||
<span className="font-extrabold text-slate-900 text-xs capitalize">{worker.preferred_location || t('any', 'Any')}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-bold">
|
||||
|
||||
@ -90,14 +90,30 @@ public function test_employer_web_registration_with_emirates_id()
|
||||
]);
|
||||
|
||||
$profile = EmployerProfile::where('phone', '501234567')->first();
|
||||
$this->assertNotNull($profile->emirates_id_number);
|
||||
$this->assertNotNull($profile->emirates_id_expiry);
|
||||
$this->assertEquals('784-1987-5493842-5', $profile->emirates_id_number);
|
||||
$this->assertEquals('2027-12-11', $profile->emirates_id_expiry);
|
||||
$this->assertEquals('Mohammad Jobaier Mohammad Abul Kalam', $profile->emirates_id_name);
|
||||
$this->assertEquals('1987-04-14', $profile->emirates_id_dob);
|
||||
$this->assertEquals('Bangladesh', $profile->nationality);
|
||||
$this->assertEquals('2025-12-12', $profile->emirates_id_issue_date);
|
||||
$this->assertEquals('Msj International Technical Services L.L.C UAE', $profile->emirates_id_employer);
|
||||
$this->assertEquals('Dubai', $profile->emirates_id_issue_place);
|
||||
$this->assertEquals('Electrician', $profile->emirates_id_occupation);
|
||||
|
||||
$this->assertDatabaseHas('sponsors', [
|
||||
'email' => 'abdullah@example.com',
|
||||
'mobile' => '501234567',
|
||||
'emirates_id_file' => null, // File is not stored
|
||||
'address' => 'Villa 14, Al Safa, Dubai',
|
||||
'emirates_id' => '784-1987-5493842-5',
|
||||
'emirates_id_name' => 'Mohammad Jobaier Mohammad Abul Kalam',
|
||||
'emirates_id_dob' => '1987-04-14',
|
||||
'nationality' => 'Bangladesh',
|
||||
'emirates_id_issue_date' => '2025-12-12',
|
||||
'emirates_id_expiry' => '2027-12-11',
|
||||
'emirates_id_employer' => 'Msj International Technical Services L.L.C UAE',
|
||||
'emirates_id_issue_place' => 'Dubai',
|
||||
'emirates_id_occupation' => 'Electrician',
|
||||
]);
|
||||
}
|
||||
|
||||
@ -144,8 +160,8 @@ public function test_employer_web_registration_with_two_sided_emirates_id()
|
||||
$this->assertTrue(session('employer_emirates_id_uploaded'));
|
||||
|
||||
$pendingRegAfterId = session('pending_employer_registration');
|
||||
$this->assertEquals('784-1988-5310327-2', $pendingRegAfterId['emirates_id_number']); // Mock fallback value
|
||||
$this->assertEquals('2028-04-11', $pendingRegAfterId['emirates_id_expiry']); // Mock fallback value
|
||||
$this->assertEquals('784-1987-5493842-5', $pendingRegAfterId['emirates_id_number']); // Mock fallback value
|
||||
$this->assertEquals('2027-12-11', $pendingRegAfterId['emirates_id_expiry']); // Mock fallback value
|
||||
|
||||
// Step 4: Payment Choice
|
||||
$responseStep4 = $this->post('/employer/register-payment', [
|
||||
@ -172,8 +188,15 @@ public function test_employer_web_registration_with_two_sided_emirates_id()
|
||||
|
||||
$profile = EmployerProfile::where('phone', '509876543')->first();
|
||||
$this->assertNotNull($profile);
|
||||
$this->assertEquals('784-1988-5310327-2', $profile->emirates_id_number);
|
||||
$this->assertEquals('2028-04-11', $profile->emirates_id_expiry);
|
||||
$this->assertEquals('784-1987-5493842-5', $profile->emirates_id_number);
|
||||
$this->assertEquals('2027-12-11', $profile->emirates_id_expiry);
|
||||
$this->assertEquals('Mohammad Jobaier Mohammad Abul Kalam', $profile->emirates_id_name);
|
||||
$this->assertEquals('1987-04-14', $profile->emirates_id_dob);
|
||||
$this->assertEquals('Bangladesh', $profile->nationality);
|
||||
$this->assertEquals('2025-12-12', $profile->emirates_id_issue_date);
|
||||
$this->assertEquals('Msj International Technical Services L.L.C UAE', $profile->emirates_id_employer);
|
||||
$this->assertEquals('Dubai', $profile->emirates_id_issue_place);
|
||||
$this->assertEquals('Electrician', $profile->emirates_id_occupation);
|
||||
$this->assertEquals('Apartment 402, Al Nahda, Sharjah', $profile->address);
|
||||
}
|
||||
}
|
||||
|
||||
@ -536,6 +536,103 @@ public function test_register_passport_and_visa_with_direct_json_payload()
|
||||
$this->assertEquals('Sponsor Name', $visaDoc->ocr_data['sponsor']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test worker registration with the specific passport fields and ocr_accuracy.
|
||||
*/
|
||||
public function test_register_passport_with_full_ocr_fields()
|
||||
{
|
||||
$response = $this->postJson('/api/workers/register', [
|
||||
'name' => 'Temp Name',
|
||||
'phone' => '+971507777777',
|
||||
'salary' => 2000,
|
||||
'password' => 'secret123',
|
||||
'language' => 'HI',
|
||||
'passport' => [
|
||||
'document_type' => 'P',
|
||||
'issuing_country' => 'HAS',
|
||||
'surname' => 'KHALED',
|
||||
'given_names' => 'KHALED AL',
|
||||
'passport_number' => 'Y34B67890',
|
||||
'nationality' => 'RE1',
|
||||
'date_of_birth' => '1990-05-14',
|
||||
'date_of_expiry' => '2022-05-07',
|
||||
'place_of_birth' => 'SHARJAH',
|
||||
'authority' => 'MINISTRY OF INTERIOR U.S. 21 is ,,,, is',
|
||||
'sex' => 'M',
|
||||
'date_of_issue' => '2017-07-05',
|
||||
'ocr_accuracy' => 98.7,
|
||||
]
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$workerId = $response->json('data.worker.id');
|
||||
|
||||
$this->assertDatabaseHas('worker_documents', [
|
||||
'worker_id' => $workerId,
|
||||
'type' => 'passport',
|
||||
'number' => 'Y34B67890',
|
||||
'expiry_date' => '2022-05-07',
|
||||
'issue_date' => '2017-07-05',
|
||||
'ocr_accuracy' => 98.7,
|
||||
]);
|
||||
|
||||
$passportDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'passport')->first();
|
||||
$this->assertNotNull($passportDoc->ocr_data);
|
||||
$this->assertEquals('KHALED AL', $passportDoc->ocr_data['given_names']);
|
||||
$this->assertEquals('KHALED', $passportDoc->ocr_data['surname']);
|
||||
$this->assertEquals('MINISTRY OF INTERIOR U.S. 21 is ,,,, is', $passportDoc->ocr_data['authority']);
|
||||
$this->assertEquals(98.7, $passportDoc->ocr_data['ocr_accuracy']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test worker registration with the specific visa fields and ocr_accuracy.
|
||||
*/
|
||||
public function test_register_visa_with_full_ocr_fields()
|
||||
{
|
||||
$response = $this->postJson('/api/workers/register', [
|
||||
'name' => 'Temp Name',
|
||||
'phone' => '+971507777778',
|
||||
'salary' => 2000,
|
||||
'password' => 'secret123',
|
||||
'language' => 'HI',
|
||||
'visa' => [
|
||||
'document_type' => 'uae_visa',
|
||||
'country' => 'United Arab Emirates',
|
||||
'visa_type' => 'ENTRY PERMIT',
|
||||
'entry_permit_no' => '77003098 / 2019 / 204',
|
||||
'issue_date' => '',
|
||||
'valid_until' => '04-MAR-2019',
|
||||
'uid_no' => '207404887',
|
||||
'full_name' => 'Mr.MUHAMMAD NADEEM RASHEED',
|
||||
'nationality' => 'PAKISTAN',
|
||||
'place_of_birth' => 'SIALKOT PAK',
|
||||
'date_of_birth' => '25-JUN-1999',
|
||||
'passport_no' => 'EN9458281',
|
||||
'profession' => 'SALES REPRESENTATIVE',
|
||||
'sponsor_name' => '',
|
||||
'ocr_accuracy' => 97.4,
|
||||
]
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$workerId = $response->json('data.worker.id');
|
||||
|
||||
$this->assertDatabaseHas('worker_documents', [
|
||||
'worker_id' => $workerId,
|
||||
'type' => 'visa',
|
||||
'number' => '77003098 / 2019 / 204',
|
||||
'expiry_date' => '2019-03-04',
|
||||
'ocr_accuracy' => 97.4,
|
||||
]);
|
||||
|
||||
$visaDoc = \App\Models\WorkerDocument::where('worker_id', $workerId)->where('type', 'visa')->first();
|
||||
$this->assertNotNull($visaDoc->ocr_data);
|
||||
$this->assertEquals('uae_visa', $visaDoc->ocr_data['document_type']);
|
||||
$this->assertEquals('ENTRY PERMIT', $visaDoc->ocr_data['visa_type']);
|
||||
$this->assertEquals('Mr.MUHAMMAD NADEEM RASHEED', $visaDoc->ocr_data['full_name']);
|
||||
$this->assertEquals(97.4, $visaDoc->ocr_data['ocr_accuracy']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test worker passport and visa registration when they are passed as JSON strings.
|
||||
*/
|
||||
|
||||
@ -23,7 +23,7 @@ export default defineConfig({
|
||||
port: 5173,
|
||||
cors: true,
|
||||
hmr: {
|
||||
host: '192.168.29.193',
|
||||
host: '192.168.0.166',
|
||||
},
|
||||
watch: {
|
||||
ignored: ['**/storage/framework/views/**'],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user