611 lines
20 KiB
Dart
611 lines
20 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
|
||
import 'package:driver/constant/collection_name.dart';
|
||
import 'package:driver/constant/constant.dart';
|
||
import 'package:driver/constant/send_notification.dart';
|
||
import 'package:driver/constant/show_toast_dialog.dart';
|
||
import 'package:driver/models/user_model.dart';
|
||
import 'package:driver/services/audio_player_service.dart';
|
||
import 'package:driver/themes/app_them_data.dart';
|
||
import 'package:driver/utils/fire_store_utils.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_map/flutter_map.dart' as flutterMap;
|
||
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
|
||
import 'package:get/get.dart';
|
||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||
import 'package:http/http.dart' as http;
|
||
import 'package:latlong2/latlong.dart' as location;
|
||
|
||
import '../models/order_model.dart';
|
||
|
||
class HomeController extends GetxController {
|
||
RxBool isLoading = true.obs;
|
||
flutterMap.MapController osmMapController = flutterMap.MapController();
|
||
RxList<flutterMap.Marker> osmMarkers = <flutterMap.Marker>[].obs;
|
||
|
||
@override
|
||
void onInit() {
|
||
getArgument();
|
||
setIcons();
|
||
getDriver();
|
||
super.onInit();
|
||
}
|
||
|
||
Rx<OrderModel> orderModel = OrderModel().obs;
|
||
Rx<OrderModel> currentOrder = OrderModel().obs;
|
||
Rx<UserModel> driverModel = UserModel().obs;
|
||
|
||
void getArgument() {
|
||
dynamic argumentData = Get.arguments;
|
||
if (argumentData != null) {
|
||
orderModel.value = argumentData['orderModel'];
|
||
}
|
||
}
|
||
|
||
Future<void> acceptOrder() async {
|
||
await AudioPlayerService.playSound(false);
|
||
ShowToastDialog.showLoader("Please wait".tr);
|
||
driverModel.value.inProgressOrderID ?? [];
|
||
driverModel.value.orderRequestData!.remove(currentOrder.value.id);
|
||
driverModel.value.inProgressOrderID!.add(currentOrder.value.id);
|
||
|
||
await FireStoreUtils.updateUser(driverModel.value);
|
||
|
||
currentOrder.value.status = Constant.driverAccepted;
|
||
currentOrder.value.driverID = driverModel.value.id;
|
||
currentOrder.value.driver = driverModel.value;
|
||
|
||
await FireStoreUtils.setOrder(currentOrder.value);
|
||
print("SendNotification ===========>");
|
||
SendNotification.sendFcmMessage(Constant.driverAcceptedNotification, currentOrder.value.author?.fcmToken ?? '', {});
|
||
SendNotification.sendFcmMessage(Constant.driverAcceptedNotification, currentOrder.value.vendor?.fcmToken ?? '', {});
|
||
ShowToastDialog.closeLoader();
|
||
}
|
||
|
||
Future<void> rejectOrder() async {
|
||
ShowToastDialog.showLoader("Please wait".tr);
|
||
// 🔊 Stop any ongoing alert sound (if playing)
|
||
await AudioPlayerService.playSound(false);
|
||
|
||
final driver = driverModel.value;
|
||
final order = currentOrder.value;
|
||
|
||
// 1️⃣ Validate order and driver
|
||
if (order.id == null || driver.id == null) {
|
||
debugPrint("⚠️ No valid order or driver found for rejection.");
|
||
return;
|
||
}
|
||
|
||
// 2️⃣ Add driver to rejected list safely
|
||
order.rejectedByDrivers ??= [];
|
||
if (!order.rejectedByDrivers!.contains(driver.id)) {
|
||
order.rejectedByDrivers!.add(driver.id);
|
||
}
|
||
|
||
// 3️⃣ Update order status
|
||
order.status = Constant.driverRejected;
|
||
|
||
// 4️⃣ Push order update to Firestore
|
||
await FireStoreUtils.setOrder(order);
|
||
|
||
// 5️⃣ Clean up driver's order tracking data safely
|
||
driver.orderRequestData?.remove(order.id);
|
||
driver.inProgressOrderID?.remove(order.id);
|
||
|
||
// 6️⃣ Update driver info in Firestore
|
||
await FireStoreUtils.updateUser(driver);
|
||
|
||
// 7️⃣ Reset order states
|
||
currentOrder.value = OrderModel();
|
||
orderModel.value = OrderModel();
|
||
|
||
// 8️⃣ Clear map visuals and UI
|
||
await clearMap();
|
||
update();
|
||
|
||
// 9️⃣ If multiple orders allowed, close dialog/screen
|
||
if (Constant.singleOrderReceive == false && Get.isOverlaysOpen) {
|
||
Get.back();
|
||
}
|
||
ShowToastDialog.closeLoader();
|
||
debugPrint("✅ Order ${order.id} rejected by driver ${driver.id}");
|
||
}
|
||
|
||
Future<void> clearMap() async {
|
||
await AudioPlayerService.playSound(false);
|
||
if (Constant.selectedMapType != 'osm') {
|
||
markers.clear();
|
||
polyLines.clear();
|
||
} else {
|
||
osmMarkers.clear();
|
||
routePoints.clear();
|
||
// osmMapController = flutterMap.MapController();
|
||
}
|
||
update();
|
||
}
|
||
|
||
Future<void> getCurrentOrder() async {
|
||
final driver = driverModel.value;
|
||
final currentId = currentOrder.value.id;
|
||
|
||
// 1️⃣ Reset if current order is invalid
|
||
if (currentId != null &&
|
||
!(driver.orderRequestData?.contains(currentId) ?? false) &&
|
||
!(driver.inProgressOrderID?.contains(currentId) ?? false)) {
|
||
await _resetCurrentOrder();
|
||
return;
|
||
}
|
||
|
||
// 2️⃣ Handle single-order mode
|
||
if (Constant.singleOrderReceive == true) {
|
||
final inProgress = driver.inProgressOrderID;
|
||
final requests = driver.orderRequestData;
|
||
|
||
if (inProgress != null && inProgress.isNotEmpty) {
|
||
_listenToOrder(inProgress.first);
|
||
return;
|
||
}
|
||
|
||
if (requests != null && requests.isNotEmpty) {
|
||
_listenToOrder(requests.first, checkInRequestData: true);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 3️⃣ Handle fallback (when orderModel has ID)
|
||
final fallbackId = orderModel.value.id;
|
||
if (fallbackId != null) {
|
||
_listenToOrder(fallbackId);
|
||
}
|
||
}
|
||
|
||
Future<void> _resetCurrentOrder() async {
|
||
currentOrder.value = OrderModel();
|
||
await clearMap();
|
||
await AudioPlayerService.playSound(false);
|
||
update();
|
||
}
|
||
|
||
/// 🔹 Listen to Firestore order updates for a specific orderId
|
||
void _listenToOrder(String orderId, {bool checkInRequestData = false}) {
|
||
FireStoreUtils.fireStore
|
||
.collection(CollectionName.vendorOrders)
|
||
.where('status', whereNotIn: [
|
||
Constant.orderCancelled,
|
||
Constant.driverRejected,
|
||
])
|
||
.where('id', isEqualTo: orderId)
|
||
.snapshots()
|
||
.listen((event) async {
|
||
if (event.docs.isEmpty) {
|
||
await _handleOrderNotFound();
|
||
return;
|
||
}
|
||
|
||
final data = event.docs.first.data();
|
||
final newOrder = OrderModel.fromJson(data);
|
||
|
||
if (checkInRequestData && !(driverModel.value.orderRequestData?.contains(newOrder.id) ?? false)) {
|
||
await _handleOrderNotFound();
|
||
return;
|
||
}
|
||
|
||
if (newOrder.rejectedByDrivers!.contains(driverModel.value.id)) {
|
||
await _handleOrderNotFound();
|
||
return;
|
||
}
|
||
|
||
currentOrder.value = newOrder;
|
||
changeData();
|
||
});
|
||
}
|
||
|
||
Future<void> _handleOrderNotFound() async {
|
||
currentOrder.value = OrderModel();
|
||
await AudioPlayerService.playSound(false);
|
||
update();
|
||
}
|
||
|
||
RxBool isChange = false.obs;
|
||
|
||
Future<void> changeData() async {
|
||
print(
|
||
"currentOrder.value.status :: ${currentOrder.value.id} :: ${currentOrder.value.status} :: ( ${orderModel.value.driver?.vendorID != null} :: ${orderModel.value.status})");
|
||
|
||
if (Constant.mapType == "inappmap") {
|
||
if (Constant.selectedMapType == "osm") {
|
||
getOSMPolyline();
|
||
} else {
|
||
getDirections();
|
||
}
|
||
}
|
||
if (currentOrder.value.status == Constant.driverPending) {
|
||
await AudioPlayerService.playSound(true);
|
||
} else {
|
||
await AudioPlayerService.playSound(false);
|
||
}
|
||
}
|
||
|
||
void getDriver() {
|
||
FireStoreUtils.fireStore.collection(CollectionName.users).doc(FireStoreUtils.getCurrentUid()).snapshots().listen(
|
||
(event) async {
|
||
if (event.exists) {
|
||
driverModel.value = UserModel.fromJson(event.data()!);
|
||
_updateCurrentLocationMarkers();
|
||
if (driverModel.value.id != null) {
|
||
isLoading.value = false;
|
||
update();
|
||
changeData();
|
||
getCurrentOrder();
|
||
}
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
GoogleMapController? mapController;
|
||
|
||
Rx<PolylinePoints> polylinePoints = PolylinePoints(apiKey: Constant.mapAPIKey).obs;
|
||
RxMap<PolylineId, Polyline> polyLines = <PolylineId, Polyline>{}.obs;
|
||
RxMap<String, Marker> markers = <String, Marker>{}.obs;
|
||
|
||
BitmapDescriptor? departureIcon;
|
||
BitmapDescriptor? destinationIcon;
|
||
BitmapDescriptor? taxiIcon;
|
||
|
||
Future<void> setIcons() async {
|
||
if (Constant.selectedMapType == 'google') {
|
||
final Uint8List departure = await Constant().getBytesFromAsset('assets/images/location_black3x.png', 100);
|
||
final Uint8List destination = await Constant().getBytesFromAsset('assets/images/location_orange3x.png', 100);
|
||
final Uint8List driver = await Constant().getBytesFromAsset('assets/images/food_delivery.png', 120);
|
||
|
||
departureIcon = BitmapDescriptor.fromBytes(departure);
|
||
destinationIcon = BitmapDescriptor.fromBytes(destination);
|
||
taxiIcon = BitmapDescriptor.fromBytes(driver);
|
||
}
|
||
}
|
||
|
||
Future<void> getDirections() async {
|
||
final order = currentOrder.value;
|
||
final driver = driverModel.value;
|
||
|
||
// 1️⃣ Safety checks
|
||
if (order.id == null) {
|
||
debugPrint("⚠️ getDirections: Order ID is null");
|
||
return;
|
||
}
|
||
|
||
final driverLoc = driver.location;
|
||
if (driverLoc == null) {
|
||
debugPrint("⚠️ getDirections: Driver location is null");
|
||
return;
|
||
}
|
||
|
||
// Icons must be loaded before proceeding
|
||
if (taxiIcon == null || destinationIcon == null || departureIcon == null) {
|
||
debugPrint("⚠️ getDirections: One or more map icons are null");
|
||
return;
|
||
}
|
||
|
||
// 2️⃣ Get start and end coordinates based on order status
|
||
LatLng? origin;
|
||
LatLng? destination;
|
||
|
||
switch (order.status) {
|
||
case Constant.orderShipped:
|
||
origin = LatLng(driverLoc.latitude ?? 0.0, driverLoc.longitude ?? 0.0);
|
||
destination = _toLatLng(order.vendor?.latitude, order.vendor?.longitude);
|
||
break;
|
||
|
||
case Constant.orderInTransit:
|
||
origin = LatLng(driverLoc.latitude ?? 0.0, driverLoc.longitude ?? 0.0);
|
||
destination = _toLatLng(
|
||
order.address?.location?.latitude,
|
||
order.address?.location?.longitude,
|
||
);
|
||
break;
|
||
|
||
case Constant.driverPending:
|
||
origin = _toLatLng(
|
||
order.author?.location?.latitude,
|
||
order.author?.location?.longitude,
|
||
);
|
||
destination = _toLatLng(order.vendor?.latitude, order.vendor?.longitude);
|
||
break;
|
||
|
||
default:
|
||
debugPrint("⚠️ getDirections: Unknown order status ${order.status}");
|
||
return;
|
||
}
|
||
|
||
if (origin == null || destination == null) {
|
||
debugPrint("⚠️ getDirections: Missing origin or destination");
|
||
return;
|
||
}
|
||
|
||
// 3️⃣ Fetch polyline route
|
||
final polylineCoordinates = await _fetchPolyline(origin, destination);
|
||
if (polylineCoordinates.isEmpty) {
|
||
debugPrint("⚠️ getDirections: No route found between origin and destination");
|
||
}
|
||
|
||
// 4️⃣ Update markers safely
|
||
markers.remove("Departure");
|
||
markers.remove("Destination");
|
||
markers.remove("Driver");
|
||
|
||
if (order.status == Constant.orderShipped || order.status == Constant.driverPending) {
|
||
markers['Departure'] = Marker(
|
||
markerId: const MarkerId('Departure'),
|
||
infoWindow: const InfoWindow(title: "Departure"),
|
||
position: _toLatLng(order.vendor?.latitude, order.vendor?.longitude) ?? const LatLng(0, 0),
|
||
icon: departureIcon!,
|
||
);
|
||
}
|
||
|
||
if (order.status == Constant.orderInTransit || order.status == Constant.driverPending) {
|
||
markers['Destination'] = Marker(
|
||
markerId: const MarkerId('Destination'),
|
||
infoWindow: const InfoWindow(title: "Destination"),
|
||
position: _toLatLng(
|
||
order.address?.location?.latitude,
|
||
order.address?.location?.longitude,
|
||
) ??
|
||
const LatLng(0, 0),
|
||
icon: destinationIcon!,
|
||
);
|
||
}
|
||
|
||
markers['Driver'] = Marker(
|
||
markerId: const MarkerId('Driver'),
|
||
infoWindow: const InfoWindow(title: "Driver"),
|
||
position: LatLng(
|
||
driverLoc.latitude ?? 0.0, // ✅ safe fallback
|
||
driverLoc.longitude ?? 0.0, // ✅ safe fallback
|
||
),
|
||
icon: taxiIcon!,
|
||
rotation: double.tryParse(driver.rotation.toString()) ?? 0,
|
||
);
|
||
|
||
// 5️⃣ Draw polyline
|
||
addPolyLine(polylineCoordinates);
|
||
}
|
||
|
||
/// Helper: safely convert to LatLng if valid
|
||
LatLng? _toLatLng(double? lat, double? lng) {
|
||
if (lat == null || lng == null) return null;
|
||
return LatLng(lat, lng);
|
||
}
|
||
|
||
/// Helper: fetch polyline safely
|
||
Future<List<LatLng>> _fetchPolyline(LatLng origin, LatLng destination) async {
|
||
try {
|
||
final result = await polylinePoints.value.getRouteBetweenCoordinates(
|
||
request: PolylineRequest(
|
||
origin: PointLatLng(origin.latitude, origin.longitude),
|
||
destination: PointLatLng(destination.latitude, destination.longitude),
|
||
mode: TravelMode.driving,
|
||
),
|
||
);
|
||
|
||
if (result.points.isEmpty) return [];
|
||
|
||
return result.points.map((p) => LatLng(p.latitude, p.longitude)).toList();
|
||
} catch (e, st) {
|
||
debugPrint("❌ getDirections _fetchPolyline error: $e\n$st");
|
||
return [];
|
||
}
|
||
}
|
||
|
||
void addPolyLine(List<LatLng> polylineCoordinates) {
|
||
// mapOsmController.clearAllRoads();
|
||
PolylineId id = const PolylineId("poly");
|
||
Polyline polyline = Polyline(
|
||
polylineId: id,
|
||
color: AppThemeData.primary300,
|
||
points: polylineCoordinates,
|
||
width: 8,
|
||
geodesic: true,
|
||
);
|
||
polyLines[id] = polyline;
|
||
update();
|
||
updateCameraLocation(polylineCoordinates.first, mapController);
|
||
}
|
||
|
||
Future<void> updateCameraLocation(
|
||
LatLng source,
|
||
GoogleMapController? mapController,
|
||
) async {
|
||
mapController!.animateCamera(
|
||
CameraUpdate.newCameraPosition(
|
||
CameraPosition(
|
||
target: source,
|
||
zoom: currentOrder.value.id == null || currentOrder.value.status == Constant.driverPending ? 16 : 20,
|
||
bearing: double.parse(driverModel.value.rotation.toString()),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
void animateToSource() {
|
||
double lat = 0.0;
|
||
double lng = 0.0;
|
||
final loc = driverModel.value.location;
|
||
if (loc != null) {
|
||
// Use string parsing to avoid nullable-toDouble issues and handle numbers/strings.
|
||
lat = double.tryParse('${loc.latitude}') ?? 0.0;
|
||
lng = double.tryParse('${loc.longitude}') ?? 0.0;
|
||
}
|
||
_updateCurrentLocationMarkers();
|
||
osmMapController.move(location.LatLng(lat, lng), 16);
|
||
}
|
||
|
||
void _updateCurrentLocationMarkers() async {
|
||
try {
|
||
final loc = driverModel.value.location;
|
||
final latLng = _safeLatLngFromLocation(loc);
|
||
|
||
// Update reactive current location
|
||
current.value = location.LatLng(latLng.latitude, latLng.longitude);
|
||
|
||
// --- OSM Section ---
|
||
try {
|
||
setOsmMapMarker();
|
||
|
||
if (latLng.latitude != 0.0 || latLng.longitude != 0.0) {
|
||
osmMapController.move(location.LatLng(latLng.latitude, latLng.longitude), 16);
|
||
}
|
||
} catch (e) {
|
||
print("OSM map move ignored (controller not ready): $e");
|
||
}
|
||
|
||
// --- GOOGLE MAP Section ---
|
||
try {
|
||
// Remove old driver marker
|
||
markers.remove("Driver");
|
||
|
||
// Create new Google Marker
|
||
markers["Driver"] = Marker(
|
||
markerId: const MarkerId("Driver"),
|
||
infoWindow: const InfoWindow(title: "Driver"),
|
||
position: LatLng(current.value.latitude, current.value.longitude),
|
||
icon: taxiIcon!,
|
||
rotation: _safeRotation(),
|
||
anchor: const Offset(0.5, 0.5),
|
||
flat: true,
|
||
);
|
||
|
||
// Animate camera to current driver location
|
||
if (mapController != null && !(current.value.latitude == 0.0 && current.value.longitude == 0.0)) {
|
||
mapController!.animateCamera(
|
||
CameraUpdate.newCameraPosition(
|
||
CameraPosition(
|
||
target: LatLng(current.value.latitude, current.value.longitude),
|
||
zoom: 16,
|
||
bearing: _safeRotation(),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
} catch (e) {
|
||
print("Google map update ignored (controller not ready): $e");
|
||
}
|
||
|
||
update();
|
||
} catch (e) {
|
||
print("_updateCurrentLocationMarkers error: $e");
|
||
}
|
||
}
|
||
|
||
double _safeRotation() {
|
||
return double.tryParse(driverModel.value.rotation.toString()) ?? 0.0;
|
||
}
|
||
|
||
LatLng _safeLatLngFromLocation(dynamic loc) {
|
||
final lat = (loc?.latitude is num) ? loc.latitude.toDouble() : 0.0;
|
||
final lng = (loc?.longitude is num) ? loc.longitude.toDouble() : 0.0;
|
||
return LatLng(lat, lng);
|
||
}
|
||
|
||
Rx<location.LatLng> source = location.LatLng(21.1702, 72.8311).obs; // Start (e.g., Surat)
|
||
Rx<location.LatLng> current = location.LatLng(21.1800, 72.8400).obs; // Moving marker
|
||
Rx<location.LatLng> destination = location.LatLng(21.2000, 72.8600).obs; // Destination
|
||
|
||
void setOsmMapMarker() {
|
||
osmMarkers.value = [
|
||
flutterMap.Marker(
|
||
point: current.value,
|
||
width: 45,
|
||
height: 45,
|
||
rotate: true,
|
||
child: Image.asset('assets/images/food_delivery.png'),
|
||
),
|
||
flutterMap.Marker(
|
||
point: source.value,
|
||
width: 40,
|
||
height: 40,
|
||
child: Image.asset('assets/images/location_black3x.png'),
|
||
),
|
||
flutterMap.Marker(
|
||
point: destination.value,
|
||
width: 40,
|
||
height: 40,
|
||
child: Image.asset('assets/images/location_orange3x.png'),
|
||
)
|
||
];
|
||
}
|
||
|
||
void getOSMPolyline() async {
|
||
try {
|
||
if (currentOrder.value.id != null) {
|
||
if (currentOrder.value.status != Constant.driverPending) {
|
||
print("Order Status :: ${currentOrder.value.status} :: OrderId :: ${currentOrder.value.id}} ::");
|
||
if (currentOrder.value.status == Constant.orderShipped) {
|
||
current.value = location.LatLng(driverModel.value.location!.latitude ?? 0.0, driverModel.value.location!.longitude ?? 0.0);
|
||
destination.value = location.LatLng(
|
||
currentOrder.value.vendor!.latitude ?? 0.0,
|
||
currentOrder.value.vendor!.longitude ?? 0.0,
|
||
);
|
||
animateToSource();
|
||
fetchRoute(current.value, destination.value).then((value) {
|
||
setOsmMapMarker();
|
||
});
|
||
} else if (currentOrder.value.status == Constant.orderInTransit) {
|
||
print(":::::::::::::${currentOrder.value.status}::::::::::::::::::44");
|
||
current.value = location.LatLng(driverModel.value.location!.latitude ?? 0.0, driverModel.value.location!.longitude ?? 0.0);
|
||
destination.value = location.LatLng(
|
||
currentOrder.value.address!.location!.latitude ?? 0.0,
|
||
currentOrder.value.address!.location!.longitude ?? 0.0,
|
||
);
|
||
setOsmMapMarker();
|
||
fetchRoute(current.value, destination.value).then((value) {
|
||
setOsmMapMarker();
|
||
});
|
||
animateToSource();
|
||
}
|
||
} else {
|
||
print("====>5");
|
||
current.value =
|
||
location.LatLng(currentOrder.value.author!.location!.latitude ?? 0.0, currentOrder.value.author!.location!.longitude ?? 0.0);
|
||
|
||
destination.value = location.LatLng(currentOrder.value.vendor!.latitude ?? 0.0, currentOrder.value.vendor!.longitude ?? 0.0);
|
||
animateToSource();
|
||
fetchRoute(current.value, destination.value).then((value) {
|
||
setOsmMapMarker();
|
||
});
|
||
animateToSource();
|
||
}
|
||
}
|
||
} catch (e) {
|
||
print('Error: $e');
|
||
}
|
||
}
|
||
|
||
RxList<location.LatLng> routePoints = <location.LatLng>[].obs;
|
||
|
||
Future<void> fetchRoute(location.LatLng source, location.LatLng destination) async {
|
||
final url = Uri.parse(
|
||
'https://router.project-osrm.org/route/v1/driving/${source.longitude},${source.latitude};${destination.longitude},${destination.latitude}?overview=full&geometries=geojson',
|
||
);
|
||
|
||
final response = await http.get(url);
|
||
|
||
if (response.statusCode == 200) {
|
||
final decoded = json.decode(response.body);
|
||
final geometry = decoded['routes'][0]['geometry']['coordinates'];
|
||
|
||
routePoints.clear();
|
||
for (var coord in geometry) {
|
||
final lon = coord[0];
|
||
final lat = coord[1];
|
||
routePoints.add(location.LatLng(lat, lon));
|
||
}
|
||
} else {
|
||
print("Failed to get route: ${response.body}");
|
||
}
|
||
}
|
||
}
|