import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_overlay_window/flutter_overlay_window.dart'; import 'package:get/get.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; import 'package:geolocator/geolocator.dart'; import 'package:http/http.dart' as http; import 'package:just_audio/just_audio.dart'; import 'package:sefer_driver/views/widgets/error_snakbar.dart'; import 'dart:math' as math; import '../../../constant/box_name.dart'; import '../../../constant/links.dart'; import '../../../env/env.dart'; import '../../../main.dart'; import '../../../print.dart'; import '../../../views/home/Captin/driver_map_page.dart'; import '../../../views/home/Captin/orderCaptin/marker_generator.dart'; import '../../../views/widgets/mydialoug.dart'; import '../../firebase/local_notification.dart'; import '../../functions/crud.dart'; import '../../functions/location_controller.dart'; import '../../home/captin/home_captain_controller.dart'; class OrderRequestController extends GetxController with WidgetsBindingObserver { // --- متغيرات التايمر --- double progress = 1.0; int duration = 15; int remainingTime = 15; Timer? _timer; bool applied = false; final locationController = Get.put(LocationController()); // 🔥 متغير لمنع تكرار القبول bool _isRideTakenHandled = false; // --- الأيقونات والماركرز --- InlqBitmap? driverIcon; Map markersMap = {}; Set get markers => markersMap.values.toSet(); // --- البيانات والتحكم --- // 🔥 تم إضافة myMapData لدعم السوكيت الجديد List? myList; Map? myMapData; IntaleqMapController? mapController; // الإحداثيات (أزلنا late لتجنب الأخطاء القاتلة) double latPassenger = 0.0; double lngPassenger = 0.0; double latDestination = 0.0; double lngDestination = 0.0; // --- متغيرات العرض --- String passengerRating = "5.0"; String tripType = "Standard"; String totalTripDistance = "--"; String totalTripDuration = "--"; String tripPrice = "--"; String timeToPassenger = "Calculating...".tr; String distanceToPassenger = "--"; // --- الخريطة --- Set polylines = {}; // حالة التطبيق والصوت bool isInBackground = false; final AudioPlayer audioPlayer = AudioPlayer(); @override Future onInit() async { // 🛑 حماية من الفتح المتكرر لنفس الطلب if (Get.arguments == null) { print("❌ OrderController Error: No arguments received."); Get.back(); // إغلاق الصفحة فوراً return; } super.onInit(); WidgetsBinding.instance.addObserver(this); _checkOverlay(); // 🔥 تهيئة البيانات هي الخطوة الأولى والأهم _initializeData(); _parseExtraData(); // 1. تجهيز أيقونة السائق await _prepareDriverIcon(); // 2. وضع الماركرز المبدئية _updateMarkers( paxTime: "...", paxDist: "", destTime: totalTripDuration, destDist: totalTripDistance); // 3. رسم مبدئي _initialMapSetup(); // 4. الاستماع للسوكيت _listenForRideTaken(); // 5. حساب المسارين await _calculateFullJourney(); // 6. تشغيل التايمر startTimer(); } // ---------------------------------------------------------------------- // 🔥🔥🔥 Smart Data Handling (List & Map Support) 🔥🔥🔥 // ---------------------------------------------------------------------- void _initializeData() { var args = Get.arguments; print("📦 Order Controller Received Type: ${args.runtimeType}"); print("📦 Order Controller Data: $args"); if (args != null) { // الحالة 1: قائمة مباشرة (Legacy / Some Firebase formats) if (args is List) { myList = args; } // الحالة 2: خريطة (Map) else if (args is Map) { // أ) هل هي قادمة من Firebase وتحتوي على DriverList؟ if (args.containsKey('DriverList')) { var listData = args['DriverList']; if (listData is List) { myList = listData; } else if (listData is String) { // أحياناً تصل كنص مشفر داخل الـ Map try { myList = jsonDecode(listData); } catch (e) { print("Error decoding DriverList: $e"); } } } // ب) هل هي قادمة من Socket بالمفاتيح الرقمية ("0", "1", ...)؟ else { myMapData = args; } } } // تعبئة الإحداثيات باستخدام الدالة الذكية _getValueAt latPassenger = _parseCoord(_getValueAt(0)); lngPassenger = _parseCoord(_getValueAt(1)); latDestination = _parseCoord(_getValueAt(3)); lngDestination = _parseCoord(_getValueAt(4)); print( "📍 Parsed Coordinates: Pax($latPassenger, $lngPassenger) -> Dest($latDestination, $lngDestination)"); } /// 🔥 دالة ذكية تجلب القيمة سواء كانت البيانات في List أو Map dynamic _getValueAt(int index) { // الأولوية للقائمة if (myList != null && index < myList!.length) { return myList![index]; } // ثم الخريطة (السوكيت) - المفاتيح عبارة عن String if (myMapData != null && myMapData!.containsKey(index.toString())) { return myMapData![index.toString()]; } return null; } /// الدالة التي يستخدمها باقي الكود لجلب البيانات كنصوص String _safeGet(int index) { var val = _getValueAt(index); if (val != null) { return val.toString(); } return ""; } double _parseCoord(dynamic val) { if (val == null) return 0.0; String s = val.toString().replaceAll(',', '').trim(); if (s.contains(' ')) s = s.split(' ')[0]; return double.tryParse(s) ?? 0.0; } void _parseExtraData() { passengerRating = _safeGet(33).isEmpty ? "5.0" : _safeGet(33); tripType = _safeGet(31); // Format numbers to avoid many decimal places String rawDist = _safeGet(5); if (rawDist.isNotEmpty) { double? d = double.tryParse(rawDist); totalTripDistance = d != null ? "${d.toStringAsFixed(1)} km" : rawDist; } String rawDur = _safeGet(19); if (rawDur.isNotEmpty) { double? d = double.tryParse(rawDur); totalTripDuration = d != null ? "${d.toStringAsFixed(0)} min" : rawDur; } String rawPrice = _safeGet(2); if (rawPrice.isNotEmpty) { double? p = double.tryParse(rawPrice); tripPrice = p != null ? p.toStringAsFixed(0) : rawPrice; } } // ---------------------------------------------------------------------- // 🔥🔥🔥 Core Logic: Concurrent API Calls & Bounds 🔥🔥🔥 // ---------------------------------------------------------------------- Future _calculateFullJourney() async { if (mapController == null) return; // Wait for controller to draw try { Position driverPos = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high); LatLng driverLatLng = LatLng(driverPos.latitude, driverPos.longitude); updateDriverLocation(driverLatLng, driverPos.heading); // Clear old polylines to avoid "ghost lines" polylines.clear(); var pickupFuture = _fetchRouteData( start: driverLatLng, end: LatLng(latPassenger, lngPassenger), color: Colors.amber, id: 'pickup_route'); var tripFuture = _fetchRouteData( start: LatLng(latPassenger, lngPassenger), end: LatLng(latDestination, lngDestination), color: Colors.green, id: 'trip_route', isDashed: true); var results = await Future.wait([pickupFuture, tripFuture]); var pickupResult = results[0]; var tripResult = results[1]; if (pickupResult != null) { distanceToPassenger = pickupResult['distance_text']; timeToPassenger = pickupResult['duration_text']; polylines.add(pickupResult['polyline']); } if (tripResult != null) { totalTripDistance = tripResult['distance_text']; totalTripDuration = tripResult['duration_text']; polylines.add(tripResult['polyline']); } await _updateMarkers( paxTime: timeToPassenger, paxDist: distanceToPassenger, destTime: totalTripDuration, destDist: totalTripDistance); // Now zoom to fit all polylines and markers zoomToFitRide(); update(); } catch (e) { print("❌ Error in Journey Calculation: $e"); } } String _formatDistance(dynamic rawDist) { if (rawDist == null || rawDist.toString().isEmpty) return "--"; double dist = double.tryParse(rawDist.toString()) ?? 0.0; if (dist <= 0) return "--"; if (dist < 1000) return "${dist.toStringAsFixed(0)} m"; return "${(dist / 1000).toStringAsFixed(1)} km"; } String _formatDuration(dynamic rawDur) { if (rawDur == null || rawDur.toString().isEmpty) return "--"; double dur = double.tryParse(rawDur.toString()) ?? 0.0; if (dur <= 0) return "1 min"; // Minimum 1 min for UI if (dur < 60) return "${dur.toStringAsFixed(0)} sec"; return "${(dur / 60).toStringAsFixed(0)} min"; } Future?> _fetchRouteData( {required LatLng start, required LatLng end, required Color color, required String id, bool isDashed = false}) async { try { if (start.latitude == 0 || end.latitude == 0) return null; if (mapController == null) return null; final saasUrl = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: { 'fromLat': start.latitude.toString(), 'fromLng': start.longitude.toString(), 'toLat': end.latitude.toString(), 'toLng': end.longitude.toString(), 'steps': 'false', 'alternatives': 'false', }); final response = await http.get(saasUrl, headers: { 'x-api-key': Env.mapSaasKey, 'Content-Type': 'application/json', }); if (response.statusCode != 200) { throw Exception("Routing request failed: ${response.statusCode}"); } final data = jsonDecode(response.body); print("🛣️ Route API Response [$id]: ${data}"); // The map-saas API returns the route data directly at the root, // with 'points' being an encoded polyline string. final String? encodedPoints = data['points']?.toString(); if (encodedPoints != null && encodedPoints.isNotEmpty) { List path = controllerDecodePolyline(encodedPoints); print("📍 Path for [$id] has ${path.length} points."); final num? rawDist = data['distance'] is num ? data['distance'] : null; final num? rawDur = data['duration'] is num ? data['duration'] : null; final distanceText = data['distance_text'] ?? _formatDistance(rawDist); final durationText = data['duration_text'] ?? _formatDuration(rawDur); Polyline polyline = Polyline( polylineId: PolylineId(id), color: color, width: 5, points: path, ); return { 'distance_text': distanceText, 'duration_text': durationText, 'polyline': polyline }; } } catch (e) { print("Route Fetch Error: $e"); } return null; } void zoomToFitRide() { if (mapController == null) return; List allPoints = []; // Add all polyline points to the bounds calculation for (var polyline in polylines) { allPoints.addAll(polyline.points); } // Fallback to basic markers if polylines are empty if (allPoints.isEmpty) { allPoints.addAll([ LatLng(latPassenger, lngPassenger), LatLng(latDestination, lngDestination), ]); } if (allPoints.isEmpty) return; double minLat = allPoints.first.latitude; double maxLat = allPoints.first.latitude; double minLng = allPoints.first.longitude; double maxLng = allPoints.first.longitude; for (var p in allPoints) { if (p.latitude < minLat) minLat = p.latitude; if (p.latitude > maxLat) maxLat = p.latitude; if (p.longitude < minLng) minLng = p.longitude; if (p.longitude > maxLng) maxLng = p.longitude; } // Add some padding to the bounds double latPad = (maxLat - minLat) * 0.25; double lngPad = (maxLng - minLng) * 0.2; mapController!.animateCamera(CameraUpdate.newLatLngBounds( LatLngBounds( southwest: LatLng(minLat - latPad, minLng - lngPad), northeast: LatLng(maxLat + latPad, maxLng + lngPad), ), )); } // ---------------------------------------------------------------------- // Markers & Setup // ---------------------------------------------------------------------- Future _prepareDriverIcon() async { driverIcon = await MarkerGenerator.createDriverMarker(); } Future _updateMarkers( {required String paxTime, required String paxDist, String? destTime, String? destDist}) async { // حماية إذا لم يتم جلب الإحداثيات if (latPassenger == 0 || latDestination == 0) return; final InlqBitmap pickupIcon = await MarkerGenerator.createCustomMarkerBitmap( title: paxTime, subtitle: paxDist, color: Colors.amber.shade900, // Matching the amber pickup line iconData: Icons.person_pin_circle, ); final InlqBitmap dropoffIcon = await MarkerGenerator.createCustomMarkerBitmap( title: destTime ?? totalTripDuration, subtitle: destDist ?? totalTripDistance, color: Colors.red.shade800, iconData: Icons.flag, ); markersMap[const MarkerId('pax')] = Marker( markerId: const MarkerId('pax'), position: LatLng(latPassenger, lngPassenger), icon: pickupIcon, anchor: const Offset(0.5, 0.85), ); markersMap[const MarkerId('dest')] = Marker( markerId: const MarkerId('dest'), position: LatLng(latDestination, lngDestination), icon: dropoffIcon, anchor: const Offset(0.5, 0.85), ); update(); } void _initialMapSetup() async { Position driverPos = await Geolocator.getCurrentPosition(); LatLng driverLatLng = LatLng(driverPos.latitude, driverPos.longitude); if (driverIcon != null) { markersMap[const MarkerId('driver')] = Marker( markerId: const MarkerId('driver'), position: driverLatLng, icon: driverIcon!, rotation: driverPos.heading, anchor: const Offset(0.5, 0.5), flat: true, zIndex: 10); } if (latPassenger != 0 && lngPassenger != 0) { polylines.add(Polyline( polylineId: const PolylineId('temp_line'), points: [driverLatLng, LatLng(latPassenger, lngPassenger)], color: Colors.grey, width: 2, )); zoomToFitRide(); } update(); } void updateDriverLocation(LatLng newPos, double heading) { if (driverIcon != null) { markersMap[const MarkerId('driver')] = Marker( markerId: const MarkerId('driver'), position: newPos, icon: driverIcon!, rotation: heading, anchor: const Offset(0.5, 0.5), flat: true, zIndex: 10, ); update(); } } void onMapCreated(IntaleqMapController controller) { mapController = controller; _calculateFullJourney(); } // --- قبول الطلب وإدارة التايمر --- void startTimer() { _timer?.cancel(); remainingTime = duration; _playAudio(); _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (remainingTime <= 0) { timer.cancel(); _stopAudio(); if (!applied) Get.back(); } else { remainingTime--; progress = remainingTime / duration; update(); } }); } void endTimer() => _timer?.cancel(); void changeApplied() => applied = true; void _playAudio() async { try { await audioPlayer.setAsset('assets/order.mp3', preload: true); await audioPlayer.setLoopMode(LoopMode.one); await audioPlayer.play(); } catch (e) { print(e); } } void _stopAudio() => audioPlayer.stop(); void _listenForRideTaken() { if (locationController.socket != null) { locationController.socket!.off('ride_taken'); locationController.socket!.on('ride_taken', (data) { if (_isRideTakenHandled) return; String takenRideId = data['ride_id'].toString(); String myCurrentRideId = _safeGet(16); String whoTookIt = data['taken_by_driver_id'].toString(); String myDriverId = box.read(BoxName.driverID).toString(); if (takenRideId == myCurrentRideId && whoTookIt != myDriverId) { _isRideTakenHandled = true; endTimer(); // 1. حذف الإشعار من شريط التنبيهات فوراً NotificationController().cancelOrderNotification(); if (Get.isSnackbarOpen) Get.closeCurrentSnackbar(); // إغلاق أي ديالوج مفتوح قسرياً if (Get.isDialogOpen ?? false) { navigatorKey.currentState?.pop(); } Get.back(); mySnackbarInfo("The order has been accepted by another driver.".tr); } }); } } // Lifecycle @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { isInBackground = true; } else if (state == AppLifecycleState.resumed) { isInBackground = false; FlutterOverlayWindow.closeOverlay(); } } void _checkOverlay() async { if (Platform.isAndroid && await FlutterOverlayWindow.isActive()) { await FlutterOverlayWindow.closeOverlay(); } } // Accept Order Logic Future acceptOrder() async { endTimer(); _stopAudio(); // 1. إرسال الطلب var res = await CRUD() .post(link: "${AppLink.ride}/rides/acceptRide.php", payload: { 'id': _safeGet(16), 'rideTimeStart': DateTime.now().toString(), 'status': 'Apply', 'passengerToken': _safeGet(9), 'driver_id': box.read(BoxName.driverID), }); Log.print('res from orderrequestpage: ${res}'); // ============================================================ // تصحيح: فحص الرد بدقة (Map أو String) // ============================================================ bool isFailure = false; if (res is Map && res['status'] == 'failure') { isFailure = true; } else if (res == 'failure') { isFailure = true; } if (isFailure) { // ⛔ حالة الفشل: الطلب مأخوذ MyDialog().getDialog( "Sorry, the order was taken by another driver.".tr, '', () { // بما أن MyDialog يغلق نفسه الآن، نحتاج Get.back() واحدة فقط لإغلاق صفحة الطلب Get.back(); }); } else { // ✅ حالة النجاح // حماية من الكراش: التأكد من وجود HomeCaptainController قبل استخدامه if (!Get.isRegistered()) { Get.put(HomeCaptainController()); } else { Get.find().changeRideId(); } box.write(BoxName.statusDriverLocation, 'on'); changeApplied(); var rideArgs = { 'passengerLocation': '${_safeGet(0)},${_safeGet(1)}', 'passengerDestination': '${_safeGet(3)},${_safeGet(4)}', 'Duration': totalTripDuration, 'totalCost': _safeGet(26), 'Distance': totalTripDistance, 'name': _safeGet(8), 'phone': _safeGet(10), 'email': _safeGet(28), 'WalletChecked': _safeGet(13), 'tokenPassenger': _safeGet(9), 'direction': 'https://www.google.com/maps/dir/${_safeGet(0)}/${_safeGet(1)}/', 'DurationToPassenger': timeToPassenger, 'rideId': _safeGet(16), 'passengerId': _safeGet(7), 'driverId': _safeGet(18), 'durationOfRideValue': totalTripDuration, 'paymentAmount': _safeGet(2), 'paymentMethod': _safeGet(13) == 'true' ? 'visa' : 'cash', 'isHaveSteps': _safeGet(20), 'step0': _safeGet(21), 'step1': _safeGet(22), 'step2': _safeGet(23), 'step3': _safeGet(24), 'step4': _safeGet(25), 'passengerWalletBurc': _safeGet(26), 'timeOfOrder': DateTime.now().toString(), 'totalPassenger': _safeGet(2), 'carType': _safeGet(31), 'kazan': _safeGet(32), 'startNameLocation': _safeGet(29), 'endNameLocation': _safeGet(30), }; box.write(BoxName.rideArguments, rideArgs); // الانتقال النهائي Get.off(() => PassengerLocationMapPage(), arguments: rideArgs); } } @override void onClose() { locationController.socket?.off('ride_taken'); audioPlayer.dispose(); WidgetsBinding.instance.removeObserver(this); _timer?.cancel(); // mapController?.dispose(); super.onClose(); } List controllerDecodePolyline(String encoded) { List points = []; int index = 0, len = encoded.length; int lat = 0, lng = 0; while (index < len) { int b, shift = 0, result = 0; do { b = encoded.codeUnitAt(index++) - 63; result |= (b & 0x1f) << shift; shift += 5; } while (b >= 0x20); int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); lat += dlat; shift = 0; result = 0; do { b = encoded.codeUnitAt(index++) - 63; result |= (b & 0x1f) << shift; shift += 5; } while (b >= 0x20); int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); lng += dlng; points.add(LatLng(lat / 1E5, lng / 1E5)); } return points; } }