974 lines
41 KiB
Diff
974 lines
41 KiB
Diff
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<bool> 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<MapDriverController>()) {
|
|
+ final mapCtrl = Get.find<MapDriverController>();
|
|
+ mapCtrl.handleLocationUpdateFromCentral(pos, speed, heading);
|
|
+ }
|
|
+
|
|
+ if (Get.isRegistered<NavigationController>()) {
|
|
+ final navCtrl = Get.find<NavigationController>();
|
|
+ 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<Polyline> polylines = {};
|
|
+ bool _hasCalculatedFullJourney = false;
|
|
|
|
// حالة التطبيق والصوت
|
|
bool isInBackground = false;
|
|
@@ -219,6 +220,11 @@ class OrderRequestController extends GetxController
|
|
// ----------------------------------------------------------------------
|
|
|
|
Future<void> _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<void> _handleLocationUpdate(Position position) async {
|
|
+ Future<void> 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<void> 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<String, dynamic> 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<void> _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,
|