Files
intaleq_driver/lib/controller/home/captin/order_request_controller.dart

743 lines
24 KiB
Dart
Executable File

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<MarkerId, Marker> markersMap = {};
Set<Marker> get markers => markersMap.values.toSet();
// --- البيانات والتحكم ---
// 🔥 تم إضافة myMapData لدعم السوكيت الجديد
List<dynamic>? myList;
Map<dynamic, dynamic>? 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<Polyline> polylines = {};
// حالة التطبيق والصوت
bool isInBackground = false;
final AudioPlayer audioPlayer = AudioPlayer();
@override
Future<void> 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<void> _calculateFullJourney() async {
// Don't block on mapController being null - we'll draw routes
// and markers first, then zoom when controller is ready
bool canZoom = mapController != null;
try {
// Reuse stored location from LocationController instead of
// making a duplicate GPS hardware call (already fetched in
// _initialMapSetup).
LatLng driverLatLng;
double driverHeading = 0.0;
if (Get.isRegistered<LocationController>()) {
final locCtrl = Get.find<LocationController>();
if (locCtrl.myLocation.latitude != 0 ||
locCtrl.myLocation.longitude != 0) {
driverLatLng = locCtrl.myLocation;
driverHeading = locCtrl.heading;
} else {
Position driverPos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
driverHeading = driverPos.heading;
}
} else {
Position driverPos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
driverHeading = driverPos.heading;
}
updateDriverLocation(driverLatLng, driverHeading);
// 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.black,
id: 'trip_route',
getSteps: 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']);
// 🔥 تخزين استجابة السيرفر كاملة (بما فيها الـ points والـ instructions)
if (tripResult['raw_response'] != null) {
box.write('cached_trip_route', tripResult['raw_response']);
}
}
await _updateMarkers(
paxTime: timeToPassenger,
paxDist: distanceToPassenger,
destTime: totalTripDuration,
destDist: totalTripDistance);
// Now zoom to fit all polylines and markers (if controller available)
if (canZoom) {
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<Map<String, dynamic>?> _fetchRouteData(
{required LatLng start,
required LatLng end,
required Color color,
required String id,
bool getSteps = false}) async {
try {
if (start.latitude == 0 || end.latitude == 0) return null;
// Don't block on mapController — route data fetch is independent
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': getSteps ? 'true' : 'false',
'alternatives': 'false',
'locale': 'ar',
});
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<LatLng> 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,
'encoded_polyline': encodedPoints,
'raw_response': response.body, // 🔥 نمرر الـ JSON كاملاً
};
}
} catch (e) {
print("Route Fetch Error: $e");
}
return null;
}
void zoomToFitRide() {
if (mapController == null) return;
List<LatLng> 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<void> _prepareDriverIcon() async {
driverIcon = await MarkerGenerator.createDriverMarker();
}
Future<void> _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<void> 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<HomeCaptainController>()) {
Get.put(HomeCaptainController());
} else {
Get.find<HomeCaptainController>().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<LatLng> controllerDecodePolyline(String encoded) {
List<LatLng> 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;
}
}