Files
tripz-llc/apps/driver/lib/views/auth/captin/ai_eg_controller.dart
T
HamzaandClaude Opus 4.8 6234e66471 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>
2026-07-16 20:01:26 +03:00

663 lines
24 KiB
Dart
Executable File

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;
}