import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart' show LatLng; import 'package:miler/helpers/miler_router.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:geolocator/geolocator.dart'; import 'package:miler/views/helpers/constants/apiconstants.dart'; import 'package:miler/providers/pickuplog/pickuplog_provider.dart'; import 'dart:io'; import 'package:minio/minio.dart'; import 'package:minio/io.dart'; import 'package:miler/utils/kalman_filter.dart'; import 'package:miler/utils/mqtt_service.dart'; import 'package:miler/controllers/connectivity_mixin.dart'; import 'package:miler/views/helpers/constants/Colorconstants.dart'; import 'package:miler/views/helpers/widgets/app_widgets.dart'; /// Testing/demo flag. When `true`, the proximity ("you are too far from the /// location") geofence is skipped so mock stops far from the rider can still be /// completed while testing the design. /// /// Tied to [kDebugMode] so it is ALWAYS `false` in release builds — the /// geofence can never be accidentally shipped disabled, but debug/testing keeps /// working with mock stops. const bool kBypassGeofenceForTesting = kDebugMode; class PickupsController extends GetxController with ConnectivityControllerMixin { MilerKalmanFilter? _kf; DateTime? _lastUpdateTime; final UpdatePickupProvider _updateProvider = UpdatePickupProvider(); 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 pickedShimmer = false.obs; final RxBool isPipEnabled = false.obs; // Distance tracking for PiP mode final RxDouble pickupableDistance = 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 _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; // ── Compliance of the stop just completed ── // // All three are computed here already — the bonus rule is literally "did he // arrive before the ETA deadline", and the distance is measured to bill // rider charges — but they were only ever written to the payload and the log. // The rider could not see whether he had made his ETA or how far off the // planned route he had gone, and neither could the Activity tab. // // Published so the completion path can stamp them onto the finished stop. // Null means "not knowable for this stop" (no ETA was set, no distance could // be measured) — which the UI states as unknown rather than as a failure. final Rxn lastOnTime = Rxn(); final Rxn lastLateBy = Rxn(); final RxDouble lastRiderKms = 0.0.obs; // Trigger to refresh pickup 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 pickupId, ) async { try { // final rng = Random(); // Unused const String region = "sgp1"; const String accessKey = "DO00NQER7N2FRYZAB2HR"; const String secretKey = "nMDewX25IBEu1FM5dakK+v28/WbW3TzBAwq913+dxP0"; const String bucketName = "doormile"; // folderName will be "picked" or "Picked up" // 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-$pickupId-$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.doormile.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"); AppFeedback.errorGlobal('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 pickup 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 = MilerKalmanFilter(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 pickupId, 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 pickupType = '', 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 = { 'pickupid': pickupId, 'orderheaderid': orderHeaderId, 'pickuplocationid': pickupLocationId, 'orderstatus': 'picked', 'pickuptime': _pickedTime, 'riderslat': ll['lat'], 'riderslon': ll['lng'], 'pickuplat': '', 'pickuplong': '', 'actualkms': calculatedKms.toStringAsFixed(2), 'pickupamt': 0.0, 'pickuptype': pickupType, '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 resp = await _updateProvider.updatePickup(payload); final ok = _isSuccess(resp); if (!ok) { debugPrint('[UPDATE][PICKED][FAILED] resp=${jsonEncode(resp)}'); } else { // --- MQTT LOGIC --- MilerMqttService().publishLog('pickup_picked', payload); } return ok; } catch (e) { debugPrint('[UPDATE][PICKED][ERROR] $e'); return false; } } // Picked up Future updatePickedupStatus({ required int pickupId, required int orderHeaderId, String ridersLat = '0', String ridersLng = '0', int pickupLocationId = 0, int smsPickup = 0, String pickupLat = '0', String pickupLng = '0', String notes = '', double pickupAmount = 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 { pickedShimmer.value = true; // ✅ PARALLEL OPTIMIZATION: Start fetching Prefs immediately final prefsFuture = SharedPreferences.getInstance(); 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 dLat = double.tryParse(pickupLat) ?? 0.0; final dLng = double.tryParse(pickupLng) ?? 0.0; // Geofence Check: Picked up -> Pickup Location final inFence = await _checkGeofence(dLat, dLng, rLat, rLng, 'Picked up'); if (!inFence) { pickedShimmer.value = false; return false; } debugPrint( '[Picked up] 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_$pickupId'; 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('[Picked up] riderTimeMinutes=$riderTimeMinutes'); } catch (e) { debugPrint('[Picked up] 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 Picked up (most accurate) // This is the actual distance the rider traveled, calculated from GPS points every 30 seconds try { final cumulativeKmStr = prefs.getString('pickup_tracking_${pickupId}_cumulativeKm') ?? ''; if (cumulativeKmStr.isNotEmpty) { final cumulativeKm = double.tryParse(cumulativeKmStr) ?? 0.0; if (cumulativeKm > 0) { calculatedRiderKms = cumulativeKm; debugPrint( '[Picked up] ✅ Using CUMULATIVE tracked distance: ${calculatedRiderKms.toStringAsFixed(4)} km', ); debugPrint( '[Picked up] 📊 This is the actual distance traveled from active to Picked up', ); } } } catch (e) { debugPrint('[Picked up] 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( '[Picked up] 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 _getPickupStartLocation(pickupId); 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( '[Picked up] ✅ 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( '[Picked up] ⚠️ 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( '[Picked up] ✅ 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( '[Picked up] ⚠️ 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( '[Picked up] ✅ 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( '[Picked up] ⚠️ Fallback straight-line (Saved Pickup): ${distanceMeters.toStringAsFixed(2)} meters = ${calculatedRiderKms.toStringAsFixed(4)} km', ); } } } // Method 4: Final fallback - calculate from pickup location to current location if (calculatedRiderKms == 0.0 && pickupLat != '0' && pickupLng != '0') { final dLat = double.tryParse(pickupLat) ?? 0.0; final dLng = double.tryParse(pickupLng) ?? 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( '[Picked up] ✅ Route distance (Pickup Location): ${calculatedRiderKms.toStringAsFixed(4)} km', ); } else { // Fallback to straight-line final distanceMeters = Geolocator.distanceBetween( dLat, dLng, riderLat, riderLng, ); calculatedRiderKms = distanceMeters / 1000.0; debugPrint( '[Picked up] ⚠️ Fallback straight-line (Pickup Location): ${distanceMeters.toStringAsFixed(2)} meters = ${calculatedRiderKms.toStringAsFixed(4)} km', ); } } } } catch (e) { debugPrint('[Picked up] 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( '[Picked up] ⚠️ WARNING: Could not calculate distance, using minimum 0.01 km (10 meters)', ); calculatedRiderKms = 0.01; // Minimum 10 meters } debugPrint( '[Picked up] Final riderkms: ${calculatedRiderKms.toStringAsFixed(4)} km', ); // ---- BONUS POINTS LOGIC ---- int bonusPoints = 0; // Reset per stop: these describe the stop being closed right now, and a // stale value from the previous one would be stamped onto this record. lastOnTime.value = null; lastLateBy.value = null; lastRiderKms.value = calculatedRiderKms; if (orderId.isNotEmpty) { try { final endKey = 'eta_endtime_$orderId'; final endSeconds = prefs.getInt(endKey); if (endSeconds != null) { final currentSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000; lastOnTime.value = currentSeconds <= endSeconds; lastLateBy.value = currentSeconds <= endSeconds ? Duration.zero : Duration(seconds: currentSeconds - endSeconds); // If Picked up before or at the end time, give bonus points if (currentSeconds <= endSeconds) { // Bonus points = riderkms rounded bonusPoints = calculatedRiderKms.round(); debugPrint( '[Picked up] 🏆 ON TIME! Bonus Points: $bonusPoints (RiderKms: $calculatedRiderKms)', ); } else { debugPrint( '[Picked up] ⏳ LATE! No Bonus Points. (Deadline: $endSeconds, Now: $currentSeconds)', ); } } else { debugPrint('[Picked up] No ETA timer found for bonus logic.'); } } catch (e) { debugPrint('[Picked up] 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( '[Picked up] ⚠️ 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 pickuptime to current time right before creating payload // This ensures pickuptime is ALWAYS passed when confirming pickup, no matter what // Setting it here (right before payload) ensures it's the exact time of confirmation final currentTime = DateTime.now(); final currentPickupTime = _formatDateTimeFull(currentTime); _pickedTime = currentPickupTime; // Update the instance variable too debugPrint('[Picked up] Setting pickuptime to: $currentPickupTime'); // 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( '[Picked up] ❌ ERROR: Invalid rider coordinates (lat=$ridersLatStr, lng=$ridersLngStr) - cannot proceed', ); pickedShimmer.value = false; return false; // Don't send invalid coordinates } // Validate pickup coordinates final pickupLatDouble = double.tryParse(pickupLat) ?? 0.0; final pickupLngDouble = double.tryParse(pickupLng) ?? 0.0; if (pickupLatDouble == 0 || pickupLngDouble == 0 || pickupLatDouble.abs() > 90 || pickupLngDouble.abs() > 180) { debugPrint( '[Picked up] ❌ ERROR: Invalid pickup coordinates (lat=$pickupLat, lng=$pickupLng) - cannot proceed', ); pickedShimmer.value = false; return false; // Don't send invalid coordinates } debugPrint( '[Picked up] ✅ Validated coordinates - Rider: ($ridersLatStr, $ridersLngStr), Pickup: ($pickupLat, $pickupLng)', ); final payload = { 'pickupid': pickupId, 'orderheaderid': orderHeaderId, 'pickuplocationid': pickupLocationId, 'orderstatus': 'Picked up', 'pickupedtime': currentPickupTime, 'pickuptime': currentPickupTime, // ALWAYS pass current time - required field, set right before payload 'smspickup': smsPickup, '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, // Guaranteed non-zero and valid 'pickuplong': pickupLng, // Guaranteed non-zero and valid 'riderkms': riderKmsString, // ALWAYS pass riderkms - never null, shows even 10 meters (0.0100 km) 'ridercharges': riderChargesAmount, 'pickupamt': pickupAmount, '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('[Picked up] Payload includes pickuptime: $currentPickupTime'); debugPrint('[Picked up] Payload includes riderkms: $riderKmsString km'); debugPrint('[Picked up] Payload includes bonuspts: $bonusPoints'); final resp = await _updateProvider.updatePickup(payload); final ok = _isSuccess(resp); // Save current RIDER GPS location as last pickup location for next pickup if (ok) { final actualLat = ll['lat'] ?? '0'; final actualLng = ll['lng'] ?? '0'; if (actualLat != '0' && actualLng != '0') { await _saveLastPickupLocation(actualLat, actualLng); } // CRITICAL: Clean up cumulative distance tracking after pickup is completed try { final cumulativeKm = prefs.getString('pickup_tracking_${pickupId}_cumulativeKm') ?? ''; if (cumulativeKm.isNotEmpty) { debugPrint( '[Picked up] 📊 Final cumulative distance used: $cumulativeKm km', ); } // Clean up tracking data await prefs.remove('pickup_tracking_${pickupId}_lastLat'); await prefs.remove('pickup_tracking_${pickupId}_lastLng'); await prefs.remove('pickup_tracking_${pickupId}_cumulativeKm'); await prefs.remove('ridertime_start_$pickupId'); final activeTracking = prefs.getStringList('active_tracking_pickup_ids') ?? []; if (activeTracking.contains(pickupId.toString())) { activeTracking.remove(pickupId.toString()); await prefs.setStringList( 'active_tracking_pickup_ids', activeTracking, ); } // ✅ Clean up ETA timer if (orderId.isNotEmpty) { await prefs.remove('eta_endtime_$orderId'); } debugPrint( '[Picked up] 🧹 Cleaned up distance tracking for pickupId: $pickupId', ); } catch (e) { debugPrint('[Picked up] Error cleaning up tracking: $e'); } } // Reset foreground pickup notification flag await setNotificationSent(false); pickedShimmer.value = false; if (!ok) { debugPrint('[UPDATE][Picked up][FAILED] resp=${jsonEncode(resp)}'); } else { // --- MQTT LOGIC --- MilerMqttService().publishLog('pickup_completed', payload); } return ok; } catch (e) { debugPrint('[UPDATE][Picked up][ERROR] $e'); pickedShimmer.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.updatePickupLive : ApiConstants.updatePickupDev; } // ---------------- SharedPrefs helpers ---------------- Future setNotificationSent(bool value) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setBool('notificationSent', value); } catch (_) {} } // ---------------- Geofencing helpers ---------------- Future _getPickupRadius() async { try { final prefs = await SharedPreferences.getInstance(); return prefs.getInt('pickupradius') ?? 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) { debugPrint('[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; AppFeedback.errorGlobal('Something went wrong'); } catch (e) { debugPrint( '[GEOFENCE] Error showing snackbar (attempt ${retryCount + 1}): $e', ); debugPrint( '[GEOFENCE] GetX context available: ${Get.key.currentContext != null}', ); Future.delayed(const Duration(milliseconds: 400), () { try { AppFeedback.errorGlobal('Something went wrong'); } catch (e2) { debugPrint('[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 { // Testing/demo bypass: skip proximity enforcement so mock stops far from // the rider can still be completed. Remove/flip for production. if (kBypassGeofenceForTesting) { if (kDebugMode) { debugPrint('[GEOFENCE] Bypassed for $action (testing flag on)'); } return true; } // 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: ColorConstants.warning, seconds: 3, ); return true; // Allow update to proceed despite missing coords } try { final radius = await _getPickupRadius(); 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: ColorConstants.warning, seconds: 3, ); return true; // Allow on error (fail-safe) } } // Get last pickup location (for calculating riderkms between pickup) Future> _getLastPickupLocation() async { try { final prefs = await SharedPreferences.getInstance(); final lat = prefs.getString('lastPickupLat') ?? ''; final lng = prefs.getString('lastPickupLng') ?? ''; if (lat.isNotEmpty && lng.isNotEmpty) { return {'lat': lat, 'lng': lng}; } } catch (_) {} return {'lat': '', 'lng': ''}; } // Save last pickup location (rider's actual GPS location, not destination) Future _saveLastPickupLocation(String lat, String lng) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('lastPickupLat', lat); await prefs.setString('lastPickupLng', lng); debugPrint('[TRACKING] Saved last pickup location: lat=$lat, lng=$lng'); } catch (e) { debugPrint('[TRACKING] Error saving last pickup location: $e'); } } // Save start location for a specific pickup (when "Start Navigation" is slid) Future _savePickupStartLocation( int pickupId, String lat, String lng, ) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('pickup_start_${pickupId}_lat', lat); await prefs.setString('pickup_start_${pickupId}_lng', lng); debugPrint( '[TRACKING] Saved start location for pickup $pickupId: lat=$lat, lng=$lng', ); } catch (e) { debugPrint('[TRACKING] Error saving pickup start location: $e'); } } // Get start location for a specific pickup Future> _getPickupStartLocation(int pickupId) async { try { final prefs = await SharedPreferences.getInstance(); final lat = prefs.getString('pickup_start_${pickupId}_lat') ?? ''; final lng = prefs.getString('pickup_start_${pickupId}_lng') ?? ''; if (lat.isNotEmpty && lng.isNotEmpty) { return {'lat': lat, 'lng': lng}; } } catch (_) {} return {'lat': '', 'lng': ''}; } // Get pickup location (for first pickup 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': ''}; } /// Road distance in kilometres between two points, or null when the router /// cannot answer — the caller then falls back to straight-line. /// /// ── Why this stopped being a Google call ── /// /// It asked the Directions API directly, with the same key that is revoked /// everywhere else in this app. Every request came back `REQUEST_DENIED`, the /// method logged it and returned null, and the caller quietly used the /// straight-line distance instead. Nothing looked broken — but road distance /// is what riders are *paid* on, and a crow-flies figure is short on every /// trip that is not a straight road. /// /// OSRM answers the same question for free. See [MilerRouter]. Future _getRouteDistanceKm( double originLat, double originLng, double destLat, double destLng, ) async { // 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; } final metres = await MilerRouter.roadDistanceMetres( LatLng(originLat, originLng), LatLng(destLat, destLng), ); if (metres == null) { debugPrint( '[ROUTE] No road distance — caller falls back to a straight line.', ); return null; } final km = metres / 1000.0; debugPrint('[ROUTE] Road distance: ${km.toStringAsFixed(4)} km'); return km; } // 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; } // ---------------- Status Updates ---------------- // Accepted Future updateAcceptedStatus({ required int pickupId, 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 = { 'pickupid': pickupId, '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', 'pickuplat': '', 'pickuplong': '', 'actualkms': '', 'pickupamt': 0.0, 'notes': notes, }; final resp = await _updateProvider.updatePickup(payload); 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 pickupId, 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('pickup_starttime_$pickupId') ?? ''; 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 = { 'pickupid': pickupId, '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', 'pickuplat': '', 'pickuplong': '', 'actualkms': '', 'pickupamt': 0.0, 'notes': notes, }; final resp = await _updateProvider.updatePickup(payload); final ok = _isSuccess(resp); if (ok) { final lat = ll['lat'] ?? '0'; final lng = ll['lng'] ?? '0'; if (lat != '0' && lng != '0') { await _savePickupStartLocation(pickupId, lat, lng); } // Save starttime to SharedPreferences for pickup logs (using pickupId as key) // This will be retrieved when creating pickup log payloads try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('pickup_starttime_$pickupId', startTime); if (orderId.isNotEmpty) { await prefs.setString('current_riding_order_id', orderId); } await prefs.setString( 'current_riding_pickup_id', pickupId.toString(), ); debugPrint( '[UPDATE][ACTIVE] Saved starttime: $startTime for pickupId: $pickupId', ); // CRITICAL: Initialize cumulative distance tracking when pickup becomes active // Reset any previous tracking data and set initial location await prefs.setString('pickup_tracking_${pickupId}_lastLat', lat); await prefs.setString('pickup_tracking_${pickupId}_lastLng', lng); await prefs.setString( 'pickup_tracking_${pickupId}_cumulativeKm', '0.0', ); final activeTracking = prefs.getStringList('active_tracking_pickup_ids') ?? []; if (!activeTracking.contains(pickupId.toString())) { activeTracking.add(pickupId.toString()); await prefs.setStringList( 'active_tracking_pickup_ids', activeTracking, ); } debugPrint( '[UPDATE][ACTIVE] 🚀 Initialized distance tracking for pickupId: $pickupId 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 pickupId, 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 = { 'pickupid': pickupId, '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', 'pickuplat': '', 'pickuplong': '', 'actualkms': '', 'pickupamt': 0.0, 'notes': notes, }; final resp = await _updateProvider.updatePickup(payload); 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 pickupId, required int orderHeaderId, String ridersLat = '0', String ridersLng = '0', String notes = '', }) async { try { final ll = await _ensureLatLng(ridersLat, ridersLng); final payload = { 'pickupid': pickupId, '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', 'pickuplat': '', 'pickuplong': '', 'actualkms': '', 'pickupamt': 0.0, 'notes': notes, }; final resp = await _updateProvider.updatePickup(payload); 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 (pickup cancelled by rider) Future updateCancelledStatus({ required int pickupId, required int orderHeaderId, String ridersLat = '0', String ridersLng = '0', String pickupLat = '0', String pickupLng = '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(pickupLat) ?? 0.0; final dLng = double.tryParse(pickupLng) ?? 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 -> Pickup 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('pickup_tracking_${pickupId}_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 lastPickup = await _getLastPickupLocation(); final lastLat = double.tryParse(lastPickup['lat'] ?? '') ?? 0.0; final lastLng = double.tryParse(lastPickup['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 pickup - 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 pickup final startLoc = await _getPickupStartLocation(pickupId); 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('pickup_tracking_${pickupId}_cumulativeKm') ?? ''; if (cumulativeKm.isNotEmpty) { debugPrint( '[CANCELLED] 📊 Final cumulative distance used: $cumulativeKm km', ); } // Clean up tracking data await prefs.remove('pickup_tracking_${pickupId}_lastLat'); await prefs.remove('pickup_tracking_${pickupId}_lastLng'); await prefs.remove('pickup_tracking_${pickupId}_cumulativeKm'); final activeTracking = prefs.getStringList('active_tracking_pickup_ids') ?? []; if (activeTracking.contains(pickupId.toString())) { activeTracking.remove(pickupId.toString()); await prefs.setStringList( 'active_tracking_pickup_ids', activeTracking, ); } debugPrint( '[CANCELLED] 🧹 Cleaned up distance tracking for pickupId: $pickupId', ); } 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 = { 'pickupid': pickupId, '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', 'pickuplat': '', 'pickuplong': '', 'riderkms': calculatedKms.toStringAsFixed(2), 'ridercharges': riderChargesAmount, 'pickupamt': 0.0, 'notes': notes, }; final resp = await _updateProvider.updatePickup(payload); final ok = _isSuccess(resp); if (ok) { // Update last pickup location to current location so next Booking starts here final actualLat = ll['lat'] ?? '0'; final actualLng = ll['lng'] ?? '0'; if (actualLat != '0' && actualLng != '0') { await _saveLastPickupLocation(actualLat, actualLng); } } if (!ok) { debugPrint('[UPDATE][CANCELLED][FAILED] resp=${jsonEncode(resp)}'); } return ok; } catch (e) { debugPrint('[UPDATE][CANCELLED][ERROR] $e'); return false; } } // Skipped (pickup skipped by rider) Future updateSkippedStatus({ required int pickupId, 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 = { 'pickupid': pickupId, 'orderheaderid': orderHeaderId, 'orderstatus': 'skipped', 'skippedtime': skippedTime, 'riderslat': ll['lat'], 'riderslon': ll['lng'], 'pickuplat': '', 'pickuplong': '', 'actualkms': '', 'pickupamt': 0.0, 'notes': notes, }; // Calculate distance for skipped Booking (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 _getPickupStartLocation(pickupId); 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 Pickup Location final lastPickup = await _getLastPickupLocation(); final lastLat = double.tryParse(lastPickup['lat'] ?? '') ?? 0.0; final lastLng = double.tryParse(lastPickup['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 Pickup): ${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 Pickup): ${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 resp = await _updateProvider.updatePickup(payload); final ok = _isSuccess(resp); if (ok) { // Update last pickup location to current location so next Booking starts here final actualLat = ll['lat'] ?? '0'; final actualLng = ll['lng'] ?? '0'; if (actualLat != '0' && actualLng != '0') { await _saveLastPickupLocation(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 pickup /// Returns true ONLY if has_live_pickup is true (verified by API) /// This ensures PiP is only enabled when there are actually active pickup Future hasActivePickups() async { try { final prefs = await SharedPreferences.getInstance(); final hasLive = prefs.getBool('has_live_pickup') ?? false; // ✅ CRITICAL: Only return true if has_live_pickup is true // Don't rely on active_pickup_order_id alone - it may be stale // The API sets has_live_pickup based on actual Booking status = "active" if (!hasLive) { // Clear stale active_pickup_order_id if has_live_pickup is false final activeOrderId = prefs.getString('active_pickup_order_id'); if (activeOrderId != null && activeOrderId.isNotEmpty) { debugPrint( '[PICKUPS_CONTROLLER] Clearing stale active_pickup_order_id: $activeOrderId (no active pickup)', ); await prefs.remove('active_pickup_order_id'); } } debugPrint( '[PICKUPS_CONTROLLER] hasActivePickups: $hasLive (has_live_pickup from API)', ); return hasLive; } catch (e) { debugPrint('[PICKUPS_CONTROLLER] Error checking active pickup: $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'); } } }