diff --git a/backend/ride/rides/acceptRide.php b/backend/ride/rides/acceptRide.php index 85c7832..487e2bc 100755 --- a/backend/ride/rides/acceptRide.php +++ b/backend/ride/rides/acceptRide.php @@ -121,6 +121,8 @@ try { c.color, c.color_hex, (SELECT ROUND(AVG(rating), 2) FROM ratingDriver WHERE driver_id = d.id) AS ratingDriver, + (SELECT COUNT(*) FROM ratingDriver WHERE driver_id = d.id) AS ratingCount, + (SELECT COUNT(*) FROM ride WHERE driver_id = d.id AND status IN ('Finished', 'finished')) AS completedRides, dt.token FROM driver d LEFT JOIN CarRegistration c ON c.driverID = d.id @@ -140,6 +142,16 @@ try { } $driverInfo['driverName'] = trim(($driverInfo['first_name'] ?? '') . ' ' . ($driverInfo['last_name'] ?? '')); $driverInfo['ratingDriver'] = $driverInfo['ratingDriver'] ?: "5.0"; + $ratingValue = (float) $driverInfo['ratingDriver']; + $ratingCount = (int) ($driverInfo['ratingCount'] ?? 0); + $completedRides = (int) ($driverInfo['completedRides'] ?? 0); + if ($ratingValue >= 4.8 && $ratingCount >= 50 && $completedRides >= 100) { + $driverInfo['driverTier'] = 'Professional driver'; + } elseif ($ratingValue >= 4.5 && $ratingCount >= 15 && $completedRides >= 30) { + $driverInfo['driverTier'] = 'Trusted driver'; + } else { + $driverInfo['driverTier'] = 'Verified driver'; + } } // ═══════════════════════════════════════════════════════════ @@ -195,4 +207,4 @@ try { } catch (PDOException $e) { error_log("[accept_ride] CRITICAL: " . $e->getMessage()); printFailure("Server error"); -} \ No newline at end of file +} diff --git a/backend/ride/rides/getRideOrderID.php b/backend/ride/rides/getRideOrderID.php index f8532ff..7bc828e 100755 --- a/backend/ride/rides/getRideOrderID.php +++ b/backend/ride/rides/getRideOrderID.php @@ -96,6 +96,17 @@ try { FROM ratingDriver WHERE ratingDriver.driver_id = :driverID_Sub ) AS ratingDriver, + ( + SELECT COUNT(*) + FROM ratingDriver + WHERE ratingDriver.driver_id = :driverID_Sub + ) AS ratingCount, + ( + SELECT COUNT(*) + FROM ride + WHERE ride.driver_id = :driverID_Sub + AND ride.status IN ('Finished', 'finished') + ) AS completedRides, driverToken.token AS token @@ -143,6 +154,16 @@ try { $finalData[$field] = $encryptionHelper->decryptData($finalData[$field]); } } + $ratingValue = (float) ($finalData['ratingDriver'] ?: 5.0); + $ratingCount = (int) ($finalData['ratingCount'] ?? 0); + $completedRides = (int) ($finalData['completedRides'] ?? 0); + if ($ratingValue >= 4.8 && $ratingCount >= 50 && $completedRides >= 100) { + $finalData['driverTier'] = 'Professional driver'; + } elseif ($ratingValue >= 4.5 && $ratingCount >= 15 && $completedRides >= 30) { + $finalData['driverTier'] = 'Trusted driver'; + } else { + $finalData['driverTier'] = 'Verified driver'; + } } echo json_encode([ @@ -155,4 +176,4 @@ try { http_response_code(500); echo json_encode(["status" => "failure", "message" => "Server Error: " . $e->getMessage()]); } -?> \ No newline at end of file +?> diff --git a/siro_driver/lib/controller/firebase/firbase_messge.dart b/siro_driver/lib/controller/firebase/firbase_messge.dart index 9de8eae..d183550 100755 --- a/siro_driver/lib/controller/firebase/firbase_messge.dart +++ b/siro_driver/lib/controller/firebase/firbase_messge.dart @@ -76,15 +76,22 @@ class FirebaseMessagesController extends GetxController { await fcmToken.subscribeToTopic("drivers"); // أو "users" حسب نوع المستخدم print("Subscribed to 'drivers' topic ✅"); - FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage? message) async { + FirebaseMessaging.instance + .getInitialMessage() + .then((RemoteMessage? message) async { if (message != null && message.data.isNotEmpty) { Log.print("🔔 FCM getInitialMessage payload: ${message.data}"); String? category = message.data['category'] ?? message.data['type']; - if (category == 'ORDER' || category == 'Order' || category == 'OrderVIP' || message.data.containsKey('DriverList')) { + if (category == 'ORDER' || + category == 'Order' || + category == 'OrderVIP' || + message.data.containsKey('DriverList')) { String? myListString = message.data['DriverList']; if (myListString != null && myListString.isNotEmpty) { - await storage.write(key: 'pending_driver_list', value: myListString); - Log.print("💾 Saved pending driver list to secure storage from getInitialMessage"); + await storage.write( + key: 'pending_driver_list', value: myListString); + Log.print( + "💾 Saved pending driver list to secure storage from getInitialMessage"); } } else { Future.delayed(const Duration(milliseconds: 1500), () { @@ -107,7 +114,6 @@ class FirebaseMessagesController extends GetxController { // fireBaseTitles(message); // } }); - FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {}); FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { if (message.data.isNotEmpty) { diff --git a/siro_driver/lib/controller/functions/background_service.dart b/siro_driver/lib/controller/functions/background_service.dart index e112b36..5d9bec9 100644 --- a/siro_driver/lib/controller/functions/background_service.dart +++ b/siro_driver/lib/controller/functions/background_service.dart @@ -9,7 +9,6 @@ import 'package:flutter_overlay_window/flutter_overlay_window.dart'; import 'package:socket_io_client/socket_io_client.dart' as IO; import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay; import 'package:get_storage/get_storage.dart'; -import 'package:geolocator/geolocator.dart' as geo; import '../../constant/box_name.dart'; import '../firebase/local_notification.dart'; @@ -129,40 +128,21 @@ Future onStart(ServiceInstance service) async { service.stopSelf(); }); - // 🔥 Location management in background isolate (Using Geolocator) - geo.Position? latestPos; - - // Listen to location changes continuously in the background - geo.Geolocator.getPositionStream( - locationSettings: geo.AndroidSettings( - accuracy: geo.LocationAccuracy.high, - distanceFilter: 10, - intervalDuration: const Duration(seconds: 10), - ), - ).listen((pos) { - latestPos = pos; - }); - - // 🔥 MERCY HEARTBEAT: Send location every 2 minutes to keep driver active in 'raids' - Timer.periodic(const Duration(minutes: 2), (timer) async { - if (socket != null && socket.connected && latestPos != null) { - try { - socket.emit('update_location', { - 'driver_id': driverId, - 'lat': latestPos!.latitude, - 'lng': latestPos!.longitude, - 'heading': latestPos!.heading, - 'speed': latestPos!.speed * 3.6, - 'status': box.read(BoxName.statusDriverLocation) ?? 'on', - 'source': 'background_heartbeat' - }); - print( - "💓 Background Mercy Heartbeat Sent: ${latestPos!.latitude}, ${latestPos!.longitude}"); - } catch (e) { - print("❌ Background Heartbeat Error: $e"); - } - } - }); + // 🚫 [Architecture Rule] NO redundant GPS stream in background service! + // LocationController is the SINGLE SOURCE OF TRUTH for all location GPS updates. + // It already uses location.enableBackgroundMode(enable: true) to keep the GPS + // stream alive even when the app is in the background. The main socket in + // LocationController handles all emitLocationToSocket() calls including heartbeat. + // + // The background service is ONLY responsible for: + // 1. Keeping the socket connection alive for receiving 'new_ride_request' + // and 'cancel_ride' events while the main isolate is paused on Android. + // 2. Showing the Android Overlay UI for incoming ride requests. + // 3. Notifications for iOS background state. + // + // Location data is not sent from the background isolate — it would conflict + // with LocationController's stream and cause duplicate GPS listeners, + // battery drain, and device freeze (as documented in driver_lifecycle.md). Timer.periodic(const Duration(seconds: 30), (timer) async { if (service is AndroidServiceInstance) { diff --git a/siro_driver/lib/controller/functions/location_controller.dart b/siro_driver/lib/controller/functions/location_controller.dart index 3367bd8..fc15ef0 100755 --- a/siro_driver/lib/controller/functions/location_controller.dart +++ b/siro_driver/lib/controller/functions/location_controller.dart @@ -19,6 +19,7 @@ import '../firebase/local_notification.dart'; import '../home/captin/home_captain_controller.dart'; import '../home/captin/map_driver_controller.dart'; import '../home/payment/captain_wallet_controller.dart'; +import '../home/navigation/navigation_controller.dart'; import 'background_service.dart'; import 'crud.dart'; @@ -539,6 +540,16 @@ class LocationController extends GetxController with WidgetsBindingObserver { } } + if (Get.isRegistered()) { + final mapCtrl = Get.find(); + mapCtrl.handleLocationUpdateFromCentral(pos, speed, heading); + } + + if (Get.isRegistered()) { + final navCtrl = Get.find(); + navCtrl.handleLocationUpdateFromCentral(pos, speed, heading); + } + await _saveBehaviorIfMoved(pos, now, currentSpeed: speed); }, onError: (e) => Log.print('❌ Location Stream Error: $e')); } diff --git a/siro_driver/lib/controller/home/captin/map_driver_controller.dart b/siro_driver/lib/controller/home/captin/map_driver_controller.dart index 8aa56ae..3baad9e 100755 --- a/siro_driver/lib/controller/home/captin/map_driver_controller.dart +++ b/siro_driver/lib/controller/home/captin/map_driver_controller.dart @@ -2570,27 +2570,19 @@ class MapDriverController extends GetxController } void _startLocationListening() { - _locationSubscription?.cancel(); - _locationSubscription = geo.Geolocator.getPositionStream( - locationSettings: const geo.LocationSettings( - accuracy: geo.LocationAccuracy.bestForNavigation, - distanceFilter: 2, - ), - ).listen((geo.Position pos) { - _handleLocationUpdate(pos); - }); + // Location stream is now centralized in LocationController to prevent device hanging. + // LocationController will call handleLocationUpdateFromCentral directly. } /// [Fix C-4] تحديث myLocation في المستمع الأساسي - void _handleLocationUpdate(geo.Position pos) { - final newLoc = LatLng(pos.latitude, pos.longitude); + void handleLocationUpdateFromCentral(LatLng newLoc, double posSpeed, double posHeading) { myLocation = newLoc; // ← [Fix C-4] تحديث الموقع الفوري _oldLoc = smoothedLocation ?? newLoc; _targetLoc = newLoc; _oldHeading = smoothedHeading; - if (pos.speed > 0.5) { - _targetHeading = pos.heading; + if (posSpeed > 0.5) { + _targetHeading = posHeading; } else { _targetHeading = _oldHeading; } diff --git a/siro_driver/lib/controller/home/captin/order_request_controller.dart b/siro_driver/lib/controller/home/captin/order_request_controller.dart index f58d4f4..169ca95 100755 --- a/siro_driver/lib/controller/home/captin/order_request_controller.dart +++ b/siro_driver/lib/controller/home/captin/order_request_controller.dart @@ -69,6 +69,7 @@ class OrderRequestController extends GetxController // --- الخريطة --- Set polylines = {}; + bool _hasCalculatedFullJourney = false; // حالة التطبيق والصوت bool isInBackground = false; @@ -219,6 +220,11 @@ class OrderRequestController extends GetxController // ---------------------------------------------------------------------- Future _calculateFullJourney() async { + if (_hasCalculatedFullJourney) { + if (mapController != null) zoomToFitRide(); + return; + } + _hasCalculatedFullJourney = true; // Don't block on mapController being null - we'll draw routes // and markers first, then zoom when controller is ready bool canZoom = mapController != null; @@ -281,7 +287,7 @@ class OrderRequestController extends GetxController totalTripDistance = tripResult['distance_text']; totalTripDuration = tripResult['duration_text']; polylines.add(tripResult['polyline']); - + // 🔥 تخزين استجابة السيرفر كاملة (بما فيها الـ points والـ instructions) if (tripResult['raw_response'] != null) { box.write('cached_trip_route', tripResult['raw_response']); diff --git a/siro_driver/lib/controller/home/navigation/navigation_controller.dart b/siro_driver/lib/controller/home/navigation/navigation_controller.dart index e607f6d..e2a4f08 100644 --- a/siro_driver/lib/controller/home/navigation/navigation_controller.dart +++ b/siro_driver/lib/controller/home/navigation/navigation_controller.dart @@ -476,32 +476,18 @@ class NavigationController extends GetxController } void _startLocationStream() { - _locationStreamSubscription?.cancel(); - // Listen to location updates with minimum distance filter of 2 meters - // This provides real-time updates without the 3-4 second delay - _locationStreamSubscription = Geolocator.getPositionStream( - locationSettings: const LocationSettings( - accuracy: LocationAccuracy.high, - distanceFilter: 2, // Update every 2 meters - ), - ).listen( - (Position position) { - _handleLocationUpdate(position); - }, - onError: (error) { - Log.print("DEBUG: Location stream error: $error"); - }, - ); + // Location stream is now centralized in LocationController to prevent device hanging. + // LocationController will call handleLocationUpdateFromCentral directly. } bool _isProcessing = false; - Future _handleLocationUpdate(Position position) async { + Future handleLocationUpdateFromCentral(LatLng newLoc, double locSpeed, double locHeading) async { if (_isProcessing) return; _isProcessing = true; try { - final newLoc = LatLng(position.latitude, position.longitude); - currentSpeed = position.speed * 3.6; // Convert m/s to km/h + currentSpeed = locSpeed; // Convert m/s to km/h already done by location controller if needed, wait location_controller sends raw speed or km/h? It sends raw speed. So we should * 3.6 + currentSpeed = locSpeed * 3.6; // Skip if movement is too small if (_lastProcessedLocation != null) { @@ -544,7 +530,7 @@ class NavigationController extends GetxController _targetLoc!.longitude, ); } else { - _targetHeading = position.heading; + _targetHeading = locHeading; } _animController?.forward(from: 0.0); diff --git a/siro_rider/lib/controller/firebase/firbase_messge.dart b/siro_rider/lib/controller/firebase/firbase_messge.dart index e24c3c8..5b54235 100644 --- a/siro_rider/lib/controller/firebase/firbase_messge.dart +++ b/siro_rider/lib/controller/firebase/firbase_messge.dart @@ -87,12 +87,6 @@ class FirebaseMessagesController extends GetxController { fireBaseTitles(message); } }); - FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async { - // Handle background message - if (message.data.isNotEmpty) { - fireBaseTitles(message); - } - }); FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { if (message.data.isNotEmpty && message.notification != null) { diff --git a/siro_rider/lib/controller/home/map/map_socket_controller.dart b/siro_rider/lib/controller/home/map/map_socket_controller.dart index 3d73bae..95fbf21 100644 --- a/siro_rider/lib/controller/home/map/map_socket_controller.dart +++ b/siro_rider/lib/controller/home/map/map_socket_controller.dart @@ -283,7 +283,7 @@ class MapSocketController extends GetxController { } final dynamic distanceValue = - data['distance_m'] ?? data['distance_meters'] ?? data['distance']; + data['distance_m'] ?? data['distance_meters']; final double? distanceMeters = double.tryParse(distanceValue?.toString() ?? ''); final int? etaSeconds = data['eta_seconds'] == null diff --git a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart index c229ad2..c264a61 100644 --- a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart +++ b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart @@ -112,6 +112,7 @@ class RideLifecycleController extends GetxController { late String driverId = ''; late String make = ''; late String model = ''; + late String gender = ''; late String carColor = ''; late String licensePlate = ''; late String driverName = ''; @@ -120,6 +121,9 @@ class RideLifecycleController extends GetxController { late String colorHex = ''; late String carYear = ''; late String driverRate = '5.0'; + late String driverRatingCount = '0'; + late String driverCompletedRides = '0'; + late String driverTier = 'Verified driver'; late String driverToken = ''; double kazan = 8; @@ -1481,7 +1485,8 @@ class RideLifecycleController extends GetxController { // إيقاف جلب السيارات المجاورة ومسحها، باستثناء السائق الذي قبل الطلب mapEngine.reloadStartApp = false; - mapEngine.markers.removeWhere((marker) => marker.markerId.value != driverId.toString()); + mapEngine.markers + .removeWhere((marker) => marker.markerId.value != driverId.toString()); mapEngine.update(); await getDriverCarsLocationToPassengerAfterApplied(); @@ -1490,8 +1495,7 @@ class RideLifecycleController extends GetxController { LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last; Log.print( '[rideAppliedFromDriver] 📍 Driver at: $driverPos, Passenger at: $passengerLocation'); - await getInitialDriverDistanceAndDuration(driverPos, passengerLocation); - await drawDriverPathOnly(driverPos, passengerLocation); + await calculateDriverToPassengerRoute(driverPos, passengerLocation); mapEngine.fitCameraToPoints(driverPos, passengerLocation); } @@ -1656,6 +1660,9 @@ class RideLifecycleController extends GetxController { driverToken = data['token']?.toString() ?? ''; carYear = data['year']?.toString() ?? ''; driverRate = data['ratingDriver']?.toString() ?? '5.0'; + driverRatingCount = data['ratingCount']?.toString() ?? '0'; + driverCompletedRides = data['completedRides']?.toString() ?? '0'; + driverTier = data['driverTier']?.toString() ?? 'Verified driver'; update(); } @@ -2221,6 +2228,15 @@ class RideLifecycleController extends GetxController { polyLines = polyLines .where((p) => !p.polylineId.value.startsWith('driver_route')) .toSet(); + polyLines = { + ...polyLines, + Polyline( + polylineId: const PolylineId('main_route'), + points: decodedPoints, + color: const Color(0xFF2196F3), + width: 6, + ) + }; } else { // مسح السلمات القديمة أولاً polyLines = polyLines @@ -2290,7 +2306,9 @@ class RideLifecycleController extends GetxController { _routeHeadingMismatchCount = 0; _isRecalculatingRoute = true; if (statusRide == 'Begin' || - currentRideState.value == RideState.inProgress) { + statusRide == 'Arrived' || + currentRideState.value == RideState.inProgress || + currentRideState.value == RideState.driverArrived) { await calculateDriverToPassengerRoute(driverPos, myDestination, isBeginPhase: true); } else { @@ -2504,6 +2522,8 @@ class RideLifecycleController extends GetxController { String icon; if (model.contains('دراجة') || make.contains('دراجة')) { icon = mapEngine.motoIcon; + } else if (gender == 'Female') { + icon = mapEngine.ladyIcon; } else { icon = mapEngine.carIcon; } @@ -3026,6 +3046,17 @@ class RideLifecycleController extends GetxController { mapEngine.playRouteAnimation( mapEngine.polylineCoordinates, mapEngine.lastComputedBounds); } + + if (driverCarsLocationToPassengerAfterApplied.isNotEmpty && + myDestination.latitude != 0 && + myDestination.longitude != 0) { + await calculateDriverToPassengerRoute( + driverCarsLocationToPassengerAfterApplied.last, + myDestination, + isBeginPhase: true, + ); + } + update(); } @@ -3903,12 +3934,37 @@ class RideLifecycleController extends GetxController { make = data['make']?.toString() ?? ''; model = data['model']?.toString() ?? ''; + gender = data['gender']?.toString() ?? ''; carColor = data['color']?.toString() ?? ''; colorHex = data['color_hex']?.toString() ?? ''; licensePlate = data['car_plate']?.toString() ?? ''; carYear = data['year']?.toString() ?? ''; + // المحاولة الفورية لرسم السائق إذا توفرت الإحداثيات في البيانات + double lat = double.tryParse( + data['latitude']?.toString() ?? data['lat']?.toString() ?? '0') ?? + 0; + double lng = double.tryParse(data['longitude']?.toString() ?? + data['lng']?.toString() ?? + '0') ?? + 0; + double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0; + + if (lat != 0 && lng != 0) { + LatLng initialPos = LatLng(lat, lng); + if (driverCarsLocationToPassengerAfterApplied.isEmpty) { + driverCarsLocationToPassengerAfterApplied.add(initialPos); + } else { + driverCarsLocationToPassengerAfterApplied[0] = initialPos; + } + // تحديث الماركر فوراً لضمان ظهوره بشكل موثوق + updateDriverMarker(initialPos, heading); + } + driverRate = data['ratingDriver']?.toString() ?? '5.0'; + driverRatingCount = data['ratingCount']?.toString() ?? '0'; + driverCompletedRides = data['completedRides']?.toString() ?? '0'; + driverTier = data['driverTier']?.toString() ?? 'Verified driver'; driverToken = data['token']?.toString() ?? ''; update(); @@ -4185,55 +4241,6 @@ class RideLifecycleController extends GetxController { ); } - Future getDistanceFromDriverAfterAcceptedRide( - String origin, String destination) async { - String apiKey = Env.mapKeyOsm; - if (origin.isEmpty) { - origin = '${passengerLocation.latitude},${passengerLocation.longitude}'; - } - var uri = Uri.parse( - '$dynamicApiUrl?origin=$origin&destination=$destination&steps=false&overview=false'); - Log.print('uri: $uri'); - - http.Response response; - Map responseData; - - try { - response = await http.get( - uri, - headers: { - 'X-API-KEY': apiKey, - }, - ).timeout(const Duration(seconds: 20)); - - if (response.statusCode != 200) { - Log.print('Error from API: ${response.statusCode}'); - isLoading = false; - update(); - return; - } - if (Get.isBottomSheetOpen ?? false) { - Get.back(); - } - isDrawingRoute = false; - - responseData = json.decode(response.body); - Log.print('responseData: $responseData'); - - if (responseData['status'] != 'ok') { - Log.print('API returned an error: ${responseData['message']}'); - isLoading = false; - update(); - return; - } - } catch (e) { - Log.print('Failed to get directions: $e'); - isLoading = false; - update(); - return; - } - } - Future _stageNiceToHave() async { Log.print('🚀 MapPassengerController: Starting _stageNiceToHave'); diff --git a/siro_rider/lib/controller/home/map/ui_interactions_controller.dart b/siro_rider/lib/controller/home/map/ui_interactions_controller.dart index 388c28e..afa97d9 100644 --- a/siro_rider/lib/controller/home/map/ui_interactions_controller.dart +++ b/siro_rider/lib/controller/home/map/ui_interactions_controller.dart @@ -4,7 +4,6 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; -import 'package:intaleq_maps/intaleq_maps.dart'; import '../../../constant/box_name.dart'; import '../../../constant/colors.dart'; @@ -15,19 +14,13 @@ import '../../../main.dart'; // contains global 'box' import '../../../print.dart'; import '../../../services/emergency_signal_service.dart'; import '../../../views/widgets/elevated_btn.dart'; -import '../../../views/widgets/mydialoug.dart'; import '../../../views/widgets/my_textField.dart'; -import '../../../views/home/map_page_passenger.dart'; -import '../../../views/widgets/error_snakbar.dart'; -import '../../../models/model/painter_copoun.dart'; import '../../functions/launch.dart'; -import '../../firebase/local_notification.dart'; import '../../firebase/notification_service.dart'; import '../../functions/crud.dart'; import '../../functions/tts.dart'; import 'ride_lifecycle_controller.dart'; import 'location_search_controller.dart'; -import 'map_engine_controller.dart'; class UiInteractionsController extends GetxController { TextEditingController sosPhonePassengerProfile = TextEditingController(); @@ -56,54 +49,54 @@ class UiInteractionsController extends GetxController { sosPhonePassengerProfile.clear(); Get.defaultDialog( - title: 'Add SOS Phone'.tr, - titleStyle: AppStyle.title, - content: Form( - key: sosFormKey, - child: Column( - children: [ - MyTextForm( - controller: sosPhonePassengerProfile, - label: 'insert sos phone'.tr, - hint: 'e.g. 0912345678 (Default +963)'.tr, - type: TextInputType.phone, - ), - const SizedBox(height: 10), - Text( - "Note: If no country code is entered, it will be saved as Syrian (+963).".tr, - style: TextStyle(fontSize: 12, color: Colors.grey), - textAlign: TextAlign.center, - ), - ], + title: 'Add SOS Phone'.tr, + titleStyle: AppStyle.title, + content: Form( + key: sosFormKey, + child: Column( + children: [ + MyTextForm( + controller: sosPhonePassengerProfile, + label: 'insert sos phone'.tr, + hint: 'e.g. 0912345678 (Default +963)'.tr, + type: TextInputType.phone, + ), + const SizedBox(height: 10), + Text( + "Note: If no country code is entered, it will be saved as Syrian (+963)." + .tr, + style: TextStyle(fontSize: 12, color: Colors.grey), + textAlign: TextAlign.center, + ), + ], + ), ), - ), - confirm: MyElevatedButton( - title: 'Save'.tr, - onPressed: () async { - if (sosFormKey.currentState!.validate()) { - Get.back(); - var numberPhone = - formatSyrianPhoneNumber(sosPhonePassengerProfile.text); - - await CRUD().post( - link: AppLink.updateprofile, - payload: { - 'id': box.read(BoxName.passengerID), - 'sosPhone': numberPhone, - }, - ); - - box.write(BoxName.sosPhonePassenger, numberPhone); - onSuccess(); - } - }, - ), - cancel: MyElevatedButton( - title: 'Cancel'.tr, - onPressed: () => Get.back(), - kolor: AppColor.redColor, - ) - ); + confirm: MyElevatedButton( + title: 'Save'.tr, + onPressed: () async { + if (sosFormKey.currentState!.validate()) { + Get.back(); + var numberPhone = + formatSyrianPhoneNumber(sosPhonePassengerProfile.text); + + await CRUD().post( + link: AppLink.updateprofile, + payload: { + 'id': box.read(BoxName.passengerID), + 'sosPhone': numberPhone, + }, + ); + + box.write(BoxName.sosPhonePassenger, numberPhone); + onSuccess(); + } + }, + ), + cancel: MyElevatedButton( + title: 'Cancel'.tr, + onPressed: () => Get.back(), + kolor: AppColor.redColor, + )); } void sosPassenger() { @@ -114,10 +107,12 @@ class UiInteractionsController extends GetxController { titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), content: Column( children: [ - Icon(Icons.warning_amber_rounded, size: 50, color: AppColor.redColor), + Icon(Icons.warning_amber_rounded, + size: 50, color: AppColor.redColor), const SizedBox(height: 10), Text( - "Do you want to send an emergency message to your SOS contact?".tr, + "Do you want to send an emergency message to your SOS contact?" + .tr, textAlign: TextAlign.center, style: AppStyle.title, ), diff --git a/siro_rider/lib/controller/local/translations.dart b/siro_rider/lib/controller/local/translations.dart index 5244054..d42c370 100644 --- a/siro_rider/lib/controller/local/translations.dart +++ b/siro_rider/lib/controller/local/translations.dart @@ -42,6 +42,7 @@ class MyTranslation extends Translations { "Arrived": "وصلنا", "Audio Recording": "تسجيل صوتي", "Call": "اتصال", + "Call Options": "خيارات الاتصال", "Call Connected": "تم فتح الاتصال", "Call Support": "اتصل بالدعم", "Call left": "مكالمات متبقية", @@ -49,6 +50,8 @@ class MyTranslation extends Translations { "Change Photo": "تغيير الصورة", "Captain": "الكابتن", "Choose from Gallery": "اختر من المعرض", + "Choose how you want to call the driver": + "اختر طريقة الاتصال بالكابتن", "Choose from contact": "اختر من جهات الاتصال", "Click to track the trip": "اضغط لتتبع المشوار", "Close panel": "إغلاق اللوحة", @@ -92,6 +95,9 @@ class MyTranslation extends Translations { "Finished": "انتهى", "Fixed Price": "سعر ثابت", "Free Call": "مكالمة مجانية", + "Professional driver": "كابتن محترف", + "Trusted driver": "كابتن موثوق", + "Verified driver": "كابتن موثق", "General": "عام", "Grant": "منح الإذن", "Have a Promo Code?": "معك كود خصم؟", @@ -178,6 +184,7 @@ class MyTranslation extends Translations { "Preferences": "التفضيلات", "Profile photo updated": "تم تحديث صورة الغلاف", "Quick Message": "رسالة سريعة", + "reviews": "تقييم", "Rating is": "التقييم هو", "Received empty route data.": "تم استلام بيانات طريق فارغة.", "Record": "تسجيل", @@ -211,6 +218,7 @@ class MyTranslation extends Translations { "Set as Work": "تحديد كالشغل", "Share": "مشاركة", "Share Trip": "مشاركة المشوار", + "Standard Call": "اتصال عادي", "Share your experience to help us improve...": "شاركنا تجربتك لنحسن خدمتنا...", "Something went wrong. Please try again.": "صار غلط. جرب مرة تانية.", @@ -271,6 +279,8 @@ class MyTranslation extends Translations { "to arrive you.": "ليوصلك.", "unknown": "غير معروف", "wait 1 minute to recive message": "استنى دقيقة لتستلم الرسالة", + "Uses cellular network": "يستخدم شبكة الهاتف", + "Voice call over internet": "مكالمة صوتية عبر الإنترنت", "with license plate": "برقم اللوحة", "witout zero": "بدون صفر", "you must insert token code": "لازم تدخل الكود", @@ -16885,7 +16895,8 @@ class MyTranslation extends Translations { "Support is Away": "سپورٹ اب دستیاب نہیں ہے", "Support is currently Online": "سپورٹ اب آن لائن ہے", "Voice Call": "صوتی کال", - "We're here to help you 24/7": "ہم چوبیس گھنٹے آپ کی مدد کے لیے حاضر ہیں", + "We're here to help you 24/7": + "ہم چوبیس گھنٹے آپ کی مدد کے لیے حاضر ہیں", "Working Hours:": "کام کے اوقات:", "1 Passenger": "1 Passenger", "2 Passengers": "2 Passengers", @@ -18446,7 +18457,8 @@ class MyTranslation extends Translations { "Support is Away": "सहायता अभी उपलब्ध नहीं है", "Support is currently Online": "सहायता अभी ऑनलाइन है", "Voice Call": "वॉइस कॉल", - "We're here to help you 24/7": "हम आपकी सहायता के लिए 24/7 उपलब्ध हैं", + "We're here to help you 24/7": + "हम आपकी सहायता के लिए 24/7 उपलब्ध हैं", "Working Hours:": "कार्य समय:", "1 Passenger": "1 Passenger", "2 Passengers": "2 Passengers", diff --git a/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart b/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart index 8168f4f..a0689dc 100644 --- a/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart +++ b/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart @@ -250,19 +250,23 @@ class ApplyOrderWidget extends StatelessWidget { Row( children: [ // صورة السائق (أصغر) - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: AppColor.primaryColor.withOpacity(0.2), width: 2), - ), - child: CircleAvatar( - radius: 22, // تصغير من 28 إلى 22 - backgroundColor: Colors.grey[200], - backgroundImage: NetworkImage( - '${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'), - onBackgroundImageError: (_, __) => - const Icon(Icons.person, color: Colors.grey, size: 20), + GestureDetector( + onTap: () => _showDriverAvatarDialog(context, controller), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: AppColor.primaryColor.withOpacity(0.2), + width: 2), + ), + child: CircleAvatar( + radius: 22, // تصغير من 28 إلى 22 + backgroundColor: Colors.grey[200], + backgroundImage: NetworkImage( + '${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'), + onBackgroundImageError: (_, __) => + const Icon(Icons.person, color: Colors.grey, size: 20), + ), ), ), @@ -299,6 +303,32 @@ class ApplyOrderWidget extends StatelessWidget { ), ], ), + const SizedBox(height: 5), + Wrap( + spacing: 6, + runSpacing: 4, + children: [ + _buildDriverBadge( + icon: Icons.verified_rounded, + text: controller.driverTier.tr, + color: AppColor.primaryColor, + ), + if (controller.driverCompletedRides != '0') + _buildDriverBadge( + icon: Icons.route_rounded, + text: + '${controller.driverCompletedRides} ${'rides'.tr}', + color: Colors.teal, + ), + if (controller.driverRatingCount != '0') + _buildDriverBadge( + icon: Icons.reviews_rounded, + text: + '${controller.driverRatingCount} ${'reviews'.tr}', + color: Colors.amber.shade800, + ), + ], + ), ], ), ), @@ -320,6 +350,11 @@ class ApplyOrderWidget extends StatelessWidget { Widget _buildMicroCarIcon( RideLifecycleController controller, Color Function(String) parseColor) { Color carColor = parseColor(controller.colorHex); + final String vehicleText = + '${controller.model} ${controller.make}'.toLowerCase(); + final bool isBike = vehicleText.contains('scooter') || + vehicleText.contains('bike') || + vehicleText.contains('دراجة'); return Container( height: 40, // تصغير من 50 width: 40, @@ -331,7 +366,8 @@ class ApplyOrderWidget extends StatelessWidget { child: ColorFiltered( colorFilter: ColorFilter.mode(carColor, BlendMode.srcIn), child: Image.asset( - box.read(BoxName.carType) == 'Scooter' || + isBike || + box.read(BoxName.carType) == 'Scooter' || box.read(BoxName.carType) == 'Pink Bike' ? 'assets/images/moto.png' : 'assets/images/car3.png', @@ -341,6 +377,81 @@ class ApplyOrderWidget extends StatelessWidget { ); } + Widget _buildDriverBadge({ + required IconData icon, + required String text, + required Color color, + }) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 11, color: color), + const SizedBox(width: 4), + Text( + text, + style: TextStyle( + color: color, + fontSize: 10.5, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + ); + } + + void _showDriverAvatarDialog( + BuildContext context, RideLifecycleController controller) { + final imageUrl = + '${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'; + Get.dialog( + Dialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 38), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), + child: Padding( + padding: const EdgeInsets.fromLTRB(18, 20, 18, 18), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + radius: 58, + backgroundColor: Colors.grey[200], + backgroundImage: NetworkImage(imageUrl), + onBackgroundImageError: (_, __) {}, + ), + const SizedBox(height: 14), + Text( + controller.driverName, + textAlign: TextAlign.center, + style: AppStyle.title.copyWith( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 6), + Text( + '${controller.driverTier.tr} • ${controller.driverRate}', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey[700], + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + barrierDismissible: true, + ); + } + Widget _buildSlimLicensePlate(String plateNumber) { return Container( width: double.infinity,