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(); final carInfoFormKey = GlobalKey(); // 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> vehicleCategoryOptions = [ {'id': 1, 'name': 'Car'.tr}, // ترجمة: سيارة {'id': 2, 'name': 'Motorcycle'.tr}, // ترجمة: دراجة نارية {'id': 3, 'name': 'Van / Bus'.tr}, // ترجمة: فان / باص ]; // قائمة أنواع الوقود final List> 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 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> 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 fields, String key, String? value) { if (value != null && value.toString().isNotEmpty) { fields[key] = value.toString(); } } /// خريطة لتخزين روابط المستندات بعد الرفع final Map docUrls = { 'driver_license_front': '', 'driver_license_back': '', 'car_license_front': '', 'car_license_back': '', }; /// التصرّف العام لاختيار/قص/ضغط/رفع الصورة حسب type Future 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 = { '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 uploadImage( File file, Map data, String link) async { final uri = Uri.parse(link); final request = http.MultipartRequest('POST', uri); // الهيدرز (كما عندك) final headers = { '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 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 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 j = {}; try { j = jsonDecode(resp.body) as Map; } 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 submitRegistration() async { // 0) دوال/مساعدات محلية void _addField(Map 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 = {}; // --- 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? json; try { json = jsonDecode(resp.body) as Map; } 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() ? Get.find() : 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 = {}; // // --- 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 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? json; // try { // json = jsonDecode(resp.body) as Map; // } 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 }