import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:geolocator/geolocator.dart'; import 'package:http/http.dart' as http; import 'package:nearle/views/helpers/constants/apiconstants.dart'; import 'package:nearle/providers/deliverylog/deliverylog_provider.dart'; import 'dart:io'; import 'package:minio/minio.dart'; import 'package:minio/io.dart'; import 'dart:math'; import 'package:nearle/utils/kalman_filter.dart'; import 'package:nearle/utils/mqtt_service.dart'; class DeliveriesController extends GetxController { NearleKalmanFilter? _kf; DateTime? _lastUpdateTime; final UpdateDeliveryProvider _updateProvider = UpdateDeliveryProvider(); bool _isSuccess(Map? resp) { final status = resp?['status']; if (status is bool) return status; if (status is String) { final v = status.toLowerCase(); return v == 'true' || v == 'success' || v == 'ok' || v == 'accepted'; } return false; } // UI flags similar to xpressrider for parity final RxBool arrivedShimmer = false.obs; final RxBool deliveredShimmer = false.obs; final RxBool isPipEnabled = false.obs; // Distance tracking for PiP mode final RxDouble deliverableDistance = 0.0.obs; // Distance in meters // Times captured in ISO-like format (yyyy-MM-dd HH:mm:ss) String _arrivedTime = ''; String _pickedTime = ''; String _activeTime = ''; String _deliveredTime = ''; String _cancelledTime = ''; // Cached last known lat/lng from device final RxString currentLat = '0'.obs; final RxString currentLng = '0'.obs; // Bonus Points Tracking final RxInt lastBonusPoints = 0.obs; // Trigger to refresh deliveries list across screens final RxInt refreshTrigger = 0.obs; void triggerRefresh() { refreshTrigger.value++; } // ---------- UPLOAD IMAGE TO DO SPACES ---------- Future uploadProofImage( File imageFile, String folderName, int userId, int deliveryId, ) async { try { // final rng = Random(); // Unused const String region = "sgp1"; const String accessKey = "DO00NQER7N2FRYZAB2HR"; const String secretKey = "nMDewX25IBEu1FM5dakK+v28/WbW3TzBAwq913+dxP0"; const String bucketName = "nearle"; // folderName will be "picked" or "delivered" // File name final now = DateTime.now(); final dateStr = "${now.year}${now.month.toString().padLeft(2, '0')}${now.day.toString().padLeft(2, '0')}"; final timeStr = "${now.hour.toString().padLeft(2, '0')}${now.minute.toString().padLeft(2, '0')}${now.second.toString().padLeft(2, '0')}"; final String fileName = '$folderName-$deliveryId-$dateStr-$timeStr.jpg'; // Object path inside the bucket // final String objectPath = "$folderName/$fileName"; final String objectPath = "support/$fileName"; // CDN URL you want final String cdnUrl = "https://images.nearle.app/$objectPath"; // Initialize Minio final minio = Minio( endPoint: "$region.digitaloceanspaces.com", accessKey: accessKey, secretKey: secretKey, region: region, useSSL: true, ); debugPrint("Uploading Proof: $objectPath"); // Upload to DO Spaces await minio.fPutObject( bucketName, objectPath, imageFile.path, metadata: {"Content-Type": "image/jpeg", "x-amz-acl": "public-read"}, ); debugPrint("Proof Uploaded Successfully: $cdnUrl"); return cdnUrl; } catch (e) { debugPrint("Proof Upload error: $e"); Get.snackbar("Error", "Image upload failed. Please try again."); return null; } } // ---------------- Location helpers ---------------- Future> _ensureLatLng(String lat, String lng) async { String outLat = lat; String outLng = lng; try { final needsFetch = (lat == '0' || lat.isEmpty || lng == '0' || lng.isEmpty); // Fast path: if we have valid coordinates, use them immediately if (!needsFetch) return {'lat': outLat, 'lng': outLng}; // Reuse recently cached coordinates first if fresh (e.g. within 30s) // For now just check if they exist to save time if (currentLat.value.isNotEmpty && currentLat.value != '0' && currentLng.value.isNotEmpty && currentLng.value != '0') { return {'lat': currentLat.value, 'lng': currentLng.value}; } final serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) return {'lat': outLat, 'lng': outLng}; LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { return {'lat': outLat, 'lng': outLng}; } Position? pos; // 1. Try Last Known Position (Instant) try { pos = await Geolocator.getLastKnownPosition(); } catch (_) {} // 2. If no last known, try current with a single balanced timeout // Reduced complicated retry logic to one solid attempt if (pos == null) { try { pos = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, // Better accuracy for deliveries timeLimit: const Duration(seconds: 4), ); } catch (_) { // Fallback to low accuracy if high fails quickly try { pos = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.low, timeLimit: const Duration(seconds: 2), ); } catch (_) {} } } if (pos != null) { final now = DateTime.now(); double outLatDouble = pos.latitude; double outLngDouble = pos.longitude; if (_kf == null) { _kf = NearleKalmanFilter(lat: outLatDouble, lng: outLngDouble); } else { final double dt = _lastUpdateTime != null ? now.difference(_lastUpdateTime!).inMilliseconds / 1000.0 : 30.0; _kf!.predict(dt); _kf!.update(outLatDouble, outLngDouble); outLatDouble = _kf!.x[0]; outLngDouble = _kf!.x[1]; } _lastUpdateTime = now; outLat = outLatDouble.toStringAsFixed(6); outLng = outLngDouble.toStringAsFixed(6); currentLat.value = outLat; currentLng.value = outLng; } return {'lat': outLat, 'lng': outLng}; } catch (_) { return {'lat': outLat, 'lng': outLng}; } } // Picked Future updatePickedStatus({ required int deliveryId, required int orderHeaderId, required int pickupLocationId, String ridersLat = '0', String ridersLng = '0', String pickupLat = '0', String pickupLng = '0', String address = '', String city = '', String state = '', String suburb = '', String postcode = '', String deliveryType = '', String notes = '', double actualKms = 0.0, String? proofImage, // New parameter }) async { try { final now = DateTime.now(); _pickedTime = _formatDateTimeFull(now); final ll = await _ensureLatLng(ridersLat, ridersLng); final rLat = double.tryParse(ll['lat'] ?? '0') ?? 0.0; final rLng = double.tryParse(ll['lng'] ?? '0') ?? 0.0; final pLat = double.tryParse(pickupLat) ?? 0.0; final pLng = double.tryParse(pickupLng) ?? 0.0; // Geofence Check: Picked -> Pickup Location final inFence = await _checkGeofence(pLat, pLng, rLat, rLng, 'Picked'); if (!inFence) return false; // Save pickup location ONLY if it hasn't been saved yet (first pickup only) final existingPickup = await _getPickupLocation(); if (existingPickup['lat']!.isEmpty && pickupLat != '0' && pickupLng != '0') { await _savePickupLocation(pickupLat, pickupLng); } else { debugPrint('[PICKED] Pickup location already exists, not overwriting'); } // Calculate distance from rider location to pickup location if coordinates are available double calculatedKms = actualKms; if (actualKms == 0.0) { try { final riderLat = double.tryParse(ll['lat'] ?? '0') ?? 0.0; final riderLng = double.tryParse(ll['lng'] ?? '0') ?? 0.0; final pickLat = double.tryParse(pickupLat) ?? 0.0; final pickLng = double.tryParse(pickupLng) ?? 0.0; if (riderLat != 0 && riderLng != 0 && pickLat != 0 && pickLng != 0) { final distanceMeters = Geolocator.distanceBetween( riderLat, riderLng, pickLat, pickLng, ); calculatedKms = distanceMeters / 1000.0; debugPrint( '[PICKED] Calculated distance: ${calculatedKms.toStringAsFixed(2)} km from rider to pickup', ); } } catch (e) { debugPrint('[PICKED] Error calculating distance: $e'); } } final payload = { 'deliveryid': deliveryId, 'orderheaderid': orderHeaderId, 'pickuplocationid': pickupLocationId, 'orderstatus': 'picked', 'pickuptime': _pickedTime, 'riderslat': ll['lat'], 'riderslon': ll['lng'], 'deliverylat': '', 'deliverylong': '', 'actualkms': calculatedKms.toStringAsFixed(2), 'deliveryamt': 0.0, 'deliverytype': deliveryType, 'address': address, 'city': city, 'state': state, 'suburb': suburb, 'postcode': postcode, 'notes': notes, 'pickupimage': proofImage ?? '', // Standard key 'proofimage': proofImage ?? '', // Fallback/Alternative key just in case }; debugPrint('[PICKED] Payload: $payload'); final url = _resolveUpdateUrl(); final resp = await _updateProvider.updatePickedDelivery(payload, url); final ok = _isSuccess(resp); if (!ok) { debugPrint('[UPDATE][PICKED][FAILED] resp=${jsonEncode(resp)}'); } else { // --- MQTT LOGIC --- NearleMqttService().publishLog('delivery_picked', payload); } return ok; } catch (e) { debugPrint('[UPDATE][PICKED][ERROR] $e'); return false; } } // Delivered Future updateDeliveredStatus({ required int deliveryId, required int orderHeaderId, String ridersLat = '0', String ridersLng = '0', int deliveryLocationId = 0, int smsDelivery = 0, String pickupLat = '0', String pickupLng = '0', String deliveryLat = '0', String deliveryLng = '0', String notes = '', double deliveryAmount = 0.0, double actualKms = 0.0, double collectionAmount = 0.0, double collectedAmount = 0.0, int collectionStatus = 0, bool wasSkipped = false, String orderId = '', // New parameter for timer/bonus logic String? proofImage, // New parameter }) async { try { deliveredShimmer.value = true; // โœ… PARALLEL OPTIMIZATION: Start fetching Prefs immediately final prefsFuture = SharedPreferences.getInstance(); final now = DateTime.now(); _deliveredTime = _formatDateTimeFull(now); final ll = await _ensureLatLng(ridersLat, ridersLng); final rLat = double.tryParse(ll['lat'] ?? '0') ?? 0.0; final rLng = double.tryParse(ll['lng'] ?? '0') ?? 0.0; final dLat = double.tryParse(deliveryLat) ?? 0.0; final dLng = double.tryParse(deliveryLng) ?? 0.0; // Geofence Check: Delivered -> Delivery Location final inFence = await _checkGeofence(dLat, dLng, rLat, rLng, 'Delivered'); if (!inFence) { deliveredShimmer.value = false; return false; } debugPrint( '[DELIVERED] Rider GPS location: lat=${ll['lat']}, lng=${ll['lng']}', ); // Wait for prefs to be ready final prefs = await prefsFuture; // ---------------- RIDER TIME (minutes) ---------------- int riderTimeMinutes = 0; try { final key = 'ridertime_start_$deliveryId'; final startStr = prefs.getString(key); if (startStr != null && startStr.isNotEmpty) { final start = DateTime.tryParse(startStr); if (start != null) { final diff = now.difference(start).inSeconds / 60.0; if (diff > 0) { // Round: 2.7 -> 3, 2.5 -> 3, 2.4 -> 2, etc. riderTimeMinutes = diff.round(); } } } debugPrint('[DELIVERED] riderTimeMinutes=$riderTimeMinutes'); } catch (e) { debugPrint('[DELIVERED] Error computing ridertime: $e'); } // CRITICAL: Always calculate riderkms - even for small distances (10 meters = 0.01 km) // This ensures riderkms is ALWAYS passed correctly, never null double calculatedRiderKms = 0.0; final riderLat = double.tryParse(ll['lat'] ?? '0') ?? 0.0; final riderLng = double.tryParse(ll['lng'] ?? '0') ?? 0.0; // PRIORITY 1: Use cumulative distance tracked from active to delivered (most accurate) // This is the actual distance the rider traveled, calculated from GPS points every 30 seconds try { final cumulativeKmStr = prefs.getString('delivery_tracking_${deliveryId}_cumulativeKm') ?? ''; if (cumulativeKmStr.isNotEmpty) { final cumulativeKm = double.tryParse(cumulativeKmStr) ?? 0.0; if (cumulativeKm > 0) { calculatedRiderKms = cumulativeKm; debugPrint( '[DELIVERED] โœ… Using CUMULATIVE tracked distance: ${calculatedRiderKms.toStringAsFixed(4)} km', ); debugPrint( '[DELIVERED] ๐Ÿ“Š This is the actual distance traveled from active to delivered', ); } } } catch (e) { debugPrint('[DELIVERED] Error getting cumulative distance: $e'); } // PRIORITY 2: Only use actualKms if cumulative distance not available and it's greater than 0 if (calculatedRiderKms == 0.0 && actualKms > 0.0 && riderLat != 0 && riderLng != 0) { calculatedRiderKms = actualKms; debugPrint( '[DELIVERED] Using provided actualKms: ${calculatedRiderKms.toStringAsFixed(2)} km', ); } // Always try to calculate distance if we have valid rider coordinates // CRITICAL: Use Google Maps Directions API for accurate ROAD distance (not straight-line) if (calculatedRiderKms == 0.0 && riderLat != 0 && riderLng != 0) { try { // Method 1: Try Start Location (Anti-Cheat) - most accurate final startLoc = await _getDeliveryStartLocation(deliveryId); final startLatStr = startLoc['lat'] ?? ''; final startLngStr = startLoc['lng'] ?? ''; if (startLatStr.isNotEmpty && startLngStr.isNotEmpty) { final startLat = double.tryParse(startLatStr) ?? 0.0; final startLng = double.tryParse(startLngStr) ?? 0.0; if (startLat != 0 && startLng != 0) { // Try Google Maps route distance first (accurate road distance) final routeKm = await _getRouteDistanceKm( startLat, startLng, riderLat, riderLng, ); if (routeKm != null && routeKm > 0) { calculatedRiderKms = routeKm; debugPrint( '[DELIVERED] โœ… Route distance (Start Location): ${calculatedRiderKms.toStringAsFixed(4)} km', ); } else { // Fallback to straight-line if API fails final distanceMeters = Geolocator.distanceBetween( startLat, startLng, riderLat, riderLng, ); calculatedRiderKms = distanceMeters / 1000.0; debugPrint( '[DELIVERED] โš ๏ธ Fallback straight-line (Start Location): ${distanceMeters.toStringAsFixed(2)} meters = ${calculatedRiderKms.toStringAsFixed(4)} km', ); } } } // Method 2: If start location failed, try provided pickup location if (calculatedRiderKms == 0.0 && pickupLat != '0' && pickupLng != '0') { final pickLat = double.tryParse(pickupLat) ?? 0.0; final pickLng = double.tryParse(pickupLng) ?? 0.0; if (pickLat != 0 && pickLng != 0) { // Try Google Maps route distance first final routeKm = await _getRouteDistanceKm( pickLat, pickLng, riderLat, riderLng, ); if (routeKm != null && routeKm > 0) { calculatedRiderKms = routeKm; debugPrint( '[DELIVERED] โœ… Route distance (Pickup Location): ${calculatedRiderKms.toStringAsFixed(4)} km', ); } else { // Fallback to straight-line final distanceMeters = Geolocator.distanceBetween( pickLat, pickLng, riderLat, riderLng, ); calculatedRiderKms = distanceMeters / 1000.0; debugPrint( '[DELIVERED] โš ๏ธ Fallback straight-line (Pickup Location): ${distanceMeters.toStringAsFixed(2)} meters = ${calculatedRiderKms.toStringAsFixed(4)} km', ); } } } // Method 3: Fallback to saved pickup location if (calculatedRiderKms == 0.0) { final pickupLoc = await _getPickupLocation(); final pickLat = double.tryParse(pickupLoc['lat'] ?? '') ?? 0.0; final pickLng = double.tryParse(pickupLoc['lng'] ?? '') ?? 0.0; if (pickLat != 0 && pickLng != 0) { // Try Google Maps route distance first final routeKm = await _getRouteDistanceKm( pickLat, pickLng, riderLat, riderLng, ); if (routeKm != null && routeKm > 0) { calculatedRiderKms = routeKm; debugPrint( '[DELIVERED] โœ… Route distance (Saved Pickup): ${calculatedRiderKms.toStringAsFixed(4)} km', ); } else { // Fallback to straight-line final distanceMeters = Geolocator.distanceBetween( pickLat, pickLng, riderLat, riderLng, ); calculatedRiderKms = distanceMeters / 1000.0; debugPrint( '[DELIVERED] โš ๏ธ Fallback straight-line (Saved Pickup): ${distanceMeters.toStringAsFixed(2)} meters = ${calculatedRiderKms.toStringAsFixed(4)} km', ); } } } // Method 4: Final fallback - calculate from delivery location to current location if (calculatedRiderKms == 0.0 && deliveryLat != '0' && deliveryLng != '0') { final dLat = double.tryParse(deliveryLat) ?? 0.0; final dLng = double.tryParse(deliveryLng) ?? 0.0; if (dLat != 0 && dLng != 0) { // Try Google Maps route distance first final routeKm = await _getRouteDistanceKm( dLat, dLng, riderLat, riderLng, ); if (routeKm != null && routeKm > 0) { calculatedRiderKms = routeKm; debugPrint( '[DELIVERED] โœ… Route distance (Delivery Location): ${calculatedRiderKms.toStringAsFixed(4)} km', ); } else { // Fallback to straight-line final distanceMeters = Geolocator.distanceBetween( dLat, dLng, riderLat, riderLng, ); calculatedRiderKms = distanceMeters / 1000.0; debugPrint( '[DELIVERED] โš ๏ธ Fallback straight-line (Delivery Location): ${distanceMeters.toStringAsFixed(2)} meters = ${calculatedRiderKms.toStringAsFixed(4)} km', ); } } } } catch (e) { debugPrint('[DELIVERED] Error calculating distance: $e'); } } // CRITICAL: Ensure riderkms is never 0 or null - use minimum value if calculation failed // Even 10 meters should be shown (0.01 km) if (calculatedRiderKms == 0.0) { debugPrint( '[DELIVERED] โš ๏ธ WARNING: Could not calculate distance, using minimum 0.01 km (10 meters)', ); calculatedRiderKms = 0.01; // Minimum 10 meters } debugPrint( '[DELIVERED] Final riderkms: ${calculatedRiderKms.toStringAsFixed(4)} km', ); // ---- BONUS POINTS LOGIC ---- int bonusPoints = 0; if (orderId.isNotEmpty) { try { final endKey = 'eta_endtime_$orderId'; final endSeconds = prefs.getInt(endKey); if (endSeconds != null) { final currentSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000; // If delivered before or at the end time, give bonus points if (currentSeconds <= endSeconds) { // Bonus points = riderkms rounded bonusPoints = calculatedRiderKms.round(); debugPrint( '[DELIVERED] ๐Ÿ† ON TIME! Bonus Points: $bonusPoints (RiderKms: $calculatedRiderKms)', ); } else { debugPrint( '[DELIVERED] โณ LATE! No Bonus Points. (Deadline: $endSeconds, Now: $currentSeconds)', ); } } else { debugPrint('[DELIVERED] No ETA timer found for bonus logic.'); } } catch (e) { debugPrint('[DELIVERED] Error calculating bonus points: $e'); } } final double riderChargeRate = await _getRiderChargeRate(); // ---- SKIP PENALTY CHECK ---- // If the rider has exceeded skip limits in the last 3 hours, forfeit bonus points final skipStatus = await checkSkipStatus(); if (skipStatus['penalty'] == true) { if (bonusPoints > 0) { debugPrint('[DELIVERED] โš ๏ธ Bonus points ($bonusPoints) forfeited due to skip penalty.'); bonusPoints = 0; } } final double riderChargesAmountRaw = riderChargeRate > 0 && calculatedRiderKms > 0 ? calculatedRiderKms * riderChargeRate : 0.0; // Ensure ridercharges is sent with 2 decimal places (Decimal(10,2) style) final double riderChargesAmount = double.parse( riderChargesAmountRaw.toStringAsFixed(2), ); // CRITICAL: Always set deliverytime to current time right before creating payload // This ensures deliverytime is ALWAYS passed when confirming delivery, no matter what // Setting it here (right before payload) ensures it's the exact time of confirmation final currentTime = DateTime.now(); final currentDeliveryTime = _formatDateTimeFull(currentTime); _deliveredTime = currentDeliveryTime; // Update the instance variable too debugPrint('[DELIVERED] Setting deliverytime to: $currentDeliveryTime'); // CRITICAL: Ensure riderkms is always a valid string, never null or empty final riderKmsString = calculatedRiderKms > 0 ? calculatedRiderKms.toStringAsFixed( 4, ) // Use 4 decimals to show even 10 meters (0.0100 km) : '0.0100'; // Fallback minimum (10 meters) // CRITICAL: Validate coordinates before creating payload - NEVER send '0' or null final ridersLatStr = ll['lat'] ?? '0'; final ridersLngStr = ll['lng'] ?? '0'; final ridersLatDouble = double.tryParse(ridersLatStr) ?? 0.0; final ridersLngDouble = double.tryParse(ridersLngStr) ?? 0.0; if (ridersLatDouble == 0 || ridersLngDouble == 0 || ridersLatDouble.abs() > 90 || ridersLngDouble.abs() > 180) { debugPrint( '[DELIVERED] โŒ ERROR: Invalid rider coordinates (lat=$ridersLatStr, lng=$ridersLngStr) - cannot proceed', ); deliveredShimmer.value = false; return false; // Don't send invalid coordinates } // Validate delivery coordinates final deliveryLatDouble = double.tryParse(deliveryLat) ?? 0.0; final deliveryLngDouble = double.tryParse(deliveryLng) ?? 0.0; if (deliveryLatDouble == 0 || deliveryLngDouble == 0 || deliveryLatDouble.abs() > 90 || deliveryLngDouble.abs() > 180) { debugPrint( '[DELIVERED] โŒ ERROR: Invalid delivery coordinates (lat=$deliveryLat, lng=$deliveryLng) - cannot proceed', ); deliveredShimmer.value = false; return false; // Don't send invalid coordinates } debugPrint( '[DELIVERED] โœ… Validated coordinates - Rider: ($ridersLatStr, $ridersLngStr), Delivery: ($deliveryLat, $deliveryLng)', ); final payload = { 'deliveryid': deliveryId, 'orderheaderid': orderHeaderId, 'deliverylocationid': deliveryLocationId, 'orderstatus': 'delivered', 'deliveredtime': currentDeliveryTime, 'deliverytime': currentDeliveryTime, // ALWAYS pass current time - required field, set right before payload 'smsdelivery': smsDelivery, 'riderslat': ridersLatStr, // Guaranteed non-zero and valid 'riderslon': ridersLngStr, // Guaranteed non-zero and valid 'raw_latitude': ll['raw_lat'] ?? '0', 'raw_longitude': ll['raw_lng'] ?? '0', 'velocity_lat': ll['velocity_lat'] ?? '0', 'velocity_lng': ll['velocity_lng'] ?? '0', 'speed': ll['speed'] ?? '0', 'heading': ll['heading'] ?? '0', 'pickuplat': pickupLat, 'pickuplong': pickupLng, 'deliverylat': deliveryLat, // Guaranteed non-zero and valid 'deliverylong': deliveryLng, // Guaranteed non-zero and valid 'riderkms': riderKmsString, // ALWAYS pass riderkms - never null, shows even 10 meters (0.0100 km) 'ridercharges': riderChargesAmount, 'deliveryamt': deliveryAmount, 'collectionamt': collectionAmount, 'collectedamt': collectedAmount, 'collectionstatus': collectionStatus, 'ridertime': riderTimeMinutes, 'notes': notes, 'wasskipped': wasSkipped, 'bonuspts': bonusPoints, // โœ… ADDED BONUS POINTS 'dropimage': proofImage ?? '', // Include in payload }; // Update observable for UI lastBonusPoints.value = bonusPoints; debugPrint( '[DELIVERED] Payload includes deliverytime: $currentDeliveryTime', ); debugPrint('[DELIVERED] Payload includes riderkms: $riderKmsString km'); debugPrint('[DELIVERED] Payload includes bonuspts: $bonusPoints'); final url = _resolveUpdateUrl(); final resp = await _updateProvider.updateDelivery(payload, url); final ok = _isSuccess(resp); // Save current RIDER GPS location as last delivery location for next delivery if (ok) { final actualLat = ll['lat'] ?? '0'; final actualLng = ll['lng'] ?? '0'; if (actualLat != '0' && actualLng != '0') { await _saveLastDeliveryLocation(actualLat, actualLng); } // CRITICAL: Clean up cumulative distance tracking after delivery is completed try { final cumulativeKm = prefs.getString('delivery_tracking_${deliveryId}_cumulativeKm') ?? ''; if (cumulativeKm.isNotEmpty) { debugPrint( '[DELIVERED] ๐Ÿ“Š Final cumulative distance used: $cumulativeKm km', ); } // Clean up tracking data await prefs.remove('delivery_tracking_${deliveryId}_lastLat'); await prefs.remove('delivery_tracking_${deliveryId}_lastLng'); await prefs.remove('delivery_tracking_${deliveryId}_cumulativeKm'); await prefs.remove('delivery_tracking_${deliveryId}_lastUpdateMs'); await prefs.remove('ridertime_start_$deliveryId'); await prefs.remove('delivery_proximity_alerted_$deliveryId'); final activeTracking = prefs.getStringList('active_tracking_delivery_ids') ?? []; if (activeTracking.contains(deliveryId.toString())) { activeTracking.remove(deliveryId.toString()); await prefs.setStringList('active_tracking_delivery_ids', activeTracking); } // โœ… Clean up ETA timer if (orderId.isNotEmpty) { await prefs.remove('eta_endtime_$orderId'); } debugPrint( '[DELIVERED] ๐Ÿงน Cleaned up distance tracking for deliveryId: $deliveryId', ); } catch (e) { debugPrint('[DELIVERED] Error cleaning up tracking: $e'); } } // Reset foreground delivery notification flag await setNotificationSent(false); deliveredShimmer.value = false; if (!ok) { debugPrint('[UPDATE][DELIVERED][FAILED] resp=${jsonEncode(resp)}'); } else { // --- MQTT LOGIC --- NearleMqttService().publishLog('delivery_completed', payload); } return ok; } catch (e) { debugPrint('[UPDATE][DELIVERED][ERROR] $e'); deliveredShimmer.value = false; return false; } } // ---------------- Date/Time helpers ---------------- String _two(int n) => n.toString().padLeft(2, '0'); String _formatDateTimeFull(DateTime dt) { final y = dt.year.toString(); final m = _two(dt.month); final d = _two(dt.day); final hh = _two(dt.hour); final mm = _two(dt.minute); final ss = _two(dt.second); return "$y-$m-$d $hh:$mm:$ss"; } String _resolveUpdateUrl() { return ApiConstants.mainRoute == 'live' ? ApiConstants.updateDeliveryLive : ApiConstants.updateDeliveryDev; } // ---------------- SharedPrefs helpers ---------------- Future setNotificationSent(bool value) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setBool('notificationSent', value); } catch (_) {} } Future getLogSecondsOrDefault([int fallback = 60]) async { try { final prefs = await SharedPreferences.getInstance(); return prefs.getInt('logSeconds') ?? fallback; } catch (_) { return fallback; } } Future persistDeliveryLogSnapshot( List> logs, ) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('deliveryLog', jsonEncode(logs)); } catch (_) {} } // ---------------- Geofencing helpers ---------------- Future _getDeliveryRadius() async { try { final prefs = await SharedPreferences.getInstance(); return prefs.getInt('deliveryradius') ?? 100; } catch (_) { return 100; } } // Helper method to show snackbar reliably in both debug and release builds void _showErrorSnackbar( String title, String message, { Color? bgColor, int seconds = 4, int retryCount = 0, }) { // Prevent infinite recursion if (retryCount > 3) { print('[GEOFENCE] Max retries reached for snackbar. Title: $title'); return; } try { final context = Get.key.currentContext; // Get bottom safe area padding final double bottomSafePadding = context != null ? MediaQuery.of(context).padding.bottom : 0; Get.snackbar( title, message, backgroundColor: bgColor ?? Colors.red, colorText: Colors.white, duration: Duration(seconds: seconds), snackPosition: SnackPosition.BOTTOM, isDismissible: true, shouldIconPulse: true, margin: EdgeInsets.fromLTRB( 12, 0, 12, 12 + bottomSafePadding, // SafeArea bottom ), borderRadius: 10, maxWidth: 400, forwardAnimationCurve: Curves.easeOutBack, reverseAnimationCurve: Curves.easeInBack, ); } catch (e) { print( '[GEOFENCE] Error showing snackbar (attempt ${retryCount + 1}): $e', ); print( '[GEOFENCE] GetX context available: ${Get.key.currentContext != null}', ); Future.delayed(const Duration(milliseconds: 400), () { try { Get.snackbar( title, message, backgroundColor: bgColor ?? Colors.red, colorText: Colors.white, snackPosition: SnackPosition.BOTTOM, ); } catch (e2) { print('[GEOFENCE] Retry snackbar failed: $e2'); if (retryCount < 2) { Future.delayed(const Duration(milliseconds: 300), () { _showErrorSnackbar( title, message, bgColor: bgColor, seconds: seconds, retryCount: retryCount + 1, ); }); } } }); } } Future _checkGeofence( double targetLat, double targetLng, double currentLat, double currentLng, String action, ) async { // Validate coordinates - ensure they are valid GPS coordinates final bool hasValidTarget = targetLat != 0 && targetLng != 0 && targetLat.abs() <= 90 && targetLng.abs() <= 180; final bool hasValidCurrent = currentLat != 0 && currentLng != 0 && currentLat.abs() <= 90 && currentLng.abs() <= 180; if (!hasValidTarget || !hasValidCurrent) { // If coordinates are missing or invalid, show error and block if (kDebugMode) { debugPrint( '[GEOFENCE] Missing or invalid coordinates for $action. Target: ($targetLat, $targetLng), Current: ($currentLat, $currentLng)', ); } // Show warning snackbar using helper method _showErrorSnackbar( 'Location Warning', 'Missing coordinates. Proceeding with update.', bgColor: Colors.orange, seconds: 3, ); return true; // Allow update to proceed despite missing coords } try { final radius = await _getDeliveryRadius(); final distance = Geolocator.distanceBetween( targetLat, targetLng, currentLat, currentLng, ); final distanceKm = distance / 1000.0; final radiusKm = radius / 1000.0; final distanceMeters = distance; if (kDebugMode) { debugPrint( '[GEOFENCE] Action: $action | Target: ($targetLat, $targetLng) | Current: ($currentLat, $currentLng) | Distance: ${distanceMeters.toStringAsFixed(1)}m (${distanceKm.toStringAsFixed(3)}km) | Radius: ${radius}m (${radiusKm.toStringAsFixed(3)}km)', ); } if (distance > radius) { // Show error snackbar with clear message using helper method _showErrorSnackbar( 'Location Error', 'You are too far from the location to mark as $action.\nDistance: ${distanceMeters.toStringAsFixed(0)} m (${distanceKm.toStringAsFixed(2)} km)\nRequired: Within $radius m', seconds: 5, ); return false; } return true; } catch (e) { if (kDebugMode) { debugPrint('[GEOFENCE] Error calculating distance: $e'); } // On error, show warning but allow (fail-safe) _showErrorSnackbar( 'Location Warning', 'Unable to verify distance. Proceeding with caution.', bgColor: Colors.orange, seconds: 3, ); return true; // Allow on error (fail-safe) } } // Get last delivery location (for calculating riderkms between deliveries) Future> _getLastDeliveryLocation() async { try { final prefs = await SharedPreferences.getInstance(); final lat = prefs.getString('lastDeliveryLat') ?? ''; final lng = prefs.getString('lastDeliveryLng') ?? ''; if (lat.isNotEmpty && lng.isNotEmpty) { return {'lat': lat, 'lng': lng}; } } catch (_) {} return {'lat': '', 'lng': ''}; } // Save last delivery location (rider's actual GPS location, not destination) Future _saveLastDeliveryLocation(String lat, String lng) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('lastDeliveryLat', lat); await prefs.setString('lastDeliveryLng', lng); debugPrint('[TRACKING] Saved last delivery location: lat=$lat, lng=$lng'); } catch (e) { debugPrint('[TRACKING] Error saving last delivery location: $e'); } } // Save start location for a specific delivery (when "Start Navigation" is slid) Future _saveDeliveryStartLocation( int deliveryId, String lat, String lng, ) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('delivery_start_${deliveryId}_lat', lat); await prefs.setString('delivery_start_${deliveryId}_lng', lng); debugPrint( '[TRACKING] Saved start location for delivery $deliveryId: lat=$lat, lng=$lng', ); } catch (e) { debugPrint('[TRACKING] Error saving delivery start location: $e'); } } // Get start location for a specific delivery Future> _getDeliveryStartLocation(int deliveryId) async { try { final prefs = await SharedPreferences.getInstance(); final lat = prefs.getString('delivery_start_${deliveryId}_lat') ?? ''; final lng = prefs.getString('delivery_start_${deliveryId}_lng') ?? ''; if (lat.isNotEmpty && lng.isNotEmpty) { return {'lat': lat, 'lng': lng}; } } catch (_) {} return {'lat': '', 'lng': ''}; } // Get pickup location (for first delivery riderkms calculation) Future> _getPickupLocation() async { try { final prefs = await SharedPreferences.getInstance(); final lat = prefs.getString('pickupLat') ?? ''; final lng = prefs.getString('pickupLng') ?? ''; if (lat.isNotEmpty && lng.isNotEmpty) { return {'lat': lat, 'lng': lng}; } } catch (_) {} return {'lat': '', 'lng': ''}; } // Google Maps API Key static const String _googleMapsApiKey = 'AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q'; // Get route distance from Google Maps Directions API (accurate road distance) // Returns distance in kilometers, or null if API fails Future _getRouteDistanceKm( double originLat, double originLng, double destLat, double destLng, ) async { try { // Validate coordinates if (originLat == 0 || originLng == 0 || destLat == 0 || destLng == 0 || originLat.abs() > 90 || originLng.abs() > 180 || destLat.abs() > 90 || destLng.abs() > 180) { debugPrint('[ROUTE] Invalid coordinates for route calculation'); return null; } // Google Maps Directions API endpoint final url = Uri.parse( 'https://maps.googleapis.com/maps/api/directions/json' '?origin=$originLat,$originLng' '&destination=$destLat,$destLng' '&key=$_googleMapsApiKey' '&units=metric' '&mode=driving', // Use driving mode for accurate road distance ); debugPrint( '[ROUTE] Requesting route distance from ($originLat, $originLng) to ($destLat, $destLng)', ); final response = await http .get(url) .timeout( const Duration(seconds: 5), onTimeout: () { debugPrint('[ROUTE] API timeout'); return http.Response('', 408); }, ); if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK' && data['routes'] != null && (data['routes'] as List).isNotEmpty) { final route = (data['routes'] as List).first; final legs = route['legs'] as List?; if (legs != null && legs.isNotEmpty) { double totalDistanceMeters = 0.0; for (final leg in legs) { final distance = leg['distance'] as Map?; if (distance != null && distance['value'] != null) { totalDistanceMeters += (distance['value'] as num).toDouble(); } } final distanceKm = totalDistanceMeters / 1000.0; debugPrint( '[ROUTE] โœ… Route distance: ${distanceKm.toStringAsFixed(4)} km (${totalDistanceMeters.toStringAsFixed(0)} meters)', ); return distanceKm; } } else { debugPrint('[ROUTE] โš ๏ธ API returned status: ${data['status']}'); } } else { debugPrint('[ROUTE] โš ๏ธ API error: ${response.statusCode}'); } } catch (e) { debugPrint('[ROUTE] โŒ Error getting route distance: $e'); } return null; // Return null if API fails - will fallback to straight-line } // Save pickup location Future _savePickupLocation(String lat, String lng) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('pickupLat', lat); await prefs.setString('pickupLng', lng); debugPrint('[TRACKING] Saved pickup location: lat=$lat, lng=$lng'); } catch (e) { debugPrint('[TRACKING] Error saving pickup location: $e'); } } Future _getRiderChargeRate() async { try { final prefs = await SharedPreferences.getInstance(); // Prefer new fuelcharge key; fall back to legacy firstmilecharge if needed Object? raw = prefs.get('fuelcharge'); raw ??= prefs.get('firstmilecharge'); if (raw is double) return raw; if (raw is int) return raw.toDouble(); if (raw is String) { return double.tryParse(raw) ?? 0.0; } } catch (_) {} return 0.0; } // Clear delivery tracking (when all deliveries are done or starting fresh) Future clearDeliveryTracking() async { try { final prefs = await SharedPreferences.getInstance(); await prefs.remove('lastDeliveryLat'); await prefs.remove('lastDeliveryLng'); await prefs.remove('pickupLat'); await prefs.remove('pickupLng'); debugPrint('[TRACKING] Cleared all delivery tracking data'); } catch (e) { debugPrint('[TRACKING] Error clearing delivery tracking: $e'); } } // ---------------- Status Updates ---------------- // Accepted Future updateAcceptedStatus({ required int deliveryId, required int orderHeaderId, String ridersLat = '0', String ridersLng = '0', String notes = '', }) async { try { final ll = await _ensureLatLng(ridersLat, ridersLng); final acceptedTime = _formatDateTimeFull(DateTime.now()); final payload = { 'deliveryid': deliveryId, 'orderheaderid': orderHeaderId, 'orderstatus': 'accepted', 'acceptedtime': acceptedTime, 'riderslat': ll['lat'], 'riderslon': ll['lng'], 'raw_latitude': ll['raw_lat'] ?? '0', 'raw_longitude': ll['raw_lng'] ?? '0', 'velocity_lat': ll['velocity_lat'] ?? '0', 'velocity_lng': ll['velocity_lng'] ?? '0', 'speed': ll['speed'] ?? '0', 'heading': ll['heading'] ?? '0', 'deliverylat': '', 'deliverylong': '', 'actualkms': '', 'deliveryamt': 0.0, 'notes': notes, }; final url = _resolveUpdateUrl(); final resp = await _updateProvider.updateDelivery(payload, url); final ok = _isSuccess(resp); if (!ok) { // Log entire response for debugging try { debugPrint('[UPDATE][ACCEPTED][FAILED] resp=${jsonEncode(resp)}'); } catch (_) {} } return ok; } catch (e) { debugPrint('[UPDATE][ACCEPTED][ERROR] $e'); return false; } } // Active (start navigation) Future updateActiveStatus({ required int deliveryId, required int orderHeaderId, String ridersLat = '0', String ridersLng = '0', String notes = '', String orderId = '', }) async { try { final now = DateTime.now(); _activeTime = _formatDateTimeFull(now); final prefs = await SharedPreferences.getInstance(); String startTime = prefs.getString('delivery_starttime_$deliveryId') ?? ''; if (startTime.isEmpty) { startTime = _formatDateTimeFull(now); } else { debugPrint('[UPDATE][ACTIVE] Using preserved starttime: $startTime'); } // Ensure we never send 0/0 for rider location. Map ll = await _ensureLatLng(ridersLat, ridersLng); if ((ll['lat'] ?? '0') == '0' || (ll['lng'] ?? '0') == '0') { try { final last = await Geolocator.getLastKnownPosition(); if (last != null) { ll = { 'lat': last.latitude.toStringAsFixed(6), 'lng': last.longitude.toStringAsFixed(6), }; } } catch (_) {} } if ((ll['lat'] ?? '0') == '0' || (ll['lng'] ?? '0') == '0') { try { final pos = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.low, timeLimit: const Duration(seconds: 3), ); ll = { 'lat': pos.latitude.toStringAsFixed(6), 'lng': pos.longitude.toStringAsFixed(6), }; } catch (_) {} } // If we still don't have a valid location, bail out to avoid sending 0/0. if ((ll['lat'] ?? '0') == '0' || (ll['lng'] ?? '0') == '0') { debugPrint('[UPDATE][ACTIVE] No valid rider coordinates; aborting'); return false; } final payload = { 'deliveryid': deliveryId, 'orderheaderid': orderHeaderId, 'orderstatus': 'active', 'activetime': _activeTime, 'starttime': startTime, // Add starttime when status changes to active 'activelat': ll['lat'], 'activelon': ll['lng'], 'riderslat': ll['lat'], 'riderslon': ll['lng'], 'raw_latitude': ll['raw_lat'] ?? '0', 'raw_longitude': ll['raw_lng'] ?? '0', 'velocity_lat': ll['velocity_lat'] ?? '0', 'velocity_lng': ll['velocity_lng'] ?? '0', 'speed': ll['speed'] ?? '0', 'heading': ll['heading'] ?? '0', 'deliverylat': '', 'deliverylong': '', 'actualkms': '', 'deliveryamt': 0.0, 'notes': notes, }; final url = _resolveUpdateUrl(); final resp = await _updateProvider.updateActiveDelivery(payload, url); final ok = _isSuccess(resp); if (ok) { final lat = ll['lat'] ?? '0'; final lng = ll['lng'] ?? '0'; if (lat != '0' && lng != '0') { await _saveDeliveryStartLocation(deliveryId, lat, lng); } // Save starttime to SharedPreferences for delivery logs (using deliveryId as key) // This will be retrieved when creating delivery log payloads try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('delivery_starttime_$deliveryId', startTime); if (orderId.isNotEmpty) { await prefs.setString('current_riding_order_id', orderId); } await prefs.setString('current_riding_delivery_id', deliveryId.toString()); debugPrint( '[UPDATE][ACTIVE] Saved starttime: $startTime for deliveryId: $deliveryId', ); // CRITICAL: Initialize cumulative distance tracking when delivery becomes active // Reset any previous tracking data and set initial location await prefs.setString('delivery_tracking_${deliveryId}_lastLat', lat); await prefs.setString('delivery_tracking_${deliveryId}_lastLng', lng); await prefs.setString( 'delivery_tracking_${deliveryId}_cumulativeKm', '0.0', ); await prefs.setInt( 'delivery_tracking_${deliveryId}_lastUpdateMs', DateTime.now().millisecondsSinceEpoch, ); final activeTracking = prefs.getStringList('active_tracking_delivery_ids') ?? []; if (!activeTracking.contains(deliveryId.toString())) { activeTracking.add(deliveryId.toString()); await prefs.setStringList('active_tracking_delivery_ids', activeTracking); } debugPrint( '[UPDATE][ACTIVE] ๐Ÿš€ Initialized distance tracking for deliveryId: $deliveryId at ($lat, $lng)', ); } catch (e) { debugPrint('[UPDATE][ACTIVE] Error saving starttime: $e'); } } else { debugPrint('[UPDATE][ACTIVE][FAILED] resp=${jsonEncode(resp)}'); } return ok; } catch (e) { debugPrint('[UPDATE][ACTIVE][ERROR] $e'); return false; } } // Arrived Future updateArrivedStatus({ required int deliveryId, required int orderHeaderId, String ridersLat = '0', String ridersLng = '0', String pickupLat = '0', String pickupLng = '0', String notes = '', }) async { // START LOADING IMMEDIATELY for better UX arrivedShimmer.value = true; try { final ll = await _ensureLatLng(ridersLat, ridersLng); final rLat = double.tryParse(ll['lat'] ?? '0') ?? 0.0; final rLng = double.tryParse(ll['lng'] ?? '0') ?? 0.0; final pLat = double.tryParse(pickupLat) ?? 0.0; final pLng = double.tryParse(pickupLng) ?? 0.0; // Geofence Check: Arrived -> Pickup Location final inFence = await _checkGeofence(pLat, pLng, rLat, rLng, 'Arrived'); if (!inFence) { arrivedShimmer.value = false; return false; } final now = DateTime.now(); _arrivedTime = _formatDateTimeFull(now); final payload = { 'deliveryid': deliveryId, 'orderheaderid': orderHeaderId, 'orderstatus': 'arrived', 'arrivaltime': _arrivedTime, 'riderslat': ll['lat'], 'riderslon': ll['lng'], 'raw_latitude': ll['raw_lat'] ?? '0', 'raw_longitude': ll['raw_lng'] ?? '0', 'velocity_lat': ll['velocity_lat'] ?? '0', 'velocity_lng': ll['velocity_lng'] ?? '0', 'speed': ll['speed'] ?? '0', 'heading': ll['heading'] ?? '0', 'deliverylat': '', 'deliverylong': '', 'actualkms': '', 'deliveryamt': 0.0, 'notes': notes, }; final url = _resolveUpdateUrl(); final resp = await _updateProvider.updateArrivedDelivery(payload, url); final ok = _isSuccess(resp); // Stop shimmer immediately after response arrivedShimmer.value = false; if (!ok) { debugPrint('[UPDATE][ARRIVED][FAILED] resp=${jsonEncode(resp)}'); } return ok; } catch (e) { debugPrint('[UPDATE][ARRIVED][ERROR] $e'); arrivedShimmer.value = false; return false; } } // Rejected (remove from queue) Future updateRejectedStatus({ required int deliveryId, required int orderHeaderId, String ridersLat = '0', String ridersLng = '0', String notes = '', }) async { try { final ll = await _ensureLatLng(ridersLat, ridersLng); final payload = { 'deliveryid': deliveryId, 'orderheaderid': orderHeaderId, 'orderstatus': 'rejected', 'riderslat': ll['lat'], 'riderslon': ll['lng'], 'raw_latitude': ll['raw_lat'] ?? '0', 'raw_longitude': ll['raw_lng'] ?? '0', 'velocity_lat': ll['velocity_lat'] ?? '0', 'velocity_lng': ll['velocity_lng'] ?? '0', 'speed': ll['speed'] ?? '0', 'heading': ll['heading'] ?? '0', 'deliverylat': '', 'deliverylong': '', 'actualkms': '', 'deliveryamt': 0.0, 'notes': notes, }; final url = _resolveUpdateUrl(); final resp = await _updateProvider.updateDelivery(payload, url); final ok = _isSuccess(resp); if (!ok) { debugPrint('[UPDATE][REJECTED][FAILED] resp=${jsonEncode(resp)}'); } return ok; } catch (e) { debugPrint('[UPDATE][REJECTED][ERROR] $e'); return false; } } // Cancelled (delivery cancelled by rider) Future updateCancelledStatus({ required int deliveryId, required int orderHeaderId, String ridersLat = '0', String ridersLng = '0', String pickupLat = '0', String pickupLng = '0', String deliveryLat = '0', String deliveryLng = '0', String notes = '', double actualKms = 0.0, bool wasSkipped = false, }) async { try { final now = DateTime.now(); _cancelledTime = _formatDateTimeFull(now); final ll = await _ensureLatLng(ridersLat, ridersLng); final riderLat = double.tryParse(ll['lat'] ?? '0') ?? 0.0; final riderLng = double.tryParse(ll['lng'] ?? '0') ?? 0.0; final dLat = double.tryParse(deliveryLat) ?? 0.0; final dLng = double.tryParse(deliveryLng) ?? 0.0; // โœ… SPECIAL CASE: Skip geofence check for "Incorrect Location" cancellation // When user cancels due to incorrect location, we don't verify range final bool isIncorrectLocation = notes.toLowerCase().contains( 'incorrect location', ); if (isIncorrectLocation) { debugPrint( '[CANCELLED] โš ๏ธ Skipping geofence check - Reason: Incorrect Location', ); } else { // Geofence Check: Cancelled -> Delivery Location (for all other reasons) final inFence = await _checkGeofence( dLat, dLng, riderLat, riderLng, 'Cancelled', ); if (!inFence) { debugPrint( '[CANCELLED] โŒ Geofence check failed - Rider not in range', ); return false; } } // PRIORITY 1: Use cumulative distance tracked from active to cancelled (most accurate) // This is the actual distance the rider traveled, calculated from GPS points every 30 seconds double calculatedKms = 0.0; try { final prefs = await SharedPreferences.getInstance(); final cumulativeKmStr = prefs.getString('delivery_tracking_${deliveryId}_cumulativeKm') ?? ''; if (cumulativeKmStr.isNotEmpty) { final cumulativeKm = double.tryParse(cumulativeKmStr) ?? 0.0; if (cumulativeKm > 0) { calculatedKms = cumulativeKm; debugPrint( '[CANCELLED] โœ… Using CUMULATIVE tracked distance: ${calculatedKms.toStringAsFixed(4)} km', ); debugPrint( '[CANCELLED] ๐Ÿ“Š This is the actual distance traveled from active to cancelled', ); } } } catch (e) { debugPrint('[CANCELLED] Error getting cumulative distance: $e'); } // PRIORITY 2: Fallback to actualKms if cumulative not available if (calculatedKms == 0.0) { calculatedKms = actualKms; } if (wasSkipped && calculatedKms == 0.0) { try { final lastDelivery = await _getLastDeliveryLocation(); final lastLat = double.tryParse(lastDelivery['lat'] ?? '') ?? 0.0; final lastLng = double.tryParse(lastDelivery['lng'] ?? '') ?? 0.0; if (lastLat != 0 && lastLng != 0 && riderLat != 0 && riderLng != 0) { final distanceMeters = Geolocator.distanceBetween( lastLat, lastLng, riderLat, riderLng, ); calculatedKms = distanceMeters / 1000.0; debugPrint( '[CANCELLED] Skipped delivery - rider travelled ${calculatedKms.toStringAsFixed(2)} km from last stop to cancellation point', ); } } catch (e) { debugPrint('[CANCELLED] Error calculating skipped distance: $e'); } } if (calculatedKms == 0.0) { try { // ANTI-CHEAT: Try to get the locked "Start Location" for this delivery final startLoc = await _getDeliveryStartLocation(deliveryId); final startLatStr = startLoc['lat'] ?? ''; final startLngStr = startLoc['lng'] ?? ''; if (startLatStr.isNotEmpty && startLngStr.isNotEmpty) { final startLat = double.tryParse(startLatStr) ?? 0.0; final startLng = double.tryParse(startLngStr) ?? 0.0; if (startLat != 0 && startLng != 0 && riderLat != 0 && riderLng != 0) { // Try Google Maps route distance first (accurate road distance) final routeKm = await _getRouteDistanceKm( startLat, startLng, riderLat, riderLng, ); if (routeKm != null && routeKm > 0) { calculatedKms = routeKm; debugPrint( '[CANCELLED] โœ… Route distance (Anti-Cheat): ${calculatedKms.toStringAsFixed(4)} km from Start Location to cancellation point', ); } else { // Fallback to straight-line if API fails final distanceMeters = Geolocator.distanceBetween( startLat, startLng, riderLat, riderLng, ); calculatedKms = distanceMeters / 1000.0; debugPrint( '[CANCELLED] โš ๏ธ Fallback straight-line (Anti-Cheat): ${calculatedKms.toStringAsFixed(4)} km', ); } } } else { // Fallback to Pickup -> Current final pickLat = double.tryParse(pickupLat) ?? 0.0; final pickLng = double.tryParse(pickupLng) ?? 0.0; if (pickLat != 0 && pickLng != 0 && riderLat != 0 && riderLng != 0) { // Try Google Maps route distance first final routeKm = await _getRouteDistanceKm( pickLat, pickLng, riderLat, riderLng, ); if (routeKm != null && routeKm > 0) { calculatedKms = routeKm; debugPrint( '[CANCELLED] โœ… Route distance: ${calculatedKms.toStringAsFixed(4)} km from pickup to cancellation point', ); } else { // Fallback to straight-line if API fails final distanceMeters = Geolocator.distanceBetween( pickLat, pickLng, riderLat, riderLng, ); calculatedKms = distanceMeters / 1000.0; debugPrint( '[CANCELLED] โš ๏ธ Fallback straight-line: ${calculatedKms.toStringAsFixed(4)} km', ); } } } } catch (e) { debugPrint('[CANCELLED] Error calculating distance: $e'); } } final double riderChargeRate = await _getRiderChargeRate(); final double riderChargesAmountRaw = riderChargeRate > 0 && calculatedKms > 0 ? calculatedKms * riderChargeRate : 0.0; final double riderChargesAmount = double.parse( riderChargesAmountRaw.toStringAsFixed(2), ); if (riderChargeRate > 0) { debugPrint( '[CANCELLED] Applying rider charge rate $riderChargeRate => ridercharges ${riderChargesAmount.toStringAsFixed(2)}', ); } // CRITICAL: Clean up cumulative distance tracking after cancellation try { final prefs = await SharedPreferences.getInstance(); final cumulativeKm = prefs.getString('delivery_tracking_${deliveryId}_cumulativeKm') ?? ''; if (cumulativeKm.isNotEmpty) { debugPrint( '[CANCELLED] ๐Ÿ“Š Final cumulative distance used: $cumulativeKm km', ); } // Clean up tracking data await prefs.remove('delivery_tracking_${deliveryId}_lastLat'); await prefs.remove('delivery_tracking_${deliveryId}_lastLng'); await prefs.remove('delivery_tracking_${deliveryId}_cumulativeKm'); await prefs.remove('delivery_tracking_${deliveryId}_lastUpdateMs'); await prefs.remove('delivery_proximity_alerted_$deliveryId'); final activeTracking = prefs.getStringList('active_tracking_delivery_ids') ?? []; if (activeTracking.contains(deliveryId.toString())) { activeTracking.remove(deliveryId.toString()); await prefs.setStringList('active_tracking_delivery_ids', activeTracking); } debugPrint( '[CANCELLED] ๐Ÿงน Cleaned up distance tracking for deliveryId: $deliveryId', ); } catch (e) { debugPrint('[CANCELLED] Error cleaning up tracking: $e'); } // CRITICAL: Validate coordinates before creating payload - NEVER send '0' or null final ridersLatStr = ll['lat'] ?? '0'; final ridersLngStr = ll['lng'] ?? '0'; final ridersLatDouble = double.tryParse(ridersLatStr) ?? 0.0; final ridersLngDouble = double.tryParse(ridersLngStr) ?? 0.0; if (ridersLatDouble == 0 || ridersLngDouble == 0 || ridersLatDouble.abs() > 90 || ridersLngDouble.abs() > 180) { debugPrint( '[CANCELLED] โŒ ERROR: Invalid rider coordinates (lat=$ridersLatStr, lng=$ridersLngStr) - cannot proceed', ); return false; // Don't send invalid coordinates } debugPrint( '[CANCELLED] โœ… Validated coordinates - Rider: ($ridersLatStr, $ridersLngStr)', ); final payload = { 'deliveryid': deliveryId, 'orderheaderid': orderHeaderId, 'orderstatus': 'cancelled', 'canceltime': _cancelledTime, 'riderslat': ridersLatStr, // Guaranteed non-zero and valid 'riderslon': ridersLngStr, // Guaranteed non-zero and valid 'raw_latitude': ll['raw_lat'] ?? '0', 'raw_longitude': ll['raw_lng'] ?? '0', 'velocity_lat': ll['velocity_lat'] ?? '0', 'velocity_lng': ll['velocity_lng'] ?? '0', 'speed': ll['speed'] ?? '0', 'heading': ll['heading'] ?? '0', 'deliverylat': '', 'deliverylong': '', 'riderkms': calculatedKms.toStringAsFixed(2), 'ridercharges': riderChargesAmount, 'deliveryamt': 0.0, 'notes': notes, }; final url = _resolveUpdateUrl(); final resp = await _updateProvider.updateDelivery(payload, url); final ok = _isSuccess(resp); if (ok) { // Update last delivery location to current location so next order starts here final actualLat = ll['lat'] ?? '0'; final actualLng = ll['lng'] ?? '0'; if (actualLat != '0' && actualLng != '0') { await _saveLastDeliveryLocation(actualLat, actualLng); } } if (!ok) { debugPrint('[UPDATE][CANCELLED][FAILED] resp=${jsonEncode(resp)}'); } return ok; } catch (e) { debugPrint('[UPDATE][CANCELLED][ERROR] $e'); return false; } } // Skipped (delivery skipped by rider) Future updateSkippedStatus({ required int deliveryId, required int orderHeaderId, String ridersLat = '0', String ridersLng = '0', String notes = '', }) async { try { final ll = await _ensureLatLng(ridersLat, ridersLng); final skippedTime = _formatDateTimeFull(DateTime.now()); final payload = { 'deliveryid': deliveryId, 'orderheaderid': orderHeaderId, 'orderstatus': 'skipped', 'skippedtime': skippedTime, 'riderslat': ll['lat'], 'riderslon': ll['lng'], 'deliverylat': '', 'deliverylong': '', 'actualkms': '', 'deliveryamt': 0.0, 'notes': notes, }; // Calculate distance for skipped order (Anti-Cheat) double calculatedKms = 0.0; try { final riderLat = double.tryParse(ll['lat'] ?? '0') ?? 0.0; final riderLng = double.tryParse(ll['lng'] ?? '0') ?? 0.0; if (riderLat != 0 && riderLng != 0) { // Try Start Location first final startLoc = await _getDeliveryStartLocation(deliveryId); final startLatStr = startLoc['lat'] ?? ''; final startLngStr = startLoc['lng'] ?? ''; if (startLatStr.isNotEmpty && startLngStr.isNotEmpty) { final startLat = double.tryParse(startLatStr) ?? 0.0; final startLng = double.tryParse(startLngStr) ?? 0.0; if (startLat != 0 && startLng != 0) { // Try Google Maps route distance first (accurate road distance) final routeKm = await _getRouteDistanceKm( startLat, startLng, riderLat, riderLng, ); if (routeKm != null && routeKm > 0) { calculatedKms = routeKm; debugPrint( '[SKIPPED] โœ… Route distance (Start Location): ${calculatedKms.toStringAsFixed(4)} km', ); } else { // Fallback to straight-line if API fails final distanceMeters = Geolocator.distanceBetween( startLat, startLng, riderLat, riderLng, ); calculatedKms = distanceMeters / 1000.0; debugPrint( '[SKIPPED] โš ๏ธ Fallback straight-line (Start Location): ${calculatedKms.toStringAsFixed(4)} km', ); } } } else { // Fallback to Last Delivery Location final lastDelivery = await _getLastDeliveryLocation(); final lastLat = double.tryParse(lastDelivery['lat'] ?? '') ?? 0.0; final lastLng = double.tryParse(lastDelivery['lng'] ?? '') ?? 0.0; if (lastLat != 0 && lastLng != 0) { // Try Google Maps route distance first final routeKm = await _getRouteDistanceKm( lastLat, lastLng, riderLat, riderLng, ); if (routeKm != null && routeKm > 0) { calculatedKms = routeKm; debugPrint( '[SKIPPED] โœ… Route distance (Last Delivery): ${calculatedKms.toStringAsFixed(4)} km', ); } else { // Fallback to straight-line if API fails final distanceMeters = Geolocator.distanceBetween( lastLat, lastLng, riderLat, riderLng, ); calculatedKms = distanceMeters / 1000.0; debugPrint( '[SKIPPED] โš ๏ธ Fallback straight-line (Last Delivery): ${calculatedKms.toStringAsFixed(4)} km', ); } } } } } catch (e) { debugPrint('[SKIPPED] Error calculating distance: $e'); } if (calculatedKms > 0) { payload['riderkms'] = calculatedKms.toStringAsFixed(2); debugPrint('[SKIPPED] Calculated riderkms: ${payload['riderkms']}'); } final url = _resolveUpdateUrl(); final resp = await _updateProvider.updateDelivery(payload, url); final ok = _isSuccess(resp); if (ok) { // Update last delivery location to current location so next order starts here final actualLat = ll['lat'] ?? '0'; final actualLng = ll['lng'] ?? '0'; if (actualLat != '0' && actualLng != '0') { await _saveLastDeliveryLocation(actualLat, actualLng); } } if (!ok) { debugPrint('[UPDATE][SKIPPED][FAILED] resp=${jsonEncode(resp)}'); } return ok; } catch (e) { debugPrint('[UPDATE][SKIPPED][ERROR] $e'); return false; } } /// Check if there are active deliveries /// Returns true ONLY if has_live_deliveries is true (verified by API) /// This ensures PiP is only enabled when there are actually active deliveries Future hasActiveDeliveries() async { try { final prefs = await SharedPreferences.getInstance(); final hasLive = prefs.getBool('has_live_deliveries') ?? false; // โœ… CRITICAL: Only return true if has_live_deliveries is true // Don't rely on active_delivery_order_id alone - it may be stale // The API sets has_live_deliveries based on actual order status = "active" if (!hasLive) { // Clear stale active_delivery_order_id if has_live_deliveries is false final activeOrderId = prefs.getString('active_delivery_order_id'); if (activeOrderId != null && activeOrderId.isNotEmpty) { debugPrint( '[DELIVERIES_CONTROLLER] Clearing stale active_delivery_order_id: $activeOrderId (no active deliveries)', ); await prefs.remove('active_delivery_order_id'); } } debugPrint( '[DELIVERIES_CONTROLLER] hasActiveDeliveries: $hasLive (has_live_deliveries from API)', ); return hasLive; } catch (e) { debugPrint( '[DELIVERIES_CONTROLLER] Error checking active deliveries: $e', ); return false; } } // ---------------- Skip Penalty Logic ---------------- /// Checks the current skip count and penalty status within the 3-hour window. /// Resets the window if 3 hours have passed since the first skip. Future> checkSkipStatus() async { try { final prefs = await SharedPreferences.getInstance(); final now = DateTime.now(); final startStr = prefs.getString('skip_window_start'); int count = prefs.getInt('skip_count') ?? 0; bool penalty = prefs.getBool('skip_penalty_active') ?? false; if (startStr != null) { final start = DateTime.tryParse(startStr); if (start != null) { final diff = now.difference(start); if (diff.inHours >= 3) { debugPrint('[SKIP_LOGIC] 3-hour window expired (elapsed: ${diff.inMinutes}m). Resetting skips.'); // Reset window count = 0; penalty = false; await prefs.remove('skip_window_start'); await prefs.remove('skip_count'); await prefs.remove('skip_penalty_active'); } } else { // Invalid date string, reset await prefs.remove('skip_window_start'); } } return {'count': count, 'penalty': penalty}; } catch (e) { debugPrint('[SKIP_LOGIC] Error checking status: $e'); return {'count': 0, 'penalty': false}; } } /// Registers a skip action. /// Increments skip count and sets penalty if specified. /// Starts 3-hour window if not already active. Future registerSkip({bool applyPenalty = false}) async { try { final prefs = await SharedPreferences.getInstance(); final now = DateTime.now(); // Ensure we are working with fresh/current state await checkSkipStatus(); // Re-fetch after potential reset int count = prefs.getInt('skip_count') ?? 0; String? startStr = prefs.getString('skip_window_start'); if (startStr == null) { // Start new window await prefs.setString('skip_window_start', now.toIso8601String()); debugPrint('[SKIP_LOGIC] Starting new 3-hour skip window.'); } count++; await prefs.setInt('skip_count', count); if (applyPenalty) { await prefs.setBool('skip_penalty_active', true); debugPrint('[SKIP_LOGIC] Penalty activated for this session.'); } debugPrint('[SKIP_LOGIC] Skip registered. Count: $count, Penalty: $applyPenalty'); } catch (e) { debugPrint('[SKIP_LOGIC] Error registering skip: $e'); } } }