771 lines
28 KiB
Dart
771 lines
28 KiB
Dart
import 'dart:convert';
|
||
import 'dart:io';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||
import 'package:get/get.dart';
|
||
import 'package:image_picker/image_picker.dart';
|
||
import 'package:image_cropper/image_cropper.dart';
|
||
import 'package:image/image.dart' as img;
|
||
import 'package:path/path.dart';
|
||
import 'package:sefer_driver/constant/links.dart';
|
||
import 'package:sefer_driver/controller/firebase/notification_service.dart';
|
||
import '../../../constant/box_name.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
// --- Final Submission ---
|
||
import 'package:path_provider/path_provider.dart' as path_provider;
|
||
|
||
import 'package:http/http.dart' as http;
|
||
import 'package:http_parser/http_parser.dart';
|
||
import 'package:mime/mime.dart';
|
||
import 'package:path/path.dart' as p;
|
||
import '../../../constant/colors.dart';
|
||
import '../../../constant/info.dart';
|
||
import '../../../main.dart';
|
||
import '../../../print.dart';
|
||
import '../../../views/widgets/error_snakbar.dart';
|
||
import '../../functions/crud.dart';
|
||
import '../../functions/encrypt_decrypt.dart';
|
||
import '../../functions/package_info.dart';
|
||
import '../captin/login_captin_controller.dart';
|
||
|
||
// You can create a simple enum to manage image types
|
||
enum ImageType {
|
||
driverLicenseFront,
|
||
driverLicenseBack,
|
||
carLicenseFront,
|
||
carLicenseBack,
|
||
}
|
||
|
||
class RegistrationController extends GetxController {
|
||
// Page Controller for managing steps
|
||
late PageController pageController;
|
||
var currentPage =
|
||
0.obs; // Use .obs for reactive updates on the step indicator
|
||
|
||
// Loading state
|
||
var isLoading = false.obs;
|
||
var isloading = false;
|
||
CroppedFile? croppedFile;
|
||
final picker = ImagePicker();
|
||
var image;
|
||
File? myImage;
|
||
String? colorHex; // سيُملى من الدروب داون
|
||
// Form Keys for validation
|
||
final driverInfoFormKey = GlobalKey<FormState>();
|
||
final carInfoFormKey = GlobalKey<FormState>();
|
||
|
||
// STEP 1: Driver Information Controllers
|
||
final firstNameController = TextEditingController();
|
||
final lastNameController = TextEditingController();
|
||
final nationalIdController = TextEditingController();
|
||
final bithdateController = TextEditingController();
|
||
final phoneController = TextEditingController(); // You can pre-fill this
|
||
final driverLicenseExpiryController = TextEditingController();
|
||
DateTime? driverLicenseExpiryDate;
|
||
|
||
// STEP 2: Car Information Controllers
|
||
final carPlateController = TextEditingController();
|
||
final carMakeController = TextEditingController();
|
||
final carModelController = TextEditingController();
|
||
final carYearController = TextEditingController();
|
||
final carColorController = TextEditingController();
|
||
final carVinController = TextEditingController(); // Chassis number
|
||
final carRegistrationExpiryController = TextEditingController();
|
||
DateTime? carRegistrationExpiryDate;
|
||
// داخل RegistrationController
|
||
|
||
// المتغيرات لتخزين القيم المختارة (لإرسالها للـ API لاحقاً)
|
||
int? selectedVehicleCategoryId; // سيخزن 1 أو 2 أو 3
|
||
int? selectedFuelTypeId; // سيخزن 1 أو 2 أو 3 أو 4
|
||
|
||
// قائمة أنواع المركبات (مطابقة لقاعدة البيانات)
|
||
final List<Map<String, dynamic>> vehicleCategoryOptions = [
|
||
{'id': 1, 'name': 'Car'.tr}, // ترجمة: سيارة
|
||
{'id': 2, 'name': 'Motorcycle'.tr}, // ترجمة: دراجة نارية
|
||
{'id': 3, 'name': 'Van / Bus'.tr}, // ترجمة: فان / باص
|
||
];
|
||
|
||
// قائمة أنواع الوقود
|
||
final List<Map<String, dynamic>> fuelTypeOptions = [
|
||
{'id': 1, 'name': 'Petrol'.tr}, // ترجمة: بنزين
|
||
{'id': 2, 'name': 'Diesel'.tr}, // ترجمة: ديزل
|
||
{'id': 3, 'name': 'Electric'.tr}, // ترجمة: كهربائي
|
||
{'id': 4, 'name': 'Hybrid'.tr}, // ترجمة: هايبرد
|
||
];
|
||
// STEP 3: Document Uploads
|
||
File? driverLicenseFrontImage;
|
||
File? driverLicenseBackImage;
|
||
File? carLicenseFrontImage;
|
||
File? carLicenseBackImage;
|
||
|
||
@override
|
||
void onInit() {
|
||
super.onInit();
|
||
pageController = PageController();
|
||
// Pre-fill phone number if it exists in storage
|
||
// phoneController.text = box.read(BoxName.phoneDriver) ?? '';
|
||
}
|
||
|
||
@override
|
||
void onClose() {
|
||
pageController.dispose();
|
||
// Dispose all other text controllers
|
||
super.onClose();
|
||
}
|
||
|
||
// --- Page Navigation ---
|
||
void goToNextStep() {
|
||
bool isValid = false;
|
||
if (currentPage.value == 0) {
|
||
// Validate Step 1
|
||
isValid = driverInfoFormKey.currentState!.validate();
|
||
if (isValid) {
|
||
// Optional: Check if license is expired
|
||
// if (driverLicenseExpiryDate != null &&
|
||
// driverLicenseExpiryDate!.isBefore(DateTime.now())) {
|
||
// Get.snackbar('Expired License', 'Your driver’s license has expired.'.tr
|
||
// ,
|
||
// snackPosition: SnackPosition.BOTTOM,
|
||
// backgroundColor: Colors.red,
|
||
// colorText: Colors.white);
|
||
// return; // Stop progression
|
||
// }
|
||
}
|
||
} else if (currentPage.value == 1) {
|
||
// Validate Step 2
|
||
isValid = carInfoFormKey.currentState!.validate();
|
||
}
|
||
|
||
if (isValid) {
|
||
pageController.nextPage(
|
||
duration: const Duration(milliseconds: 300),
|
||
curve: Curves.easeIn,
|
||
);
|
||
}
|
||
}
|
||
|
||
void goToPreviousStep() {
|
||
pageController.previousPage(
|
||
duration: const Duration(milliseconds: 300),
|
||
curve: Curves.easeIn,
|
||
);
|
||
}
|
||
|
||
// --- Image Picking ---
|
||
Future<void> pickImage(ImageType type) async {
|
||
try {
|
||
final picker = ImagePicker();
|
||
final picked = await picker.pickImage(
|
||
source: ImageSource.camera,
|
||
imageQuality: 95, // جودة أولية من الكاميرا
|
||
maxWidth: 3000, // نسمح بصورة كبيرة ثم نصغّر نحن
|
||
);
|
||
if (picked == null) return;
|
||
|
||
// قصّ الصورة
|
||
final cropped = await ImageCropper().cropImage(
|
||
sourcePath: picked.path,
|
||
uiSettings: [
|
||
AndroidUiSettings(
|
||
toolbarTitle: 'Cropper'.tr,
|
||
toolbarColor: AppColor.accentColor,
|
||
toolbarWidgetColor: AppColor.redColor,
|
||
initAspectRatio: CropAspectRatioPreset.original,
|
||
lockAspectRatio: false,
|
||
),
|
||
IOSUiSettings(title: 'Cropper'.tr),
|
||
],
|
||
);
|
||
if (cropped == null) return; // المستخدم ألغى
|
||
|
||
// قراءة bytes + التصحيح حسب EXIF ثم التصغير
|
||
final rawBytes = await File(cropped.path).readAsBytes();
|
||
final decoded = img.decodeImage(rawBytes);
|
||
if (decoded == null) throw Exception('Decode image failed');
|
||
|
||
// تصحيح اتجاه الصورة (EXIF)
|
||
final fixed = img.bakeOrientation(decoded);
|
||
|
||
// تصغير لعرض 800px (عدّل عند الحاجة)
|
||
final resized = img.copyResize(fixed, width: 800);
|
||
|
||
// حفظ مؤقت بصيغة JPG
|
||
final tmpDir = await getTemporaryDirectory();
|
||
final outPath =
|
||
'${tmpDir.path}/doc_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||
final outFile = File(outPath);
|
||
await outFile.writeAsBytes(img.encodeJpg(resized, quality: 85));
|
||
|
||
// عيّن الملف في المتغير الصحيح حسب النوع
|
||
if (outFile != null) {
|
||
switch (type) {
|
||
case ImageType.driverLicenseFront:
|
||
driverLicenseFrontImage = File(outFile.path);
|
||
break;
|
||
case ImageType.driverLicenseBack:
|
||
driverLicenseBackImage = File(outFile.path);
|
||
break;
|
||
case ImageType.carLicenseFront:
|
||
carLicenseFrontImage = File(outFile.path);
|
||
break;
|
||
case ImageType.carLicenseBack:
|
||
carLicenseBackImage = File(outFile.path);
|
||
break;
|
||
}
|
||
update(); // Use update() to refresh the GetBuilder UI
|
||
}
|
||
|
||
update(); // لتحديث الـ UI
|
||
|
||
// // الإرسال للذكاء الاصطناعي
|
||
// await sendToAI(type, imageFile: outFile);
|
||
} catch (e) {
|
||
Get.snackbar('Error'.tr, '${'An unexpected error occurred:'.tr} $e');
|
||
}
|
||
}
|
||
|
||
// ثابت: 20 لون سيارة شائع
|
||
static const List<Map<String, String>> kCarColorOptions = [
|
||
{'key': 'color.white', 'hex': '#FFFFFF'},
|
||
{'key': 'color.black', 'hex': '#000000'},
|
||
{'key': 'color.silver', 'hex': '#C0C0C0'},
|
||
{'key': 'color.gray', 'hex': '#808080'},
|
||
{'key': 'color.gunmetal', 'hex': '#2A3439'},
|
||
{'key': 'color.red', 'hex': '#C62828'},
|
||
{'key': 'color.blue', 'hex': '#1565C0'},
|
||
{'key': 'color.navy', 'hex': '#0D47A1'},
|
||
{'key': 'color.green', 'hex': '#2E7D32'},
|
||
{'key': 'color.darkGreen', 'hex': '#1B5E20'},
|
||
{'key': 'color.beige', 'hex': '#D7CCC8'},
|
||
{'key': 'color.brown', 'hex': '#5D4037'},
|
||
{'key': 'color.maroon', 'hex': '#800000'},
|
||
{'key': 'color.burgundy', 'hex': '#800020'},
|
||
{'key': 'color.yellow', 'hex': '#F9A825'},
|
||
{'key': 'color.orange', 'hex': '#EF6C00'},
|
||
{'key': 'color.gold', 'hex': '#D4AF37'},
|
||
{'key': 'color.bronze', 'hex': '#CD7F32'},
|
||
{'key': 'color.champagne', 'hex': '#EFE1C6'},
|
||
{'key': 'color.purple', 'hex': '#6A1B9A'},
|
||
];
|
||
|
||
Color hexToColor(String hex) {
|
||
var v = hex.replaceAll('#', '');
|
||
if (v.length == 6) v = 'FF$v';
|
||
return Color(int.parse(v, radix: 16));
|
||
}
|
||
|
||
//uploadSyrianDocs
|
||
// دالة مساعدة: تضيف الحقل إذا كان له قيمة
|
||
void _addField(Map<String, String> fields, String key, String? value) {
|
||
if (value != null && value.toString().isNotEmpty) {
|
||
fields[key] = value.toString();
|
||
}
|
||
}
|
||
|
||
/// خريطة لتخزين روابط المستندات بعد الرفع
|
||
final Map<String, String> docUrls = {
|
||
'driver_license_front': '',
|
||
'driver_license_back': '',
|
||
'car_license_front': '',
|
||
'car_license_back': '',
|
||
};
|
||
|
||
/// التصرّف العام لاختيار/قص/ضغط/رفع الصورة حسب type
|
||
Future<void> choosImage(String link, String imageType) async {
|
||
try {
|
||
final pickedImage = await picker.pickImage(
|
||
source: ImageSource.camera,
|
||
preferredCameraDevice: CameraDevice.rear,
|
||
);
|
||
if (pickedImage == null) return;
|
||
|
||
image = File(pickedImage.path);
|
||
|
||
final croppedFile = await ImageCropper().cropImage(
|
||
sourcePath: image!.path,
|
||
uiSettings: [
|
||
AndroidUiSettings(
|
||
toolbarTitle: 'Cropper'.tr,
|
||
toolbarColor: AppColor.blueColor,
|
||
toolbarWidgetColor: AppColor.yellowColor,
|
||
initAspectRatio: CropAspectRatioPreset.original,
|
||
lockAspectRatio: false,
|
||
),
|
||
IOSUiSettings(title: 'Cropper'.tr),
|
||
],
|
||
);
|
||
if (croppedFile == null) return;
|
||
|
||
// صورة للمعاينة داخل التطبيق
|
||
myImage = File(croppedFile.path);
|
||
|
||
isloading = true;
|
||
update();
|
||
|
||
// ضغط (وأيضاً يمكنك إضافة rotateImageIfNeeded قبل/بعد الضغط إن رغبت)
|
||
final File compressedImage = await compressImage(File(croppedFile.path));
|
||
|
||
// تجهيز الحقول
|
||
final driverId = box.read(BoxName.driverID);
|
||
|
||
final payload = <String, String>{
|
||
'driverID': driverId,
|
||
'imageType': imageType, // مثال: driver_license_front
|
||
};
|
||
|
||
// الرفع وإرجاع الرابط
|
||
final String imageUrl = await uploadImage(compressedImage, payload, link);
|
||
|
||
// حفظ الرابط محلياً حسب النوع
|
||
docUrls[imageType] = imageUrl;
|
||
|
||
Log.print('✅ Uploaded $imageType => $imageUrl');
|
||
} catch (e, st) {
|
||
Log.print('❌ Error in choosImage: $e\n$st');
|
||
mySnackeBarError('Image Upload Failed'.tr);
|
||
} finally {
|
||
isloading = false;
|
||
update();
|
||
}
|
||
}
|
||
|
||
/// ترفع الملف وترجع رابط الصورة النهائي كـ String
|
||
Future<String> uploadImage(
|
||
File file, Map<String, String> data, String link) async {
|
||
final uri = Uri.parse(link);
|
||
final request = http.MultipartRequest('POST', uri);
|
||
|
||
// الهيدرز (كما عندك)
|
||
final headers = <String, String>{
|
||
'Authorization':
|
||
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}',
|
||
'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
||
};
|
||
request.headers.addAll(headers);
|
||
|
||
// اسم الملف: driverID.jpg (اختياري)
|
||
final forcedName = '${box.read(BoxName.driverID) ?? 'image'}.jpg';
|
||
|
||
// إضافة الملف (من المسار مباشرة أسلم من الـ stream)
|
||
request.files.add(
|
||
await http.MultipartFile.fromPath(
|
||
'image', // تأكد أنه نفس اسم الحقل على السيرفر
|
||
file.path,
|
||
filename: forcedName,
|
||
),
|
||
);
|
||
|
||
// الحقول الإضافية
|
||
data.forEach((k, v) => request.fields[k] = v);
|
||
|
||
// الإرسال
|
||
final streamed = await request.send();
|
||
final res = await http.Response.fromStream(streamed);
|
||
|
||
if (res.statusCode != 200) {
|
||
throw Exception(
|
||
'Failed to upload image: ${res.statusCode} - ${res.body}');
|
||
}
|
||
|
||
// نحاول استخراج رابط الصورة من أكثر من مفتاح محتمل
|
||
final body = jsonDecode(res.body);
|
||
final String? url = body['url'] ??
|
||
body['file_link'] ??
|
||
body['image_url'] ??
|
||
(body['data'] is Map ? body['data']['url'] : null);
|
||
|
||
if (url == null || url.isEmpty) {
|
||
// لو السيرفر يرجع هيكل مختلف، عدّل هنا المفتاح حسب استجابتك الفعلية
|
||
throw Exception(
|
||
'Upload succeeded but no image URL found in response: ${res.body}');
|
||
}
|
||
|
||
return url;
|
||
}
|
||
|
||
Future<File> compressImage(File file) async {
|
||
final dir = await path_provider.getTemporaryDirectory();
|
||
final targetPath = "${dir.absolute.path}/temp.jpg";
|
||
|
||
var result = await FlutterImageCompress.compressAndGetFile(
|
||
file.absolute.path,
|
||
targetPath,
|
||
quality: 70,
|
||
minWidth: 1024,
|
||
minHeight: 1024,
|
||
);
|
||
|
||
return File(result!.path);
|
||
}
|
||
|
||
// دالة رفع إلى السيرفر السوري: ترجع file_url (Signed URL)
|
||
Future<String> uploadToSyria({
|
||
required String docType,
|
||
required File file,
|
||
required Uri syrianUploadUri,
|
||
required String authHeader,
|
||
required String hmacHeader,
|
||
required String driverId,
|
||
Duration timeout = const Duration(seconds: 60),
|
||
http.Client? clientOverride,
|
||
}) async {
|
||
final client = clientOverride ?? http.Client();
|
||
try {
|
||
final mime = lookupMimeType(file.path) ?? 'image/jpeg';
|
||
final parts = mime.split('/');
|
||
|
||
final req = http.MultipartRequest('POST', syrianUploadUri);
|
||
req.headers.addAll({
|
||
'Authorization': authHeader,
|
||
'X-HMAC-Auth': hmacHeader,
|
||
});
|
||
|
||
req.fields['driver_id'] = driverId;
|
||
req.fields['doc_type'] = docType;
|
||
|
||
req.files.add(
|
||
await http.MultipartFile.fromPath(
|
||
'file',
|
||
file.path,
|
||
filename: p.basename(file.path),
|
||
contentType: MediaType(parts.first, parts.last),
|
||
),
|
||
);
|
||
|
||
// ====== الطباعة قبل الإرسال ======
|
||
// Log.print('--- Syrian Upload Request ---');
|
||
// Log.print('URL: $syrianUploadUri');
|
||
// // Log.print('Method: POST');
|
||
// // Log.print('Headers: ${req.headers}');
|
||
// Log.print('Fields: ${req.fields}');
|
||
// // Log.print(
|
||
// // 'File: ${file.path} (${await file.length()} bytes, mime: $mime)');
|
||
// Log.print('-----------------------------');
|
||
|
||
// الإرسال
|
||
final streamed = await client.send(req).timeout(timeout);
|
||
final resp = await http.Response.fromStream(streamed);
|
||
|
||
// ====== الطباعة بعد الاستجابة ======
|
||
// Log.print('--- Syrian Upload Response ---');
|
||
Log.print('Status: ${resp.statusCode}');
|
||
// Log.print('Headers: ${resp.headers}');
|
||
// Log.print('Body: ${resp.body}');
|
||
// Log.print('-------------------------------');
|
||
|
||
Map<String, dynamic> j = {};
|
||
try {
|
||
j = jsonDecode(resp.body) as Map<String, dynamic>;
|
||
} catch (e) {
|
||
Log.print('⚠️ Failed to parse JSON: $e');
|
||
}
|
||
|
||
// التحمّل لشكلين من الـ JSON:
|
||
final statusOk = j['status'] == 'success';
|
||
final fileUrl = (j['file_url'] ?? j['message']?['file_url'])?.toString();
|
||
final fileName =
|
||
(j['file_name'] ?? j['message']?['file_name'])?.toString();
|
||
|
||
if (resp.statusCode == 200 &&
|
||
statusOk &&
|
||
(fileUrl?.isNotEmpty ?? false)) {
|
||
// Log.print(
|
||
// '✅ Syrian upload success: $fileUrl (file: ${fileName ?? "-"})');
|
||
return fileUrl!;
|
||
}
|
||
|
||
throw Exception(
|
||
'❌ Syrian upload failed ($docType): ${j['message'] ?? resp.body}');
|
||
} finally {
|
||
if (clientOverride == null) client.close();
|
||
}
|
||
}
|
||
|
||
Future<void> submitRegistration() async {
|
||
// 0) دوال/مساعدات محلية
|
||
void _addField(Map<String, String> fields, String key, String? value) {
|
||
if (value != null && value.isNotEmpty) {
|
||
fields[key] = value;
|
||
}
|
||
}
|
||
|
||
// 1) تحقق من وجود الروابط
|
||
final driverFrontUrl = docUrls['driver_license_front'];
|
||
final driverBackUrl = docUrls['driver_license_back'];
|
||
final carFrontUrl = docUrls['car_license_front'];
|
||
final carBackUrl = docUrls['car_license_back'];
|
||
|
||
isLoading.value = true;
|
||
update();
|
||
|
||
final registerUri = Uri.parse(AppLink.register_driver_and_car);
|
||
final client = http.Client();
|
||
|
||
try {
|
||
// ترويسات مشتركة
|
||
final bearer =
|
||
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}';
|
||
final hmac = '${box.read(BoxName.hmac)}';
|
||
|
||
final req = http.MultipartRequest('POST', registerUri);
|
||
req.headers.addAll({
|
||
'Authorization': bearer,
|
||
'X-HMAC-Auth': hmac,
|
||
});
|
||
|
||
final fields = <String, String>{};
|
||
|
||
// --- Driver Data ---
|
||
_addField(fields, 'id', box.read(BoxName.driverID)?.toString());
|
||
_addField(fields, 'first_name', firstNameController.text);
|
||
_addField(fields, 'last_name', lastNameController.text);
|
||
_addField(fields, 'phone', box.read(BoxName.phoneDriver) ?? '');
|
||
_addField(fields, 'national_number', nationalIdController.text);
|
||
_addField(fields, 'birthdate', bithdateController.text);
|
||
_addField(fields, 'expiry_date', driverLicenseExpiryController.text);
|
||
_addField(fields, 'password', 'generated_password_or_token');
|
||
_addField(fields, 'status', 'yet');
|
||
_addField(fields, 'email', 'Not specified');
|
||
_addField(fields, 'gender', 'Male'); // يفضل ربطها بـ Dropdown أيضاً
|
||
|
||
// --- Car Data ---
|
||
_addField(fields, 'vin', 'yet');
|
||
_addField(fields, 'car_plate', carPlateController.text);
|
||
_addField(fields, 'make', carMakeController.text);
|
||
_addField(fields, 'model', carModelController.text);
|
||
_addField(fields, 'year', carYearController.text);
|
||
_addField(
|
||
fields,
|
||
'expiration_date',
|
||
driverLicenseExpiryController
|
||
.text); // تأكد من أن هذا تاريخ انتهاء السيارة وليس الرخصة
|
||
_addField(fields, 'color', carColorController.text);
|
||
|
||
if (colorHex != null && colorHex!.isNotEmpty) {
|
||
_addField(fields, 'color_hex', colorHex!);
|
||
}
|
||
_addField(fields, 'owner',
|
||
'${firstNameController.text} ${lastNameController.text}');
|
||
|
||
// ============================================================
|
||
// 🔥 التعديل الجديد: إرسال الأرقام (IDs) لتصنيف المركبة والوقود
|
||
// ============================================================
|
||
|
||
// 1. إرسال رقم تصنيف المركبة (1=سيارة, 2=دراجة...)
|
||
if (selectedVehicleCategoryId != null) {
|
||
_addField(fields, 'vehicle_category_id',
|
||
selectedVehicleCategoryId.toString());
|
||
} else {
|
||
_addField(fields, 'vehicle_category_id', '1'); // قيمة افتراضية (سيارة)
|
||
}
|
||
|
||
// 2. إرسال رقم ونوع الوقود
|
||
if (selectedFuelTypeId != null) {
|
||
// إرسال الرقم (للبحث السريع)
|
||
_addField(fields, 'fuel_type_id', selectedFuelTypeId.toString());
|
||
|
||
// إرسال الاسم نصاً (للتوافق مع العمود القديم 'fuel' إذا لزم الأمر)
|
||
// نبحث عن الاسم داخل القائمة بناءً على الرقم المختار
|
||
final fuelObj = fuelTypeOptions.firstWhere(
|
||
(e) => e['id'] == selectedFuelTypeId,
|
||
orElse: () => {'name': 'Petrol'});
|
||
_addField(fields, 'fuel', fuelObj['name'].toString());
|
||
} else {
|
||
_addField(fields, 'fuel_type_id', '1');
|
||
_addField(fields, 'fuel', 'Petrol');
|
||
}
|
||
|
||
// --- روابط الصور ---
|
||
_addField(fields, 'driver_license_front', driverFrontUrl!);
|
||
_addField(fields, 'driver_license_back', driverBackUrl!);
|
||
_addField(fields, 'car_license_front', carFrontUrl!);
|
||
_addField(fields, 'car_license_back', carBackUrl!);
|
||
|
||
req.fields.addAll(fields);
|
||
|
||
// 3) الإرسال
|
||
final streamed =
|
||
await client.send(req).timeout(const Duration(seconds: 60));
|
||
final resp = await http.Response.fromStream(streamed);
|
||
|
||
// 4) معالجة الاستجابة
|
||
Map<String, dynamic>? json;
|
||
try {
|
||
json = jsonDecode(resp.body) as Map<String, dynamic>;
|
||
} catch (_) {}
|
||
|
||
if (resp.statusCode == 200 && json?['status'] == 'success') {
|
||
Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr,
|
||
backgroundColor: Colors.green, colorText: Colors.white);
|
||
|
||
// منطق التوكن والإشعارات وتسجيل الدخول...
|
||
final email = box.read(BoxName.emailDriver);
|
||
final driverID = box.read(BoxName.driverID);
|
||
|
||
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||
await CRUD().post(link: AppLink.addTokensDriver, payload: {
|
||
'captain_id': (box.read(BoxName.driverID)).toString(),
|
||
'token': (box.read(BoxName.tokenDriver)).toString(),
|
||
'fingerPrint': fingerPrint.toString(),
|
||
});
|
||
|
||
NotificationService.sendNotification(
|
||
target: 'service',
|
||
title: 'New Driver Registration',
|
||
body: 'Driver $driverID has submitted registration.',
|
||
isTopic: true,
|
||
category: 'new_service_request',
|
||
);
|
||
|
||
final c = Get.isRegistered<LoginDriverController>()
|
||
? Get.find<LoginDriverController>()
|
||
: Get.put(LoginDriverController());
|
||
c.loginWithGoogleCredential(driverID, email);
|
||
} else {
|
||
final msg = (json?['message'] ?? 'Registration failed.').toString();
|
||
Get.snackbar('Error'.tr, msg,
|
||
backgroundColor: Colors.red, colorText: Colors.white);
|
||
}
|
||
} catch (e) {
|
||
Get.snackbar('Error'.tr, 'Error: $e',
|
||
backgroundColor: Colors.red, colorText: Colors.white);
|
||
} finally {
|
||
client.close();
|
||
isLoading.value = false;
|
||
update();
|
||
}
|
||
}
|
||
// // 1) تحقق من الصور
|
||
// if (driverLicenseFrontImage == null ||
|
||
// driverLicenseBackImage == null ||
|
||
// carLicenseFrontImage == null ||
|
||
// carLicenseBackImage == null) {
|
||
// Get.snackbar(
|
||
// 'Missing Documents'.tr, 'Please upload all 4 required documents.'.tr,
|
||
// snackPosition: SnackPosition.BOTTOM,
|
||
// backgroundColor: Colors.orange,
|
||
// colorText: Colors.white);
|
||
// return;
|
||
// }
|
||
|
||
// isLoading.value = true;
|
||
|
||
// final uri = Uri.parse(
|
||
// 'https://intaleq.xyz/intaleq/auth/syria/driver/register_driver_and_car.php',
|
||
// );
|
||
|
||
// final client = http.Client();
|
||
// try {
|
||
// final req = http.MultipartRequest('POST', uri);
|
||
|
||
// // مهم: لا تضع Content-Type يدويًا، الـ MultipartRequest يتكفّل فيه ببناء boundary.
|
||
// final headers = {
|
||
// 'Authorization':
|
||
// 'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}',
|
||
// 'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
||
// };
|
||
// // 2) الحقول النصية
|
||
// final fields = <String, String>{};
|
||
|
||
// // --- Driver Data ---
|
||
// _addField(fields, 'id', box.read(BoxName.driverID)?.toString());
|
||
// _addField(fields, 'first_name', firstNameController.text);
|
||
// _addField(fields, 'last_name', lastNameController.text);
|
||
// _addField(fields, 'phone', box.read(BoxName.phoneDriver) ?? '');
|
||
// _addField(fields, 'national_number', nationalIdController.text);
|
||
// _addField(fields, 'expiry_date', driverLicenseExpiryController.text);
|
||
// _addField(
|
||
// fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك
|
||
// _addField(fields, 'status', 'yet');
|
||
// _addField(fields, 'email',
|
||
// 'Not specified'); // سكربت السيرفر سيحوّلها null ويبني ايميل افتراضي
|
||
// _addField(fields, 'gender', 'Male');
|
||
|
||
// // --- Car Data (مطابقة لما يتوقّعه السكربت) ---
|
||
// _addField(fields, 'vin', 'carVinController.text);');
|
||
// _addField(fields, 'car_plate', carPlateController.text);
|
||
// _addField(fields, 'make', carMakeController.text);
|
||
// _addField(fields, 'model', carModelController.text);
|
||
// _addField(fields, 'year', carYearController.text);
|
||
// _addField(fields, 'expiration_date', 'carRegistrationExpiryController');
|
||
// _addField(fields, 'color', carColorController.text);
|
||
// _addField(fields, 'fuel', 'Gasoline'); // أو حسب اختيارك
|
||
// _addField(fields, 'color_hex', colorHex); // مهم
|
||
// // لو عندك حقول إضافية مطلوبة بالسكربت (مالك المركبة / الكود اللوني / الوقود) مرّرها:
|
||
// _addField(fields, 'owner',
|
||
// firstNameController.text + ' ' + lastNameController.text);
|
||
// // if (colorHex != null) _addField(fields, 'color_hex', colorHex);
|
||
// // if (fuelType != null) _addField(fields, 'fuel', fuelType);
|
||
// req.headers.addAll(headers);
|
||
// req.fields.addAll(fields);
|
||
|
||
// // 3) الملفات (4 صور) — مفاتيحها مطابقة للسكربت
|
||
// Future<void> addFile(String field, File file) async {
|
||
// final mime = lookupMimeType(file.path) ?? 'image/jpeg';
|
||
// final parts = mime.split('/');
|
||
// final mediaType = MediaType(parts.first, parts.last);
|
||
// req.files.add(
|
||
// await http.MultipartFile.fromPath(
|
||
// field,
|
||
// file.path,
|
||
// filename: p.basename(file.path),
|
||
// contentType: mediaType,
|
||
// ),
|
||
// );
|
||
// }
|
||
|
||
// await addFile('driver_license_front', driverLicenseFrontImage!);
|
||
// await addFile('driver_license_back', driverLicenseBackImage!);
|
||
// await addFile('car_license_front', carLicenseFrontImage!);
|
||
// await addFile('car_license_back', carLicenseBackImage!);
|
||
|
||
// // 4) الإرسال
|
||
// final streamed =
|
||
// await client.send(req).timeout(const Duration(seconds: 60));
|
||
// final resp = await http.Response.fromStream(streamed);
|
||
|
||
// // 5) فحص النتيجة
|
||
// Map<String, dynamic>? json;
|
||
// try {
|
||
// json = jsonDecode(resp.body) as Map<String, dynamic>;
|
||
// } catch (_) {}
|
||
|
||
// if (resp.statusCode == 200 &&
|
||
// json != null &&
|
||
// json['status'] == 'success') {
|
||
// // ممكن يرجّع driverID, carRegID, documents
|
||
// final driverID =
|
||
// (json['data']?['driverID'] ?? json['driverID'])?.toString();
|
||
// if (driverID != null && driverID.isNotEmpty) {
|
||
// box.write(BoxName.driverID, driverID);
|
||
// }
|
||
|
||
// Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr,
|
||
// snackPosition: SnackPosition.BOTTOM,
|
||
// backgroundColor: Colors.green,
|
||
// colorText: Colors.white);
|
||
|
||
// // TODO: انتقل للصفحة التالية أو حدّث الحالة…
|
||
// } else {
|
||
// final msg =
|
||
// (json?['message'] ?? 'Registration failed. Please try again.')
|
||
// .toString();
|
||
// Get.snackbar('Error'.tr, msg,
|
||
// snackPosition: SnackPosition.BOTTOM,
|
||
// backgroundColor: Colors.red,
|
||
// colorText: Colors.white);
|
||
// }
|
||
// } catch (e) {
|
||
// Get.snackbar('Error'.tr, '${'An unexpected error occurred:'.tr} $e',
|
||
// snackPosition: SnackPosition.BOTTOM,
|
||
// backgroundColor: Colors.red,
|
||
// colorText: Colors.white);
|
||
// } finally {
|
||
// client.close();
|
||
// isLoading.value = false;
|
||
// }
|
||
// }
|
||
|
||
// Helpers
|
||
}
|