- 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>
663 lines
24 KiB
Dart
Executable File
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;
|
|
}
|