diff --git a/android/app/build.gradle b/android/app/build.gradle index 7f19805..58f1122 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -42,10 +42,10 @@ android { // Merged the two defaultConfig sections into one. This is the correct way. defaultConfig { applicationId = "com.intaleq_driver" - minSdk = 23 + minSdkVersion = 23 targetSdk = 36 - versionCode = 36 - versionName = '1.0.36' // I've used the higher version name + versionCode = 47 + versionName = '1.0.47' // I've used the higher version name multiDexEnabled = true ndk { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f76d10a..52809f5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,177 +1,106 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/shamcashsend.png b/assets/images/shamcashsend.png new file mode 100644 index 0000000..0814517 Binary files /dev/null and b/assets/images/shamcashsend.png differ diff --git a/lib/constant/box_name.dart b/lib/constant/box_name.dart index ffddc57..e1ded10 100755 --- a/lib/constant/box_name.dart +++ b/lib/constant/box_name.dart @@ -11,6 +11,7 @@ class BoxName { "rideArgumentsFromBackground"; static const String FCM_PRIVATE_KEY = "FCM_PRIVATE_KEY"; static const String hmac = "hmac"; + static const String ttsEnabled = "ttsEnabled"; static const String walletType = "walletType"; static const String fingerPrint = "fingerPrint"; static const String updateInterval = "updateInterval"; diff --git a/lib/controller/auth/captin/invit_controller.dart b/lib/controller/auth/captin/invit_controller.dart index ab16045..e12a904 100755 --- a/lib/controller/auth/captin/invit_controller.dart +++ b/lib/controller/auth/captin/invit_controller.dart @@ -326,15 +326,45 @@ Download the Intaleq app now and enjoy your ride! return input; // Fallback for unrecognized formats } + String normalizeSyrianPhone(String input) { + String phone = input.trim(); + + // احذف كل شيء غير أرقام + phone = phone.replaceAll(RegExp(r'[^0-9]'), ''); + + // إذا يبدأ بـ 0 → احذفها + if (phone.startsWith('0')) { + phone = phone.substring(1); + } + + // إذا يبدأ بـ 963 مكررة → احذف التكرار + while (phone.startsWith('963963')) { + phone = phone.substring(3); + } + + // إذا يبدأ بـ 963 ولكن داخله كمان 963 → خليه مرة واحدة فقط + if (phone.startsWith('963') && phone.length > 12) { + phone = phone.substring(phone.length - 9); // آخر 9 أرقام + } + + // الآن إذا كان بلا 963 → أضفها + if (!phone.startsWith('963')) { + phone = '963' + phone; + } + + return phone; + } + /// Sends an invitation to a potential new driver. void sendInvite() async { if (invitePhoneController.text.isEmpty) { mySnackeBarError('Please enter a phone number'.tr); return; } + // Format Syrian phone number: remove leading 0 and add +963 String formattedPhoneNumber = - _formatSyrianPhoneNumber(invitePhoneController.text); - if (formattedPhoneNumber.length < 13) { + normalizeSyrianPhone(invitePhoneController.text); + if (formattedPhoneNumber.length != 12) { mySnackeBarError('Please enter a correct phone'.tr); return; } @@ -370,22 +400,32 @@ Download the Intaleq app now and enjoy your ride! mySnackeBarError('Please enter a phone number'.tr); return; } - String formattedPhoneNumber = - _formatSyrianPhoneNumber(invitePhoneController.text); - if (formattedPhoneNumber.length < 13) { + + // Format Syrian phone number: remove leading 0 and add +963 + String formattedPhoneNumber = invitePhoneController.text.trim(); + if (formattedPhoneNumber.startsWith('0')) { + formattedPhoneNumber = formattedPhoneNumber.substring(1); + } + formattedPhoneNumber = '+963$formattedPhoneNumber'; + + if (formattedPhoneNumber.length < 12) { + // +963 + 9 digits = 12+ mySnackeBarError('Please enter a correct phone'.tr); return; } - var response = - await CRUD().post(link: AppLink.addInvitationPassenger, payload: { - "driverId": box.read(BoxName.driverID), - "inviterPassengerPhone": formattedPhoneNumber, - }); + var response = await CRUD().post( + link: AppLink.addInvitationPassenger, + payload: { + "driverId": box.read(BoxName.driverID), + "inviterPassengerPhone": formattedPhoneNumber, + }, + ); if (response != 'failure') { - var d = (response); + var d = response; mySnackbarSuccess('Invite sent successfully'.tr); + String message = '${'*Intaleq APP CODE*'.tr}\n\n' '${"Use this code in registration".tr}\n\n' '${"To get a gift for both".tr}\n\n' diff --git a/lib/controller/auth/captin/login_captin_controller.dart b/lib/controller/auth/captin/login_captin_controller.dart index 3d65d9f..9af23a7 100755 --- a/lib/controller/auth/captin/login_captin_controller.dart +++ b/lib/controller/auth/captin/login_captin_controller.dart @@ -26,6 +26,7 @@ import '../../../views/auth/captin/otp_page.dart'; import '../../../views/auth/captin/otp_token_page.dart'; import '../../../views/auth/syria/pending_driver_page.dart'; import '../../firebase/firbase_messge.dart'; +import '../../firebase/local_notification.dart'; import '../../firebase/notification_service.dart'; import '../../functions/encrypt_decrypt.dart'; import '../../functions/package_info.dart'; @@ -177,11 +178,11 @@ class LoginDriverController extends GetxController { Uri.parse(AppLink.loginFirstTimeDriver), body: payload, ); - // Log.print('response0: ${response0.body}'); - // Log.print('request: ${response0.request}'); + Log.print('response0: ${response0.body}'); + Log.print('request: ${response0.request}'); if (response0.statusCode == 200) { final decodedResponse1 = jsonDecode(response0.body); - // Log.print('decodedResponse1: ${decodedResponse1}'); + Log.print('decodedResponse1: ${decodedResponse1}'); final jwt = decodedResponse1['jwt']; box.write(BoxName.jwt, c(jwt)); @@ -253,6 +254,40 @@ class LoginDriverController extends GetxController { .join(''); } + bool isInviteDriverFound = false; + + Future updateInvitationCodeFromRegister() async { + var res = await CRUD().post( + link: AppLink.updateDriverInvitationDirectly, + payload: { + "inviterDriverPhone": box.read(BoxName.phoneDriver).toString(), + // "driverId": box.read(BoxName.driverID).toString(), + }, + ); + Log.print('invite: ${res}'); + + if (res['status'] != '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', ''); + + NotificationService.sendNotification( + target: (res)['message'][0]['token'].toString(), + title: 'You have received a gift token!'.tr, + body: 'for '.tr + box.read(BoxName.phoneDriver).toString(), + isTopic: false, // Important: this is a token + tone: 'tone2', + driverList: [], category: 'You have received a gift token!', + ); + } else { + // mySnackeBarError( + // "You dont have invitation code".tr); // Localized error message + } + } + loginWithGoogleCredential(String driverID, email) async { isloading = true; update(); @@ -263,7 +298,7 @@ class LoginDriverController extends GetxController { // 'email': email ?? 'yet', 'id': driverID, }); - // Log.print('res: ${res}'); + Log.print('loginWithGoogleCredential: ${res}'); if (res == 'failure') { await isPhoneVerified(); isloading = false; // <--- أضفت هذا أيضاً @@ -314,6 +349,13 @@ class LoginDriverController extends GetxController { } else if (int.parse(d['year'].toString()) < 2002) { box.write(BoxName.carTypeOfDriver, 'Awfar Car'); } + +// add invitations + if (box.read(BoxName.isInstall) == null || + box.read(BoxName.isInstall).toString() == '0') { + updateInvitationCodeFromRegister(); + } + // updateAppTester(AppInformation.appName); if (d['status'].toString() != 'yet') { var token = await CRUD().get( diff --git a/lib/controller/auth/captin/phone_helper_controller.dart b/lib/controller/auth/captin/phone_helper_controller.dart index 357fff5..200cd96 100644 --- a/lib/controller/auth/captin/phone_helper_controller.dart +++ b/lib/controller/auth/captin/phone_helper_controller.dart @@ -18,14 +18,77 @@ class PhoneAuthHelper { static final String _sendOtpUrl = '${_baseUrl}sendWhatsAppDriver.php'; static final String _verifyOtpUrl = '${_baseUrl}verifyOtp.php'; static final String _registerUrl = '${_baseUrl}register_driver.php'; + static String formatSyrianPhone(String phone) { + // Remove spaces, symbols, +, -, () + phone = phone.replaceAll(RegExp(r'[ \-\(\)\+]'), '').trim(); + + // Normalize 00963 → 963 + if (phone.startsWith('00963')) { + phone = phone.replaceFirst('00963', '963'); + } + + // Normalize 0963 → 963 + if (phone.startsWith('0963')) { + phone = phone.replaceFirst('0963', '963'); + } + if (phone.startsWith('096309')) { + phone = phone.replaceFirst('096309', '963'); + } + + // NEW: Fix 96309xxxx → 9639xxxx + if (phone.startsWith('96309')) { + phone = '9639' + phone.substring(5); // remove the "0" after 963 + } + + // If starts with 9630 → correct to 9639 + if (phone.startsWith('9630')) { + phone = '9639' + phone.substring(4); + } + + // If already in correct format: 9639xxxxxxxx + if (phone.startsWith('9639') && phone.length == 12) { + return phone; + } + + // If starts with 963 but missing the 9 + if (phone.startsWith('963') && phone.length > 3) { + // Ensure it begins with 9639 + if (!phone.startsWith('9639')) { + phone = '9639' + phone.substring(3); + } + return phone; + } + + // If starts with 09xxxxxxxx → 9639xxxxxxxx + if (phone.startsWith('09')) { + return '963' + phone.substring(1); + } + + // If 9xxxxxxxx (9 digits) + if (phone.startsWith('9') && phone.length == 9) { + return '963' + phone; + } + + // If starts with incorrect 0xxxxxxx → assume Syrian and fix + if (phone.startsWith('0') && phone.length == 10) { + return '963' + phone.substring(1); + } + + return phone; + } /// Sends an OTP to the provided phone number. static Future sendOtp(String phoneNumber) async { try { + final fixedPhone = formatSyrianPhone(phoneNumber); + Log.print('fixedPhone: $fixedPhone'); + final response = await CRUD().post( link: _sendOtpUrl, - payload: {'receiver': phoneNumber}, + payload: {'receiver': fixedPhone}, ); + Log.print('fixedPhone: ${fixedPhone}'); + if (response != 'failure') { final data = (response); Log.print('data: ${data}'); @@ -49,10 +112,12 @@ class PhoneAuthHelper { /// Verifies the OTP and logs the user in. static Future verifyOtp(String phoneNumber) async { try { + final fixedPhone = formatSyrianPhone(phoneNumber); + Log.print('fixedPhone: $fixedPhone'); final response = await CRUD().post( link: _verifyOtpUrl, payload: { - 'phone_number': phoneNumber, + 'phone_number': fixedPhone, }, ); @@ -80,7 +145,7 @@ class PhoneAuthHelper { // ✅ رقم الهاتف تم التحقق منه لكن السائق غير مسجل // mySnackbarSuccess('Phone verified. Please complete registration.'); // Get.offAll(() => SyrianCardAI()); - Get.offAll(() => RegistrationView()); + Get.to(() => RegistrationView()); } } else { mySnackeBarError(data['message'] ?? 'Verification failed.'); diff --git a/lib/controller/auth/captin/register_captin_controller.dart b/lib/controller/auth/captin/register_captin_controller.dart index 53763c3..eeac62c 100755 --- a/lib/controller/auth/captin/register_captin_controller.dart +++ b/lib/controller/auth/captin/register_captin_controller.dart @@ -280,7 +280,7 @@ class RegisterCaptainController extends GetxController { // box.read(BoxName.emailDriver).toString(), // ); // Get.offAll(() => SyrianCardAI()); - Get.offAll(() => RegistrationView()); + Get.to(() => RegistrationView()); // } else { // Get.snackbar('title', 'message'); // } diff --git a/lib/controller/auth/syria/registration_controller.dart b/lib/controller/auth/syria/registration_controller.dart index fb67c8e..706e6da 100644 --- a/lib/controller/auth/syria/registration_controller.dart +++ b/lib/controller/auth/syria/registration_controller.dart @@ -58,6 +58,7 @@ class RegistrationController extends GetxController { 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; @@ -101,14 +102,15 @@ class RegistrationController extends GetxController { 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.', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white); - return; // Stop progression - } + // 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 @@ -495,6 +497,7 @@ class RegistrationController extends GetxController { _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', 'generate_your_password_here'); // عدّل حسب منطقك diff --git a/lib/controller/firebase/firbase_messge.dart b/lib/controller/firebase/firbase_messge.dart index b4c948a..4a42660 100755 --- a/lib/controller/firebase/firbase_messge.dart +++ b/lib/controller/firebase/firbase_messge.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart'; +import 'package:sefer_driver/views/home/Captin/orderCaptin/order_speed_request.dart'; import 'package:sefer_driver/views/widgets/error_snakbar.dart'; import 'package:sefer_driver/views/widgets/mydialoug.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; @@ -117,7 +118,7 @@ class FirebaseMessagesController extends GetxController { driverToken = myList[14].toString(); Get.put(HomeCaptainController()).changeRideId(); update(); - Get.to(() => OrderRequestPage(), arguments: { + Get.to(() => OrderSpeedRequest(), arguments: { 'myListString': myListString, 'DriverList': myList, 'body': body diff --git a/lib/controller/functions/gemeni.dart b/lib/controller/functions/gemeni.dart index af1e987..f070d29 100755 --- a/lib/controller/functions/gemeni.dart +++ b/lib/controller/functions/gemeni.dart @@ -114,7 +114,7 @@ class AI extends GetxController { // 'tone2', // Type of notification // ); NotificationService.sendNotification( - target: jsonDecode(res)['message'][0]['token'].toString(), + target: (res)['message'][0]['token'].toString(), title: 'You have received a gift token!'.tr, body: 'for '.tr + box.read(BoxName.phoneDriver).toString(), isTopic: false, // Important: this is a token diff --git a/lib/controller/functions/launch.dart b/lib/controller/functions/launch.dart index 26a7026..32b588e 100755 --- a/lib/controller/functions/launch.dart +++ b/lib/controller/functions/launch.dart @@ -8,11 +8,32 @@ void showInBrowser(String url) async { } Future makePhoneCall(String phoneNumber) async { + // 1. تنظيف الرقم (إزالة المسافات والفواصل) + String formattedNumber = phoneNumber.replaceAll(RegExp(r'\s+'), ''); + + // 2. التحقق من طول الرقم لتحديد طريقة التنسيق + if (formattedNumber.length > 6) { + // --- التعديل المطلوب --- + if (formattedNumber.startsWith('09')) { + // إذا كان يبدأ بـ 09 (رقم موبايل سوري محلي) + // نحذف أول خانة (الصفر) ونضيف +963 + formattedNumber = '+963${formattedNumber.substring(1)}'; + } else if (!formattedNumber.startsWith('+')) { + // إذا لم يكن يبدأ بـ + (ولم يكن يبدأ بـ 09)، نضيف + في البداية + // هذا للحفاظ على منطقك القديم للأرقام الدولية الأخرى + formattedNumber = '+$formattedNumber'; + } + } + + // 3. التنفيذ (Launch) final Uri launchUri = Uri( scheme: 'tel', - path: phoneNumber, + path: formattedNumber, ); - await launchUrl(launchUri); + + if (await canLaunchUrl(launchUri)) { + await launchUrl(launchUri); + } } void launchCommunication( diff --git a/lib/controller/functions/log_out.dart b/lib/controller/functions/log_out.dart index ca286a2..7b3634c 100755 --- a/lib/controller/functions/log_out.dart +++ b/lib/controller/functions/log_out.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:sefer_driver/views/home/on_boarding_page.dart'; import 'package:sefer_driver/views/widgets/error_snakbar.dart'; import 'package:flutter/material.dart'; @@ -9,12 +7,10 @@ import 'package:sefer_driver/constant/colors.dart'; import 'package:sefer_driver/constant/links.dart'; import 'package:sefer_driver/controller/functions/crud.dart'; import 'package:sefer_driver/main.dart'; -import 'package:sefer_driver/onbording_page.dart'; import 'package:sefer_driver/views/widgets/elevated_btn.dart'; import 'package:sefer_driver/views/widgets/my_textField.dart'; import '../../constant/style.dart'; -import 'encrypt_decrypt.dart'; class LogOutController extends GetxController { TextEditingController checkTxtController = TextEditingController(); diff --git a/lib/controller/functions/package_info.dart b/lib/controller/functions/package_info.dart index 6c313da..24a708b 100755 --- a/lib/controller/functions/package_info.dart +++ b/lib/controller/functions/package_info.dart @@ -48,7 +48,7 @@ Future getPackageInfo() async { void showUpdateDialog(BuildContext context) { final String storeUrl = Platform.isAndroid ? 'https://play.google.com/store/apps/details?id=com.intaleq_driver' - : 'https://apps.apple.com/ae/app/intaleq-driver/id6502189302'; + : 'https://apps.apple.com/jo/app/intaleq-driver/id6482995159'; showGeneralDialog( context: context, diff --git a/lib/controller/home/captin/behavior_controller.dart b/lib/controller/home/captin/behavior_controller.dart index e5cf6a7..1c51f4b 100644 --- a/lib/controller/home/captin/behavior_controller.dart +++ b/lib/controller/home/captin/behavior_controller.dart @@ -52,31 +52,58 @@ class DriverBehaviorController extends GetxController { double totalSpeed = 0; int hardBrakes = 0; double totalDistance = 0; + + // متغيرات للمقارنة مع النقطة السابقة double? prevLat, prevLng; + DateTime? prevTime; + + // ترتيب البيانات حسب الوقت لضمان دقة الحساب (اختياري لكن مفضل) + // data.sort((a, b) => a['created_at'].compareTo(b['created_at'])); for (var item in data) { - double speed = item['speed'] ?? 0; - double lat = item['lat'] ?? 0; - double lng = item['lng'] ?? 0; - double acc = item['acceleration'] ?? 0; + // 1. قراءة البيانات بالأسماء الصحيحة من الجدول + double lat = item['latitude'] ?? item['lat'] ?? 0.0; + double lng = item['longitude'] ?? item['lng'] ?? 0.0; + double acc = item['acceleration'] ?? 0.0; - if (speed > maxSpeed) maxSpeed = speed; - totalSpeed += speed; + // قراءة الوقت لحساب السرعة + DateTime currentTime = + DateTime.tryParse(item['created_at'].toString()) ?? DateTime.now(); - // ✅ Hard brake threshold + double currentSpeed = 0; + + // 2. حساب السرعة والمسافة إذا وجدت نقطة سابقة + if (prevLat != null && prevLng != null && prevTime != null) { + double distKm = _calculateDistance(prevLat, prevLng, lat, lng); + int timeDiffSeconds = currentTime.difference(prevTime).inSeconds; + + if (timeDiffSeconds > 0) { + // السرعة (كم/س) = (المسافة بالكيلومتر * 3600) / الزمن بالثواني + currentSpeed = (distKm * 3600) / timeDiffSeconds; + } + + totalDistance += distKm; + } + + // تحديث القيم الإحصائية + if (currentSpeed > maxSpeed) maxSpeed = currentSpeed; + totalSpeed += currentSpeed; + + // حساب الفرملة القوية (يعتمد على التسارع المحفوظ مسبقاً) if (acc.abs() > 3.0) hardBrakes++; - // ✅ Distance between points - if (prevLat != null && prevLng != null) { - totalDistance += _calculateDistance(prevLat, prevLng, lat, lng); - } + // حفظ النقطة الحالية لتكون هي "السابقة" في الدورة التالية prevLat = lat; prevLng = lng; + prevTime = currentTime; } - double avgSpeed = totalSpeed / data.length; + // تجنب القسمة على صفر + double avgSpeed = (data.length > 1) ? totalSpeed / (data.length - 1) : 0; + + // حساب تقييم السلوك double behaviorScore = 100 - (hardBrakes * 5) - ((maxSpeed > 100) ? 10 : 0); - behaviorScore = behaviorScore.clamp(0, 100); + behaviorScore = behaviorScore.clamp(0.0, 100.0); return { 'max_speed': maxSpeed, diff --git a/lib/controller/home/captin/duration_controller .dart b/lib/controller/home/captin/duration_controller .dart index 2807572..3945a41 100755 --- a/lib/controller/home/captin/duration_controller .dart +++ b/lib/controller/home/captin/duration_controller .dart @@ -35,7 +35,7 @@ class DurationController extends GetxController { getStaticDriver() async { isLoading = true; update(); - var res = await CRUD().getWallet( + var res = await CRUD().get( link: AppLink.driverStatistic, payload: {'driverID': box.read(BoxName.driverID)}); if (res == 'failure') { diff --git a/lib/controller/home/captin/home_captain_controller.dart b/lib/controller/home/captin/home_captain_controller.dart index 89dff0b..ca7e284 100755 --- a/lib/controller/home/captin/home_captain_controller.dart +++ b/lib/controller/home/captin/home_captain_controller.dart @@ -51,7 +51,7 @@ class HomeCaptainController extends GetxController { String totalMoneyInSEFER = '0'; String totalDurationToday = '0'; Timer? timer; - late LatLng myLocation = const LatLng(32, 36); + late LatLng myLocation = const LatLng(33.5138, 36.2765); String totalPoints = '0'; String countRefuse = '0'; bool mapType = false; @@ -99,7 +99,7 @@ class HomeCaptainController extends GetxController { isActive = !isActive; if (isActive) { - if (double.parse(totalPoints) > -300) { + if (double.parse(totalPoints) > -30000) { locationController.startLocationUpdates(); HapticFeedback.heavyImpact(); // locationBackController.startBackLocation(); @@ -188,22 +188,25 @@ class HomeCaptainController extends GetxController { // late GoogleMapController mapHomeCaptainController; GoogleMapController? mapHomeCaptainController; + // final locationController = Get.find(); + + // --- FIX 2: Smart Map Creation --- void onMapCreated(GoogleMapController controller) { mapHomeCaptainController = controller; - controller.getVisibleRegion(); - // Animate camera to user location (optional) - controller.animateCamera( - CameraUpdate.newLatLng(Get.find().myLocation), - ); - } - // قم بإنشائه مباشرة - // final MapController mapController = MapController(); - // bool isMapReady = false; - // void onMapReady() { - // isMapReady = true; - // print("Map is ready to be moved!"); - // } + // Check actual location before moving camera + var currentLoc = locationController.myLocation; + if (currentLoc.latitude != 0 && currentLoc.longitude != 0) { + controller.animateCamera( + CameraUpdate.newLatLng(currentLoc), + ); + } else { + // Optional: Move to default city view instead of ocean + controller.animateCamera( + CameraUpdate.newLatLngZoom(myLocation, 10), + ); + } + } void savePeriod(Duration period) { final periods = box.read>(BoxName.periods) ?? []; @@ -234,7 +237,14 @@ class HomeCaptainController extends GetxController { getlocation() async { isLoading = true; update(); + // This ensures we try to get a fix, but map doesn't crash if it fails await Get.find().getLocation(); + + var loc = Get.find().myLocation; + if (loc.latitude != 0) { + myLocation = loc; + } + isLoading = false; update(); } @@ -267,7 +277,7 @@ class HomeCaptainController extends GetxController { void onInit() async { // await locationBackController.requestLocationPermission(); Get.put(FirebaseMessagesController()); - // addToken(); + addToken(); await getlocation(); onButtonSelected(); getDriverRate(); @@ -283,61 +293,27 @@ class HomeCaptainController extends GetxController { getRefusedOrderByCaptain(); box.write(BoxName.statusDriverLocation, 'off'); locationController.addListener(() { - // فقط إذا كان السائق "متصل" والخريطة جاهزة + // Only animate if active, map is ready, AND location is valid (not 0,0) if (isActive && mapHomeCaptainController != null) { - mapHomeCaptainController!.animateCamera( - CameraUpdate.newCameraPosition( - CameraPosition( - target: locationController.myLocation, // الموقع الجديد - zoom: 17.5, - tilt: 50.0, - bearing: locationController.heading, // اتجاه السيارة + var loc = locationController.myLocation; + + if (loc.latitude != 0 && loc.longitude != 0) { + mapHomeCaptainController!.animateCamera( + CameraUpdate.newCameraPosition( + CameraPosition( + target: loc, + zoom: 17.5, + tilt: 50.0, + bearing: locationController.heading, + ), ), - ), - ); + ); + } } }); // LocationController().getLocation(); super.onInit(); } - // void getRefusedOrderByCaptain() async { - // // Get today's date in YYYY-MM-DD format - // String today = DateTime.now().toString().substring(0, 10); - - // String driverId = box.read(BoxName.driverID).toString(); - - // String customQuery = ''' - // SELECT COUNT(*) AS count - // FROM ${TableName.driverOrdersRefuse} - // WHERE driver_id = '$driverId' - // AND DATE(created_at) = '$today' - // '''; - - // try { - // List> results = - // await sql.getCustomQuery(customQuery); - // countRefuse = results[0]['count'].toString(); - // update(); - // if (int.parse(countRefuse) > 3) { - // box.write(BoxName.statusDriverLocation, 'on'); - // locationController.stopLocationUpdates(); - // Get.defaultDialog( - // // backgroundColor: CupertinoColors.destructiveRed, - // barrierDismissible: false, - // title: 'You Are Stopped For this Day !'.tr, - // content: Text( - // 'You Refused 3 Rides this Day that is the reason \nSee you Tomorrow!' - // .tr, - // style: AppStyle.title, - // ), - // confirm: MyElevatedButton( - // title: 'Ok , See you Tomorrow'.tr, - // onPressed: () => Get.back())); - // } else { - // box.write(BoxName.statusDriverLocation, 'off'); - // } - // } catch (e) {} - // } addToken() async { String? fingerPrint = await storage.read(key: BoxName.fingerPrint); @@ -346,14 +322,8 @@ class HomeCaptainController extends GetxController { 'captain_id': (box.read(BoxName.driverID)).toString(), 'fingerPrint': (fingerPrint).toString() }; - Log.print('payload: ${payload}'); + // Log.print('payload: ${payload}'); CRUD().post(link: AppLink.addTokensDriver, payload: payload); - - await CRUD().post( - link: "${AppLink.paymentServer}/ride/firebase/addDriver.php", - payload: payload); - // MapDriverController().driverCallPassenger(); - // box.write(BoxName.statusDriverLocation, 'off'); } getPaymentToday() async { @@ -468,6 +438,7 @@ class HomeCaptainController extends GetxController { void dispose() { activeTimer?.cancel(); stopTimer(); + mapHomeCaptainController?.dispose(); // Dispose controller super.dispose(); } } diff --git a/lib/controller/home/captin/map_driver_controller.dart b/lib/controller/home/captin/map_driver_controller.dart index 85c4fc5..b5f4106 100755 --- a/lib/controller/home/captin/map_driver_controller.dart +++ b/lib/controller/home/captin/map_driver_controller.dart @@ -6,6 +6,7 @@ import 'dart:math' as math; import 'package:http/http.dart' as http; import 'package:sefer_driver/controller/home/captin/behavior_controller.dart'; import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart'; +import 'package:sefer_driver/views/widgets/error_snakbar.dart'; import 'package:sefer_driver/views/widgets/mydialoug.dart'; import 'package:bubble_head/bubble.dart'; import 'package:flutter/material.dart'; @@ -63,14 +64,14 @@ class MapDriverController extends GetxController { late String timeOfOrder; late String duration; late String totalCost; - late String distance; - late String passengerName; + String distance = '0'; + String? passengerName; late String passengerEmail; late String totalPricePassenger; late String passengerPhone; late String rideId; late String isHaveSteps; - late String paymentAmount; + String paymentAmount = '0'; late String paymentMethod; late String passengerId; late String driverId; @@ -115,17 +116,21 @@ class MapDriverController extends GetxController { List> routeSteps = []; String currentInstruction = ""; int currentStepIndex = 0; + bool isTtsEnabled = false; + + void toggleTts() { + isTtsEnabled = !isTtsEnabled; + box.write(BoxName.ttsEnabled, isTtsEnabled); + update(); // تحديث الواجهة + } @override void onClose() { - print("--- Controller is closing. Cleaning up listener. ---"); - - // This will stop the listener when you leave the page + print("--- Controller is closing. Cleaning up. ---"); + _rideTimer?.cancel(); // إيقاف العداد عند الخروج + timer.cancel(); _posSub?.cancel(); _navigationTimer?.cancel(); - // It's also good practice to dispose the map controller - mapController?.dispose(); - super.onClose(); } @@ -197,55 +202,101 @@ class MapDriverController extends GetxController { cancelTripFromDriverAfterApplied() async { if (formKeyCancel.currentState!.validate()) { + // 1. إيقاف استقبال التحديثات فوراً box.write(BoxName.statusDriverLocation, 'off'); - NotificationService.sendNotification( - target: tokenPassenger.toString(), - title: "Cancel Trip from driver", - body: - "Trip Cancelled from driver. We are looking for a new driver. Please wait." - .tr, - isTopic: false, // Important: this is a token - tone: 'cancel', - driverList: [], category: "Cancel Trip from driver", - ); - await CRUD() - .post(link: "${AppLink.server}/ride/rides/update.php", payload: { - "id": (rideId).toString(), // Convert to String - "status": 'CancelFromDriverAfterApply' - }); - await CRUD() - .post(link: "${AppLink.rideServer}/rides/update.php", payload: { - "id": (rideId).toString(), // Convert to String - "status": 'CancelFromDriverAfterApply' - }); - CRUD().postFromDialogue( - link: '${AppLink.seferCairoServer}/driver_order/add.php', - payload: { - 'driver_id': box.read(BoxName.driverID).toString(), - // box.read(BoxName.driverID).toString(), - 'order_id': (rideId).toString(), - 'status': 'CancelFromDriverAfterApply' - }); - await CRUD().post( - link: - "${AppLink.server}/ride/cancelRide/addCancelTripFromDriverAfterApplied.php", - payload: { - "order_id": (rideId).toString(), - "driver_id": box.read(BoxName.driverID).toString(), - "status": 'reject After Applied', - "notes": (cancelTripCotroller.text).toString() - }); + // إظهار لودينج لمنع المستخدم من الضغط مرتين + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); - sql.insertData({ - 'order_id': (rideId), - 'created_at': DateTime.now().toString(), - 'driver_id': box.read(BoxName.driverID).toString(), - }, TableName.driverOrdersRefuse); - box.write(BoxName.rideStatus, 'Cancel'); - Log.print('rideStatus from map 240 : ${box.read(BoxName.rideStatus)}'); - Get.find().getRefusedOrderByCaptain(); - Get.offAll(() => HomeCaptain()); + try { + // 2. إرسال الإشعار للراكب + NotificationService.sendNotification( + target: tokenPassenger.toString(), + title: "Cancel Trip from driver", + body: + "Trip Cancelled from driver. We are looking for a new driver. Please wait." + .tr, + isTopic: false, + tone: 'cancel', + driverList: [], + category: "Cancel Trip from driver", + ); + + // 3. تحديث السيرفرات (نستخدم await لضمان وصول الأمر للسيرفر قبل إغلاق الصفحة) + // ملاحظة: تأكد من صحة هذه الروابط + CRUD().post(link: "${AppLink.server}/ride/rides/update.php", payload: { + "id": (rideId).toString(), + "status": 'CancelFromDriverAfterApply' + }); + + CRUD().post(link: "${AppLink.rideServer}/rides/update.php", payload: { + "id": (rideId).toString(), + "status": 'CancelFromDriverAfterApply' + }); + + // هذا الرابط كان يسبب مشكلة HTML، وضعناه في try منفصل كي لا يوقف العملية + try { + CRUD().postFromDialogue( + link: '${AppLink.server}/driver_order/add.php', + payload: { + 'driver_id': box.read(BoxName.driverID).toString(), + 'order_id': (rideId).toString(), + 'status': 'CancelFromDriverAfterApply' + }); + } catch (e) { + Log.print("Error logging driver order: $e"); + } + + CRUD().post( + link: + "${AppLink.ride}/cancelRide/addCancelTripFromDriverAfterApplied.php", + payload: { + "order_id": (rideId).toString(), + "driver_id": box.read(BoxName.driverID).toString(), + "status": 'reject After Applied', + "notes": (cancelTripCotroller.text).toString() + }); + + // 4. تنظيف البيانات المحلية + // CRUD().post( + // link: "${AppLink.ride}/cancelRide/deletArgumets.php", + // payload: { + // "driver_id": box.read(BoxName.driverID).toString(), + // }); + + box.remove(BoxName.rideArgumentsFromBackground); + box.remove(BoxName.rideArguments); + box.write(BoxName.statusDriverLocation, 'off'); + + // 5. حل مشكلة الكراش (SQL UNIQUE constraint failed) + // نضع الكود داخل try-catch، إذا كان الرقم موجوداً مسبقاً سيتم تجاهل الإضافة ولن ينهار التطبيق + try { + await sql.insertData({ + 'order_id': (rideId), + 'created_at': DateTime.now().toString(), + 'driver_id': box.read(BoxName.driverID).toString(), + }, TableName.driverOrdersRefuse); + } catch (e) { + Log.print( + "Order already exists in refused list, skipping insert. Error: $e"); + } + + box.write(BoxName.rideStatus, 'Cancel'); + Log.print('rideStatus from map 240 : ${box.read(BoxName.rideStatus)}'); + + // تحديث القائمة الرئيسية + Get.find().getRefusedOrderByCaptain(); + + // إغلاق اللودينج والانتقال + if (Get.isDialogOpen ?? false) Get.back(); + Get.offAll(() => HomeCaptain()); + } catch (e) { + // في حال حدوث أي خطأ غير متوقع، نغلق اللودينج ونخرج أيضاً لضمان عدم تعليق التطبيق + if (Get.isDialogOpen ?? false) Get.back(); + Log.print("Error during cancellation: $e"); + Get.offAll(() => HomeCaptain()); + } } } @@ -335,25 +386,58 @@ class MapDriverController extends GetxController { bool isSocialPressed = false; driverCallPassenger() async { String scam = await getDriverScam(); - if (scam != 'failure') { - if (int.parse(scam) > 3) { - box.write(BoxName.statusDriverLocation, 'on'); - Get.find().stopLocationUpdates(); - await CRUD().post(link: AppLink.addNotificationCaptain, payload: { + + // نحول القيمة لرقم بشكل آمن + int scamCount = int.tryParse(scam) ?? 0; + + // لو فوق الحد المطلوب + if (scamCount > 3) { + box.write(BoxName.statusDriverLocation, 'on'); + Get.find().stopLocationUpdates(); + + await CRUD().post( + link: AppLink.addNotificationCaptain, + payload: { 'driverID': box.read(BoxName.driverID), 'title': 'scams operations'.tr, 'body': 'you have connect to passengers and let them cancel the order'.tr, - }); - } else if (isSocialPressed == true) { - box.write(BoxName.statusDriverLocation, 'off'); - await CRUD().post(link: AppLink.addDriverScam, payload: { + }, + ); + + return; + } + + // تسجيل التحايل بشكل طبيعي + if (isSocialPressed == true && passengerId != null && rideId != null) { + box.write(BoxName.statusDriverLocation, 'off'); + + await CRUD().post( + link: AppLink.addDriverScam, + payload: { 'driverID': box.read(BoxName.driverID), 'passengerID': passengerId, 'rideID': rideId, - 'isDriverCalledPassenger': '$isSocialPressed' - }); - } + 'isDriverCalledPassenger': 'true', + }, + ); + } + } + +// دالة مساعدة لحماية التطبيق من كراش الخرائط + Future safeAnimateCamera(CameraUpdate cameraUpdate) async { + // 1. تحقق مما إذا كان الكونترولر مغلقاً (المستخدم خرج من الصفحة) + if (isClosed) return; + + // 2. تحقق من أن متغير الخريطة موجود + if (mapController == null) return; + + try { + // 3. حاول التحريك + await mapController!.animateCamera(cameraUpdate); + } catch (e) { + // 4. إذا حدث خطأ (مثل أن الخريطة لم تعد موجودة في الذاكرة)، تجاهله ولا توقف التطبيق + Log.print("SafeAnimateCamera Error ignored: $e"); } } @@ -484,12 +568,12 @@ class MapDriverController extends GetxController { for (var i = 0; i < remainingTimeTimerRideBegin; i++) { await Future.delayed(const Duration(seconds: 1)); - mapController!.animateCamera( + await safeAnimateCamera( CameraUpdate.newCameraPosition( CameraPosition( bearing: Get.find().heading, target: myLocation, - zoom: 17, // Adjust zoom level as needed + zoom: 17, ), ), ); @@ -539,7 +623,7 @@ class MapDriverController extends GetxController { double distance2 = await calculateDistanceBetweenDriverAndPassengerLocation(); - if (distance2 > 60) { + if (distance2 > 100) { MyDialog().getDialog('Your are far from passenger location'.tr, 'go to your passenger location before\nPassenger cancel trip'.tr, () { Get.back(); @@ -688,12 +772,12 @@ class MapDriverController extends GetxController { 'rideStatus from map (refactored) : ${box.read(BoxName.rideStatus)}'); // --- 1. Calculate Total Cost (Logic remains the same) --- - if (price < 16000) { + if (price < 17200) { totalCost = (carType == 'Comfort' || carType == 'Mishwar Vip' || carType == 'Lady') ? '20000' - : '16000'; + : '17200'; } else if (price < double.parse(totalPricePassenger)) { totalCost = totalPricePassenger; } else { @@ -707,11 +791,11 @@ class MapDriverController extends GetxController { box.write(BoxName.statusDriverLocation, 'off'); // --- 2. Prepare Payloads for Consolidated API Calls --- - final nowString = DateTime.now().toString(); + // final nowString = DateTime.now().toString(); final rideUpdatePayload = { 'rideId': rideId.toString(), - 'rideTimeFinish': nowString, + // 'rideTimeFinish': nowString, 'status': 'Finished', 'price': totalCost, }; @@ -734,10 +818,10 @@ class MapDriverController extends GetxController { try { List> apiCalls = []; - apiCalls.add(CRUD().post( - link: "${AppLink.rideServer}/ride/rides/finish_ride_updates.php", - payload: rideUpdatePayload, - )); + // apiCalls.add(CRUD().post( + // link: "${AppLink.rideServer}/rides/finish_ride_updates.php", + // payload: rideUpdatePayload, + // )); apiCalls.add(CRUD().post( link: "${AppLink.ride}/rides/finish_ride_updates.php", payload: rideUpdatePayload, @@ -752,14 +836,18 @@ class MapDriverController extends GetxController { // --- 4. *** CRITICAL STEP: Verify BOTH results were successful *** --- // Assuming CRUD().post returns a Map like {'success': true, 'message': '...'} - final rideUpdateResult = results[0]; - final paymentResult = results[1]; + final rideUpdate1 = results[0]; + // final rideUpdate2 = results[1]; + final paymentRealResult = results[1]; // هذا هو الدفع الحقيقي! - if (rideUpdateResult['status'] == 'success' && - paymentResult['status'] == 'success') { + // المنطق: يجب أن ينجح تحديث واحد للرحلة على الأقل + يجب أن ينجح الدفع + bool isRideUpdated = (rideUpdate1['status'] == 'success'); + bool isPaymentSuccess = (paymentRealResult['status'] == 'success'); + + if (isRideUpdated && isPaymentSuccess) { // --- SUCCESS: Both API calls succeeded, now proceed --- - Get.back(); // Dismiss the loading indicator + // Dismiss the loading indicator Get.put(DriverBehaviorController()) .sendSummaryToServer(driverId, rideId); @@ -778,6 +866,7 @@ class MapDriverController extends GetxController { ], category: 'Driver Finish Trip', ); + Get.back(); Get.to(() => RatePassenger(), arguments: { 'passengerId': passengerId, @@ -790,7 +879,7 @@ class MapDriverController extends GetxController { // The transaction on the server side would have been rolled back. // We throw an exception to be caught by the catch block, which will revert the UI state. throw Exception( - 'One of the server operations failed. Ride Update: ${rideUpdateResult['message']} | Payment: ${paymentResult['message']}'); + 'Failed. Ride1: ${rideUpdate1['status']}, Payment: ${paymentRealResult['status']}'); } } catch (e) { // --- CATCH ALL ERRORS (Network, Server Failure, etc.) --- @@ -847,220 +936,287 @@ class MapDriverController extends GetxController { /// - نضيف فقط "الزيادة" فوق التسعيرة المقتبسة (وقت زائد + كم زائد). /// - نعكس العمولة kazán مرة واحدة على الزيادة (وليس كل ثانية). - void rideIsBeginPassengerTimer() async { - // === مراجع عامة === + // ========================================================= + // الدالة الرئيسية المعدلة (العداد) + // ========================================================= + // متغيرات العداد الجديد + Timer? _rideTimer; + DateTime? _rideStartTime; + + void rideIsBeginPassengerTimer() { + // 1. تنظيف أي عداد سابق + _rideTimer?.cancel(); + + // 2. تحديد وقت البدء الفعلي (لحساب الوقت بدقة حتى لو علق الجهاز) + _rideStartTime = DateTime.now(); + + // 3. تجهيز الثوابت والقيم المبدئية مرة واحدة (Performance) final hc = Get.find(); final loc = Get.find(); - // أسعار/كم من السيرفر - final double perKmSpeedBase = hc.speedPrice; // مثال: 2900 - final double perKmComfortRaw = hc.comfortPrice; // مثال: 3600 - final double perKmDelivery = hc.deliveryPrice; // للدليفري - final double perKmVanRaw = hc.familyPrice; // فان - const double electricUpliftKm = 400; // كهرباء = كومفورت + 400/كم + // الأسعار + final double perKmSpeedBase = hc.speedPrice; + final double perKmComfortRaw = hc.comfortPrice; + final double perKmDelivery = hc.deliveryPrice; + final double perKmVanRaw = hc.familyPrice; + const double electricUpliftKm = 400; final double perKmElectricRaw = perKmComfortRaw + electricUpliftKm; - // أسعار الدقيقة (ل.س/دقيقة) - final double perMinNature = hc.naturePrice; // طبيعي - final double perMinLate = hc.latePrice; // ليل - final double perMinHeavy = hc.heavyPrice; // ذروة + // أسعار الدقائق + final double perMinNature = hc.naturePrice; + final double perMinLate = hc.latePrice; + final double perMinHeavy = hc.heavyPrice; - // Long/بعيد - const double longSpeedThresholdKm = 40.0; - const double longSpeedPerKm = 2600.0; + // القيم القادمة من الحجز + final double basePassengerQuote = safeParseDouble(totalCost); + final int quotedMinutes = safeParseInt(duration) != 0 + ? safeParseInt(duration) + : safeParseInt(durationOfRideValue); + final double kazanPct = safeParseDouble(kazan) / 100.0; - const double mediumDistThresholdKm = 25.0; // >25كم - const double longDistThresholdKm = 35.0; // >35كم - const double longTripPerMin = 600.0; - const int minuteCapMedium = 60; // سقف 60 دقيقة - const int minuteCapLong = 60; // سقف 60 دقيقة - const int freeMinutesLong = 10; // عفو 10 دقائق عند >35كم - - const double extraReduction100 = 0.07; // +7% فوق تخفيض >40كم عند >100كم - const double maxReductionCap = 0.35; // سقف إجمالي للتخفيض - - // ——— الأساسات المقتبسة من شاشة التسعير ——— - final double basePassengerQuote = double.tryParse(totalCost) ?? 0.0; - - // الدقائق المقتبسة (من نفس المصدر الذي تستخدمه مسبقًا) - final int quotedMinutes = - int.tryParse(duration) ?? int.tryParse(durationOfRideValue) ?? 0; - - // ——— المسافة المخططة من Notification/OnInit ——— - // حاول أولًا من متغيّر الرحلة (تمريره عند onInit من النوتيفيكيشن): + // تحديد المسافة المخططة double plannedKm = 0.0; try { - // أمثلة أسماء: غيّر وفق ما عندك - // 1) إن كانت محفوظة في هذا الكنترولر: this.plannedDistanceKm - plannedKm = (int.parse(distance) as num).toDouble(); + plannedKm = safeParseDouble(distance); } catch (_) {} final double startKm = loc.totalDistance; if (plannedKm <= 0) { - // fallback ثالث: خذ المسافة الحيّة وقت البدء كأساس plannedKm = (startKm > 0) ? startKm : 0.0; } - // ——— سياق المطار (لا نضيف 20,000 هنا مرة ثانية) ——— - bool _isAirport(String s) => - s.toLowerCase().contains('airport') || - s.contains('مطار') || - s.contains('المطار'); - final bool airportCtx = - _isAirport(startNameLocation) || _isAirport(endNameLocation); - - // عمولة الراكب (kazan ٪) - final double kazanPct = (double.tryParse(kazan) ?? 0.0) / 100.0; - - // ——— أدوات ——— - double _perMinuteByTime(DateTime now) { - final h = now.hour; - if (airportCtx) return perMinLate; // مطار = دقيقة ليل (مثل منطقك القديم) - if (h >= 21 || h < 1) return perMinLate; // ليل - if (h >= 14 && h <= 17) return perMinHeavy; // ذروة - return perMinNature; // طبيعي - } - - bool _isLongSpeed(double km) => km > longSpeedThresholdKm; - - double _distanceReductionPct(double km) { - double r40 = 0.0; - if (perKmSpeedBase > 0) { - r40 = (1.0 - (longSpeedPerKm / perKmSpeedBase)) - .clamp(0.0, maxReductionCap); - } - if (km > 100.0) - return (r40 + extraReduction100).clamp(0.0, maxReductionCap); - if (km > 40.0) return r40; - return 0.0; - } - - // فلترة اهتزاز GPS - const double jitterMeters = 10.0; + // فلترة الاهتزاز (Jitter Filter) double lastKmForNoise = startKm; + const double jitterMeters = 10.0; - // تحديث كل 5 ثواني - final int loopCapSec = (quotedMinutes + 3600) * 60; // أمان + // 4. بدء العداد الدوري (كل 1 ثانية لتحديث الواجهة بسلاسة) + _rideTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + // أمان: إذا انتهت الرحلة، أوقف العداد + if (box.read(BoxName.rideStatus) != 'Begin') { + timer.cancel(); + return; + } - for (int sec = 0; sec <= loopCapSec; sec += 5) { - await Future.delayed(const Duration(seconds: 5)); final now = DateTime.now(); + // حساب الثواني المنقضية بدقة من ساعة النظام + final int elapsedSeconds = now.difference(_rideStartTime!).inSeconds; - // كم حي (مع فلترة الاهتزاز) + // -- تحديث المسافة (فلترة الضجيج) -- double liveKm = loc.totalDistance; final double deltaMeters = (liveKm - lastKmForNoise) * 1000.0; + + // إذا كانت الحركة أقل من 10 أمتار، نعتبرها اهتزاز GPS ولا نزيدها if (deltaMeters < jitterMeters) { liveKm = lastKmForNoise; } else { lastKmForNoise = liveKm; } - // كم معتمد لقواعد التسعير - final double plannedForRulesKm = (plannedKm > 0) ? plannedKm : liveKm; + // -- تحديث السعر (كل 5 ثواني لتخفيف الحسابات، أو كل ثانية إذا أردت دقة لحظية) -- + if (elapsedSeconds % 1 == 0) { + // جعلناها كل ثانية لدقة العرض - // LongSpeed لسعر/كم Speed - final bool isLong = _isLongSpeed(plannedForRulesKm); + // استدعاء دالة حساب السعر المفصولة (أنظف للكود) + price = _calculateCurrentPrice( + now: now, + elapsedSeconds: elapsedSeconds, + liveKm: liveKm, + plannedKm: plannedKm, + startKm: startKm, + quotedMinutes: quotedMinutes, + basePassengerQuote: basePassengerQuote, + kazanPct: kazanPct, - // نسبة التخفيض للفئات الأخرى (Comfort/Electric/Van) في كل الأوقات - final double reductionPct = _distanceReductionPct(plannedForRulesKm); - - // سعر/كم فعّال لكل فئة - final double perKmSpeed = isLong ? longSpeedPerKm : perKmSpeedBase; - final double perKmBalash = (perKmSpeed - 500).clamp(0, double.infinity); - final double perKmComfort = - (perKmComfortRaw * (1.0 - reductionPct)).clamp(0, double.infinity); - final double perKmElectric = - (perKmElectricRaw * (1.0 - reductionPct)).clamp(0, double.infinity); - final double perKmVan = - (perKmVanRaw * (1.0 - reductionPct)).clamp(0, double.infinity); - - final double perKmForType = () { - switch (carType) { - case 'Speed': - return perKmSpeed; - case 'Awfar Car': - return perKmBalash; - case 'Comfort': - case 'Mishwar Vip': - case 'RayehGaiComfort': - case 'Lady': - return perKmComfort; - case 'Electric': - return perKmElectric; - case 'Van': - return perKmVan; - case 'Delivery': - return perKmDelivery; - default: - return perKmSpeed; - } - }(); - - // قواعد الدقيقة (تطبّق على كل الأوقات بما فيها المطار) - double perMinEff = _perMinuteByTime(now); - int capMinutes = 1 << 30; // لا سقف افتراضيًا - int freeMinutes = 0; - - if (plannedForRulesKm > longDistThresholdKm) { - perMinEff = longTripPerMin; // 600/د - capMinutes = minuteCapLong; // سقف 60 - freeMinutes = freeMinutesLong; // عفو 10 - } else if (plannedForRulesKm > mediumDistThresholdKm) { - perMinEff = longTripPerMin; // 600/د - capMinutes = minuteCapMedium; // سقف 60 + // تمرير الأسعار + perKmSpeedBase: perKmSpeedBase, + perKmComfortRaw: perKmComfortRaw, + perKmDelivery: perKmDelivery, + perKmVanRaw: perKmVanRaw, + perKmElectricRaw: perKmElectricRaw, + perMinNature: perMinNature, + perMinLate: perMinLate, + perMinHeavy: perMinHeavy, + ); } - // دقائق منقضية - final int elapsedMinutes = (sec ~/ 60); - - // دقائق إضافية فوق المقتبس (مع السقف/العفو) - int extraMinutes = 0; - final int rawExtra = elapsedMinutes - quotedMinutes - freeMinutes; - if (rawExtra > 0) { - extraMinutes = rawExtra; - if (capMinutes < (1 << 30)) { - final int maxBill = (capMinutes - quotedMinutes - freeMinutes); - if (extraMinutes > maxBill) - extraMinutes = maxBill.clamp(0, extraMinutes); - } - } - - // كيلومترات إضافية: إن لم يكن لدينا plannedKm «حقيقي»، نقيس على لحظة البدء - double extraKm = liveKm - plannedForRulesKm; - if (plannedKm <= 0) { - extraKm = (liveKm - startKm); - } - if (extraKm < 0) extraKm = 0; - - // زيادات قبل العمولة (بدون إضافة مطار ثابتة — فهي مضافة في التسعير الأولي) - final double extraBefore = - (extraMinutes * perMinEff) + (extraKm * perKmForType); - - // السعر المعروض = المقتبس + الزيادات * (1 + kazan) - price = basePassengerQuote + (extraBefore * (1.0 + kazanPct)); - - // تحديث واجهة + // -- تحديث عناصر الواجهة -- speed = loc.speed * 3.6; - remainingTimeTimerRideBegin = - (quotedMinutes * 60 - sec).clamp(0, 1 << 30); - progressTimerRideBegin = (quotedMinutes == 0) - ? 0 - : (sec / (quotedMinutes * 60)).clamp(0.0, 1.0); + + // حساب الوقت المتبقي (Countdown) + int totalQuotedSeconds = quotedMinutes * 60; + int remainingSeconds = totalQuotedSeconds - elapsedSeconds; + + if (remainingSeconds < 0) remainingSeconds = 0; + + remainingTimeTimerRideBegin = remainingSeconds; + progressTimerRideBegin = (totalQuotedSeconds == 0) + ? 0.0 + : (elapsedSeconds / totalQuotedSeconds).clamp(0.0, 1.0); + + // تغيير تصميم الصفحة في الدقائق الأخيرة remainingTimeTimerRideBegin < 60 ? driverEndPage = 160 : 100; - updateMarker(); - update(); - } - } // int durationOfRide = int.parse(durationOfRideValue); - // double latePrice = Get.find().latePrice; - // update(); - // int infinity = 40000; - // if (carType == 'Comfort' || - // carType == 'Mishwar Vip' || - // carType == 'RayehGaiComfort') { - // durationOfRide = infinity; - // update(); - // } + // إيقاف بث الموقع للسائق في آخر دقيقة (حسب منطقك القديم) + if (remainingTimeTimerRideBegin < 60) { + box.write(BoxName.statusDriverLocation, 'off'); + } + updateMarker(); + update(); // تحديث GetBuilder + }); + } + + // ========================================================= + // دالة حساب السعر (منطق مفصول ونظيف) + // ========================================================= + double _calculateCurrentPrice({ + required DateTime now, + required int elapsedSeconds, + required double liveKm, + required double plannedKm, + required double startKm, + required int quotedMinutes, + required double basePassengerQuote, + required double kazanPct, + // الأسعار + required double perKmSpeedBase, + required double perKmComfortRaw, + required double perKmDelivery, + required double perKmVanRaw, + required double perKmElectricRaw, + required double perMinNature, + required double perMinLate, + required double perMinHeavy, + }) { + // الثوابت الخاصة بالمسافات الطويلة + const double longSpeedThresholdKm = 40.0; + const double longSpeedPerKm = 2600.0; + const double mediumDistThresholdKm = 25.0; + const double longDistThresholdKm = 35.0; + const double longTripPerMin = 600.0; + const int minuteCapMedium = 60; + const int minuteCapLong = 60; + const int freeMinutesLong = 10; + const double extraReduction100 = 0.07; + const double maxReductionCap = 0.35; + + // 1. تحديد سياق المطار + bool isAirport(String s) => + s.toLowerCase().contains('airport') || s.contains('مطار'); + final bool airportCtx = + isAirport(startNameLocation) || isAirport(endNameLocation); + + // 2. تحديد المسافة المعتمدة للحساب + final double plannedForRulesKm = (plannedKm > 0) ? plannedKm : liveKm; + + // 3. تحديد نسبة التخفيض للمسافات الطويلة + double reductionPct = 0.0; + if (plannedForRulesKm > 40.0) { + double r40 = 0.0; + if (perKmSpeedBase > 0) { + r40 = (1.0 - (longSpeedPerKm / perKmSpeedBase)) + .clamp(0.0, maxReductionCap); + } + reductionPct = r40; + if (plannedForRulesKm > 100.0) { + reductionPct = (r40 + extraReduction100).clamp(0.0, maxReductionCap); + } + } + + // 4. تحديد سعر الكيلومتر بناءً على النوع والمسافة + final bool isLong = plannedForRulesKm > longSpeedThresholdKm; + + // حساب أسعار الكيلومتر المخفضة + final double finalPerKmSpeed = isLong ? longSpeedPerKm : perKmSpeedBase; + final double finalPerKmBalash = + (finalPerKmSpeed - 500).clamp(0, double.infinity); + final double finalPerKmComfort = (perKmComfortRaw * (1.0 - reductionPct)); + final double finalPerKmElectric = (perKmElectricRaw * (1.0 - reductionPct)); + final double finalPerKmVan = (perKmVanRaw * (1.0 - reductionPct)); + + double perKmForType; + switch (carType) { + case 'Speed': + perKmForType = finalPerKmSpeed; + break; + case 'Awfar Car': + perKmForType = finalPerKmBalash; + break; + case 'Comfort': + case 'Mishwar Vip': + case 'RayehGaiComfort': + case 'Lady': + perKmForType = finalPerKmComfort; + break; + case 'Electric': + perKmForType = finalPerKmElectric; + break; + case 'Van': + perKmForType = finalPerKmVan; + break; + case 'Delivery': + perKmForType = perKmDelivery; + break; + default: + perKmForType = finalPerKmSpeed; + } + + // 5. تحديد سعر الدقيقة الفعلي + double perMinEff; + int capMinutes = 1 << 30; // رقم كبير جداً (لا سقف) + int freeMinutes = 0; + + // قواعد المسافات + if (plannedForRulesKm > longDistThresholdKm) { + perMinEff = longTripPerMin; + capMinutes = minuteCapLong; + freeMinutes = freeMinutesLong; + } else if (plannedForRulesKm > mediumDistThresholdKm) { + perMinEff = longTripPerMin; + capMinutes = minuteCapMedium; + } else { + // القواعد الزمنية العادية + final h = now.hour; + if (airportCtx) { + perMinEff = perMinLate; + } else if (h >= 21 || h < 1) { + perMinEff = perMinLate; + } else if (h >= 14 && h <= 17) { + perMinEff = perMinHeavy; + } else { + perMinEff = perMinNature; + } + } + + // 6. حساب الدقائق الإضافية + final int elapsedMinutes = (elapsedSeconds ~/ 60); + int extraMinutes = 0; + final int rawExtra = elapsedMinutes - quotedMinutes - freeMinutes; + + if (rawExtra > 0) { + extraMinutes = rawExtra; + // تطبيق سقف الدقائق إن وجد + if (capMinutes < (1 << 30)) { + final int maxBillable = (capMinutes - quotedMinutes - freeMinutes); + if (extraMinutes > maxBillable) { + extraMinutes = maxBillable < 0 ? 0 : maxBillable; + } + } + } + + // 7. حساب الكيلومترات الإضافية + double extraKm = liveKm - plannedForRulesKm; + if (plannedKm <= 0) { + extraKm = liveKm - startKm; + } + if (extraKm < 0) extraKm = 0; + + // 8. الحساب النهائي + final double extraCost = + (extraMinutes * perMinEff) + (extraKm * perKmForType); + + // إضافة العمولة فقط على الزيادة + return basePassengerQuote + (extraCost * (1.0 + kazanPct)); + } // price = double.parse(totalCost); // for (int i = 0; i <= durationOfRide; i++) { @@ -1356,102 +1512,131 @@ class MapDriverController extends GetxController { return 'Unknown Location (Defaulting to Jordan)'; } + // استبدل دالة getRoute الحالية في الكنترولر بهذا الكود المعدل + // هذه النسخة تحتوي على خاصية إعادة المحاولة (Retry) وتستخدم API routec.intaleq.xyz + +// استبدل دالة getRoute الحالية في الكنترولر بهذا الكود المعدل + // هذه النسخة تحتوي على خاصية إعادة المحاولة (Retry) وتستخدم API routec.intaleq.xyz + Future getRoute({ required LatLng origin, required LatLng destination, - required Color routeColor, // تم الإبقاء عليه كما طلبت + required Color routeColor, }) async { - // إظهار مؤشر التحميل لو رغبت - // isLoading.value = true; - getLocationArea(origin.latitude, origin.longitude); + // 1. إعداد المتغيرات + // getLocationArea(origin.latitude, origin.longitude); String _dynamicApiUrl = 'https://routec.intaleq.xyz/route'; - - // استخدام مفتاح الـ API الخاص بك (افترضتُ أنه موجود في Env) final String _routeApiKey = Env.mapKeyOsm; var url = '$_dynamicApiUrl?origin=${origin.latitude},${origin.longitude}&destination=${destination.latitude},${destination.longitude}&steps=true&overview=full'; - var response; - try { - response = await http.get( - Uri.parse(url), - headers: {'X-API-KEY': _routeApiKey}, - ); - } catch (e) { - print("Error calling route API: $e"); - // isLoading.value = false; - // أظهر رسالة خطأ - return; + // 2. منطق التكرار (Retry Logic) + var responseData; + int retryCount = 0; + const int maxRetries = 3; // عدد المحاولات القصوى + + while (retryCount < maxRetries) { + try { + if (retryCount > 0) { + print("Retrying route fetch... Attempt ${retryCount + 1}"); + } + + var response = await http.get( + Uri.parse(url), + headers: {'X-API-KEY': _routeApiKey}, + ); + + if (response.statusCode == 200) { + var decoded = jsonDecode(response.body); + + // التحقق من صلاحية البيانات (يجب أن تكون status == ok) + if (decoded != null && decoded['status'] == 'ok') { + responseData = decoded; + print("Route fetched successfully on attempt ${retryCount + 1}"); + break; // نجاح! الخروج من الحلقة + } else { + print("API returned status: ${decoded?['status']} (Not OK)"); + } + } else { + print("Route API returned HTTP error: ${response.statusCode}"); + } + } catch (e) { + print("Exception calling route API: $e"); + } + + // إذا وصلنا هنا، فهذا يعني فشل المحاولة + retryCount++; + if (retryCount < maxRetries) { + // انتظار ثانيتين قبل المحاولة التالية لتخفيف الضغط وإعطاء وقت للشبكة + await Future.delayed(Duration(seconds: 2)); + } } - if (response.statusCode != 200) { - print("Route API returned error: ${response.statusCode}"); - // isLoading.value = false; - return; - } - - final responseData = jsonDecode(response.body); - + // 3. التحقق النهائي بعد انتهاء المحاولات if (responseData == null || responseData['status'] != 'ok') { + print("Failed to fetch route after $maxRetries attempts."); + // هنا يمكنك إظهار رسالة للمستخدم: "تعذر رسم المسار، يرجى التحقق من الشبكة" + mySnackeBarError("تعذر رسم المسار، يرجى التحقق من الشبكة"); // isLoading.value = false; - // أظهر رسالة خطأ return; } - _resetRouteState(); // تنظيف الحالة القديمة قبل رسم الجديد + // 4. معالجة البيانات (Parsing) - فقط إذا نجحنا في جلبها + _resetRouteState(); // تنظيف الحالة القديمة - // --- 2. "ترجمة" الاستجابة الجديدة لتناسب الكود القديم --- + try { + // استخراج النقاط ورسم المسار + final pointsString = responseData["polyline"]; - // استخراج النقاط ورسم المسار المبدئي - // الكود القديم كان يتوقع: route["overview_polyline"]["points"] - // الكود الجديد يوفر: responseData["polyline"] - final pointsString = responseData["polyline"]; + // تأكد من أن دالة فك التشفير موجودة لديك + _allPointsForActiveRoute = decodePolylineToLatLng(pointsString); + upcomingPathPoints.assignAll(_allPointsForActiveRoute); - // افترضتُ أن لديك دالة اسمها decodePolylineToLatLng - // إذا كان اسمها decodePolylineIsolate، قم بتغيير الاسم هنا - _allPointsForActiveRoute = decodePolylineToLatLng(pointsString); - upcomingPathPoints.assignAll(_allPointsForActiveRoute); + // استخراج الخطوات + final stepsList = List>.from(responseData['steps']); - // استخراج خطوات الملاحة - // الكود القديم كان يتوقع: List>.from(leg['steps']) - // الكود الجديد يوفر: List>.from(responseData['steps']) - final stepsList = List>.from(responseData['steps']); + // تعديل هيكلة الخطوات لتتناسب مع الكود القديم + for (var step in stepsList) { + // إنشاء التعليمات من الـ maneuver + step['html_instructions'] = _createInstructionFromManeuver(step); - // [مهم جداً] إضافة الحقول التي يتوقعها الكود القديم - for (var step in stepsList) { - // الكود القديم يتوقع 'html_instructions' - // سنقوم بإنشائها من بيانات 'maneuver' - step['html_instructions'] = _createInstructionFromManeuver(step); + // ضبط الموقع النهائي للخطوة + if (step['maneuver'] != null && step['maneuver']['location'] != null) { + var loc = step['maneuver']['location']; // [lng, lat] + step['end_location'] = {'lat': loc[1], 'lng': loc[0]}; + } + } - // الكود القديم قد يتوقع 'end_location' - var loc = step['maneuver']['location']; // [lng, lat] - step['end_location'] = {'lat': loc[1], 'lng': loc[0]}; + activeRouteSteps.assignAll(stepsList); + _prepareStepData(activeRouteSteps); + + // نطق التعليمات الأولية + if (activeRouteSteps.isNotEmpty) { + currentInstruction = + _parseInstruction(activeRouteSteps[0]['html_instructions']); + + if (isTtsEnabled) { + if (Get.isRegistered()) { + Get.find().speakText(currentInstruction); + } else { + Get.put(TextToSpeechController()).speakText(currentInstruction); + } + } + } + + // تحديث حدود الكاميرا (Bounds) + if (_allPointsForActiveRoute.isNotEmpty) { + final bounds = _boundsFromLatLngList(_allPointsForActiveRoute); + _fitToBounds(bounds); + } + + update(); // تحديث الواجهة + } catch (e) { + print("Critical Error parsing route data: $e"); + // هذا الـ Catch يحميك من أي تغيير غير متوقع في شكل البيانات القادمة من السيرفر } - - activeRouteSteps.assignAll(stepsList); - _prepareStepData(activeRouteSteps); // هذه الدالة ستعمل الآن كما هي - - // تحديث التعليمات الأولية - if (activeRouteSteps.isNotEmpty) { - currentInstruction = - _parseInstruction(activeRouteSteps[0]['html_instructions']); - Get.isRegistered() - ? Get.find().speakText(currentInstruction) - : Get.put(TextToSpeechController()).speakText(currentInstruction); - } - - // تحديث الكاميرا لتناسب المسار الجديد - // الكود القديم كان يتوقع: route["bounds"] - // الكود الجديد لا يوفرها، لذا سنقوم بحسابها يدوياً - if (_allPointsForActiveRoute.isNotEmpty) { - final bounds = _boundsFromLatLngList(_allPointsForActiveRoute); - _fitToBounds(bounds); // ستعمل هذه الدالة الآن كما هي - } - - // isLoading.value = false; - update(); // تحديث الواجهة مرة واحدة بعد كل العمليات } String _createInstructionFromManeuver(Map step) { @@ -1758,8 +1943,8 @@ class MapDriverController extends GetxController { html.replaceAll(RegExp(r'<[^>]*>'), ''); Future _fitToBounds(LatLngBounds b, {double padding = 60}) async { - await mapController - ?.animateCamera(CameraUpdate.newLatLngBounds(b, padding)); + // نستخدم الدالة الآمنة التي أنشأناها + await safeAnimateCamera(CameraUpdate.newLatLngBounds(b, padding)); } double distanceBetweenDriverAndPassengerWhenConfirm = 0; @@ -1821,7 +2006,7 @@ class MapDriverController extends GetxController { // Fit the camera to the bounds var cameraUpdate = CameraUpdate.newLatLngBounds(boundsData!, 140); - mapController!.animateCamera(cameraUpdate); + await safeAnimateCamera(cameraUpdate); update(); } } @@ -2190,7 +2375,7 @@ class MapDriverController extends GetxController { // Fit the camera to the bounds var cameraUpdate = CameraUpdate.newLatLngBounds(boundsData, 140); - mapController!.animateCamera(cameraUpdate); + safeAnimateCamera(cameraUpdate); } void changePassengerInfoWindow() { @@ -2207,66 +2392,58 @@ class MapDriverController extends GetxController { } argumentLoading() async { - passengerLocation = Get.arguments['passengerLocation']; - passengerDestination = Get.arguments['passengerDestination']; - duration = Get.arguments['Duration']; - totalCost = Get.arguments['totalCost']; - passengerId = Get.arguments['passengerId']; - driverId = Get.arguments['driverId']; - distance = Get.arguments['Distance']; - passengerName = Get.arguments['name']; - passengerEmail = Get.arguments['email']; - totalPricePassenger = Get.arguments['totalPassenger']; - passengerPhone = Get.arguments['phone']; - walletChecked = Get.arguments['WalletChecked']; - tokenPassenger = Get.arguments['tokenPassenger']; - direction = Get.arguments['direction']; - durationToPassenger = Get.arguments['DurationToPassenger']; - rideId = Get.arguments['rideId']; - durationOfRideValue = Get.arguments['durationOfRideValue']; - paymentAmount = Get.arguments['paymentAmount']; - paymentMethod = Get.arguments['paymentMethod']; - isHaveSteps = Get.arguments['isHaveSteps']; - step0 = Get.arguments['step0']; - step1 = Get.arguments['step1']; - step2 = Get.arguments['step2']; - step3 = Get.arguments['step3']; - step4 = Get.arguments['step4']; - passengerWalletBurc = Get.arguments['passengerWalletBurc']; - timeOfOrder = Get.arguments['timeOfOrder']; - carType = Get.arguments['carType']; - kazan = Get.arguments['kazan']; - startNameLocation = Get.arguments['startNameLocation']; - endNameLocation = Get.arguments['endNameLocation']; + try { + passengerLocation = Get.arguments['passengerLocation']; + passengerDestination = Get.arguments['passengerDestination']; + duration = Get.arguments['Duration']; + totalCost = Get.arguments['totalCost']; + passengerId = Get.arguments['passengerId']; + driverId = Get.arguments['driverId']; + distance = Get.arguments['Distance']; + passengerName = Get.arguments['name']; + passengerEmail = Get.arguments['email']; + totalPricePassenger = Get.arguments['totalPassenger']; + passengerPhone = Get.arguments['phone']; + walletChecked = Get.arguments['WalletChecked']; + tokenPassenger = Get.arguments['tokenPassenger']; + direction = Get.arguments['direction']; + durationToPassenger = Get.arguments['DurationToPassenger']; + rideId = Get.arguments['rideId']; + durationOfRideValue = Get.arguments['durationOfRideValue']; + paymentAmount = Get.arguments['paymentAmount']; + paymentMethod = Get.arguments['paymentMethod']; + isHaveSteps = Get.arguments['isHaveSteps']; + step0 = Get.arguments['step0']; + step1 = Get.arguments['step1']; + step2 = Get.arguments['step2']; + step3 = Get.arguments['step3']; + step4 = Get.arguments['step4']; + passengerWalletBurc = Get.arguments['passengerWalletBurc']; + timeOfOrder = Get.arguments['timeOfOrder']; + carType = Get.arguments['carType']; + kazan = Get.arguments['kazan']; + startNameLocation = Get.arguments['startNameLocation']; + endNameLocation = Get.arguments['endNameLocation']; // Parse to double - latlng(passengerLocation, passengerDestination); + latlng(passengerLocation, passengerDestination); - String lat = Get.find().myLocation.latitude.toString(); - String lng = Get.find().myLocation.longitude.toString(); - String origin = '$lat,$lng'; - // Set the origin and destination coordinates for the Google Maps directions request. - Future.delayed(const Duration(seconds: 1)); - getRoute( - origin: Get.find().myLocation, - destination: latLngPassengerLocation, - routeColor: Colors.yellow // أو أي لون - ); - // getRoute( - // origin: latLngPassengerLocation, - // destination: latLngPassengerDestination, - // routeColor: Colors.blue // أو أي لون - // ); - // getMap(origin, passengerLocation); - // isHaveSteps == 'haveSteps' - // ? ( - // await getMapDestination(step0, step1), - // await getMapDestination(step1, step2), - // step3 == '' ? await getMapDestination(step2, step3) : () {}, - // step4 == '' ? await getMapDestination(step3, step4) : () {}, - // ) - // : await getMapDestination(passengerLocation, passengerDestination); - update(); + String lat = + Get.find().myLocation.latitude.toString(); + String lng = + Get.find().myLocation.longitude.toString(); + String origin = '$lat,$lng'; + // Set the origin and destination coordinates for the Google Maps directions request. + Future.delayed(const Duration(seconds: 1)); + getRoute( + origin: Get.find().myLocation, + destination: latLngPassengerLocation, + routeColor: Colors.yellow // أو أي لون + ); + update(); + } catch (e) { + Log.print("Error parsing arguments: $e"); + } } latlng(String passengerLocation, passengerDestination) { @@ -2287,7 +2464,7 @@ class MapDriverController extends GetxController { late Duration durationToAdd; int hours = 0; int minutes = 0; - late String carType; + String carType = ''; late String kazan; late String startNameLocation; late String endNameLocation; @@ -2329,35 +2506,29 @@ class MapDriverController extends GetxController { Future startListeningStepNavigation() async { _posSub?.cancel(); _navigationTimer?.cancel(); - Position? pos; + + // تعريف متغير لتخزين آخر موقع + Position? lastKnownPosition; + _posSub = Geolocator.getPositionStream( locationSettings: LocationSettings( accuracy: LocationAccuracy.high, distanceFilter: 5, - // حدّث كل ~5 متر لتقليل الاهتزاز) ), ).listen((position) { - // خزّن آخر موقع معروف، لكن لا تعالجه فوراً myLocation = LatLng(position.latitude, position.longitude); heading = position.heading; + lastKnownPosition = position; // تحديث الموقع update(); }); + _navigationTimer = Timer.periodic( - Duration( - seconds: box.read(BoxName.updateInterval) ?? 5, - ), (timer) { - // تأكد أن لدينا موقع صالح قبل المعالجة - if (myLocation.latitude != 0) { - // استدعِ دالة المعالجة الثقيلة هنا - onLocationUpdated(pos!); + Duration(seconds: box.read(BoxName.updateInterval) ?? 5), (timer) { + // التحقق من أن الموقع ليس null وأن الكنترولر لم يتم إغلاقه + if (lastKnownPosition != null && !isClosed) { + onLocationUpdated(lastKnownPosition!); } }); - // _posSub = Geolocator.getPositionStream( - // locationSettings: const LocationSettings( - // accuracy: LocationAccuracy.high, - // distanceFilter: 5, // حدّث كل ~5 متر لتقليل الاهتزاز - // ), - // ).listen((pos) => _onLocationTick(LatLng(pos.latitude, pos.longitude))); } void stopListeningStepNavigation() { @@ -2365,3 +2536,16 @@ class MapDriverController extends GetxController { _posSub = null; } } + +double safeParseDouble(dynamic value, {double defaultValue = 0.0}) { + if (value == null) return defaultValue; + if (value is double) return value; + if (value is int) return value.toDouble(); + return double.tryParse(value.toString()) ?? defaultValue; +} + +int safeParseInt(dynamic value, {int defaultValue = 0}) { + if (value == null) return defaultValue; + if (value is int) return value; + return int.tryParse(value.toString()) ?? defaultValue; +} diff --git a/lib/controller/home/captin/order_request_controller.dart b/lib/controller/home/captin/order_request_controller.dart index 592803d..2eac98a 100755 --- a/lib/controller/home/captin/order_request_controller.dart +++ b/lib/controller/home/captin/order_request_controller.dart @@ -188,7 +188,7 @@ class OrderRequestController extends GetxController { if (remainingTime == 0 && _timerActive) { if (applied == false) { endTimer(); - refuseOrder(orderID); + //refuseOrder(orderID); } } } @@ -211,29 +211,6 @@ class OrderRequestController extends GetxController { } } - void refuseOrder( - orderID, - ) async { - await CRUD().postFromDialogue(link: AppLink.addDriverOrder, payload: { - 'driver_id': box.read(BoxName.driverID), - 'order_id': (orderID), - 'status': 'Refused' - }); - await CRUD().post(link: AppLink.updateRides, payload: { - 'id': (orderID), - 'status': 'Refused', - 'driver_id': box.read(BoxName.driverID), - }); - // if (AppLink.endPoint != AppLink.seferCairoServer) { - // CRUD().post(link: '${AppLink.endPoint}/rides/update.php', payload: { - // 'id': (orderID), - // 'status': 'Refused', - // 'driver_id': box.read(BoxName.driverID), - // }); - // } - update(); - } - addRideToNotificationDriverString( orderID, String startLocation, diff --git a/lib/controller/local/phone_intel/countries.dart b/lib/controller/local/phone_intel/countries.dart index 13e4bab..528d4fa 100644 --- a/lib/controller/local/phone_intel/countries.dart +++ b/lib/controller/local/phone_intel/countries.dart @@ -6603,7 +6603,7 @@ const List countries = [ code: "SY", dialCode: "963", minLength: 9, - maxLength: 9, + maxLength: 10, ), Country( name: "Taiwan", diff --git a/lib/controller/local/translations.dart b/lib/controller/local/translations.dart index 3991d5f..06d247c 100755 --- a/lib/controller/local/translations.dart +++ b/lib/controller/local/translations.dart @@ -59,7 +59,16 @@ class MyTranslation extends Translations { 'witout zero': 'بدون صفر', 'You Can Cancel the Trip and get Cost From ': 'يمكنك إلغاء الرحلة واسترداد التكلفة من ', + 'Please enter a correct phone': 'يرجى إدخال رقم هاتف صحيح', + 'Only Syrian phone numbers are allowed': + 'يسمح بأرقام الهواتف السورية فقط', + 'Go to passenger:': 'اذهب إلى الراكب:', + 'Birth year must be 4 digits': + 'يجب أن يكون سنة الميلاد مكونة من 4 أرقام', + 'Required field': 'حقل مطلوب', + 'You are not near': 'أنت لست بالقرب من', 'Please enter your phone number': 'يرجى إدخال رقم هاتفك', + 'Enter a valid year': 'أدخل سنة صحيحة', 'Phone number seems too short': 'يبدو أن رقم الهاتف قصير جدًا', 'You have upload Criminal documents': 'لقد قمت بتحميل وثائق جنائية', 'Close': 'إغلاق', @@ -658,7 +667,17 @@ Raih Gai: For same-day return trips longer than 50km. "phone number of driver": "رقم هاتف السائق", "Transfer budget": "نقل الميزانية", "Comfort": "كمفورت", - "Speed": "سبيد", + "Speed": "سعر ثابت", + 'Insert Emergency Number': 'أدخل رقم الطوارئ', + 'Emergency Number': 'رقم الطوارئ', + 'Save': 'حفظ', + 'Stay': 'ابقى', + 'Exit': 'خروج', + 'Waiting': 'انتظار', + 'Your data will be erased after 2 weeks\nAnd you will can\'t return to use app after 1 month ': + ',سيتم مسح بياناتك بعد أسبوعين\nولن تتمكن من العودة لاستخدام التطبيق بعد شهر واحد ', + "You are in an active ride. Leaving this screen might stop tracking. Are you sure you want to exit?": + "أنت في رحلة نشطة. قد يؤدي مغادرة هذه الشاشة إلى إيقاف التتبع. هل أنت متأكد أنك تريد الخروج؟", "Lady": "ليدي", "Permission denied": "تم رفض الإذن", "Contact permission is required to pick a contact": diff --git a/lib/controller/payment/smsPaymnet/payment_services.dart b/lib/controller/payment/smsPaymnet/payment_services.dart index 62b1b28..9558744 100644 --- a/lib/controller/payment/smsPaymnet/payment_services.dart +++ b/lib/controller/payment/smsPaymnet/payment_services.dart @@ -1,74 +1,54 @@ -// لإضافة هذه الحزمة، قم بتشغيل الأمر التالي في الـ Terminal -// flutter pub add intl - import 'dart:async'; -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import 'package:sefer_driver/constant/links.dart'; import 'package:sefer_driver/controller/functions/crud.dart'; - import '../../../constant/box_name.dart'; import '../../../main.dart'; +import '../../../print.dart'; -/// خدمة لإدارة عمليات الدفع المتعلقة بنظام الدفع عبر الرسائل القصيرة +// ... (PaymentService class remains unchanged) ... class PaymentService { - final String _baseUrl = "${AppLink.paymentServer}/sms_webhook"; + final String _baseUrl = "${AppLink.paymentServer}/ride/shamcash"; - Future createInvoice({ - required String userPhone, - required double amount, - }) async { - final url = "$_baseUrl/create_invoice.php"; + Future createInvoice({required double amount}) async { + final url = "$_baseUrl/create_invoice_shamcash.php"; try { final response = await CRUD().postWallet( link: url, payload: { - 'user_phone': userPhone.toString(), 'driverID': box.read(BoxName.driverID), 'amount': amount.toString(), }, - ).timeout(const Duration(seconds: 15)); // إضافة مهلة للطلب + ).timeout(const Duration(seconds: 15)); if (response != 'failure') { - final data = (response); + final data = response; if (data['status'] == 'success' && data['invoice_number'] != null) { - debugPrint( - "تم إنشاء الفاتورة بنجاح. الرقم: ${data['invoice_number']}"); return data['invoice_number'].toString(); - } else { - debugPrint("فشل في إنشاء الفاتورة من السيرفر: ${data['message']}"); - return null; } - } else { - debugPrint("خطأ في السيرفر عند إنشاء الفاتورة: ${response.statusCode}"); - return null; } + return null; } catch (e) { - debugPrint("حدث استثناء عند إنشاء الفاتورة: $e"); return null; } } - /// دالة للتحقق من حالة فاتورة واحدة Future checkInvoiceStatus(String invoiceNumber) async { - final url = "$_baseUrl/check_invoice_status.php"; + final url = "$_baseUrl/check_status.php"; try { final response = await CRUD().postWallet(link: url, payload: { 'invoice_number': invoiceNumber, - }).timeout(const Duration(seconds: 10)); // مهلة للشبكة + }).timeout(const Duration(seconds: 10)); if (response != 'failure') { - final data = (response); + final data = response; return data['status'] == 'success' && data['invoice_status'] == 'completed'; } return false; } catch (e) { - debugPrint("خطأ أثناء التحقق من الفاتورة: $e"); return false; } } @@ -86,14 +66,15 @@ class PaymentScreenSmsProvider extends StatefulWidget { final double amount; final String providerName; final String providerLogo; - final String paymentPhoneNumber; + final String qrImagePath; const PaymentScreenSmsProvider({ super.key, required this.amount, this.providerName = 'شام كاش', this.providerLogo = 'assets/images/shamCash.png', - this.paymentPhoneNumber = '963942542053', + this.qrImagePath = 'assets/images/shamcashsend.png', + // removed paymentPhoneNumber }); @override @@ -106,7 +87,6 @@ class _PaymentScreenSmsProviderState extends State { Timer? _pollingTimer; PaymentStatus _status = PaymentStatus.creatingInvoice; String? _invoiceNumber; - final String phone = box.read(BoxName.phoneWallet); @override void initState() { @@ -116,17 +96,14 @@ class _PaymentScreenSmsProviderState extends State { @override void dispose() { - _pollingTimer?.cancel(); // مهم جداً: إلغاء المؤقت عند الخروج من الشاشة + _pollingTimer?.cancel(); super.dispose(); } void _createAndPollInvoice() async { setState(() => _status = PaymentStatus.creatingInvoice); - - final invoiceNumber = await _paymentService.createInvoice( - userPhone: phone, - amount: widget.amount, - ); + final invoiceNumber = + await _paymentService.createInvoice(amount: widget.amount); if (invoiceNumber != null && mounted) { setState(() { @@ -140,7 +117,7 @@ class _PaymentScreenSmsProviderState extends State { } void _startPolling(String invoiceNumber) { - const timeoutDuration = Duration(minutes: 3); + const timeoutDuration = Duration(minutes: 5); var elapsed = Duration.zero; _pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { @@ -150,64 +127,57 @@ class _PaymentScreenSmsProviderState extends State { if (mounted) setState(() => _status = PaymentStatus.paymentTimeout); return; } - - debugPrint("Polling... Checking invoice status for: $invoiceNumber"); final isCompleted = await _paymentService.checkInvoiceStatus(invoiceNumber); if (isCompleted && mounted) { timer.cancel(); setState(() => _status = PaymentStatus.paymentSuccess); - // TODO: تحديث رصيد المستخدم أو تنفيذ الإجراءات اللازمة } }); } - /// دالة جديدة لمعالجة محاولة الرجوع للخلف - void _onPopInvoked(bool didPop) async { - // إذا كان الرجوع قد تم بالفعل (مثلاً من خلال Navigator.pop)، لا تفعل شيئاً - if (didPop) return; - - // إذا كان المستخدم ينتظر الدفع، أظهر له حوار التأكيد + Future _onPopInvoked() async { if (_status == PaymentStatus.waitingForPayment) { - final shouldPop = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('هل أنت متأكد؟'), - content: const Text('إذا خرجت الآن، سيتم إلغاء عملية الدفع الحالية.'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('البقاء'), + return (await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('إلغاء العملية؟', textAlign: TextAlign.right), + content: const Text( + 'الخروج الآن سيؤدي لإلغاء متابعة عملية الدفع.', + textAlign: TextAlign.right), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('البقاء')), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('خروج', + style: TextStyle(color: Colors.red))), + ], ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text('الخروج'), - ), - ], - ), - ); - - // إذا وافق المستخدم على الخروج، قم بإغلاق الشاشة - if (shouldPop ?? false) { - Navigator.of(context).pop(); - } + )) ?? + false; } + return true; } @override Widget build(BuildContext context) { - // استخدام PopScope بدلاً من WillPopScope - return PopScope( - // منع الرجوع التلقائي فقط في حالة انتظار الدفع - canPop: _status != PaymentStatus.waitingForPayment, - // استدعاء دالة التحقق عند محاولة الرجوع - onPopInvoked: _onPopInvoked, + return WillPopScope( + onWillPop: _onPopInvoked, child: Scaffold( - appBar: AppBar(title: Text("الدفع عبر ${widget.providerName}")), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Center( - child: _buildContentByStatus(), + backgroundColor: Colors.grey[50], + appBar: AppBar( + title: Text("دفع عبر ${widget.providerName}"), + centerTitle: true, + elevation: 0, + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Center(child: _buildContentByStatus()), ), ), ), @@ -222,7 +192,7 @@ class _PaymentScreenSmsProviderState extends State { children: [ CircularProgressIndicator(), SizedBox(height: 20), - Text("جاري إنشاء فاتورة الدفع...", style: TextStyle(fontSize: 16)), + Text("جاري إنشاء رقم البيان...", style: TextStyle(fontSize: 16)), ], ); case PaymentStatus.waitingForPayment: @@ -237,94 +207,195 @@ class _PaymentScreenSmsProviderState extends State { Widget _buildWaitingForPaymentUI() { final currencyFormat = NumberFormat.decimalPattern('ar_SY'); - final invoiceText = _invoiceNumber ?? '------'; + final invoiceText = _invoiceNumber ?? '---'; return SingleChildScrollView( + physics: const BouncingScrollPhysics(), child: Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Image.asset(widget.providerLogo, width: 96), - const SizedBox(height: 16), - Text("تعليمات الدفع", style: Theme.of(context).textTheme.titleLarge), - const SizedBox(height: 12), - Card( - elevation: 1.5, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16), + // 1. المبلغ (تصميم مميز) + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 15), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue.shade800, Colors.blue.shade600]), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.25), + blurRadius: 15, + offset: const Offset(0, 8)) + ], + ), + child: Column( + children: [ + const Text("المبلغ المطلوب", + style: TextStyle(color: Colors.white70, fontSize: 14)), + const SizedBox(height: 8), + Text( + "${currencyFormat.format(widget.amount)} ل.س", + style: const TextStyle( + color: Colors.white, + fontSize: 32, + fontWeight: FontWeight.bold), + ), + ], + ), + ), + + const SizedBox(height: 30), + + // 2. التعليمات والنسخ (الجزء الأهم) + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.grey.shade100, + blurRadius: 10, + offset: const Offset(0, 4)) + ], + ), + child: Column( + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.orange.shade50, shape: BoxShape.circle), + child: Icon(Icons.priority_high_rounded, + color: Colors.orange.shade800, size: 20), + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + "انسخ الرقم أدناه وضعه في خانة (الملاحظات) عند الدفع.", + style: TextStyle( + fontSize: 14, fontWeight: FontWeight.w600), + ), + ), + ], + ), + const SizedBox(height: 20), + InkWell( + onTap: () { + Clipboard.setData(ClipboardData(text: invoiceText)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text("تم نسخ رقم البيان ✅", + textAlign: TextAlign.center), + backgroundColor: Colors.green.shade600, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + margin: const EdgeInsets.all(20), + ), + ); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 15, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: Colors.blue.shade200, width: 1.5), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("رقم البيان (Invoice ID)", + style: TextStyle( + fontSize: 12, color: Colors.grey)), + Text(invoiceText, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + letterSpacing: 1.5)), + ], + ), + const Icon(Icons.copy_rounded, + color: Colors.blue, size: 24), + ], + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 30), + + // 3. الـ QR Code (قابل للاختيار/الضغط) + const Text("امسح الرمز للدفع", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87)), + const SizedBox(height: 15), + + GestureDetector( + onTap: () { + // تأثير بصري بسيط عند الضغط (أو تكبير الصورة في Dialog) + showDialog( + context: context, + builder: (ctx) => Dialog( + backgroundColor: Colors.transparent, + child: InteractiveViewer( + child: Image.asset(widget.qrImagePath), + ), + ), + ); + }, + child: Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.grey.shade300), + boxShadow: [ + BoxShadow( + color: Colors.grey.shade200, + blurRadius: 10, + spreadRadius: 2) + ], + ), child: Column( children: [ - _StepTile(number: 1, text: "افتح تطبيق محفظتك الإلكترونية."), - _StepTile(number: 2, text: "اختر خدمة تحويل الأموال."), - _StepTile( - number: 3, - text: - "أدخل المبلغ المطلوب: ${currencyFormat.format(widget.amount)} ل.س"), - _StepTile(number: 4, text: "حوّل إلى الرقم التالي:"), - // --- التعديل هنا --- - ListTile( - contentPadding: EdgeInsets.zero, - title: Text( - widget.paymentPhoneNumber, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - letterSpacing: 1.2), - ), - trailing: OutlinedButton.icon( - onPressed: () async { - await Clipboard.setData( - ClipboardData(text: widget.paymentPhoneNumber)); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("تم نسخ رقم الهاتف"))); - } - }, - icon: const Icon(Icons.copy, size: 18), - label: const Text("نسخ"), - ), + Image.asset( + widget.qrImagePath, + width: 180, + height: 180, + fit: BoxFit.contain, + errorBuilder: (c, o, s) => const Icon(Icons.qr_code_2, + size: 100, color: Colors.grey), ), - // --- نهاية التعديل --- const SizedBox(height: 8), - _StepTile( - number: 5, - text: "هام: انسخ رقم القسيمة والصقه في خانة \"البيان\"."), - ListTile( - contentPadding: EdgeInsets.zero, - title: Text(invoiceText, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - letterSpacing: 1.5)), - trailing: OutlinedButton.icon( - onPressed: _invoiceNumber == null - ? null - : () async { - await Clipboard.setData( - ClipboardData(text: invoiceText)); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("تم نسخ رقم القسيمة"))); - } - }, - icon: const Icon(Icons.copy, size: 18), - label: const Text("نسخ"), - ), - ), + const Text("اضغط للتكبير", + style: TextStyle(fontSize: 10, color: Colors.grey)), ], ), ), ), + + const SizedBox(height: 40), + + // مؤشر الانتظار + const LinearProgressIndicator(backgroundColor: Colors.white), + const SizedBox(height: 10), + const Text("ننتظر إشعار الدفع تلقائياً...", + style: TextStyle(color: Colors.grey, fontSize: 12)), const SizedBox(height: 20), - const LinearProgressIndicator(minHeight: 2), - const SizedBox(height: 12), - Text("بانتظار تأكيد الدفع...", - style: TextStyle(color: Colors.grey.shade700)), - const SizedBox(height: 4), - const Text("هذه الشاشة ستتحدث تلقائيًا", - style: TextStyle(color: Colors.grey)), ], ), ); @@ -334,14 +405,26 @@ class _PaymentScreenSmsProviderState extends State { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.check_circle, color: Colors.green, size: 80), + const Icon(Icons.verified_rounded, color: Colors.green, size: 100), const SizedBox(height: 20), const Text("تم الدفع بنجاح!", - style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text("العودة"), + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + const Text("تم إضافة الرصيد والمكافأة إلى حسابك", + style: TextStyle(color: Colors.grey)), + const SizedBox(height: 40), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12))), + onPressed: () => Navigator.of(context).pop(), + child: const Text("متابعة", style: TextStyle(fontSize: 18)), + ), ), ], ); @@ -351,47 +434,41 @@ class _PaymentScreenSmsProviderState extends State { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.error, color: Colors.red, size: 80), + Icon(Icons.error_outline_rounded, color: Colors.red.shade400, size: 80), const SizedBox(height: 20), Text( _status == PaymentStatus.paymentTimeout - ? "انتهى الوقت المحدد للدفع" - : "حدث خطأ ما", + ? "انتهى الوقت" + : "لم يتم التحقق", style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold), - textAlign: TextAlign.center, ), - const SizedBox(height: 8), - const Text("يرجى المحاولة مرة أخرى.", style: TextStyle(fontSize: 16)), - const SizedBox(height: 20), - ElevatedButton( - onPressed: _createAndPollInvoice, - child: const Text("المحاولة مرة أخرى"), + const SizedBox(height: 15), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 30), + child: Text( + "لم يصلنا إشعار الدفع. هل تأكدت من وضع (رقم البيان) في الملاحظات؟", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey, height: 1.5)), ), + const SizedBox(height: 40), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12))), + onPressed: _createAndPollInvoice, + icon: const Icon(Icons.refresh), + label: const Text("حاول مرة أخرى"), + ), + ), + const SizedBox(height: 15), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("إلغاء", style: TextStyle(color: Colors.grey)), + ) ], ); } } - -// ويدجت مساعد لعرض خطوات التعليمات بشكل أنيق -class _StepTile extends StatelessWidget { - final int number; - final String text; - const _StepTile({required this.number, required this.text}); - - @override - Widget build(BuildContext context) { - return ListTile( - contentPadding: EdgeInsets.zero, - leading: CircleAvatar( - radius: 12, - backgroundColor: Theme.of(context).primaryColor, - child: Text("$number", - style: const TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.bold)), - ), - title: Text(text), - ); - } -} diff --git a/lib/views/auth/captin/otp_page.dart b/lib/views/auth/captin/otp_page.dart index 6602211..8ab90d2 100644 --- a/lib/views/auth/captin/otp_page.dart +++ b/lib/views/auth/captin/otp_page.dart @@ -356,9 +356,14 @@ class _PhoneNumberScreenState extends State { validator: (phone) { if (phone == null || phone.number.isEmpty) { return 'Please enter your phone number'.tr; + } // Check if the number is a Syrian number + if (phone.countryISOCode != 'SY') { + return 'Only Syrian phone numbers are allowed'.tr; } // Check if the national number part starts with '0' - if (phone.number.startsWith('0')) { + if (phone.completeNumber.startsWith('96309') || + phone.completeNumber.startsWith('+9630') || + phone.completeNumber.startsWith('09')) { return 'Please enter the number without the leading 0'.tr; } if (phone.completeNumber.length < 10) { diff --git a/lib/views/auth/syria/registration_view.dart b/lib/views/auth/syria/registration_view.dart index 3df65eb..77b1525 100644 --- a/lib/views/auth/syria/registration_view.dart +++ b/lib/views/auth/syria/registration_view.dart @@ -121,6 +121,29 @@ class RegistrationView extends StatelessWidget { }, ), const SizedBox(height: 16), + TextFormField( + controller: c.bithdateController, + decoration: InputDecoration( + labelText: 'سنة الميلاد'.tr, + hintText: '1999'.tr, + border: const OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + validator: (v) { + if (v == null || v.isEmpty) { + return 'Required field'.tr; + } + if (v.length != 4) { + return 'Birth year must be 4 digits'.tr; + } + // Optional: check if it’s a valid number + if (int.tryParse(v) == null) { + return 'Enter a valid year'.tr; + } + return null; + }, + ), + const SizedBox(height: 16), TextFormField( controller: c.driverLicenseExpiryController, decoration: InputDecoration( diff --git a/lib/views/home/Captin/About Us/settings_captain.dart b/lib/views/home/Captin/About Us/settings_captain.dart index 4e6c5b2..6bff296 100755 --- a/lib/views/home/Captin/About Us/settings_captain.dart +++ b/lib/views/home/Captin/About Us/settings_captain.dart @@ -39,18 +39,18 @@ class SettingsCaptain extends StatelessWidget { subtitle: 'Change the app language'.tr, onTap: () => Get.to(() => const Language()), ), - _buildListTile( - icon: Icons.flag_outlined, - title: 'Change Country'.tr, - subtitle: 'Get features for your country'.tr, - onTap: () => Get.to( - () => MyScafolld( - title: 'Change Country'.tr, - body: [CountryPickerFromSetting()], - isleading: true, - ), - ), - ), + // _buildListTile( + // icon: Icons.flag_outlined, + // title: 'Change Country'.tr, + // subtitle: 'Get features for your country'.tr, + // onTap: () => Get.to( + // () => MyScafolld( + // title: 'Change Country'.tr, + // body: [CountryPickerFromSetting()], + // isleading: true, + // ), + // ), + // ), ], ), const SizedBox(height: 20), diff --git a/lib/views/home/Captin/driver_map_page.dart b/lib/views/home/Captin/driver_map_page.dart index 951d7b1..6552a05 100755 --- a/lib/views/home/Captin/driver_map_page.dart +++ b/lib/views/home/Captin/driver_map_page.dart @@ -1,3 +1,4 @@ +import 'package:sefer_driver/constant/box_name.dart'; import 'package:sefer_driver/constant/style.dart'; import 'package:sefer_driver/views/widgets/elevated_btn.dart'; import 'package:flutter/material.dart'; @@ -6,6 +7,7 @@ import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart'; import '../../../constant/colors.dart'; import '../../../controller/functions/location_controller.dart'; +import '../../../main.dart'; import '../../Rate/rate_passenger.dart'; import '../../widgets/my_textField.dart'; import 'mapDriverWidgets/driver_end_ride_bar.dart'; @@ -14,114 +16,169 @@ import 'mapDriverWidgets/google_map_app.dart'; import 'mapDriverWidgets/passenger_info_window.dart'; import 'mapDriverWidgets/sos_connect.dart'; -// Changed: تم إعادة بناء الصفحة بالكامل لتكون أكثر تنظيمًا class PassengerLocationMapPage extends StatelessWidget { PassengerLocationMapPage({super.key}); final LocationController locationController = Get.put(LocationController()); final MapDriverController mapDriverController = Get.put(MapDriverController()); + // Helper function to show exit confirmation dialog + Future showExitDialog() async { + bool? result = await Get.defaultDialog( + title: "Warning".tr, + titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), + middleText: + "You are in an active ride. Leaving this screen might stop tracking. Are you sure you want to exit?" + .tr, + middleTextStyle: AppStyle.title, + barrierDismissible: false, + confirm: MyElevatedButton( + title: 'Stay'.tr, + kolor: AppColor.greenColor, + onPressed: () => Get.back(result: false), // Return false (Don't pop) + ), + cancel: MyElevatedButton( + title: 'Exit'.tr, + kolor: AppColor.redColor, + onPressed: () => Get.back(result: true), // Return true (Allow pop) + ), + ); + return result ?? false; + } + @override Widget build(BuildContext context) { - // New: استخدام addPostFrameCallback لضمان أن تحميل البيانات يتم بعد بناء الواجهة WidgetsBinding.instance.addPostFrameCallback((_) { if (Get.arguments != null && Get.arguments is Map) { mapDriverController.argumentLoading(); mapDriverController.startTimerToShowPassengerInfoWindowFromDriver(); - } else { - // في حال عدم وجود arguments، يتم التعامل مع هذا الخطأ - Get.snackbar("Error", "No order data found."); - Get.back(); } }); - return Scaffold( - body: SafeArea( - child: Stack( - children: [ - // 1. الخريطة في الخلفية - GoogleDriverMap(locationController: locationController), + // ✅ Added PopScope to intercept back button + return PopScope( + canPop: false, // Prevents immediate popping + onPopInvokedWithResult: (didPop, result) async { + if (didPop) { + return; + } + // Show dialog + final shouldExit = await showExitDialog(); + if (shouldExit) { + Get.back(); // Manually pop if confirmed + } + }, + child: Scaffold( + body: SafeArea( + child: Stack( + children: [ + // 1. Map + GoogleDriverMap(locationController: locationController), - // 2. شريط تعليمات الطريق في الأعلى - const InstructionsOfRoads(), + // 2. Instructions + const InstructionsOfRoads(), - // 4. نافذة معلومات الراكب في الأسفل (تظهر قبل بدء الرحلة) + // 3. Passenger Info + Positioned( + top: 0, + left: 0, + right: 0, + child: PassengerInfoWindow(), + ), - PassengerInfoWindow(), - // 3. زر إلغاء الرحلة في الأعلى يسارًا + // 4. Cancel Widget + CancelWidget(mapDriverController: mapDriverController), - CancelWidget(mapDriverController: mapDriverController), - // Changed: تم تعديل تصميم زر الإلغاء ليكون أيقونة بسيطة في الأعلى - // 5. شريط معلومات وإنهاء الرحلة (يظهر بعد بدء الرحلة) - driverEndRideBar(), + // 5. End Ride Bar + driverEndRideBar(), - // 6. أزرار الطوارئ والاتصال - SosConnect(), + // 6. SOS + SosConnect(), - // 7. دائرة عرض السرعة - speedCircle(), - GoogleMapApp(), - // 8. نافذة عرض السعر النهائي (تظهر بعد انتهاء الرحلة) - const PricesWindow(), - ], - ), - )); + // 7. Speed + speedCircle(), + + // 8. External Map + Positioned( + bottom: 100, + right: 10, + child: GoogleMapApp(), + ), + + // 9. Prices Window + const PricesWindow(), + ], + ), + )), + ); } } -// New: تصميم جديد لشريط تعليمات الطريق في أعلى الشاشة +// ... The rest of your widgets (InstructionsOfRoads, CancelWidget, etc.) remain unchanged ... +// ... Keep the code below exactly as you had it in the previous snippet ... + class InstructionsOfRoads extends StatelessWidget { const InstructionsOfRoads({super.key}); @override Widget build(BuildContext context) { - return GetBuilder( - builder: (controller) => - // يتم إظهار التعليمات فقط إذا كانت متوفرة - controller.currentInstruction.isNotEmpty - ? Positioned( - bottom: 10, - left: MediaQuery.of(context).size.width * 0.15, - right: MediaQuery.of(context).size.width * 0.15, - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(30), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], + return Positioned( + bottom: 10, + left: MediaQuery.of(context).size.width * 0.15, + right: MediaQuery.of(context).size.width * 0.15, + child: GetBuilder( + builder: (controller) => controller.currentInstruction.isNotEmpty + ? AnimatedContainer( + duration: const Duration(milliseconds: 300), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 4), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.directions, - color: AppColor.primaryColor), - const SizedBox(width: 10), - Expanded( - child: Text( - controller.currentInstruction, - style: AppStyle.title.copyWith(fontSize: 16), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ), - ], + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.directions, color: AppColor.primaryColor), + const SizedBox(width: 10), + Expanded( + child: Text( + controller.currentInstruction, + style: AppStyle.title.copyWith(fontSize: 16), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), ), - ), - ) - : const SizedBox(), // في حالة عدم وجود تعليمات، لا يظهر شيء + const SizedBox(width: 10), + InkWell( + onTap: () { + controller.toggleTts(); + }, + child: Icon( + controller.isTtsEnabled + ? Icons.volume_up + : Icons.volume_off, + color: controller.isTtsEnabled + ? AppColor.greenColor + : Colors.grey, + ), + ), + ], + ), + ) + : const SizedBox(), + ), ); } } -// Changed: تم تعديل تصميم وموضع زر الإلغاء ليكون أيقونة بسيطة في الأعلى class CancelWidget extends StatelessWidget { const CancelWidget({ super.key, @@ -137,7 +194,6 @@ class CancelWidget extends StatelessWidget { left: 10, child: GetBuilder( builder: (controller) { - // يظهر زر الإلغاء فقط قبل انتهاء الرحلة if (controller.isRideFinished) return const SizedBox.shrink(); return GestureDetector( @@ -199,7 +255,6 @@ class CancelWidget extends StatelessWidget { } } -// Changed: تم تعديل تصميم نافذة السعر لتكون أكثر وضوحًا class PricesWindow extends StatelessWidget { const PricesWindow({ super.key, diff --git a/lib/views/home/Captin/home_captain/home_captin.dart b/lib/views/home/Captin/home_captain/home_captin.dart index 27b0b2b..6e9ec8f 100755 --- a/lib/views/home/Captin/home_captain/home_captin.dart +++ b/lib/views/home/Captin/home_captain/home_captin.dart @@ -621,45 +621,95 @@ class FloatingActionButtons extends StatelessWidget { const SizedBox( height: 5, ), + // هذا الكود يوضع داخل الـ Stack في ملف الواجهة (HomeCaptain View) + box.read(BoxName.rideStatus) == 'Applied' || box.read(BoxName.rideStatus) == 'Begin' ? Positioned( bottom: Get.height * .2, - right: 6, - child: AnimatedContainer( - duration: const Duration(microseconds: 200), - width: homeCaptainController.widthMapTypeAndTraffic, - decoration: BoxDecoration( - border: Border.all(color: AppColor.blueColor), - color: AppColor.secondaryColor, - borderRadius: BorderRadius.circular(15)), - child: GestureDetector( - onLongPress: () { - box.write(BoxName.rideStatus, 'delete'); - homeCaptainController.update(); - }, - child: IconButton( - onPressed: () { - box.read(BoxName.rideStatus) == 'Applied' - ? { - Get.to(() => PassengerLocationMapPage(), - arguments: - box.read(BoxName.rideArguments)), - Get.put(MapDriverController()) - .changeRideToBeginToPassenger() - } - : { - Get.to(() => PassengerLocationMapPage(), - arguments: - box.read(BoxName.rideArguments)), - Get.put(MapDriverController()) - .startRideFromStartApp() - }; - }, - icon: const Icon( - Icons.directions_rounded, - size: 29, - color: AppColor.blueColor, + // جعلنا الزر يظهر في المنتصف أو يمتد ليكون واضحاً جداً + right: 20, + left: 20, + child: Center( + child: AnimatedContainer( + duration: const Duration( + milliseconds: + 200), // تم تصحيح microseconds إلى milliseconds لحركة أنعم + // أزلنا العرض الثابت homeCaptainController.widthMapTypeAndTraffic لكي يتسع للنص + // width: homeCaptainController.widthMapTypeAndTraffic, + decoration: BoxDecoration( + border: Border.all( + color: AppColor.blueColor, + width: 2), // تعريض الإطار قليلاً + color: AppColor.secondaryColor, // لون الخلفية + borderRadius: BorderRadius.circular( + 30), // تدوير الحواف ليشبه الأزرار الحديثة + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 4), + ) + ]), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(30), + onLongPress: () { + // وظيفة الحذف عند الضغط الطويل (للطوارئ) + box.write(BoxName.rideStatus, 'delete'); + homeCaptainController.update(); + }, + onTap: () { + // نفس منطقك الأصلي للانتقال + if (box.read(BoxName.rideStatus) == 'Applied') { + Get.to(() => PassengerLocationMapPage(), + arguments: box.read(BoxName.rideArguments)); + Get.put(MapDriverController()) + .changeRideToBeginToPassenger(); + } else { + Get.to(() => PassengerLocationMapPage(), + arguments: box.read(BoxName.rideArguments)); + Get.put(MapDriverController()) + .startRideFromStartApp(); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), + child: Row( + mainAxisSize: + MainAxisSize.min, // حجم الزر على قد المحتوى + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons + .directions_car_filled_rounded, // تغيير الأيقونة لسيارة أو اتجاهات لتكون معبرة أكثر + size: 24, + color: AppColor.blueColor, + ), + const SizedBox( + width: 10), // مسافة بين الأيقونة والنص + Text( + "متابعة الرحلة", // النص الواضح للسائق + style: const TextStyle( + color: AppColor.blueColor, + fontSize: 16, + fontWeight: FontWeight.bold, + fontFamily: + 'Cairo', // تأكد من نوع الخط المستخدم عندك + ), + ), + if (box.read(BoxName.rideStatus) == + 'Begin') ...[ + const SizedBox(width: 5), + // إضافة مؤشر صغير (نقطة حمراء) إذا كانت الرحلة قد بدأت بالفعل (اختياري) + const Icon(Icons.circle, + size: 8, color: Colors.green) + ] + ], + ), + ), ), ), ), diff --git a/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart b/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart index 2c23685..dd4617d 100755 --- a/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart +++ b/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart @@ -219,9 +219,10 @@ Future checkForPendingOrderFromServer() async { link: AppLink.getArgumentAfterAppliedFromBackground, payload: {'driver_id': driverId}, ); + Log.print('response: ${response}'); // Assuming the server returns order data if found, or 'failure'/'none' if not - if (response != 'failure') { + if (response['status'] == 'success') { final Map orderInfoFromServer = response['message']; final Map rideArguments = _transformServerDataToAppArguments(orderInfoFromServer); diff --git a/lib/views/home/Captin/mapDriverWidgets/driver_end_ride_bar.dart b/lib/views/home/Captin/mapDriverWidgets/driver_end_ride_bar.dart index 0078f8f..aa09a5f 100755 --- a/lib/views/home/Captin/mapDriverWidgets/driver_end_ride_bar.dart +++ b/lib/views/home/Captin/mapDriverWidgets/driver_end_ride_bar.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; import 'package:slide_to_act/slide_to_act.dart'; import 'package:vibration/vibration.dart'; import 'dart:io'; @@ -11,82 +12,88 @@ import '../../../../controller/home/captin/map_driver_controller.dart'; import '../../../widgets/elevated_btn.dart'; // Changed: إعادة تصميم كاملة للشريط ليصبح شريطًا علويًا عند بدء الرحلة -GetBuilder driverEndRideBar() { - return GetBuilder( - builder: (controller) => AnimatedPositioned( - duration: const Duration(milliseconds: 300), - // New: يظهر الشريط من الأعلى عندما تبدأ الرحلة - top: controller.isRideStarted ? 0 : -200, - left: 0, - right: 0, - child: Card( - margin: EdgeInsets.zero, - elevation: 10, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Column( - children: [ - // -- معلومات الرحلة -- - if (controller.carType != 'Mishwar Vip') - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildInfoColumn( - icon: Icons.social_distance, - text: '${controller.distance} ${'KM'.tr}', - label: 'Distance'.tr, - ), - _buildInfoColumn( - icon: Icons.timelapse, - text: controller.hours > 1 - ? '${controller.hours}h ${controller.minutes}m' - : '${controller.minutes}m', - label: 'Time'.tr, - ), - _buildInfoColumn( - icon: Icons.money_sharp, - text: '${controller.paymentAmount} ${'SYP'.tr}', - label: 'Price'.tr, - ), - ], - ), - if (controller.carType != 'Mishwar Vip') - const Divider(height: 20), +// ملف: driver_end_ride_bar.dart - // -- مؤقت الرحلة المتبقي (إن وجد) -- - _builtTimerAndCarType(), +Widget driverEndRideBar() { + // 1. Positioned هي الوالد المباشر (لأنها داخل Stack في الصفحة الرئيسية) + return Positioned( + top: 0, + left: 0, + right: 0, + // 2. GetBuilder يكون في الداخل + child: GetBuilder( + builder: (controller) => AnimatedContainer( + duration: const Duration(milliseconds: 300), + // 3. نستخدم التحريك (Translation) لإخفاء الشريط وإظهاره بدلاً من تغيير الـ top + transform: Matrix4.translationValues( + 0, controller.isRideStarted ? 0 : -250, 0), + child: Card( + margin: EdgeInsets.zero, + elevation: 10, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)), + ), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + child: Column( + children: [ + if (controller.carType != 'Mishwar Vip') + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildInfoColumn( + icon: Icons.social_distance, + text: '${controller.distance} ${'KM'.tr}', + label: 'Distance'.tr, + ), + _buildInfoColumn( + icon: Icons.timelapse, + text: controller.hours > 1 + ? '${controller.hours}h ${controller.minutes}m' + : '${controller.minutes}m', + label: 'Time'.tr, + ), + _buildInfoColumn( + icon: Icons.money_sharp, + text: + '${NumberFormat('#,##0').format(double.tryParse(controller.paymentAmount.toString()) ?? 0)} ${'SYP'.tr}', + label: 'Price'.tr, + ), + ], + ), - const SizedBox(height: 12), - - // -- زر إنهاء الرحلة المنزلق -- - SlideAction( - height: 55, - borderRadius: 15, - elevation: 4, - text: 'Slide to End Trip'.tr, - textStyle: AppStyle.title.copyWith( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.white, + // ... بقية الكود كما هو (الأزرار والمؤقت) + if (controller.carType != 'Mishwar Vip') + const Divider(height: 20), + const _builtTimerAndCarType(), + const SizedBox(height: 12), + SlideAction( + height: 55, + borderRadius: 15, + elevation: 4, + text: 'Slide to End Trip'.tr, + textStyle: AppStyle.title.copyWith( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + outerColor: AppColor.redColor, + innerColor: Colors.white, + sliderButtonIcon: const Icon( + Icons.arrow_forward_ios, + color: AppColor.redColor, + size: 24, + ), + sliderRotate: false, + onSubmit: () { + HapticFeedback.mediumImpact(); + controller.finishRideFromDriver(); + return null; + }, ), - outerColor: AppColor.redColor, - innerColor: Colors.white, - sliderButtonIcon: const Icon( - Icons.arrow_forward_ios, - color: AppColor.redColor, - size: 24, - ), - sliderRotate: false, - onSubmit: () { - HapticFeedback.mediumImpact(); - controller.finishRideFromDriver(); - return null; // New: onSubmit now returns null - }, - ), - ], + ], + ), ), ), ), @@ -116,100 +123,119 @@ class _builtTimerAndCarType extends StatelessWidget { @override Widget build(BuildContext context) { - final controller = Get.find(); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // -- نوع السيارة -- - Container( - decoration: AppStyle.boxDecoration1.copyWith(color: Colors.grey[200]), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text( - controller.carType, - style: AppStyle.title.copyWith(fontWeight: FontWeight.bold), - ), - ), - // -- مؤقت الرحلة -- - if (controller.carType != 'Comfort' && - controller.carType != 'Mishwar Vip' && - controller.carType != 'Lady') ...[ - const SizedBox(width: 10), - Expanded( - child: Container( - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - gradient: LinearGradient( - colors: [ - controller.remainingTimeTimerRideBegin < 60 - ? AppColor.redColor.withOpacity(0.8) - : AppColor.greenColor.withOpacity(0.8), - controller.remainingTimeTimerRideBegin < 60 - ? AppColor.redColor - : AppColor.greenColor, - ], - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Stack( - alignment: Alignment.center, - children: [ - LinearProgressIndicator( - backgroundColor: Colors.transparent, - valueColor: AlwaysStoppedAnimation( - Colors.white.withOpacity(0.2)), - minHeight: 40, - value: controller.progressTimerRideBegin.toDouble(), - ), - Text( - controller.stringRemainingTimeRideBegin, - style: AppStyle.title.copyWith( - color: Colors.white, fontWeight: FontWeight.bold), - ), - ], - ), - ), + // نستخدم GetBuilder هنا لضمان تحديث العداد في كل ثانية + return GetBuilder(builder: (controller) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // -- نوع السيارة -- + Container( + decoration: + AppStyle.boxDecoration1.copyWith(color: Colors.grey[200]), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + controller.carType.tr, + style: AppStyle.title.copyWith(fontWeight: FontWeight.bold), ), - ) + ), + + // -- مؤقت الرحلة -- + if (controller.carType != 'Comfort' && + controller.carType != 'Mishwar Vip' && + controller.carType != 'Lady') ...[ + const SizedBox(width: 10), + Expanded( + child: Container( + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + colors: [ + controller.remainingTimeTimerRideBegin < 60 + ? AppColor.redColor.withOpacity(0.8) + : AppColor.greenColor.withOpacity(0.8), + controller.remainingTimeTimerRideBegin < 60 + ? AppColor.redColor + : AppColor.greenColor, + ], + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Stack( + alignment: Alignment.center, + children: [ + LinearProgressIndicator( + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation( + Colors.white.withOpacity(0.2)), + minHeight: 40, + // تأكد من أن هذه القيمة بين 0.0 و 1.0 في الكونترولر + value: controller.progressTimerRideBegin.toDouble(), + ), + Text( + controller.stringRemainingTimeRideBegin, + style: AppStyle.title.copyWith( + color: Colors.white, fontWeight: FontWeight.bold), + ), + ], + ), + ), + ), + ) + ], ], - ], - ); + ); + }); } } // Changed: تم تعديل مكان ومظهر دائرة السرعة -GetBuilder speedCircle() { - if (Get.find().speed > 100) { - if (Platform.isIOS) { - HapticFeedback.selectionClick(); - } else { - Vibration.vibrate(duration: 1000); - } - Get.defaultDialog( - barrierDismissible: false, - titleStyle: AppStyle.title, - title: 'Speed Over'.tr, - middleText: 'Please slow down'.tr, - middleTextStyle: AppStyle.title, - confirm: MyElevatedButton( - title: 'I will slow down'.tr, - onPressed: () => Get.back(), - ), - ); - } - return GetBuilder( - builder: (controller) { - return controller.isRideStarted - ? Positioned( - // New: تم وضع دائرة السرعة في الأسفل يمينًا - bottom: 25, - left: 3, - child: Container( +// غيرنا نوع الإرجاع إلى Widget بدلاً من GetBuilder +Widget speedCircle() { + // التحقق من السرعة يمكن أن يبقى هنا أو داخل الـ builder + // لكن التنبيهات (Vibration/Dialog) يفضل أن تكون داخل الـ builder لتجنب تكرارها أثناء إعادة البناء الخارجية + + return Positioned( + // New: Positioned الآن هي الوالد المباشر (يجب وضع هذه الدالة داخل Stack في الصفحة الرئيسية) + bottom: 25, + left: 3, + child: GetBuilder( + builder: (controller) { + // التحقق من التنبيهات هنا + if (controller.speed > 100) { + // نستخدم addPostFrameCallback لضمان عدم استدعاء الـ Dialog أثناء عملية البناء + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!Get.isDialogOpen!) { + // تجنب فتح أكثر من نافذة + if (Platform.isIOS) { + HapticFeedback.selectionClick(); + } else { + Vibration.vibrate(duration: 1000); + } + Get.defaultDialog( + barrierDismissible: false, + titleStyle: AppStyle.title, + title: 'Speed Over'.tr, + middleText: 'Please slow down'.tr, + middleTextStyle: AppStyle.title, + confirm: MyElevatedButton( + title: 'I will slow down'.tr, + onPressed: () => Get.back(), + ), + ); + } + }); + } + + return controller.isRideStarted + ? Container( decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, - boxShadow: [BoxShadow(blurRadius: 5, color: Colors.black26)], + boxShadow: const [ + BoxShadow(blurRadius: 5, color: Colors.black26) + ], border: Border.all( width: 4, color: controller.speed > 100 @@ -227,13 +253,13 @@ GetBuilder speedCircle() { controller.speed.toStringAsFixed(0), style: AppStyle.number.copyWith(fontSize: 24), ), - Text("km/h", style: TextStyle(fontSize: 10)), + const Text("km/h", style: TextStyle(fontSize: 10)), ], ), ), - ), - ) - : const SizedBox(); - }, + ) + : const SizedBox(); // إذا لم تبدأ الرحلة نخفي العنصر وهو داخل الـ Positioned + }, + ), ); } diff --git a/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart b/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart index 400a7aa..4b5949d 100755 --- a/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart +++ b/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart @@ -1,37 +1,43 @@ -import 'package:sefer_driver/views/widgets/mydialoug.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sefer_driver/constant/colors.dart'; -import 'package:sefer_driver/constant/info.dart'; -import 'package:sefer_driver/controller/firebase/firbase_messge.dart'; import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart'; import 'package:sefer_driver/views/widgets/elevated_btn.dart'; - import '../../../../constant/box_name.dart'; import '../../../../constant/style.dart'; import '../../../../controller/firebase/notification_service.dart'; import '../../../../main.dart'; -import '../../../../print.dart'; +import 'package:sefer_driver/views/widgets/mydialoug.dart'; -// Changed: إعادة تصميم كاملة لتصبح شريط معلومات علوي مدمج class PassengerInfoWindow extends StatelessWidget { PassengerInfoWindow({super.key}); - final fcm = Get.isRegistered() - ? Get.find() - : Get.put(FirebaseMessagesController()); + + // Optimization: defining static styles here avoids rebuilding them every frame + final TextStyle _labelStyle = + AppStyle.title.copyWith(color: Colors.grey[600], fontSize: 13); + final TextStyle _valueStyle = + AppStyle.title.copyWith(fontWeight: FontWeight.bold, fontSize: 18); + @override Widget build(BuildContext context) { + // Get safe area top padding (for Notches/Status bars) + final double topPadding = MediaQuery.of(context).padding.top; + final double topMargin = topPadding + 10; // Safe area + 10px spacing + return GetBuilder( builder: (controller) => AnimatedPositioned( duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, - // Changed: تم تغيير الموضع من الأسفل إلى الأعلى - top: controller.isPassengerInfoWindow ? 15.0 : -200.0, + // FIX: Use calculated top margin to avoid hiding behind status bar + top: controller.isPassengerInfoWindow ? topMargin : -250.0, left: 15.0, right: 15.0, child: Card( - elevation: 8, - shadowColor: Colors.black.withOpacity(0.3), + // Optimization: Lower elevation slightly for smoother animation on cheap phones + elevation: 4, + shadowColor: Colors.black.withOpacity(0.2), + color: Colors.white, + surfaceTintColor: Colors.white, // Fix for Material 3 tinting shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), @@ -41,14 +47,12 @@ class PassengerInfoWindow extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // New: صف علوي للمعلومات الأساسية _buildTopInfoRow(controller), const Divider(height: 16), - // Changed: الأزرار الآن في صف أفقي ومدمج if (!controller.isRideBegin) _buildActionButtons(controller), - // New: مؤشر انتظار الراكب المدمج + // Optimization: Only render linear indicator if needed if (controller.remainingTimeInPassengerLocatioWait < 300 && controller.remainingTimeInPassengerLocatioWait != 0 && !controller.isRideBegin) ...[ @@ -56,7 +60,6 @@ class PassengerInfoWindow extends StatelessWidget { _buildWaitingIndicator(controller), ], - // زر الإلغاء بعد انتهاء وقت الانتظار if (controller.isdriverWaitTimeEnd && !controller.isRideBegin) ...[ const SizedBox(height: 10), @@ -70,35 +73,33 @@ class PassengerInfoWindow extends StatelessWidget { ); } - // New: ودجت لعرض المعلومات العلوية بشكل مدمج Widget _buildTopInfoRow(MapDriverController controller) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, // Align top children: [ - // معلومات الراكب Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Go to passenger:'.tr, style: _labelStyle), + const SizedBox(height: 2), Text( - 'Go to passenger:'.tr, - style: AppStyle.title - .copyWith(color: Colors.grey[600], fontSize: 13), - ), - Text( - controller.passengerName, - style: AppStyle.title - .copyWith(fontWeight: FontWeight.bold, fontSize: 18), + controller.passengerName ?? 'loading...', + style: _valueStyle, + maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), - // معلومات المسافة والزمن - Row( + const SizedBox(width: 10), // Spacing between name and chips + Column( + // Changed to Column for better layout on small screens + crossAxisAlignment: CrossAxisAlignment.end, children: [ _buildInfoChip(Icons.map_outlined, '${controller.distance} km'), - const SizedBox(width: 8), + const SizedBox(height: 6), // Vertical spacing _buildInfoChip( Icons.timer_outlined, controller.hours > 1 @@ -111,10 +112,9 @@ class PassengerInfoWindow extends StatelessWidget { ); } - // New: ودجت مخصص لعرض المعلومات بشكل أنيق Widget _buildInfoChip(IconData icon, String text) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: AppColor.primaryColor.withOpacity(0.1), borderRadius: BorderRadius.circular(20), @@ -122,144 +122,164 @@ class PassengerInfoWindow extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, color: AppColor.primaryColor, size: 16), - const SizedBox(width: 4), - Text(text, - style: TextStyle( - color: AppColor.primaryColor, fontWeight: FontWeight.bold)), + Icon(icon, color: AppColor.primaryColor, size: 14), // Smaller icon + const SizedBox(width: 6), + Text( + text, + style: TextStyle( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: 12 // Slightly smaller font for chips + ), + ), ], ), ); } - // Changed: إعادة تصميم أزرار الإجراءات لتكون أكثر دمجًا Widget _buildActionButtons(MapDriverController controller) { return Row( children: [ if (controller.isArrivedSend) Expanded( - child: ElevatedButton.icon( - icon: const Icon(Icons.location_on, size: 18), - label: Text('I Arrive'.tr), - style: ElevatedButton.styleFrom( - backgroundColor: AppColor.yellowColor, - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - ), - onPressed: () async { - controller.getRoute( - origin: controller.latLngPassengerLocation, - destination: controller.latLngPassengerDestination, - routeColor: Colors.blue // أو أي لون - ); - if (await controller - .calculateDistanceBetweenDriverAndPassengerLocation() < - 140) { - // fcm.sendNotificationToDriverMAP( - // 'Hi ,I Arrive your site', - // 'I Arrive at your site'.tr, - // controller.tokenPassenger, - // [], - // 'ding.wav', - // ); - Log.print( - 'controller.tokenPassenger: ${controller.tokenPassenger}'); + flex: 1, + child: SizedBox( + height: 45, // Fixed height for consistency + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.yellowColor, + foregroundColor: Colors.black, + padding: EdgeInsets.zero, // Reduce padding to fit text + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + ), + onPressed: () async { + // LOGIC FIX: Check distance FIRST + double distance = await controller + .calculateDistanceBetweenDriverAndPassengerLocation(); - NotificationService.sendNotification( - target: controller.tokenPassenger.toString(), - title: 'Hi ,I Arrive your site'.tr, - body: 'I Arrive at your site'.tr, - isTopic: false, // Important: this is a token - tone: 'ding', - driverList: [], category: 'Hi ,I Arrive your site', - ); - controller.startTimerToShowDriverWaitPassengerDuration(); - controller.isArrivedSend = false; - } else { - MyDialog().getDialog( - 'You are not near the passenger location'.tr, - 'Please go to the pickup location exactly'.tr, - () => Get.back()); - } - }, + if (distance < 140) { + // Only draw route and send notif if close enough + controller.getRoute( + origin: controller.latLngPassengerLocation, + destination: controller.latLngPassengerDestination, + routeColor: Colors.blue); + + NotificationService.sendNotification( + target: controller.tokenPassenger.toString(), + title: 'Hi ,I Arrive your site'.tr, + body: 'I Arrive at your site'.tr, + isTopic: false, + tone: 'ding', + driverList: [], + category: 'Hi ,I Arrive your site', + ); + controller.startTimerToShowDriverWaitPassengerDuration(); + controller.isArrivedSend = false; + } else { + MyDialog().getDialog( + 'You are not near'.tr, // Shortened title + 'Please go to the pickup location exactly'.tr, + () => Get.back()); + } + }, + // Using Row instead of .icon constructor for better control + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.location_on, size: 16), + const SizedBox(width: 4), + Flexible( + child: Text('I Arrive'.tr, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12))), + ], + ), + ), ), ), if (controller.isArrivedSend) const SizedBox(width: 8), Expanded( - flex: 2, - child: ElevatedButton.icon( - icon: const Icon(Icons.play_arrow_rounded, size: 20), - label: Text('Start the Ride'.tr, - style: const TextStyle(fontWeight: FontWeight.bold)), - style: ElevatedButton.styleFrom( - backgroundColor: AppColor.greenColor, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), + flex: 2, // Give "Start" button more space + child: SizedBox( + height: 45, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.greenColor, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + ), + onPressed: () { + MyDialog().getDialog( + "Is the Passenger in your Car?".tr, + "Don't start trip if passenger not in your car".tr, + () async { + await controller.startRideFromDriver(); + Get.back(); + }, + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.play_arrow_rounded, size: 22), + const SizedBox(width: 6), + Flexible( + child: Text('Start the Ride'.tr, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.bold))), + ], + ), ), - onPressed: () { - MyDialog().getDialog( - "Is the Passenger in your Car?".tr, - "Don't start trip if passenger not in your car".tr, - () async { - await controller.startRideFromDriver(); - Get.back(); - }, - ); - }, ), ), ], ); } - // Changed: مؤشر الانتظار الآن أكثر دمجًا Widget _buildWaitingIndicator(MapDriverController controller) { - return ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Stack( - alignment: Alignment.center, - children: [ - LinearProgressIndicator( - backgroundColor: AppColor.greyColor.withOpacity(0.3), + return Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: LinearProgressIndicator( + backgroundColor: AppColor.greyColor.withOpacity(0.2), + // Ternary for color is fine color: controller.remainingTimeInPassengerLocatioWait < 60 ? AppColor.redColor : AppColor.greenColor, - minHeight: 25, + minHeight: 8, // Thinner looks more modern value: controller.progressInPassengerLocationFromDriver.toDouble(), ), - Text( - controller.stringRemainingTimeWaitingPassenger, - style: AppStyle.title.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 13, - shadows: [ - Shadow(color: Colors.black.withOpacity(0.5), blurRadius: 2) - ]), + ), + const SizedBox(height: 4), + Text( + "${'Waiting'.tr}: ${controller.stringRemainingTimeWaitingPassenger}", + style: AppStyle.title.copyWith( + color: Colors.grey[700], + fontWeight: FontWeight.bold, + fontSize: 12, ), - ], - ), + ), + ], ); } - // New: زر الإلغاء بعد انتهاء الانتظار Widget _buildCancelAfterWaitButton(MapDriverController controller) { return MyElevatedButton( - title: 'You Can Cancel the Trip and get Cost From '.tr + - AppInformation.appName.tr, + title: 'Cancel Trip & Get Cost'.tr, // Shortened text kolor: AppColor.gold, onPressed: () { MyDialog().getDialog('Are you sure to cancel?'.tr, '', () async { NotificationService.sendNotification( target: controller.tokenPassenger.toString(), title: 'Driver Cancelled Your Trip'.tr, - body: - 'You will need to pay the cost to the driver, or it will be deducted from your next trip', - isTopic: false, // Important: this is a token + body: 'You will need to pay the cost...', + isTopic: false, tone: 'cancel', - driverList: [], category: 'Driver Cancelled Your Trip', + driverList: [], + category: 'Driver Cancelled Your Trip', ); box.write(BoxName.rideStatus, 'Cancel'); await controller.addWaitingTimeCostFromPassengerToDriverWallet(); diff --git a/lib/views/home/Captin/orderCaptin/order_over_lay.dart b/lib/views/home/Captin/orderCaptin/order_over_lay.dart index cc8bb6b..218ae42 100755 --- a/lib/views/home/Captin/orderCaptin/order_over_lay.dart +++ b/lib/views/home/Captin/orderCaptin/order_over_lay.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_overlay_window/flutter_overlay_window.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; import 'package:just_audio/just_audio.dart'; import 'package:sefer_driver/constant/api_key.dart'; import '../../../../constant/box_name.dart'; @@ -213,13 +214,14 @@ class _OrderOverlayState extends State await _closeOverlay(); return; } - - var res = await CRUD().post(link: AppLink.updateStausFromSpeed, payload: { - 'id': orderData!.orderId, - 'rideTimeStart': DateTime.now().toString(), - 'status': 'Apply', - 'driver_id': box.read(BoxName.driverID), - }); + var res = await CRUD().post( + link: "${AppLink.ride}/rides/updateStausFromSpeed.php", + payload: { + 'id': orderData!.orderId, + 'rideTimeStart': DateTime.now().toString(), + 'status': 'Apply', + 'driver_id': box.read(BoxName.driverID), + }); List bodyToPassenger = [ _getData(6).toString(), _getData(8).toString(), @@ -340,16 +342,12 @@ class _OrderOverlayState extends State _log("Driver ID is null, cannot refuse order"); return; } - await _crud.post(link: AppLink.addDriverOrder, payload: { + _crud.post(link: AppLink.addDriverOrder, payload: { 'driver_id': driverId, 'order_id': orderID, 'status': 'Refused' }); - await _crud.post(link: AppLink.updateRides, payload: { - 'id': orderID, - 'status': 'Refused', - 'driver_id': driverId, - }); + _log("Order $orderID refused successfully"); } catch (e) { _log("Error in _apiRefuseOrder for $orderID: $e"); @@ -509,9 +507,16 @@ class _OrderOverlayState extends State children: [ Expanded( flex: 3, - child: _buildHighlightInfo("\$${order.price}", "السعر".tr, - Icons.monetization_on_rounded, AppColors.priceHighlight, - isLarge: true), + child: _buildHighlightInfo( + // التعديل هنا 👇 + "${NumberFormat('#,##0').format(order.price)} ل.س", + // أو يمكنك استخدام "SYP" بدلاً من "ل.س" + + "السعر".tr, + Icons.monetization_on_rounded, + AppColors.priceHighlight, + isLarge: true, + ), ), const SizedBox(width: 12), Expanded( diff --git a/lib/views/home/Captin/orderCaptin/order_request_page.dart b/lib/views/home/Captin/orderCaptin/order_request_page.dart index 4708b9c..265e3ac 100755 --- a/lib/views/home/Captin/orderCaptin/order_request_page.dart +++ b/lib/views/home/Captin/orderCaptin/order_request_page.dart @@ -248,38 +248,31 @@ class _OrderRequestPageState extends State { kolor: AppColor.greenColor, title: 'Accept Order'.tr, onPressed: () async { - Get.put(HomeCaptainController()).changeRideId(); - box.write(BoxName.statusDriverLocation, 'on'); - controller.endTimer(); - controller.changeApplied(); - var res = await CRUD().post( - link: AppLink.updateStausFromSpeed, - payload: { - 'id': (controller.myList[16]), - 'rideTimeStart': DateTime.now().toString(), - 'status': 'Apply', - 'driver_id': box.read(BoxName.driverID), - }); - CRUD().post( link: - "${AppLink.endPoint}/ride/rides/updateStausFromSpeed.php", + "${AppLink.ride}/rides/updateStausFromSpeed.php", payload: { 'id': (controller.myList[16]), 'rideTimeStart': DateTime.now().toString(), 'status': 'Apply', 'driver_id': box.read(BoxName.driverID), }); + if (res == 'failure') { MyDialog().getDialog( "This ride is already applied by another driver." .tr, '', () { Get.back(); - // Get.back(); + Get.back(); }); } else { - await CRUD().postFromDialogue( + Get.put(HomeCaptainController()).changeRideId(); + box.write(BoxName.statusDriverLocation, 'on'); + controller.endTimer(); + controller.changeApplied(); + + CRUD().postFromDialogue( link: AppLink.addDriverOrder, payload: { 'driver_id': @@ -386,33 +379,26 @@ class _OrderRequestPageState extends State { title: 'Refuse Order'.tr, onPressed: () async { controller.endTimer(); - List bodyToPassenger = [ - box.read(BoxName.driverID).toString(), - box.read(BoxName.nameDriver).toString(), - box.read(BoxName.tokenDriver).toString(), - ]; + // List bodyToPassenger = [ + // box.read(BoxName.driverID).toString(), + // box.read(BoxName.nameDriver).toString(), + // box.read(BoxName.tokenDriver).toString(), + // ]; - // FirebaseMessagesController() - // .sendNotificationToPassengerToken( - // 'Order Under Review'.tr, - // '${box.read(BoxName.nameDriver)} ${'is reviewing your order. They may need more information or a higher price.'.tr}', - // controller.myList[9].toString(), - // bodyToPassenger, - // 'notification'); - NotificationService.sendNotification( - target: controller.myList[9].toString(), - title: 'Order Under Review'.tr, - body: - '${box.read(BoxName.nameDriver)} ${'is reviewing your order. They may need more information or a higher price.'.tr}', - isTopic: false, // Important: this is a token - tone: 'start', - driverList: [], category: 'Order Under Review', - ); + // NotificationService.sendNotification( + // target: controller.myList[9].toString(), + // title: 'Order Under Review'.tr, + // body: + // '${box.read(BoxName.nameDriver)} ${'is reviewing your order. They may need more information or a higher price.'.tr}', + // isTopic: false, // Important: this is a token + // tone: 'start', + // driverList: bodyToPassenger, + // category: 'Order Under Review', + // ); - controller.refuseOrder( - EncryptionHelper.instance.encryptData( - controller.myList[16].toString()), - ); + // controller.refuseOrder( + // (controller.myList[16].toString()), + // ); controller.addRideToNotificationDriverString( controller.myList[16].toString(), controller.myList[29].toString(), diff --git a/lib/views/home/Captin/orderCaptin/order_speed_request.dart b/lib/views/home/Captin/orderCaptin/order_speed_request.dart index f12fd6b..d6932cd 100755 --- a/lib/views/home/Captin/orderCaptin/order_speed_request.dart +++ b/lib/views/home/Captin/orderCaptin/order_speed_request.dart @@ -1,21 +1,25 @@ -import 'dart:convert'; // Though not directly used in this version's UI logic, often kept for model interactions. +import 'dart:convert'; -import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart'; import 'package:sefer_driver/constant/box_name.dart'; import 'package:sefer_driver/controller/firebase/firbase_messge.dart'; -import 'package:sefer_driver/main.dart'; // For `box` +import 'package:sefer_driver/main.dart'; +import 'package:sefer_driver/print.dart'; import 'package:sefer_driver/views/home/Captin/driver_map_page.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; -import '../../../../constant/colors.dart'; // Your AppColor -import '../../../../constant/links.dart'; // Your AppLink -import '../../../../constant/style.dart'; // Your AppStyle + +import '../../../../constant/colors.dart'; +import '../../../../constant/links.dart'; +import '../../../../constant/style.dart'; import '../../../../controller/firebase/notification_service.dart'; import '../../../../controller/functions/crud.dart'; import '../../../../controller/functions/launch.dart'; import '../../../../controller/home/captin/order_request_controller.dart'; -import '../../../widgets/elevated_btn.dart'; // Your MyElevatedButton +import '../../../widgets/elevated_btn.dart'; +import '../../../widgets/mydialoug.dart'; class OrderSpeedRequest extends StatelessWidget { OrderSpeedRequest({super.key}); @@ -23,7 +27,7 @@ class OrderSpeedRequest extends StatelessWidget { final OrderRequestController orderRequestController = Get.put(OrderRequestController()); - // Helper to make myList access more readable and safer + // دالة مساعدة لاستخراج البيانات بأمان (Null Safety) String _getData(int index, {String defaultValue = ''}) { if (orderRequestController.myList.length > index && orderRequestController.myList[index] != null) { @@ -34,39 +38,9 @@ class OrderSpeedRequest extends StatelessWidget { @override Widget build(BuildContext context) { - // Define AppBar first to get its height for body calculations - final appBar = AppBar( - title: Text('Speed Order'.tr), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => Get.back(), - ), - backgroundColor: AppColor.primaryColor, // Example color, adjust as needed - elevation: 2.0, - ); - - final double appBarHeight = appBar.preferredSize.height; - final MediaQueryData mediaQueryData = MediaQuery.of(context); - final double screenHeight = mediaQueryData.size.height; - final double statusBarHeight = mediaQueryData.padding.top; - final double bottomSystemPadding = mediaQueryData.padding.bottom; - - // Calculate available height for the Scaffold's body content - // Subtracting status bar, app bar, and bottom system padding (like navigation bar) - final double availableBodyHeight = - screenHeight - appBarHeight - statusBarHeight - bottomSystemPadding; - - // Define overall padding for the body content - const EdgeInsets bodyContentPadding = - EdgeInsets.symmetric(horizontal: 10.0, vertical: 8.0); - - // Calculate the height for the main content Column, considering the body's own padding - double mainColumnHeight = availableBodyHeight - bodyContentPadding.vertical; - if (mainColumnHeight < 0) mainColumnHeight = 0; - return GetBuilder( builder: (controller) { - // Pre-extract data for readability and safety + // --- استخراج البيانات بشكل نظيف --- final String price = double.tryParse(_getData(2))?.toStringAsFixed(2) ?? 'N/A'; final bool isComfortTrip = _getData(31) == 'Comfort'; @@ -75,484 +49,185 @@ class OrderSpeedRequest extends StatelessWidget { final String pickupName = _getData(12); final String pickupDetails = '(${_getData(11)})'; final String pickupFullAddress = _getData(29); - final String dropoffName = _getData(5); final String dropoffDetails = '(${_getData(4)})'; final String dropoffFullAddress = _getData(30); final String passengerName = _getData(8); final String passengerRating = _getData(33); - final bool isVisaPayment = _getData(13) == 'true'; final bool hasSteps = _getData(20) == 'haveSteps'; final String mapUrl = 'https://www.google.com/maps/dir/${_getData(0)}/${_getData(1)}/'; - final String rideId = _getData(16); // Commonly used ID + final String rideId = _getData(16); return Scaffold( - appBar: appBar, - backgroundColor: AppColor.secondaryColor ?? - Colors.grey[100], // Background for the page + appBar: AppBar( + title: Text('Speed Order'.tr), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Get.back(), + ), + backgroundColor: AppColor.primaryColor, + elevation: 2.0, + ), + backgroundColor: AppColor.secondaryColor ?? Colors.grey[100], body: SafeArea( - // Ensures content is not obscured by system UI child: Padding( padding: - bodyContentPadding, // Apply overall padding to the body's content area - child: SizedBox( - // Constrain the height of the main layout Column - height: mainColumnHeight, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // --- MAP SECTION --- - SizedBox( - height: - Get.height * 0.28, // Relative to total screen height - child: ClipRRect( - borderRadius: BorderRadius.circular(15.0), - child: GoogleMap( - initialCameraPosition: CameraPosition( - zoom: 12, - target: - Get.find().myLocation), - cameraTargetBounds: - CameraTargetBounds(controller.bounds), - myLocationButtonEnabled: false, - trafficEnabled: false, - buildingsEnabled: false, - mapToolbarEnabled: false, - myLocationEnabled: true, - markers: { - Marker( - markerId: MarkerId('MyLocation'.tr), - position: LatLng( - controller.latPassengerLocation, - controller.lngPassengerLocation), - icon: controller.startIcon), - Marker( - markerId: MarkerId('Destination'.tr), - position: LatLng( - controller.latPassengerDestination, - controller.lngPassengerDestination), - icon: controller.endIcon), - }, - polylines: { - Polyline( - zIndex: 1, - consumeTapEvents: true, - geodesic: true, - endCap: Cap.buttCap, - startCap: Cap.buttCap, - visible: true, - polylineId: const PolylineId('routeOrder'), - points: controller.pointsDirection, - color: AppColor.primaryColor, - width: 3, - ), - }, + const EdgeInsets.symmetric(horizontal: 10.0, vertical: 8.0), + child: Column( + children: [ + // 1. قسم الخريطة (ارتفاع ثابت) + SizedBox( + height: Get.height * 0.28, + child: ClipRRect( + borderRadius: BorderRadius.circular(15.0), + child: GoogleMap( + initialCameraPosition: CameraPosition( + zoom: 12, + target: Get.find().myLocation, ), + cameraTargetBounds: + CameraTargetBounds(controller.bounds), + myLocationButtonEnabled: false, + trafficEnabled: false, + buildingsEnabled: false, + mapToolbarEnabled: false, + myLocationEnabled: true, + markers: { + Marker( + markerId: MarkerId('MyLocation'.tr), + position: LatLng(controller.latPassengerLocation, + controller.lngPassengerLocation), + icon: controller.startIcon), + Marker( + markerId: MarkerId('Destination'.tr), + position: LatLng( + controller.latPassengerDestination, + controller.lngPassengerDestination), + icon: controller.endIcon), + }, + polylines: { + Polyline( + zIndex: 1, + consumeTapEvents: true, + geodesic: true, + endCap: Cap.buttCap, + startCap: Cap.buttCap, + visible: true, + polylineId: const PolylineId('routeOrder'), + points: controller.pointsDirection, + color: AppColor.primaryColor, + width: 3, + ), + }, ), ), - const SizedBox(height: 8), + ), - // --- PRICE & TRIP TYPE SECTION --- - Card( - elevation: 3, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - price, - style: AppStyle.headTitle.copyWith( - color: AppColor.primaryColor, - fontWeight: FontWeight.bold, - fontSize: 28), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - carType, - style: AppStyle.title.copyWith( - color: AppColor.greenColor, - fontWeight: FontWeight.bold), - ), - if (isComfortTrip) - Row( - children: [ - const Icon(Icons.ac_unit, - color: AppColor.blueColor, size: 18), - const SizedBox(width: 4), - Text('Air condition Trip'.tr, - style: AppStyle.subtitle - .copyWith(fontSize: 13)), - ], - ), - ], - ), - ], - ), - ), - ), - const SizedBox(height: 8), + const SizedBox(height: 8), - // --- EXPANDED SECTION FOR SCROLLABLE (BUT NOT USER-SCROLLABLE) CONTENT --- - Expanded( - child: SingleChildScrollView( - physics: - const NeverScrollableScrollPhysics(), // Prevents user scrolling - child: Column( - mainAxisSize: MainAxisSize - .min, // Takes minimum vertical space needed - children: [ - _buildLocationCard( - icon: Icons.arrow_circle_up, - iconColor: AppColor.greenColor, - title: pickupName, - subtitle: pickupDetails, - fullAddress: pickupFullAddress, - ), - const SizedBox(height: 8), - _buildLocationCard( - icon: Icons.arrow_circle_down, - iconColor: AppColor.redColor, - title: dropoffName, - subtitle: dropoffDetails, - fullAddress: dropoffFullAddress, - ), - const SizedBox(height: 8), - // --- PAYMENT, STEPS & DIRECTIONS INFO --- - Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon( - isVisaPayment - ? Icons.credit_card - : Icons - .payments_outlined, // Using payments_outlined for cash - color: isVisaPayment - ? AppColor.deepPurpleAccent - : AppColor.greenColor, - size: 24, - ), - const SizedBox(width: 8), - Text( - isVisaPayment ? 'Visa'.tr : 'Cash'.tr, - style: AppStyle.title.copyWith( - fontWeight: FontWeight.w600), - ), - ], - ), - if (hasSteps) - Expanded( - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Icon( - Icons - .format_list_numbered_rtl_outlined, - color: AppColor.bronze, - size: 24), - const SizedBox(width: 4), - Flexible( - child: Text( - 'Trip has Steps'.tr, - style: AppStyle.title.copyWith( - color: AppColor.bronze, - fontSize: 13), - overflow: TextOverflow.ellipsis, - )), - ], - ), - ), - TextButton.icon( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - tapTargetSize: - MaterialTapTargetSize.shrinkWrap, - alignment: Alignment.centerRight, - ), - onPressed: () => showInBrowser(mapUrl), - icon: const Icon( - Icons.directions_outlined, - color: AppColor.blueColor, - size: 20), - label: Text("Directions".tr, - style: AppStyle.subtitle.copyWith( - color: AppColor.blueColor, - fontSize: 13)), - ), - ], - ), - ), - ), - const SizedBox(height: 8), - // --- PASSENGER INFO --- - Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, vertical: 10.0), - child: Row( - children: [ - const Icon(Icons.person_outline, - color: AppColor.greyColor, size: 22), - const SizedBox(width: 10), - Expanded( - child: Text( - passengerName, - style: AppStyle.title, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 10), - const Icon(Icons.star_rounded, - color: Colors.amber, size: 20), - Text( - passengerRating, - style: AppStyle.title.copyWith( - fontWeight: FontWeight.bold), - ), - ], - ), - ), - ), - ], - ), - ), - ), - // const SizedBox(height: 8), // Spacer before action buttons if needed + // 2. بطاقة السعر + _buildPriceCard(price, carType, isComfortTrip), - // --- ACTION BUTTONS & TIMER --- - Padding( - padding: const EdgeInsets.only(top: 8.0, bottom: 5.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + const SizedBox(height: 8), + + // 3. التفاصيل القابلة للتمرير (تأخذ المساحة المتبقية) + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: MyElevatedButton( - kolor: AppColor.greenColor, - title: 'Accept Order'.tr, - onPressed: () async { - Get.put(HomeCaptainController()).changeRideId(); - box.write(BoxName.statusDriverLocation, 'on'); - var res = await CRUD().post( - link: AppLink.updateStausFromSpeed, - payload: { - 'id': rideId, - 'rideTimeStart': DateTime.now().toString(), - 'status': 'Apply', - 'driver_id': box.read(BoxName.driverID), - }, - ); - - CRUD().post( - link: - "${AppLink.server}/ride/rides/updateStausFromSpeed.php", - payload: { - 'id': rideId, - 'rideTimeStart': DateTime.now().toString(), - 'status': 'Apply', - 'driver_id': box.read(BoxName.driverID), - }, - ); - - if (res != "failure") { - box.write(BoxName.statusDriverLocation, 'on'); - controller.changeApplied(); - List bodyToPassenger = [ - box.read(BoxName.driverID).toString(), - box.read(BoxName.nameDriver).toString(), - box.read(BoxName.tokenDriver).toString(), - rideId.toString(), - ]; - // Get.put(FirebaseMessagesController()) - // .sendNotificationToDriverMAP( - // 'Accepted Ride', - // 'your ride is applied'.tr, - // controller.arguments?['DriverList'] - // ?[9] - // ?.toString() ?? - // _getData(9), // Safer access - // bodyToPassenger, - // 'start.wav'); - NotificationService.sendNotification( - target: controller.arguments?['DriverList'] - ?[9] - ?.toString() ?? - _getData(9), - title: 'Accepted Ride'.tr, - body: 'your ride is applied'.tr, - isTopic: - false, // Important: this is a token - tone: 'start', - driverList: [], category: 'Accepted Ride', - ); - - // Using rideId (_getData(16)) for order_id consistently - CRUD().postFromDialogue( - link: AppLink.addDriverOrder, - payload: { - 'driver_id': _getData( - 6), // Driver ID from the order data - 'order_id': rideId, - 'status': 'Apply' - }); - - CRUD().post( - link: - "${AppLink.rideServer}/driver_order/add.php", - payload: { - 'driver_id': _getData(6), - 'order_id': rideId, - 'status': 'Apply' - }); - - Get.back(); // Go back from order request screen - box.write(BoxName.rideArguments, { - 'passengerLocation': _getData(0), - 'passengerDestination': _getData(1), - 'Duration': _getData(4), - 'totalCost': _getData(26), - 'Distance': _getData(5), - 'name': _getData(8), - 'phone': _getData(10), - 'email': _getData(28), - 'WalletChecked': _getData(13), - 'tokenPassenger': _getData(9), - 'direction': mapUrl, - 'DurationToPassenger': _getData(15), - 'rideId': rideId, - 'passengerId': _getData(7), - 'driverId': box - .read(BoxName.driverID) - .toString(), // Current driver accepting - 'durationOfRideValue': _getData(19), - 'paymentAmount': _getData(2), - 'paymentMethod': _getData(13) == 'true' - ? 'visa' - : 'cash', - 'isHaveSteps': _getData(20), - 'step0': _getData(21), - 'step1': _getData(22), - 'step2': _getData(23), - 'step3': _getData(24), - 'step4': _getData(25), - 'passengerWalletBurc': _getData(26), - 'timeOfOrder': DateTime.now().toString(), - 'totalPassenger': _getData( - 2), // This is likely trip cost for passenger - 'carType': _getData(31), - 'kazan': - _getData(32), // Driver's commission/cut - 'startNameLocation': _getData(29), - 'endNameLocation': _getData(30), - }); - Get.to(() => PassengerLocationMapPage(), - arguments: - box.read(BoxName.rideArguments)); - } else { - Get.defaultDialog( - title: - "This ride is already taken by another driver." - .tr, - middleText: '', - titleStyle: AppStyle.title, - middleTextStyle: AppStyle.title, - confirm: MyElevatedButton( - title: 'Ok'.tr, - onPressed: () { - Get.back(); // Close dialog - Get.back(); // Close order request screen - })); - } - }, - ), + _buildLocationCard( + icon: Icons.arrow_circle_up, + iconColor: AppColor.greenColor, + title: pickupName, + subtitle: pickupDetails, + fullAddress: pickupFullAddress, ), - const SizedBox(width: 10), - // --- TIMER --- - GetBuilder( - id: 'timerUpdate', // Ensure controller calls update(['timerUpdate']) for this - builder: (timerCtrl) { - final isNearEnd = - timerCtrl.remainingTimeSpeed <= 5; - return SizedBox( - width: 60, - height: 60, - child: Stack( - alignment: Alignment.center, - children: [ - CircularProgressIndicator( - value: timerCtrl.progressSpeed, - color: isNearEnd - ? Colors.redAccent - : AppColor.primaryColor, - strokeWidth: 5, - backgroundColor: Colors.grey.shade300, - ), - Text('${timerCtrl.remainingTimeSpeed}', - style: AppStyle.headTitle2.copyWith( - color: isNearEnd - ? Colors.redAccent - : AppColor.writeColor ?? - Colors.black, - )), - ], - ), - ); - }, - ), - const SizedBox(width: 10), - Expanded( - child: MyElevatedButton( - title: 'Refuse Order'.tr, - onPressed: () async { - controller.endTimer(); - controller.refuseOrder(rideId); - controller.addRideToNotificationDriverString( - rideId, - _getData(29), - _getData(30), - '${DateTime.now().year}-${DateTime.now().month}-${DateTime.now().day}', - '${DateTime.now().hour}:${DateTime.now().minute}', - _getData(2), - _getData(7), - 'wait', - _getData(31), - _getData(33), - _getData(2), - _getData(5), - _getData(4)); - // Get.back(); // refuseOrder or endTimer should handle navigation if needed. - // If not, add Get.back(); here. - }, - kolor: AppColor.redColor, - ), + const SizedBox(height: 8), + _buildLocationCard( + icon: Icons.arrow_circle_down, + iconColor: AppColor.redColor, + title: dropoffName, + subtitle: dropoffDetails, + fullAddress: dropoffFullAddress, ), + const SizedBox(height: 8), + _buildInfoCard(isVisaPayment, hasSteps, mapUrl), + const SizedBox(height: 8), + _buildPassengerCard(passengerName, passengerRating), ], ), ), - ], - ), + ), + + // 4. الأزرار والمؤقت (مثبتة في الأسفل) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // زر القبول + Expanded( + child: MyElevatedButton( + kolor: AppColor.greenColor, + title: 'Accept Order'.tr, + onPressed: () => _handleAcceptOrder(controller), + ), + ), + const SizedBox(width: 10), + + // المؤقت + GetBuilder( + id: 'timerUpdate', + builder: (timerCtrl) { + final isNearEnd = timerCtrl.remainingTimeSpeed <= 5; + return SizedBox( + width: 60, + height: 60, + child: Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator( + value: timerCtrl.progressSpeed, + color: isNearEnd + ? Colors.redAccent + : AppColor.primaryColor, + strokeWidth: 5, + backgroundColor: Colors.grey.shade300, + ), + Text( + '${timerCtrl.remainingTimeSpeed}', + style: AppStyle.headTitle2.copyWith( + color: isNearEnd + ? Colors.redAccent + : AppColor.writeColor ?? Colors.black, + ), + ), + ], + ), + ); + }, + ), + + const SizedBox(width: 10), + // زر الرفض + Expanded( + child: MyElevatedButton( + title: 'Refuse Order'.tr, + onPressed: () => + _handleRefuseOrder(controller, rideId), + kolor: AppColor.redColor, + ), + ), + ], + ), + ), + ], ), ), ), @@ -561,7 +236,142 @@ class OrderSpeedRequest extends StatelessWidget { ); } - // Helper widget for location cards to reduce repetition and improve readability + // --- WIDGET BUILDERS (لبناء الواجهة بشكل نظيف) --- + + Widget _buildPriceCard(String price, String carType, bool isComfortTrip) { + return Card( + elevation: 3, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + price, + style: AppStyle.headTitle.copyWith( + color: AppColor.primaryColor, + fontWeight: FontWeight.bold, + fontSize: 28), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + carType, + style: AppStyle.title.copyWith( + color: AppColor.greenColor, fontWeight: FontWeight.bold), + ), + if (isComfortTrip) + Row( + children: [ + const Icon(Icons.ac_unit, + color: AppColor.blueColor, size: 18), + const SizedBox(width: 4), + Text('Air condition Trip'.tr, + style: AppStyle.subtitle.copyWith(fontSize: 13)), + ], + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildInfoCard(bool isVisaPayment, bool hasSteps, String mapUrl) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + isVisaPayment ? Icons.credit_card : Icons.payments_outlined, + color: isVisaPayment + ? AppColor.deepPurpleAccent + : AppColor.greenColor, + size: 24, + ), + const SizedBox(width: 8), + Text( + isVisaPayment ? 'Visa'.tr : 'Cash'.tr, + style: AppStyle.title.copyWith(fontWeight: FontWeight.w600), + ), + ], + ), + if (hasSteps) + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.format_list_numbered_rtl_outlined, + color: AppColor.bronze, size: 24), + const SizedBox(width: 4), + Flexible( + child: Text( + 'Trip has Steps'.tr, + style: AppStyle.title + .copyWith(color: AppColor.bronze, fontSize: 13), + overflow: TextOverflow.ellipsis, + )), + ], + ), + ), + TextButton.icon( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + alignment: Alignment.centerRight, + ), + onPressed: () => showInBrowser(mapUrl), + icon: const Icon(Icons.directions_outlined, + color: AppColor.blueColor, size: 20), + label: Text("Directions".tr, + style: AppStyle.subtitle + .copyWith(color: AppColor.blueColor, fontSize: 13)), + ), + ], + ), + ), + ); + } + + Widget _buildPassengerCard(String name, String rating) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), + child: Row( + children: [ + const Icon(Icons.person_outline, + color: AppColor.greyColor, size: 22), + const SizedBox(width: 10), + Expanded( + child: Text( + name, + style: AppStyle.title, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 10), + const Icon(Icons.star_rounded, color: Colors.amber, size: 20), + Text( + rating, + style: AppStyle.title.copyWith(fontWeight: FontWeight.bold), + ), + ], + ), + ), + ); + } + Widget _buildLocationCard( {required IconData icon, required Color iconColor, @@ -571,8 +381,7 @@ class OrderSpeedRequest extends StatelessWidget { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - margin: const EdgeInsets.symmetric( - vertical: 4), // Add a little vertical margin between cards + margin: const EdgeInsets.symmetric(vertical: 4), child: Padding( padding: const EdgeInsets.all(10.0), child: Row( @@ -585,8 +394,7 @@ class OrderSpeedRequest extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "$title $subtitle" - .trim(), // Trim to avoid extra spaces if subtitle is empty + "$title $subtitle".trim(), style: AppStyle.title.copyWith(fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -597,7 +405,7 @@ class OrderSpeedRequest extends StatelessWidget { fullAddress, style: AppStyle.subtitle .copyWith(fontSize: 13, color: AppColor.greyColor), - maxLines: 2, // Allow up to 2 lines for address + maxLines: 2, overflow: TextOverflow.ellipsis, ), ] @@ -609,4 +417,142 @@ class OrderSpeedRequest extends StatelessWidget { ), ); } + + // --- منطق التعامل مع الطلبات (Logic Handlers) --- + + Future _handleAcceptOrder(OrderRequestController controller) async { + // 1. محاولة تحديث الحالة في السيرفر + // 1. إظهار لودينج وإيقاف التفاعل + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + // هذا الرابط يجب أن يكون لملف PHP الآمن الذي يحتوي على rowCount + var res = await CRUD().post(link: AppLink.updateStausFromSpeed, payload: { + 'id': (controller.myList[16]), + 'rideTimeStart': DateTime.now().toString(), + 'status': 'Apply', + 'driver_id': box.read(BoxName.driverID), + }); + Log.print('oreder response update: ${res}'); +// 2. إغلاق اللودينج بمجرد وصول الرد + Get.back(); // إغلاق اللودينج + // 2. معالجة الفشل (Robust Error Handling) + // نفحص إذا كانت النتيجة فشل سواء وصلت كنص أو كـ JSON + bool isFailed = false; + if (res == 'failure') isFailed = true; + + if (res is Map && res['status'] == 'failure') isFailed = true; + + if (isFailed) { + MyDialog().getDialog( + "This ride is already applied by another driver.".tr, '', () { + Get.back(); // يغلق نافذة التنبيه (Dialog) + Get.back(); // يغلق صفحة الطلب بالكامل (Screen) ويرجع للخريطة + }); + return; // توقف تام للكود هنا، لن يتم تنفيذ أي سطر بالأسفل + } + + // 3. معالجة النجاح (Success Handling) + + // إيقاف المؤقت وتحديث الواجهة + controller.endTimer(); + controller.changeApplied(); + + // تحديث حالة السائق في التطبيق + Get.put(HomeCaptainController()).changeRideId(); + box.write(BoxName.statusDriverLocation, 'on'); + + // *هام*: تم حذف استدعاء الـ API الثاني المكرر هنا لأنه غير ضروري وقد يسبب مشاكل + + // تسجيل الطلب في سجل السائقين (Driver History) + CRUD().postFromDialogue(link: AppLink.addDriverOrder, payload: { + 'driver_id': (controller.myList[6].toString()), + 'order_id': (controller.myList[16].toString()), + 'status': 'Apply' + }); + + // إرسال إشعار للراكب + List bodyToPassenger = [ + controller.myList[6].toString(), + controller.myList[8].toString(), + controller.myList[9].toString(), + ]; + + NotificationService.sendNotification( + target: controller.myList[9].toString(), + title: "Accepted Ride".tr, + body: 'your ride is Accepted'.tr, + isTopic: false, + tone: 'start', + driverList: bodyToPassenger, + category: 'Accepted Ride', + ); + + // حفظ البيانات في الصندوق (Box) للانتقال للصفحة التالية + box.write(BoxName.rideArguments, { + 'passengerLocation': controller.myList[0].toString(), + 'passengerDestination': controller.myList[1].toString(), + 'Duration': controller.myList[4].toString(), + 'totalCost': controller.myList[26].toString(), + 'Distance': controller.myList[5].toString(), + 'name': controller.myList[8].toString(), + 'phone': controller.myList[10].toString(), + 'email': controller.myList[28].toString(), + 'WalletChecked': controller.myList[13].toString(), + 'tokenPassenger': controller.myList[9].toString(), + 'direction': + 'https://www.google.com/maps/dir/${controller.myList[0]}/${controller.myList[1]}/', + 'DurationToPassenger': controller.myList[15].toString(), + 'rideId': (controller.myList[16].toString()), + 'passengerId': (controller.myList[7].toString()), + 'driverId': (controller.myList[18].toString()), + 'durationOfRideValue': controller.myList[19].toString(), + 'paymentAmount': controller.myList[2].toString(), + 'paymentMethod': + controller.myList[13].toString() == 'true' ? 'visa' : 'cash', + 'isHaveSteps': controller.myList[20].toString(), + 'step0': controller.myList[21].toString(), + 'step1': controller.myList[22].toString(), + 'step2': controller.myList[23].toString(), + 'step3': controller.myList[24].toString(), + 'step4': controller.myList[25].toString(), + 'passengerWalletBurc': controller.myList[26].toString(), + 'timeOfOrder': DateTime.now().toString(), + 'totalPassenger': controller.myList[2].toString(), + 'carType': controller.myList[31].toString(), + 'kazan': controller.myList[32].toString(), + 'startNameLocation': controller.myList[29].toString(), + 'endNameLocation': controller.myList[30].toString(), + }); + + // الانتقال لصفحة تتبع الراكب + Get.back(); // يغلق صفحة الطلب الحالية + Get.to(() => PassengerLocationMapPage(), + arguments: box.read(BoxName.rideArguments)); + Log.print( + 'box.read(BoxName.rideArguments): ${box.read(BoxName.rideArguments)}'); + } + + void _handleRefuseOrder(OrderRequestController controller, String rideId) { + controller.endTimer(); + // controller.refuseOrder(rideId); + + // تسجيل الرفض في الإشعارات المحلية للسائق + controller.addRideToNotificationDriverString( + rideId, + _getData(29), + _getData(30), + '${DateTime.now().year}-${DateTime.now().month}-${DateTime.now().day}', + '${DateTime.now().hour}:${DateTime.now().minute}', + _getData(2), + _getData(7), + 'wait', + _getData(31), + _getData(33), + _getData(2), + _getData(5), + _getData(4)); + + // الخروج من الصفحة بعد الرفض + Get.back(); + } } diff --git a/lib/views/home/my_wallet/points_captain.dart b/lib/views/home/my_wallet/points_captain.dart index 0a5f24e..0d32d70 100755 --- a/lib/views/home/my_wallet/points_captain.dart +++ b/lib/views/home/my_wallet/points_captain.dart @@ -66,48 +66,48 @@ class PointsCaptain extends StatelessWidget { color: AppColor.blueColor, size: 70), ], )), - GestureDetector( - onTap: () async { - Get.back(); - Get.defaultDialog( - barrierDismissible: false, - title: 'Insert Wallet phone number'.tr, - content: Form( - key: paymentController.formKey, - child: MyTextForm( - controller: - paymentController.walletphoneController, - label: 'Insert Wallet phone number'.tr, - hint: '963941234567', - type: TextInputType.phone)), - confirm: MyElevatedButton( - title: 'OK'.tr, - onPressed: () async { - Get.back(); - if (paymentController.formKey.currentState! - .validate()) { - box.write( - BoxName.phoneWallet, - paymentController - .walletphoneController.text); - await payWithMTNWallet( - context, pricePoint.toString(), 'SYP'); - } - })); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Pay by MTN Wallet'.tr), - const SizedBox(width: 10), - Image.asset( - 'assets/images/cashMTN.png', - width: 70, - height: 70, - fit: BoxFit.fill, - ), - ], - )), + // GestureDetector( + // onTap: () async { + // Get.back(); + // Get.defaultDialog( + // barrierDismissible: false, + // title: 'Insert Wallet phone number'.tr, + // content: Form( + // key: paymentController.formKey, + // child: MyTextForm( + // controller: + // paymentController.walletphoneController, + // label: 'Insert Wallet phone number'.tr, + // hint: '963941234567', + // type: TextInputType.phone)), + // confirm: MyElevatedButton( + // title: 'OK'.tr, + // onPressed: () async { + // Get.back(); + // if (paymentController.formKey.currentState! + // .validate()) { + // box.write( + // BoxName.phoneWallet, + // paymentController + // .walletphoneController.text); + // await payWithMTNWallet( + // context, pricePoint.toString(), 'SYP'); + // } + // })); + // }, + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text('Pay by MTN Wallet'.tr), + // const SizedBox(width: 10), + // Image.asset( + // 'assets/images/cashMTN.png', + // width: 70, + // height: 70, + // fit: BoxFit.fill, + // ), + // ], + // )), GestureDetector( onTap: () async { @@ -153,51 +153,26 @@ class PointsCaptain extends StatelessWidget { )), GestureDetector( onTap: () async { - Get.back(); - Get.defaultDialog( - barrierDismissible: false, - title: 'Insert Wallet phone number'.tr, - content: Form( - key: paymentController.formKey, - child: MyTextForm( - controller: - paymentController.walletphoneController, - label: 'Insert Wallet phone number'.tr, - hint: '963941234567', - type: TextInputType.phone)), - confirm: MyElevatedButton( - title: 'OK'.tr, - onPressed: () async { - Get.back(); - if (paymentController.formKey.currentState! - .validate()) { - box.write( - BoxName.phoneWallet, - paymentController - .walletphoneController.text); - // await payWithSyriaTelWallet( - // context, pricePoint.toString(), 'SYP'); - bool isAuthSupported = - await LocalAuthentication() - .isDeviceSupported(); - if (isAuthSupported) { - bool didAuthenticate = - await LocalAuthentication() - .authenticate( - localizedReason: - 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع', - ); - if (!didAuthenticate) { - if (Get.isDialogOpen ?? false) Get.back(); - print( - "❌ User did not authenticate with biometrics"); - return; - } - } - Get.to(() => PaymentScreenSmsProvider( - amount: pricePoint)); - } - })); + // التحقق بالبصمة قبل أي شيء + bool isAuthSupported = + await LocalAuthentication().isDeviceSupported(); + + if (isAuthSupported) { + bool didAuthenticate = + await LocalAuthentication().authenticate( + localizedReason: + 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع', + ); + + if (!didAuthenticate) { + print("❌ User did not authenticate with biometrics"); + return; + } + } + + // الانتقال مباشرة لإنشاء الفاتورة بعد النجاح بالبصمة + Get.to( + () => PaymentScreenSmsProvider(amount: pricePoint)); }, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/views/home/my_wallet/walet_captain.dart b/lib/views/home/my_wallet/walet_captain.dart index e92f5cc..b44dfd4 100755 --- a/lib/views/home/my_wallet/walet_captain.dart +++ b/lib/views/home/my_wallet/walet_captain.dart @@ -113,9 +113,11 @@ class WalletCaptainRefactored extends StatelessWidget { child: Column( children: [ Text( - '${'Total Points is'.tr} 💎', - style: AppStyle.headTitle2 - .copyWith(color: Colors.white, fontWeight: FontWeight.bold), + 'رصيد التشغيل 💎', + style: AppStyle.headTitle2.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ), textAlign: TextAlign.center, ), const SizedBox(height: 8), diff --git a/lib/views/notification/available_rides_page.dart b/lib/views/notification/available_rides_page.dart index 981edb9..e66f8bc 100755 --- a/lib/views/notification/available_rides_page.dart +++ b/lib/views/notification/available_rides_page.dart @@ -283,22 +283,14 @@ class RideAvailableCard extends StatelessWidget { // --- Ride Acceptance Logic --- // This logic is copied exactly from your original code. void _acceptRide() async { - var res = await CRUD().post(link: AppLink.updateStausFromSpeed, payload: { - 'id': rideInfo['id'], - 'rideTimeStart': DateTime.now().toString(), - 'status': 'Apply', - 'driver_id': box.read(BoxName.driverID), - }); - if (AppLink.endPoint.toString() != AppLink.seferCairoServer) { - CRUD().post( - link: '${AppLink.endPoint}rides/updateStausFromSpeed.php', - payload: { - 'id': rideInfo['id'], - 'rideTimeStart': DateTime.now().toString(), - 'status': 'Apply', - 'driver_id': box.read(BoxName.driverID), - }); - } + var res = await CRUD().post( + link: '${AppLink.endPoint}rides/updateStausFromSpeed.php', + payload: { + 'id': rideInfo['id'], + 'rideTimeStart': DateTime.now().toString(), + 'status': 'Apply', + 'driver_id': box.read(BoxName.driverID), + }); if (res != "failure") { List bodyToPassenger = [ @@ -312,46 +304,23 @@ class RideAvailableCard extends StatelessWidget { 'order_id': rideInfo['id'], 'status': 'Apply' }); - await CRUD().post(link: AppLink.updateRides, payload: { - 'id': rideInfo['id'], - 'DriverIsGoingToPassenger': DateTime.now().toString(), - 'status': 'Applied' - }); + // await CRUD().post(link: AppLink.updateRides, payload: { + // 'id': rideInfo['id'], + // 'DriverIsGoingToPassenger': DateTime.now().toString(), + // 'status': 'Applied' + // }); await CRUD().post( link: AppLink.updateWaitingRide, payload: {'id': rideInfo['id'], 'status': 'Applied'}); - if (AppLink.endPoint.toString() != AppLink.seferCairoServer) { - CRUD().postFromDialogue( - link: '${AppLink.endPoint}/driver_order/add.php', - payload: { - 'driver_id': box.read(BoxName.driverID), - 'order_id': rideInfo['id'], - 'status': 'Apply' - }); - CRUD().post(link: '${AppLink.endPoint}/rides/update.php', payload: { - 'id': rideInfo['id'], - 'DriverIsGoingToPassenger': DateTime.now().toString(), - 'status': 'Applied' - }); - CRUD().post( - link: - "${AppLink.endPoint}/ride/notificationCaptain/updateWaitingTrip.php", - payload: {'id': rideInfo['id'], 'status': 'Applied'}); - } + // if (AppLink.endPoint.toString() != AppLink.seferCairoServer) { - // FirebaseMessagesController().sendNotificationToPassengerToken( - // "Accepted Ride".tr, - // 'your ride is Accepted'.tr, - // rideInfo['passengerToken'].toString(), - // bodyToPassenger, - // 'start.wav'); NotificationService.sendNotification( target: rideInfo['passengerToken'].toString(), title: 'Accepted Ride'.tr, body: 'your ride is Accepted'.tr, isTopic: false, // Important: this is a token tone: 'start', - driverList: [], category: 'Accepted Ride', + driverList: bodyToPassenger, category: 'Accepted Ride', ); Get.back(); Get.to(() => PassengerLocationMapPage(), arguments: { @@ -385,18 +354,15 @@ class RideAvailableCard extends StatelessWidget { 'totalPassenger': rideInfo['price'].toString(), 'carType': rideInfo['carType'].toString(), 'kazan': Get.find().kazan.toString(), + 'startNameLocation': rideInfo['startName'].toString(), + 'endNameLocation': rideInfo['endName'].toString(), }); } else { MyDialog().getDialog( "This ride is already taken by another driver.".tr, '', () { CRUD().post( link: AppLink.deleteAvailableRide, payload: {'id': rideInfo['id']}); - if (AppLink.endPoint.toString() != AppLink.seferCairoServer) { - CRUD().post( - link: - '${AppLink.endPoint}/ride/notificationCaptain/deleteAvailableRide.php', - payload: {'id': rideInfo['id']}); - } + Get.back(); }); } diff --git a/lib/views/widgets/mydialoug.dart b/lib/views/widgets/mydialoug.dart index 016888c..e6f8976 100755 --- a/lib/views/widgets/mydialoug.dart +++ b/lib/views/widgets/mydialoug.dart @@ -52,7 +52,7 @@ class MyDialog extends GetxController { title: Column( children: [ Text( - title, + title.tr, style: AppStyle.title.copyWith( fontSize: 20, fontWeight: FontWeight.w700, diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 852fa1a..f9105be 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,5 +4,9 @@ com.apple.security.app-sandbox + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.network.client + diff --git a/pubspec.yaml b/pubspec.yaml index 5e9257d..cc3651d 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -144,6 +144,7 @@ flutter: - assets/ - assets/images/ - assets/fonts/ + - shorebird.yaml fonts: # - family: mohanad diff --git a/shorebird.yaml b/shorebird.yaml new file mode 100644 index 0000000..7fe5195 --- /dev/null +++ b/shorebird.yaml @@ -0,0 +1,14 @@ +# This file is used to configure the Shorebird updater used by your app. +# Learn more at https://docs.shorebird.dev +# This file does not contain any sensitive information and should be checked into version control. + +# Your app_id is the unique identifier assigned to your app. +# It is used to identify your app when requesting patches from Shorebird's servers. +# It is not a secret and can be shared publicly. +app_id: eceba4d4-b399-4a26-a9ab-25f6e328bf7d + +# auto_update controls if Shorebird should automatically update in the background on launch. +# If auto_update: false, you will need to use package:shorebird_code_push to trigger updates. +# https://pub.dev/packages/shorebird_code_push +# Uncomment the following line to disable automatic updates. +# auto_update: false