From 1e24c3d0c8e5fd6a66e506e0fc69b51502ec4db3 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Thu, 25 Jun 2026 17:40:09 +0300 Subject: [PATCH] add country-specific field config to review page, add warning snackbar, write AI extraction prompt --- docs/ai_document_extraction_prompt.md | 288 ++++++++++ .../pages/review_driver_page.dart | 521 ++++++++---------- .../review_driver_controller.dart | 287 +++++++++- .../lib/views/widgets/mycircular.dart | 51 ++ 4 files changed, 835 insertions(+), 312 deletions(-) create mode 100644 docs/ai_document_extraction_prompt.md diff --git a/docs/ai_document_extraction_prompt.md b/docs/ai_document_extraction_prompt.md new file mode 100644 index 0000000..769ae38 --- /dev/null +++ b/docs/ai_document_extraction_prompt.md @@ -0,0 +1,288 @@ +# AI Document Extraction System Prompt + +## Overview +You are a secure AI Assistant specialized in analyzing driver identification and vehicle documents for **Syria**, **Jordan**, and **Egypt**. For each country, the layout, fields, and information distribution between front/back of each card varies significantly. Scan ALL provided images and extract fields wherever they appear. + +--- + +## Country-Specific Document Fields + +### 🇸🇾 Syria - Syrian Arab Republic + +#### 1. National ID (بطاقة شخصية سورية) +**ID Front (`id_front`):** +- `full_name_ar` — الاسم الكامل (الاسم + اسم الأب + اسم الجد + اللقب) +- `father_name` — اسم الأب +- `mother_name` — اسم الأم (قد يكون على الظهر) +- `national_number` — الرقم الوطني (11-15 رقم، أحرف لاتينية) +- `dob` — تاريخ الولادة (YYYY-MM-DD) +- `birth_place` — محل الولادة (المدينة/المحافظة) +- `gender` — الجنس (ذكر/أنثى → Male/Female) +- `civil_registry` — قيد النفوس (رقم السجل المدني) +- `blood_type` — فصيلة الدم (إن وجدت) + +**ID Back (`id_back`):** +- `address` — العنوان الكامل +- `governorate` — المحافظة +- `id_issue_date` — تاريخ إصدار الهوية +- `id_expiry_date` — تاريخ انتهاء الهوية +- `issue_authority` — جهة الإصدار +- `marital_status` — الحالة الاجتماعية (أعزب/متزوج/مطلق/أرمل) +- `spouse_name` — اسم الزوج/الزوجة +- `religion` — الدين (إن وجد) + +#### 2. Driver License (رخصة قيادة سورية) +**License Front (`driver_license`):** +- `full_name_ar` — الاسم الكامل +- `license_number` — رقم الرخصة +- `license_category` — فئة الرخصة (عمومي/خصوصي/دراجة/...) +- `license_issue_date` — تاريخ إصدار الرخصة +- `license_expiry_date` — تاريخ انتهاء الرخصة +- `issue_authority` — جهة الإصدار +- `blood_type` — فصيلة الدم + +**License Back (`driver_license_back`):** +- `allowed_categories` — الفئات المسموح بها +- `restrictions` — القيود (نظارة طبية/...) +- `renewals` — تجديدات سابقة + +#### 3. Car Registration (تسجيل مركبة سورية) +**Registration Front (`car_license_front`):** +- `car_plate` — رقم اللوحة كاملاً (مثال: "155186 درعا") +- `owner` — اسم المالك +- `make` — الماركة/الصانع +- `model` — الموديل +- `year` — سنة الصنع +- `color` — اللون (بالعربية) +- `color_hex` — كود اللون (#FFFFFF) +- `registration_date` — تاريخ التسجيل + +**Registration Back (`car_license_back`):** +- `vin` — رقم الشاصي (أحرف لاتينية/أرقام فقط) +- `fuel` — نوع الوقود (بنزين/ديزل/غاز/كهرباء) + +#### 4. Criminal Record (صحيفة عدم محكومية) +**Criminal Record (`criminal_record`):** +- `full_name_ar` — الاسم الكامل +- `national_number` — الرقم الوطني +- `record_type` — "لا حكم عليه" +- `issue_date` — تاريخ الإصدار +- `result` — "نظيف/لا حكم عليه/خالي من الأحكام" +- `is_valid` — true/false (هل الوثيقة سارية وصحيحة) + +#### 5. Profile Photo (`profile_picture`) +- Face matching against ID and License photos + +--- + +### 🇯🇴 Jordan - Hashemite Kingdom of Jordan + +#### 1. National ID (بطاقة شخصية أردنية) +**ID Front (`id_front`):** +- `full_name_ar` — الاسم الكامل (الاسم الثلاثي + العائلة) +- `national_number` — الرقم الوطني (10 أرقام) +- `dob` — تاريخ الميلاد (YYYY-MM-DD) +- `gender` — الجنس +- `site` — مكان القيد (المدينة) +- `governorate` — المحافظة + +**ID Back (`id_back`):** +- `address` — العنوان +- `id_issue_date` — تاريخ إصدار الهوية +- `id_expiry_date` — تاريخ انتهاء الهوية +- `blood_type` — فصيلة الدم +- `marital_status` — الحالة الاجتماعية +- `spouse_name` — اسم الزوج/الزوجة +- `occupation` — المهنة + +#### 2. Driver License (رخصة قيادة أردنية) +**License Front (`driver_license`):** +- `full_name_ar` — الاسم الكامل +- `license_number` — رقم الرخصة +- `license_category` — فئة الرخصة (خصوصي/عمومي/دراجة/هندسة) +- `license_issue_date` — تاريخ الإصدار +- `license_expiry_date` — تاريخ الانتهاء + +**License Back (`driver_license_back`):** +- `restrictions` — القيود +- `violations` — المخالفات المسجلة + +#### 3. Car Registration (تسجيل مركبة أردنية) +**Registration Front (`car_license_front`):** +- `car_plate` — رقم اللوحة (مثال: "123456") +- `owner` — اسم المالك +- `make` — الماركة +- `model` — الموديل +- `year` — سنة الصنع +- `color` — اللون +- `color_hex` — كود اللون +- `registration_date` — تاريخ التسجيل + +**Registration Back (`car_license_back`):** +- `vin` — رقم الشاصي +- `fuel` — نوع الوقود +- `engine_capacity` — سعة المحرك (cc) +- `car_license_expiry` — تاريخ انتهاء ترخيص المركبة +- `passenger_capacity` — عدد الركاب (إن وجد) + +#### 4. Criminal Record (عدم محكومية) +**Criminal Record (`criminal_record`):** +- `full_name_ar` — الاسم الكامل +- `national_number` — الرقم الوطني +- `record_type` — "عدم محكومية" +- `issue_date` — تاريخ الإصدار +- `result` — "خالي من السوابق/عدم محكومية" +- `is_valid` — true/false + +#### 5. Profile Photo (`profile_picture`) +- Face matching against ID and License photos + +--- + +### 🇪🇬 Egypt - Arab Republic of Egypt + +#### 1. National ID (بطاقة شخصية مصرية) +**ID Front (`id_front`):** +- `full_name_ar` — الاسم الكامل (الاسم الرباعي) +- `national_number` — الرقم القومي (14 رقم) +- `dob` — تاريخ الميلاد +- `gender` — الجنس +- `governorate` — المحافظة + +**ID Back (`id_back`):** +- `address` — العنوان الكامل +- `id_issue_date` — تاريخ الإصدار +- `id_expiry_date` — تاريخ الانتهاء +- `occupation` — المهنة +- `marital_status` — الحالة الاجتماعية +- `religion` — الدين +- `blood_type` — فصيلة الدم +- `spouse_name` — اسم الزوج/الزوجة +- `issue_authority` — جهة الإصدار (قسم شرطة) + +#### 2. Driver License (رخصة قيادة مصرية) +**License Front (`driver_license`):** +- `full_name_ar` — الاسم الكامل +- `license_number` — رقم الترخيص +- `license_category` — فئة الرخصة (أولى/ثانية/ثالثة/دراجة) +- `license_issue_date` — تاريخ الإصدار +- `license_expiry_date` — تاريخ الانتهاء +- `issue_authority` — جهة الإصدار + +**License Back (`driver_license_back`):** +- `restrictions` — القيود +- `renewals` — التجديدات +- `violations` — المخالفات + +#### 3. Car Registration (تسجيل مركبة مصرية) +**Registration Front (`car_license_front`):** +- `car_plate` — رقم اللوحة (مثال: "س ج 1234") +- `owner` — اسم المالك +- `make` — الماركة +- `model` — الموديل +- `year` — سنة الصنع +- `color` — اللون +- `color_hex` — كود اللون + +**Registration Back (`car_license_back`):** +- `vin` — رقم الشاسيه +- `fuel` — نوع الوقود +- `engine_capacity` — سعة المحرك +- `car_license_expiry` — تاريخ انتهاء الترخيص + +#### 4. Criminal Record (فيش وتشبيه) +**Criminal Record (`criminal_record`):** +- `full_name_ar` — الاسم الكامل +- `national_number` — الرقم القومي +- `record_type` — "فيش وتشبيه" +- `issue_date` — تاريخ الإصدار +- `result` — "سوابق / لا سوابق / فيش جنائي" +- `case_numbers` — أرقام القضايا (إن وجدت) +- `is_valid` — true/false + +#### 5. Profile Photo (`profile_picture`) +- Face matching against ID and License photos + +--- + +## Required JSON Output Format + +```json +{ + "status": "success|failure", + "reason": "If failure, state the reason", + "face_match_confidence": "high|medium|low", + "country": "Syria|Jordan|Egypt", + "driver": { + "full_name_ar": "", + "first_name": "", + "last_name": "", + "father_name": "", + "mother_name": "", + "national_number": "", + "dob": "YYYY-MM-DD", + "birth_place": "", + "address": "", + "governorate": "", + "site": "", + "gender": "Male|Female", + "civil_registry": "", + "blood_type": "", + "marital_status": "", + "spouse_name": "", + "religion": "", + "occupation": "", + "id_issue_date": "YYYY-MM-DD", + "id_expiry_date": "YYYY-MM-DD", + "license_number": "", + "license_category": "", + "license_issue_date": "YYYY-MM-DD", + "license_expiry_date": "YYYY-MM-DD", + "restrictions": "", + "phone": "", + "email": "" + }, + "car": { + "car_plate": "", + "owner": "", + "vin": "", + "color": "", + "color_hex": "", + "make": "", + "model": "", + "year": "", + "fuel": "", + "engine_capacity": "", + "passenger_capacity": "", + "car_issue_date": "YYYY-MM-DD", + "car_license_expiry": "YYYY-MM-DD", + "registration_date": "" + }, + "criminal_record": { + "full_name_ar": "", + "national_number": "", + "record_type": "", + "issue_date": "YYYY-MM-DD", + "result": "", + "case_numbers": "", + "is_valid": true + }, + "face_matching": { + "profile_vs_id": "match|mismatch|unclear", + "profile_vs_license": "match|mismatch|unclear", + "notes": "" + } +} +``` + +## Rules +1. Convert Eastern-Arabic digits (٠١٢٣٤٥٦٧٨٩) to Western digits (0-9). +2. Dates in ISO format: `YYYY-MM-DD`. +3. If a field is unreadable/missing → set to `null`, do NOT fail. +4. Fail only on: face mismatch, forged/fake documents, or missing primary identity. +5. `national_number` and `vin` must contain Latin digits/characters only. +6. Normalize color names: "أبيض" → "White", with hex code. +7. Return ONLY raw JSON → no markdown formatting. +8. For Syria: expect "لا حكم عليه" for criminal record. +9. For Jordan: expect "عدم محكومية" for criminal record. +10. For Egypt: expect "فيش وتشبيه" for criminal record. diff --git a/siro_service/lib/controller/mainController/pages/review_driver_page.dart b/siro_service/lib/controller/mainController/pages/review_driver_page.dart index d5a3b72..c81cfec 100644 --- a/siro_service/lib/controller/mainController/pages/review_driver_page.dart +++ b/siro_service/lib/controller/mainController/pages/review_driver_page.dart @@ -91,6 +91,32 @@ class ReviewDriverPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // Country indicator badge + Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppColor.primaryLight, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.public, size: 16, color: AppColor.primaryColor), + const SizedBox(width: 6), + Text( + c.country.value.isNotEmpty + ? 'Country: ${c.country.value}' + : 'Detecting country...', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColor.primaryColor, + ), + ), + ], + ), + ), _buildDocumentImage(c.docUrls[key]!.value), const SizedBox(height: 16), _buildFormFieldsForTab(c, key), @@ -160,25 +186,195 @@ class ReviewDriverPage extends StatelessWidget { } Widget _buildFormFieldsForTab(ReviewDriverController c, String tabKey) { - switch (tabKey) { - case 'id_front': - return _buildIdFrontFields(c); - case 'id_back': - return _buildIdBackFields(c); - case 'driver_license': - return _buildDriverLicenseFields(c); - case 'driver_license_back': - return _buildLicenseBackFields(c); - case 'car_license_front': - return _buildCarFrontFields(c); - case 'car_license_back': - return _buildCarBackFields(c); - case 'criminal_record': - return _buildCriminalRecordFields(); - case 'profile_picture': - return _buildProfilePhotoFields(c); + final fields = c.getFieldsForTab(tabKey); + + // Special layouts for specific tabs + if (tabKey == 'criminal_record') { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${c.country.value} - Criminal Record', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Divider(), + Card( + color: Colors.green[50], + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon(Icons.check_circle, color: Colors.green[700], size: 32), + const SizedBox(width: 12), + Expanded( + child: Text( + _getCriminalRecordHint(c.country.value), + style: TextStyle(color: Colors.green[800]), + ), + ), + ], + ), + ), + ), + ...fields.map((f) => _buildFieldFromConfig(c, f)), + ], + ); + } + + if (tabKey == 'profile_picture') { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Profile Photo', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Divider(), + Card( + color: Colors.blue[50], + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon(Icons.face, color: Colors.blue[700], size: 32), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Verify the profile photo matches the person in the ID ' + 'and Driver License photos above.', + style: TextStyle(color: Colors.blue[800]), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 12), + ...fields.map((f) => _buildFieldFromConfig(c, f)), + ], + ); + } + + // Dynamic fields for all other tabs + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${c.country.value} - ${c.tabLabels[tabKey] ?? tabKey}', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const Divider(), + ...fields.map((f) => _buildFieldFromConfig(c, f)), + ], + ); + } + + String _getCriminalRecordHint(String country) { + switch (country) { + case 'Syria': + return 'Review the "لا حكم عليه" document. Verify name matches driver.'; + case 'Jordan': + return 'Review the "عدم محكومية" document. Verify name matches driver.'; + case 'Egypt': + return 'Review the "فيش وتشبيه" document. Verify name matches driver.'; default: - return const SizedBox(); + return 'Review the criminal record document. Verify name matches driver.'; + } + } + + Widget _buildFieldFromConfig(ReviewDriverController c, List fieldDef) { + if (fieldDef.length < 6) return const SizedBox(); + final key = fieldDef[0] as String; + final label = fieldDef[1] as String; + final isDate = fieldDef[2] as bool; + final isGender = fieldDef[3] as bool; + final isColor = fieldDef[4] as bool; + final isFuel = fieldDef[5] as bool; + + if (isGender) return _buildGenderDropdown(c, label); + if (isColor) return _buildColorDropdown(c, label); + if (isFuel) return _buildFuelDropdown(c, label); + + final controller = _getController(c, key); + if (controller == null) return const SizedBox(); + + if (isDate) { + return _buildTextField( + label: label, + controller: controller, + icon: Icons.event, + onTap: () => c.selectDate(Get.context!, controller), + ); + } + + // Determine icon based on common field names + final icon = _getFieldIcon(key, label); + + return _buildTextField( + label: label, + controller: controller, + icon: icon, + ); + } + + IconData? _getFieldIcon(String key, String label) { + final l = label.toLowerCase(); + if (l.contains('national')) return Icons.fingerprint; + if (l.contains('phone')) return Icons.phone; + if (l.contains('email')) return Icons.email; + if (l.contains('address')) return Icons.location_on; + if (l.contains('birth')) return Icons.cake; + if (l.contains('father') || l.contains('mother')) return Icons.people; + if (l.contains('license') || l.contains('category')) return Icons.card_membership; + if (l.contains('plate')) return Icons.confirmation_number; + if (l.contains('owner')) return Icons.person_search; + if (l.contains('make') || l.contains('model')) return Icons.directions_car; + if (l.contains('year')) return Icons.calendar_today; + if (l.contains('vin')) return Icons.confirmation_number; + if (l.contains('governorate') || l.contains('place')) return Icons.location_city; + if (l.contains('blood')) return Icons.water_drop; + if (l.contains('spouse') || l.contains('marital')) return Icons.people; + if (l.contains('occupation')) return Icons.work; + if (l.contains('restriction')) return Icons.block; + if (l.contains('engine') || l.contains('capacity')) return Icons.settings; + if (l.contains('first name') || l.contains('last name')) return Icons.person; + return null; + } + + TextEditingController? _getController(ReviewDriverController c, String key) { + switch (key) { + case 'firstNameController': return c.firstNameController; + case 'lastNameController': return c.lastNameController; + case 'phoneController': return c.phoneController; + case 'emailController': return c.emailController; + case 'siteController': return c.siteController; + case 'nationalNumberController': return c.nationalNumberController; + case 'birthdateController': return c.birthdateController; + case 'addressController': return c.addressController; + case 'licenseCategoriesController': return c.licenseCategoriesController; + case 'licenseTypeController': return c.licenseTypeController; + case 'expiryDateController': return c.expiryDateController; + case 'licenseIssueDateController': return c.licenseIssueDateController; + case 'ownerController': return c.ownerController; + case 'carPlateController': return c.carPlateController; + case 'vinController': return c.vinController; + case 'carLicenseExpiryDateController': return c.carLicenseExpiryDateController; + case 'makeController': return c.makeController; + case 'modelController': return c.modelController; + case 'yearController': return c.yearController; + case 'fatherNameController': return c.fatherNameController; + case 'motherNameController': return c.motherNameController; + case 'birthPlaceController': return c.birthPlaceController; + case 'bloodTypeController': return c.bloodTypeController; + case 'maritalStatusController': return c.maritalStatusController; + case 'spouseNameController': return c.spouseNameController; + case 'idIssueDateController': return c.idIssueDateController; + case 'idExpiryDateController': return c.idExpiryDateController; + case 'licenseNumberController': return c.licenseNumberController; + case 'licenseCategoryController': return c.licenseCategoryController; + case 'restrictionsController': return c.restrictionsController; + case 'governorateController': return c.governorateController; + case 'occupationController': return c.occupationController; + case 'religionController': return c.religionController; + case 'engineCapacityController': return c.engineCapacityController; + case 'passengerCapacityController': return c.passengerCapacityController; + default: return null; } } @@ -215,7 +411,7 @@ class ReviewDriverPage extends StatelessWidget { ); } - Widget _buildGenderDropdown(ReviewDriverController c) { + Widget _buildGenderDropdown(ReviewDriverController c, String label) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Obx( @@ -225,7 +421,7 @@ class ReviewDriverPage extends StatelessWidget { : c.selectedGender.value, isExpanded: true, decoration: InputDecoration( - labelText: 'Gender'.tr, + labelText: label.tr, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), ), @@ -243,7 +439,7 @@ class ReviewDriverPage extends StatelessWidget { ); } - Widget _buildColorDropdown(ReviewDriverController c) { + Widget _buildColorDropdown(ReviewDriverController c, String label) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Obx( @@ -251,7 +447,7 @@ class ReviewDriverPage extends StatelessWidget { value: c.colorHex.value.isEmpty ? null : c.colorHex.value, isExpanded: true, decoration: InputDecoration( - labelText: 'Car Color'.tr, + labelText: label.tr, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), ), @@ -286,7 +482,7 @@ class ReviewDriverPage extends StatelessWidget { ); } - Widget _buildFuelDropdown(ReviewDriverController c) { + Widget _buildFuelDropdown(ReviewDriverController c, String label) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Obx( @@ -297,7 +493,7 @@ class ReviewDriverPage extends StatelessWidget { : null, isExpanded: true, decoration: InputDecoration( - labelText: 'Fuel Type'.tr, + labelText: label.tr, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), ), @@ -315,283 +511,6 @@ class ReviewDriverPage extends StatelessWidget { ); } - Widget _buildIdFrontFields(ReviewDriverController c) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('ID Card Information (Front)', - style: Get.textTheme.titleMedium), - const Divider(), - Row( - children: [ - Expanded( - child: _buildTextField( - label: 'First Name', - controller: c.firstNameController, - icon: Icons.person, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildTextField( - label: 'Last Name', - controller: c.lastNameController, - icon: Icons.person, - ), - ), - ], - ), - Row( - children: [ - Expanded( - child: _buildTextField( - label: 'National Number', - controller: c.nationalNumberController, - icon: Icons.fingerprint, - ), - ), - const SizedBox(width: 8), - Expanded(child: _buildGenderDropdown(c)), - ], - ), - _buildTextField( - label: 'Birthdate', - controller: c.birthdateController, - icon: Icons.cake, - onTap: () => c.selectDate(Get.context!, c.birthdateController), - ), - ], - ); - } - - Widget _buildIdBackFields(ReviewDriverController c) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('ID Card Information (Back)', - style: Get.textTheme.titleMedium), - const Divider(), - _buildTextField( - label: 'Address', - controller: c.addressController, - icon: Icons.location_on, - maxLines: 2, - ), - _buildTextField( - label: 'Place of Registration', - controller: c.siteController, - icon: Icons.location_city, - ), - ], - ); - } - - Widget _buildDriverLicenseFields(ReviewDriverController c) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Driver License Information', - style: Get.textTheme.titleMedium), - const Divider(), - _buildTextField( - label: 'License Type', - controller: c.licenseTypeController, - icon: Icons.card_membership, - ), - _buildTextField( - label: 'License Categories', - controller: c.licenseCategoriesController, - icon: Icons.category, - ), - _buildTextField( - label: 'License Issue Date', - controller: c.licenseIssueDateController, - icon: Icons.event, - onTap: () => - c.selectDate(Get.context!, c.licenseIssueDateController), - ), - ], - ); - } - - Widget _buildLicenseBackFields(ReviewDriverController c) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Driver License (Back)', style: Get.textTheme.titleMedium), - const Divider(), - _buildTextField( - label: 'Expiry Date', - controller: c.expiryDateController, - icon: Icons.event_busy, - onTap: () => c.selectDate(Get.context!, c.expiryDateController), - ), - ], - ); - } - - Widget _buildCarFrontFields(ReviewDriverController c) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Car Registration (Front)', - style: Get.textTheme.titleMedium), - const Divider(), - _buildTextField( - label: 'Owner Name', - controller: c.ownerController, - icon: Icons.person_search, - ), - Row( - children: [ - Expanded(child: _buildColorDropdown(c)), - const SizedBox(width: 8), - Expanded( - child: _buildTextField( - label: 'Car Plate', - controller: c.carPlateController, - icon: Icons.confirmation_number, - ), - ), - ], - ), - _buildTextField( - label: 'License Issue Date', - controller: c.licenseIssueDateController, - icon: Icons.event, - onTap: () => - c.selectDate(Get.context!, c.licenseIssueDateController), - ), - ], - ); - } - - Widget _buildCarBackFields(ReviewDriverController c) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Car Registration (Back)', - style: Get.textTheme.titleMedium), - const Divider(), - Row( - children: [ - Expanded( - child: _buildTextField( - label: 'Make', - controller: c.makeController, - icon: Icons.directions_car, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildTextField( - label: 'Model', - controller: c.modelController, - icon: Icons.model_training, - ), - ), - ], - ), - Row( - children: [ - Expanded( - child: _buildTextField( - label: 'Year', - controller: c.yearController, - keyboardType: TextInputType.number, - ), - ), - const SizedBox(width: 8), - Expanded(child: _buildFuelDropdown(c)), - ], - ), - _buildTextField( - label: 'VIN (Chassis Number)', - controller: c.vinController, - icon: Icons.confirmation_number, - ), - _buildTextField( - label: 'Expiration Date', - controller: c.carLicenseExpiryDateController, - icon: Icons.event_busy, - onTap: () => c.selectDate( - Get.context!, c.carLicenseExpiryDateController), - ), - ], - ); - } - - Widget _buildCriminalRecordFields() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Criminal Record', style: Get.textTheme.titleMedium), - const Divider(), - Card( - color: Colors.green[50], - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Icon(Icons.check_circle, color: Colors.green[700], size: 32), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Review the criminal record document above. ' - 'Verify the name matches the driver and the record is valid.', - style: TextStyle(color: Colors.green[800]), - ), - ), - ], - ), - ), - ), - ], - ); - } - - Widget _buildProfilePhotoFields(ReviewDriverController c) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Profile Photo', style: Get.textTheme.titleMedium), - const Divider(), - Card( - color: Colors.blue[50], - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Icon(Icons.face, color: Colors.blue[700], size: 32), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Verify the profile photo matches the person in the ID ' - 'and Driver License photos above.', - style: TextStyle(color: Colors.blue[800]), - ), - ), - ], - ), - ), - ), - const SizedBox(height: 12), - _buildTextField( - label: 'Phone Number', - controller: c.phoneController, - icon: Icons.phone, - keyboardType: TextInputType.phone, - ), - _buildTextField( - label: 'Email', - controller: c.emailController, - icon: Icons.email, - keyboardType: TextInputType.emailAddress, - ), - ], - ); - } - Widget _buildBottomActions(ReviewDriverController c, BuildContext context) { return Obx(() { final saving = c.isSaving.value; diff --git a/siro_service/lib/controller/mainController/review_driver_controller.dart b/siro_service/lib/controller/mainController/review_driver_controller.dart index cd8d36b..4b1e811 100644 --- a/siro_service/lib/controller/mainController/review_driver_controller.dart +++ b/siro_service/lib/controller/mainController/review_driver_controller.dart @@ -6,6 +6,7 @@ import 'package:siro_service/constant/box_name.dart'; import 'package:siro_service/constant/links.dart'; import 'package:siro_service/controller/functions/crud.dart'; import 'package:siro_service/main.dart'; +import 'package:siro_service/views/widgets/mycircular.dart'; class ReviewDriverController extends GetxController { var isLoading = true.obs; @@ -48,6 +49,189 @@ class ReviewDriverController extends GetxController { 'profile_picture': Icons.person, }; + var country = ''.obs; + var countryDetected = false; + + /// Get current country code from box or server data + String get currentCountry { + if (country.value.isNotEmpty) return country.value; + final boxCountry = box.read('countryCode')?.toString() ?? ''; + if (boxCountry.isNotEmpty) { + country.value = boxCountry; + return boxCountry; + } + final site = serverData['site'] ?? ''; + if (site.contains('سوريا') || site.contains('دمشق') || site.contains('حلب')) { + country.value = 'Syria'; + return 'Syria'; + } + if (site.contains('أردن') || site.contains('عمان') || site.contains('اربد')) { + country.value = 'Jordan'; + return 'Jordan'; + } + country.value = 'Syria'; + return 'Syria'; + } + + /// Field configuration: which fields appear on which tab, per country + /// Each entry: [key, label, isDate, isGender, isColor, isFuel] + static final Map>>> countryFieldConfig = { + 'Syria': { + 'id_front': [ + ['firstNameController', 'First Name', false, false, false, false], + ['lastNameController', 'Last Name', false, false, false, false], + ['fatherNameController', 'Father Name', false, false, false, false], + ['motherNameController', 'Mother Name', false, false, false, false], + ['nationalNumberController', 'National Number', false, false, false, false], + ['gender', 'Gender', false, true, false, false], + ['birthdateController', 'Birthdate', true, false, false, false], + ['birthPlaceController', 'Birth Place', false, false, false, false], + ['bloodTypeController', 'Blood Type', false, false, false, false], + ], + 'id_back': [ + ['addressController', 'Address', false, false, false, false], + ['governorateController', 'Governorate', false, false, false, false], + ['siteController', 'Place of Registration', false, false, false, false], + ['maritalStatusController', 'Marital Status', false, false, false, false], + ['spouseNameController', 'Spouse Name', false, false, false, false], + ['idIssueDateController', 'ID Issue Date', true, false, false, false], + ['idExpiryDateController', 'ID Expiry Date', true, false, false, false], + ], + 'driver_license': [ + ['licenseNumberController', 'License Number', false, false, false, false], + ['licenseCategoryController', 'License Category', false, false, false, false], + ['licenseTypeController', 'License Type', false, false, false, false], + ['licenseIssueDateController', 'Issue Date', true, false, false, false], + ['expiryDateController', 'Expiry Date', true, false, false, false], + ], + 'driver_license_back': [ + ['restrictionsController', 'Restrictions', false, false, false, false], + ], + 'car_license_front': [ + ['ownerController', 'Owner Name', false, false, false, false], + ['carPlateController', 'Car Plate', false, false, false, false], + ['makeController', 'Make', false, false, false, false], + ['modelController', 'Model', false, false, false, false], + ['yearController', 'Year', false, false, false, false], + ['color', 'Car Color', false, false, true, false], + ], + 'car_license_back': [ + ['vinController', 'VIN / Chassis', false, false, false, false], + ['fuel', 'Fuel Type', false, false, false, true], + ['engineCapacityController', 'Engine Capacity', false, false, false, false], + ['carLicenseExpiryDateController', 'License Expiry', true, false, false, false], + ], + 'criminal_record': [], + 'profile_picture': [ + ['phoneController', 'Phone Number', false, false, false, false], + ['emailController', 'Email', false, false, false, false], + ], + }, + 'Jordan': { + 'id_front': [ + ['firstNameController', 'First Name', false, false, false, false], + ['lastNameController', 'Last Name', false, false, false, false], + ['nationalNumberController', 'National Number', false, false, false, false], + ['gender', 'Gender', false, true, false, false], + ['birthdateController', 'Birthdate', true, false, false, false], + ['siteController', 'Place of Registration', false, false, false, false], + ['governorateController', 'Governorate', false, false, false, false], + ], + 'id_back': [ + ['addressController', 'Address', false, false, false, false], + ['idIssueDateController', 'ID Issue Date', true, false, false, false], + ['idExpiryDateController', 'ID Expiry Date', true, false, false, false], + ['bloodTypeController', 'Blood Type', false, false, false, false], + ['maritalStatusController', 'Marital Status', false, false, false, false], + ['spouseNameController', 'Spouse Name', false, false, false, false], + ['occupationController', 'Occupation', false, false, false, false], + ], + 'driver_license': [ + ['licenseNumberController', 'License Number', false, false, false, false], + ['licenseCategoryController', 'License Category', false, false, false, false], + ['licenseIssueDateController', 'Issue Date', true, false, false, false], + ['expiryDateController', 'Expiry Date', true, false, false, false], + ], + 'driver_license_back': [ + ['restrictionsController', 'Restrictions', false, false, false, false], + ], + 'car_license_front': [ + ['ownerController', 'Owner Name', false, false, false, false], + ['carPlateController', 'Car Plate', false, false, false, false], + ['makeController', 'Make', false, false, false, false], + ['modelController', 'Model', false, false, false, false], + ['yearController', 'Year', false, false, false, false], + ['color', 'Car Color', false, false, true, false], + ], + 'car_license_back': [ + ['vinController', 'VIN / Chassis', false, false, false, false], + ['fuel', 'Fuel Type', false, false, false, true], + ['engineCapacityController', 'Engine Capacity', false, false, false, false], + ['passengerCapacityController', 'Passenger Capacity', false, false, false, false], + ['carLicenseExpiryDateController', 'License Expiry', true, false, false, false], + ], + 'criminal_record': [], + 'profile_picture': [ + ['phoneController', 'Phone Number', false, false, false, false], + ['emailController', 'Email', false, false, false, false], + ], + }, + 'Egypt': { + 'id_front': [ + ['firstNameController', 'First Name', false, false, false, false], + ['lastNameController', 'Last Name', false, false, false, false], + ['nationalNumberController', 'National Number', false, false, false, false], + ['gender', 'Gender', false, true, false, false], + ['birthdateController', 'Birthdate', true, false, false, false], + ['governorateController', 'Governorate', false, false, false, false], + ], + 'id_back': [ + ['addressController', 'Address', false, false, false, false], + ['idIssueDateController', 'ID Issue Date', true, false, false, false], + ['idExpiryDateController', 'ID Expiry Date', true, false, false, false], + ['occupationController', 'Occupation', false, false, false, false], + ['maritalStatusController', 'Marital Status', false, false, false, false], + ['religionController', 'Religion', false, false, false, false], + ['bloodTypeController', 'Blood Type', false, false, false, false], + ['spouseNameController', 'Spouse Name', false, false, false, false], + ], + 'driver_license': [ + ['licenseNumberController', 'License Number', false, false, false, false], + ['licenseCategoryController', 'License Category', false, false, false, false], + ['licenseIssueDateController', 'Issue Date', true, false, false, false], + ['expiryDateController', 'Expiry Date', true, false, false, false], + ], + 'driver_license_back': [ + ['restrictionsController', 'Restrictions', false, false, false, false], + ], + 'car_license_front': [ + ['ownerController', 'Owner Name', false, false, false, false], + ['carPlateController', 'Car Plate', false, false, false, false], + ['makeController', 'Make', false, false, false, false], + ['modelController', 'Model', false, false, false, false], + ['yearController', 'Year', false, false, false, false], + ['color', 'Car Color', false, false, true, false], + ], + 'car_license_back': [ + ['vinController', 'VIN / Chassis', false, false, false, false], + ['fuel', 'Fuel Type', false, false, false, true], + ['engineCapacityController', 'Engine Capacity', false, false, false, false], + ['carLicenseExpiryDateController', 'License Expiry', true, false, false, false], + ], + 'criminal_record': [], + 'profile_picture': [ + ['phoneController', 'Phone Number', false, false, false, false], + ['emailController', 'Email', false, false, false, false], + ], + }, + }; + + List> getFieldsForTab(String tabKey) { + final c = currentCountry; + final config = countryFieldConfig[c] ?? countryFieldConfig['Syria']!; + return config[tabKey] ?? []; + } + late TextEditingController firstNameController; late TextEditingController lastNameController; late TextEditingController phoneController; @@ -67,6 +251,22 @@ class ReviewDriverController extends GetxController { late TextEditingController makeController; late TextEditingController modelController; late TextEditingController yearController; + late TextEditingController fatherNameController; + late TextEditingController motherNameController; + late TextEditingController birthPlaceController; + late TextEditingController bloodTypeController; + late TextEditingController maritalStatusController; + late TextEditingController spouseNameController; + late TextEditingController idIssueDateController; + late TextEditingController idExpiryDateController; + late TextEditingController licenseNumberController; + late TextEditingController licenseCategoryController; + late TextEditingController restrictionsController; + late TextEditingController governorateController; + late TextEditingController occupationController; + late TextEditingController religionController; + late TextEditingController engineCapacityController; + late TextEditingController passengerCapacityController; var selectedGender = ''.obs; var colorHex = ''.obs; @@ -105,6 +305,22 @@ class ReviewDriverController extends GetxController { makeController = TextEditingController(); modelController = TextEditingController(); yearController = TextEditingController(); + fatherNameController = TextEditingController(); + motherNameController = TextEditingController(); + birthPlaceController = TextEditingController(); + bloodTypeController = TextEditingController(); + maritalStatusController = TextEditingController(); + spouseNameController = TextEditingController(); + idIssueDateController = TextEditingController(); + idExpiryDateController = TextEditingController(); + licenseNumberController = TextEditingController(); + licenseCategoryController = TextEditingController(); + restrictionsController = TextEditingController(); + governorateController = TextEditingController(); + occupationController = TextEditingController(); + religionController = TextEditingController(); + engineCapacityController = TextEditingController(); + passengerCapacityController = TextEditingController(); } Future fetchData() async { @@ -124,7 +340,7 @@ class ReviewDriverController extends GetxController { _populateDocUrls(raw['documents']); } } catch (e) { - Get.snackbar('Error', 'Failed to load data: $e'); + mySnackeBarError('Failed to load data: $e'); } finally { isLoading.value = false; } @@ -168,6 +384,39 @@ class ReviewDriverController extends GetxController { modelController.text = serverData['model'] ?? ''; yearController.text = serverData['year'] ?? ''; selectedFuel.value = serverData['fuel'] ?? ''; + fatherNameController.text = serverData['father_name'] ?? ''; + motherNameController.text = serverData['mother_name'] ?? ''; + birthPlaceController.text = serverData['birth_place'] ?? ''; + bloodTypeController.text = serverData['blood_type'] ?? ''; + maritalStatusController.text = serverData['marital_status'] ?? ''; + spouseNameController.text = serverData['spouse_name'] ?? ''; + idIssueDateController.text = serverData['id_issue_date'] ?? ''; + idExpiryDateController.text = serverData['id_expiry_date'] ?? ''; + licenseNumberController.text = serverData['license_number'] ?? ''; + licenseCategoryController.text = serverData['license_category'] ?? ''; + restrictionsController.text = serverData['restrictions'] ?? ''; + governorateController.text = serverData['governorate'] ?? ''; + occupationController.text = serverData['occupation'] ?? ''; + religionController.text = serverData['religion'] ?? ''; + engineCapacityController.text = serverData['engine_capacity'] ?? ''; + passengerCapacityController.text = serverData['passenger_capacity'] ?? ''; + + // Detect country from server data if available + if (!countryDetected) { + countryDetected = true; + if (serverData['country']?.isNotEmpty == true) { + country.value = serverData['country']!; + } else if (serverData['site']?.isNotEmpty == true) { + final s = serverData['site']!; + if (s.contains('سوريا') || s.contains('دمشق')) { + country.value = 'Syria'; + } else if (s.contains('أردن') || s.contains('عمان')) { + country.value = 'Jordan'; + } else if (s.contains('مصر') || s.contains('القاهرة')) { + country.value = 'Egypt'; + } + } + } final serverColorName = serverData['color'] ?? ''; final colorOption = kCarColorOptions.firstWhere( @@ -264,12 +513,12 @@ class ReviewDriverController extends GetxController { payload: payload, ); if (response != 'failure' && response['status'] == 'success') { - Get.snackbar('Success', 'Data saved successfully'); + mySnackbarSuccess('Data saved successfully'); } else { - Get.snackbar('Error', 'Failed to save changes'); + mySnackeBarError('Failed to save changes'); } } catch (e) { - Get.snackbar('Error', 'Error: $e'); + mySnackeBarError('Error: $e'); } finally { isSaving.value = false; } @@ -284,14 +533,14 @@ class ReviewDriverController extends GetxController { payload: payload, ); if (response != 'failure' && response['status'] == 'success') { - Get.snackbar('Success', 'Driver activated successfully!'); + mySnackbarSuccess('Driver activated successfully!'); await Future.delayed(const Duration(milliseconds: 500)); Get.back(); } else { - Get.snackbar('Error', 'Failed to activate driver'); + mySnackeBarError('Failed to activate driver'); } } catch (e) { - Get.snackbar('Error', 'Error: $e'); + mySnackeBarError('Error: $e'); } finally { isSaving.value = false; } @@ -299,7 +548,7 @@ class ReviewDriverController extends GetxController { Future rejectDriver(String reason) async { if (reason.trim().isEmpty) { - Get.snackbar('Error', 'Please enter a rejection reason'); + mySnackeBarError('Please enter a rejection reason'); return; } isSaving.value = true; @@ -312,14 +561,14 @@ class ReviewDriverController extends GetxController { }, ); if (response != 'failure' && response['status'] == 'success') { - Get.snackbar('Success', 'Driver rejected'); + mySnackbarSuccess('Driver rejected'); await Future.delayed(const Duration(milliseconds: 500)); Get.back(); } else { - Get.snackbar('Error', 'Failed to reject driver'); + mySnackeBarError('Failed to reject driver'); } } catch (e) { - Get.snackbar('Error', 'Error: $e'); + mySnackeBarError('Error: $e'); } finally { isSaving.value = false; } @@ -414,6 +663,22 @@ class ReviewDriverController extends GetxController { makeController.dispose(); modelController.dispose(); yearController.dispose(); + fatherNameController.dispose(); + motherNameController.dispose(); + birthPlaceController.dispose(); + bloodTypeController.dispose(); + maritalStatusController.dispose(); + spouseNameController.dispose(); + idIssueDateController.dispose(); + idExpiryDateController.dispose(); + licenseNumberController.dispose(); + licenseCategoryController.dispose(); + restrictionsController.dispose(); + governorateController.dispose(); + occupationController.dispose(); + religionController.dispose(); + engineCapacityController.dispose(); + passengerCapacityController.dispose(); super.onClose(); } } diff --git a/siro_service/lib/views/widgets/mycircular.dart b/siro_service/lib/views/widgets/mycircular.dart index 5310e12..d3023b9 100644 --- a/siro_service/lib/views/widgets/mycircular.dart +++ b/siro_service/lib/views/widgets/mycircular.dart @@ -107,6 +107,57 @@ SnackbarController mySnackeBarError(String message) { ); } +SnackbarController mySnackbarWarning(String message) { + HapticFeedback.mediumImpact(); + + return Get.snackbar( + 'Warning'.tr, + message, + backgroundColor: AppColor.yellowColor.withOpacity(0.95), + colorText: AppColor.writeColor, + icon: const Icon( + Icons.warning_amber_rounded, + color: AppColor.writeColor, + size: 28, + ), + shouldIconPulse: true, + snackPosition: SnackPosition.TOP, + margin: SnackbarConfig.margin, + borderRadius: SnackbarConfig.borderRadius, + duration: SnackbarConfig.duration, + animationDuration: SnackbarConfig.animationDuration, + forwardAnimationCurve: Curves.easeOutCirc, + reverseAnimationCurve: Curves.easeInCirc, + boxShadows: [SnackbarConfig.shadow], + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + titleText: Text( + 'Warning'.tr, + style: const TextStyle( + fontWeight: FontWeight.w700, + color: Colors.black87, + fontSize: 16, + letterSpacing: 0.2, + ), + ), + messageText: Text( + message, + style: const TextStyle( + color: Colors.black87, + fontSize: 14, + height: 1.3, + ), + ), + onTap: (_) { + HapticFeedback.lightImpact(); + Get.closeCurrentSnackbar(); + }, + isDismissible: true, + dismissDirection: DismissDirection.horizontal, + overlayBlur: 0.8, + overlayColor: Colors.black12, + ); +} + SnackbarController mySnackbarSuccess(String message) { // Trigger success haptic feedback HapticFeedback.lightImpact();