feat: Gemini AI (doc OCR + face match) + role-assignment endpoints
- GeminiService (integrations/gemini, @Global): extractDocument (OCR fields) + faceMatch; graceful if no GEMINI_API_KEY - documents: face-match now real via Gemini; POST /admin/documents/:id/ocr extracts fields for CS auto-fill - roles: PATCH /admin/users/:id/role (admin) + POST /platform/users/role (bootstrap via x-platform-secret) - config: gemini + platform.secret; .env.example entries Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+662
@@ -0,0 +1,662 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:sefer_driver/controller/firebase/notification_service.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/auth/captin/login_captin_controller.dart';
|
||||
import '../../../controller/firebase/local_notification.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../home/Captin/home_captain/home_captin.dart';
|
||||
import '../../widgets/mydialoug.dart'; // تأكد من إضافة هذه المكتبة
|
||||
|
||||
class AI extends GetxController {
|
||||
// --- متغيرات الحالة ---
|
||||
bool isLoading = false;
|
||||
bool isEgypt = true; // لتحديد جنسية السائق
|
||||
|
||||
// --- متغيرات تخزين بيانات الوثائق ---
|
||||
Map<String, dynamic> responseIdEgyptDriverLicense = {};
|
||||
Map<String, dynamic> responseIdCardDriverEgyptFront = {};
|
||||
Map<String, dynamic> responseIdCardDriverEgyptBack = {};
|
||||
Map<String, dynamic> responseIdEgyptFront = {};
|
||||
Map<String, dynamic> responseIdEgyptBack = {};
|
||||
Map<String, dynamic> responseCriminalRecordEgypt = {};
|
||||
// للمقيمين الأجانب
|
||||
Map<String, dynamic> responseNonIdCardFront = {};
|
||||
Map<String, dynamic> responseNonIdCardBack = {};
|
||||
|
||||
// --- دالة تغيير الجنسية ---
|
||||
void changeNationality() {
|
||||
isEgypt = !isEgypt;
|
||||
update();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 1. الدالة الأساسية للذكاء الاصطناعي (رفع، تحقق، استخراج)
|
||||
// ===========================================================================
|
||||
Future<void> allMethodForAI(String type) async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
// 1. التقاط الصورة
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? pickedImage = await picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
preferredCameraDevice: CameraDevice.rear,
|
||||
);
|
||||
|
||||
if (pickedImage == null) {
|
||||
isLoading = false;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. معالجة الصورة (قص)
|
||||
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||
sourcePath: pickedImage.path,
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: 'ضبط الصورة',
|
||||
toolbarColor: AppColor.primaryColor,
|
||||
toolbarWidgetColor: Colors.white,
|
||||
initAspectRatio: CropAspectRatioPreset.original,
|
||||
lockAspectRatio: false,
|
||||
),
|
||||
IOSUiSettings(
|
||||
title: 'ضبط الصورة',
|
||||
doneButtonTitle: 'تم',
|
||||
cancelButtonTitle: 'إلغاء',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (croppedFile == null) {
|
||||
isLoading = false;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. ضغط الصورة
|
||||
File processedImage = File(croppedFile.path);
|
||||
File compressedImage = await testCompressAndGetFile(
|
||||
processedImage,
|
||||
processedImage.path,
|
||||
);
|
||||
|
||||
// 4. تجهيز الطلب
|
||||
var request = http.MultipartRequest("POST",
|
||||
Uri.parse('${AppLink.server}/auth/captin/ai_document_egypt.php'));
|
||||
|
||||
request.fields['type'] = type;
|
||||
request.fields['driver_id'] =
|
||||
box.read(BoxName.driverID)?.toString() ?? 'unknown';
|
||||
|
||||
// تأمين الهيدر من القيم الفارغة
|
||||
String jwt = box.read(BoxName.jwt) ?? '';
|
||||
String cleanJwt = r(jwt).split(AppInformation.addd)[0];
|
||||
|
||||
final headers = <String, String>{
|
||||
'Authorization': 'Bearer $cleanJwt',
|
||||
'X-HMAC-Auth': '${box.read(BoxName.hmac) ?? ''}',
|
||||
};
|
||||
request.headers.addAll(headers);
|
||||
|
||||
var stream = http.ByteStream(compressedImage.openRead());
|
||||
var length = await compressedImage.length();
|
||||
var multipartFile = http.MultipartFile('image', stream, length,
|
||||
filename: basename(compressedImage.path));
|
||||
|
||||
request.files.add(multipartFile);
|
||||
|
||||
// 5. إرسال الطلب
|
||||
Log.print("Uploading image... type: $type");
|
||||
var response = await request.send();
|
||||
var responseBody = await http.Response.fromStream(response);
|
||||
|
||||
Log.print("Response Code: ${response.statusCode}");
|
||||
Log.print("Raw Response Body: ${responseBody.body}");
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// تنظيف الرد من أي نصوص زائدة (PHP Logs)
|
||||
String cleanJsonString = responseBody.body;
|
||||
int firstBrace = cleanJsonString.indexOf('{');
|
||||
int lastBrace = cleanJsonString.lastIndexOf('}');
|
||||
|
||||
if (firstBrace != -1 && lastBrace != -1) {
|
||||
cleanJsonString =
|
||||
cleanJsonString.substring(firstBrace, lastBrace + 1);
|
||||
} else {
|
||||
throw Exception("No valid JSON found in response");
|
||||
}
|
||||
|
||||
var jsonResponse = jsonDecode(cleanJsonString);
|
||||
|
||||
if (jsonResponse['status'] == 'success') {
|
||||
// --- التعديل هنا: استخراج البيانات بشكل صحيح ---
|
||||
Map<String, dynamic> aiData = {};
|
||||
|
||||
// 1. محاولة قراءة البيانات من message.data (كما في اللوج الأخير)
|
||||
if (jsonResponse['message'] is Map &&
|
||||
jsonResponse['message'].containsKey('data')) {
|
||||
aiData = jsonResponse['message']['data'];
|
||||
}
|
||||
// 2. محاولة قراءة البيانات من data مباشرة (احتياط)
|
||||
else if (jsonResponse.containsKey('data') &&
|
||||
jsonResponse['data'] is Map) {
|
||||
aiData = jsonResponse['data'];
|
||||
}
|
||||
// 3. حالة أخيرة: إذا كانت الرسالة نفسها هي البيانات
|
||||
else if (jsonResponse['message'] is Map) {
|
||||
aiData = jsonResponse['message'];
|
||||
}
|
||||
|
||||
if (aiData.containsKey('is_wrong_document') &&
|
||||
aiData['is_wrong_document'] == true) {
|
||||
String detectedEnglish = aiData['detected_type'] ?? '';
|
||||
String detectedArabic = _translateDetectedType(detectedEnglish);
|
||||
|
||||
MyDialog().getDialog(
|
||||
'وثيقة غير صحيحة'.tr,
|
||||
'لقد قمت برفع: $detectedArabic\nالمطلوب هو: ${_getReadableName(type)}\n\nيرجى إعادة المحاولة.'
|
||||
.tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
} else {
|
||||
// النجاح: تخزين البيانات
|
||||
_assignDataToVariable(type, aiData);
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'فشل التحليل'.tr,
|
||||
jsonResponse['message'] is String
|
||||
? jsonResponse['message']
|
||||
: 'لم يتم استخراج بيانات',
|
||||
() => Get.back());
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog('خطأ في السيرفر'.tr, 'كود: ${response.statusCode}',
|
||||
() => Get.back());
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
Log.print("Error inside allMethodForAI: $e");
|
||||
Log.print("Stacktrace: $stacktrace");
|
||||
|
||||
MyDialog().getDialog(
|
||||
'خطأ'.tr, 'حدث خطأ غير متوقع.\nالسبب: $e', () => Get.back());
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
// 2. معالجة الصورة (قص)
|
||||
|
||||
// ===========================================================================
|
||||
// 2. دالة التسجيل الموحدة (السائق + السيارة) - Transaction
|
||||
// ===========================================================================
|
||||
Future<void> registerCompleteAccount() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
final loginDriverController = Get.put(LoginDriverController());
|
||||
|
||||
// 1. كلمة المرور
|
||||
String passwordRaw = loginDriverController.passwordController.text.trim();
|
||||
if (passwordRaw.isEmpty) {
|
||||
passwordRaw =
|
||||
'${box.read(BoxName.phoneDriver)}${box.read(BoxName.driverID)}';
|
||||
}
|
||||
|
||||
// 2. التحقق من الهوية
|
||||
String driverId = box.read(BoxName.driverID)?.toString() ?? '';
|
||||
String email = box.read(BoxName.emailDriver)?.toString() ?? '';
|
||||
String phone = box.read(BoxName.phoneDriver)?.toString() ?? '';
|
||||
|
||||
if (driverId.isEmpty || phone.isEmpty) {
|
||||
isLoading = false;
|
||||
update();
|
||||
mySnackeBarError("Missing Registration Info".tr);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. استخراج الاسم الأول والأخير من (name_arabic)
|
||||
// حسب ملف الـ AI الخاص بك، المفتاح هو 'name_arabic' للرخصة
|
||||
String fullArabicName = (isEgypt
|
||||
? responseIdEgyptDriverLicense['name_arabic']
|
||||
: responseNonIdCardFront['full_name'])
|
||||
?.toString() ??
|
||||
'';
|
||||
|
||||
// تنظيف الاسم وتقسيمه
|
||||
fullArabicName = fullArabicName.trim().replaceAll(RegExp(r'\s+'), ' ');
|
||||
List<String> nameParts = fullArabicName.split(' ');
|
||||
|
||||
// إذا كان الاسم العربي فارغ، نحاول الإنجليزي أو أي اسم متاح
|
||||
if (nameParts.isEmpty || nameParts[0].isEmpty) {
|
||||
String altName = responseIdEgyptFront['full_name']?.toString() ?? '';
|
||||
nameParts = altName.trim().split(' ');
|
||||
}
|
||||
|
||||
String firstName = nameParts.isNotEmpty ? nameParts.first : '';
|
||||
String lastName = nameParts.length > 1 ? nameParts.last : '';
|
||||
|
||||
// 4. تجهيز البيانات
|
||||
Map<String, dynamic> driverData = {};
|
||||
|
||||
if (isEgypt) {
|
||||
driverData = {
|
||||
'is_egypt': '1',
|
||||
'first_name': firstName, // تم التعديل
|
||||
'last_name': lastName, // تم التعديل
|
||||
'name_arabic': fullArabicName,
|
||||
'national_number': responseIdEgyptDriverLicense['national_number'] ??
|
||||
responseIdEgyptBack['nationalID'] ??
|
||||
'',
|
||||
'name_english': responseIdEgyptDriverLicense['name_english'] ?? '',
|
||||
'gender': responseIdEgyptBack['gender'] ?? 'Male',
|
||||
'license_type': responseIdEgyptDriverLicense['license_type'] ?? 'خاصة',
|
||||
'license_categories': responseIdEgyptDriverLicense['license_type'] ??
|
||||
'', // استخدام النوع كفئة إذا لم تتوفر الفئات
|
||||
'birthdate': extractDOB(
|
||||
responseIdEgyptDriverLicense['national_number'].toString()),
|
||||
'address': responseIdEgyptFront['address'] ?? '',
|
||||
'occupation': responseIdEgyptBack['occupation'] ?? 'Driver',
|
||||
'religion': responseIdEgyptBack['religion'] ?? '',
|
||||
'maritalStatus': responseIdEgyptBack['maritalStatus'] ?? '',
|
||||
'card_id': responseIdEgyptBack['nationalID'] ?? '', // رقم البطاقة
|
||||
'site': responseIdEgyptFront['address'] ?? '',
|
||||
'education': responseIdEgyptBack['education'] ?? '',
|
||||
'employmentType': '',
|
||||
'bankCode': 'yet',
|
||||
'accountBank': 'yet',
|
||||
};
|
||||
} else {
|
||||
driverData = {
|
||||
'is_egypt': '0',
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'name_arabic': fullArabicName,
|
||||
'national_number': responseNonIdCardFront['passport_no'] ?? '',
|
||||
'name_english': '',
|
||||
'gender': responseNonIdCardFront['gender'] ?? 'Male',
|
||||
'license_type': 'Foreign',
|
||||
'license_categories': 'Foreign',
|
||||
'birthdate': responseNonIdCardFront['birthdate'] ?? '',
|
||||
'address': responseNonIdCardFront['address'] ?? '',
|
||||
'occupation': responseNonIdCardBack['workStatus'] ?? '',
|
||||
'religion': responseNonIdCardFront['country'] ?? '',
|
||||
'maritalStatus': '',
|
||||
'card_id': responseNonIdCardFront['card_id'] ?? '',
|
||||
'site': '',
|
||||
'education': '',
|
||||
'employmentType': '',
|
||||
'bankCode': '',
|
||||
'accountBank': '',
|
||||
};
|
||||
}
|
||||
|
||||
// إضافة البيانات الأساسية
|
||||
driverData.addAll({
|
||||
'id': driverId,
|
||||
'email': email,
|
||||
'phone': phone,
|
||||
'password': passwordRaw,
|
||||
});
|
||||
|
||||
// بيانات السيارة
|
||||
Map<String, dynamic> carData = {
|
||||
'vin': responseIdCardDriverEgyptBack['chassis'].toString(),
|
||||
'car_plate': responseIdCardDriverEgyptFront['car_plate'].toString(),
|
||||
'make': responseIdCardDriverEgyptBack['make'].toString(),
|
||||
'model': responseIdCardDriverEgyptBack['model'].toString(),
|
||||
'year': responseIdCardDriverEgyptBack['year'].toString(),
|
||||
'expiration_date':
|
||||
responseIdCardDriverEgyptFront['LicenseExpirationDate'].toString(),
|
||||
'color': responseIdCardDriverEgyptBack['color'].toString(),
|
||||
'color_hex': responseIdCardDriverEgyptBack['color_hex'].toString(),
|
||||
'owner': responseIdCardDriverEgyptFront['owner'].toString(),
|
||||
'displacement': responseIdCardDriverEgyptBack['displacement'].toString(),
|
||||
'fuel': responseIdCardDriverEgyptBack['fuel'].toString(),
|
||||
'registration_date':
|
||||
responseIdCardDriverEgyptBack['inspection_date'].toString(),
|
||||
};
|
||||
|
||||
// تحويل الكل إلى String
|
||||
Map<String, dynamic> finalPayload = {};
|
||||
driverData
|
||||
.forEach((key, value) => finalPayload[key] = value?.toString() ?? '');
|
||||
carData
|
||||
.forEach((key, value) => finalPayload[key] = value?.toString() ?? '');
|
||||
|
||||
try {
|
||||
Log.print("Sending Payload: $finalPayload");
|
||||
|
||||
var res = await CRUD().post(
|
||||
link: '${AppLink.authCaptin}/register_complete.php',
|
||||
payload: finalPayload);
|
||||
|
||||
Log.print("Raw Response: $res");
|
||||
|
||||
if (res == 'failure' || res == null) {
|
||||
mySnackeBarError("Connection Failed".tr);
|
||||
isLoading = false;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
// 🟢 الإصلاح الرئيسي للكراش: التعامل مع الرد سواء كان Map أو String
|
||||
Map<String, dynamic> statusResponse = {};
|
||||
|
||||
if (res is Map) {
|
||||
statusResponse = Map<String, dynamic>.from(res);
|
||||
} else if (res is String) {
|
||||
try {
|
||||
statusResponse = jsonDecode(res);
|
||||
} catch (e) {
|
||||
Log.print("JSON Parse Error: $e");
|
||||
// في حال فشل الفك، نعتبره فشل
|
||||
mySnackeBarError("Invalid Server Data".tr);
|
||||
isLoading = false;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
|
||||
if (statusResponse['status'] == 'success') {
|
||||
box.write(BoxName.phoneVerified, '1');
|
||||
mySnackbarSuccess("Registration Successful".tr);
|
||||
await addToken();
|
||||
NotificationService.sendNotification(
|
||||
target: 'service',
|
||||
title: 'New Driver',
|
||||
body: 'New driver registered: $fullArabicName',
|
||||
isTopic: true,
|
||||
category: 'new_service_request',
|
||||
);
|
||||
|
||||
Get.offAll(() => HomeCaptain());
|
||||
} else {
|
||||
String msg = statusResponse['message']?.toString() ?? "Error";
|
||||
mySnackeBarError(msg.tr);
|
||||
}
|
||||
} catch (e) {
|
||||
isLoading = false;
|
||||
update();
|
||||
Log.print("Critical Error: $e");
|
||||
mySnackeBarError("Unexpected Error".tr);
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 3. دوال مساعدة (Helpers)
|
||||
// ===========================================================================
|
||||
addToken() async {
|
||||
String? fingerPrint = await storage.read(key: BoxName.fingerPrint);
|
||||
final payload = {
|
||||
'token': (box.read(BoxName.tokenDriver)),
|
||||
'captain_id': (box.read(BoxName.driverID)).toString(),
|
||||
'fingerPrint': (fingerPrint).toString()
|
||||
};
|
||||
Log.print('payload for addToken: ${payload}');
|
||||
CRUD().post(link: AppLink.addTokensDriver, payload: payload);
|
||||
}
|
||||
|
||||
// ضغط الصورة
|
||||
Future<File> testCompressAndGetFile(File file, String targetPath) async {
|
||||
var newPath = targetPath
|
||||
.replaceAll(".jpg", "_compressed.webp")
|
||||
.replaceAll(".jpeg", "_compressed.webp");
|
||||
|
||||
var result = await FlutterImageCompress.compressAndGetFile(
|
||||
file.absolute.path,
|
||||
newPath,
|
||||
quality: 88,
|
||||
format: CompressFormat.webp,
|
||||
rotate: 0,
|
||||
);
|
||||
|
||||
return File(result!.path);
|
||||
}
|
||||
|
||||
// استخراج تاريخ الميلاد من الرقم القومي المصري
|
||||
String extractDOB(String nationalId) {
|
||||
if (nationalId.length != 14) return '';
|
||||
String century = nationalId[0] == '2' ? '19' : '20';
|
||||
String year = nationalId.substring(1, 3);
|
||||
String month = nationalId.substring(3, 5);
|
||||
String day = nationalId.substring(5, 7);
|
||||
return "$century$year-$month-$day";
|
||||
}
|
||||
|
||||
// توزيع البيانات القادمة من Gemini
|
||||
void _assignDataToVariable(String type, Map<String, dynamic> data) {
|
||||
switch (type) {
|
||||
case 'driver_license':
|
||||
responseIdEgyptDriverLicense = data;
|
||||
break;
|
||||
case 'car_front':
|
||||
responseIdCardDriverEgyptFront = data;
|
||||
break;
|
||||
case 'car_back':
|
||||
responseIdCardDriverEgyptBack = data;
|
||||
break;
|
||||
case 'id_front':
|
||||
responseIdEgyptFront = data;
|
||||
break;
|
||||
case 'id_back':
|
||||
responseIdEgyptBack = data;
|
||||
break;
|
||||
case 'non_id_front':
|
||||
responseNonIdCardFront = data;
|
||||
break;
|
||||
case 'non_id_back':
|
||||
responseNonIdCardBack = data;
|
||||
break;
|
||||
case 'criminal_record':
|
||||
responseCriminalRecordEgypt = data;
|
||||
break;
|
||||
}
|
||||
update(); // تحديث الواجهة لعرض البيانات الجديدة
|
||||
}
|
||||
|
||||
// ترجمة أنواع الوثائق للعربية لرسائل الخطأ
|
||||
String _getReadableName(String type) {
|
||||
switch (type) {
|
||||
case 'driver_license':
|
||||
return 'رخصة القيادة (الوجه)';
|
||||
case 'car_front':
|
||||
return 'رخصة السيارة (الوجه - اللوحة)';
|
||||
case 'car_back':
|
||||
return 'رخصة السيارة (الخلف - البيانات)';
|
||||
case 'id_front':
|
||||
return 'البطاقة الشخصية (الوجه)';
|
||||
case 'id_back':
|
||||
return 'البطاقة الشخصية (الخلف)';
|
||||
case 'non_id_front':
|
||||
return 'إقامة أجانب (الوجه)';
|
||||
case 'non_id_back':
|
||||
return 'إقامة أجانب (الخلف)';
|
||||
case 'criminal_record':
|
||||
return 'صحيفة الحالة الجنائية (الفيش)';
|
||||
|
||||
default:
|
||||
return 'الوثيقة المطلوبة';
|
||||
}
|
||||
}
|
||||
|
||||
// ترجمة ما اكتشفه الذكاء الاصطناعي (لرسالة الخطأ)
|
||||
String _translateDetectedType(String englishText) {
|
||||
String lower = englishText.toLowerCase();
|
||||
if (lower.contains('back') && lower.contains('vehicle')) {
|
||||
return 'ظهر رخصة السيارة';
|
||||
}
|
||||
if (lower.contains('front') && lower.contains('vehicle')) {
|
||||
return 'وجه رخصة السيارة';
|
||||
}
|
||||
if (lower.contains('back') && lower.contains('id')) {
|
||||
return 'ظهر البطاقة الشخصية';
|
||||
}
|
||||
if (lower.contains('front') && lower.contains('id')) {
|
||||
return 'وجه البطاقة الشخصية';
|
||||
}
|
||||
if (lower.contains('driver')) return 'رخصة قيادة';
|
||||
if (lower.contains('car')) return 'رخصة سيارة';
|
||||
return 'وثيقة مختلفة';
|
||||
}
|
||||
|
||||
// التحقق من صحة البيانات (هل الحقول فارغة؟)
|
||||
bool _isValidDocumentData(String type, Map<String, dynamic> data) {
|
||||
if (data.containsKey('is_wrong_document') &&
|
||||
data['is_wrong_document'] == true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'driver_license':
|
||||
return (data['national_number'] != null &&
|
||||
data['national_number'].toString().length > 5) ||
|
||||
(data['name_arabic'] != null);
|
||||
case 'car_front':
|
||||
return data['car_plate'] != null;
|
||||
case 'car_back':
|
||||
return data['chassis'] != null ||
|
||||
data['motor'] != null ||
|
||||
data['color'] != null;
|
||||
case 'id_front':
|
||||
return data['full_name'] != null || data['card_id'] != null;
|
||||
case 'id_back':
|
||||
return data['nationalID'] != null;
|
||||
case 'criminal_record':
|
||||
return data['NationalID'] != null || data['FullName'] != null;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool isInviteDriverFound = false;
|
||||
Future updateInvitationCodeFromRegister() async {
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.updateDriverInvitationDirectly,
|
||||
payload: {
|
||||
"inviterDriverPhone": '+2${box.read(BoxName.phoneDriver).toString()}',
|
||||
"driverId": box.read(BoxName.driverID).toString(),
|
||||
},
|
||||
);
|
||||
|
||||
if (res != 'failure') {
|
||||
isInviteDriverFound = true;
|
||||
update();
|
||||
// mySnackbarSuccess("Code approved".tr); // Localized success message
|
||||
box.write(BoxName.isInstall, '1');
|
||||
NotificationController().showNotification(
|
||||
"Code approved".tr, "Code approved".tr, 'tone2', '');
|
||||
// Notification text with dynamic token
|
||||
NotificationService.sendNotification(
|
||||
target: jsonDecode(res)['message'][0]['token'].toString(),
|
||||
title: 'You have received a gift token!'.tr,
|
||||
body: 'for '.tr + box.read(BoxName.phoneDriver).toString(),
|
||||
category: 'gift_token',
|
||||
tone: 'tone2',
|
||||
isTopic: false,
|
||||
);
|
||||
} else {
|
||||
// mySnackeBarError(
|
||||
// "You dont have invitation code".tr); // Localized error message
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// حفظ الفيش الجنائي فقط (تحديث حالة السائق)
|
||||
// ===========================================================================
|
||||
Future<void> saveCriminalRecordOnly() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
// تجهيز البيانات
|
||||
var payload = {
|
||||
'driver_id': box.read(BoxName.driverID) ?? box.read(BoxName.driverID),
|
||||
'status_result':
|
||||
responseCriminalRecordEgypt['InspectionResult'] ?? 'Unknown',
|
||||
'issue_date': responseCriminalRecordEgypt['IssueDate'] ?? '',
|
||||
'national_id': responseCriminalRecordEgypt['NationalID'] ?? '',
|
||||
// يفضل إرسال الاسم أيضاً للتأكيد
|
||||
'full_name': responseCriminalRecordEgypt['FullName'] ?? '',
|
||||
};
|
||||
|
||||
// الاتصال بملف PHP الجديد
|
||||
// تأكد من إضافة هذا الرابط في ملف links.dart
|
||||
var res = await CRUD().post(
|
||||
link: '${AppLink.server}${'/auth/captin/update_criminal_record.php'}',
|
||||
payload: payload);
|
||||
|
||||
var json = jsonDecode(res);
|
||||
|
||||
if (json['status'] == 'success') {
|
||||
// نجاح
|
||||
Get.snackbar(
|
||||
"تم بنجاح".tr,
|
||||
"تم التحقق من صحيفة الحالة الجنائية وحفظها في ملفك".tr,
|
||||
backgroundColor: AppColor.greenColor,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
|
||||
// العودة للصفحة الرئيسية أو الإعدادات
|
||||
Get.offAll(() => HomeCaptain());
|
||||
} else {
|
||||
// فشل
|
||||
Get.snackbar(
|
||||
"تنبيه".tr,
|
||||
"فشل الحفظ: ${json['message']}".tr,
|
||||
backgroundColor: AppColor.redColor,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Error saving criminal record: $e");
|
||||
Get.snackbar("خطأ".tr, "حدث خطأ أثناء الاتصال بالسيرفر".tr,
|
||||
backgroundColor: AppColor.redColor, colorText: Colors.white);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
// Future updatePassengersInvitation() async {
|
||||
// if (formKey.currentState!.validate()) {
|
||||
// var res = await CRUD().post(
|
||||
// link: AppLink.updatePassengersInvitation,
|
||||
// payload: {"inviteCode": invitationCodeController.text});
|
||||
// if (res != 'failure') {
|
||||
// isInviteDriverFound = true;
|
||||
// update();
|
||||
// mySnackbarSuccess("Code approved".tr);
|
||||
// } else {
|
||||
// mySnackeBarError("Code not approved".tr);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// إضافة بسيطة لتسهيل قراءة أول وآخر عنصر في المصفوفة (Extension)
|
||||
extension ListExtension<E> on List<E> {
|
||||
E? get firstOrNull => isEmpty ? null : first;
|
||||
E? get lastOrNull => isEmpty ? null : last;
|
||||
}
|
||||
Reference in New Issue
Block a user