Files
doormile_milderapp/lib/controllers/pickups_controller.dart
2026-08-11 13:16:33 +05:30

1982 lines
70 KiB
Dart

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<String, dynamic>? 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<bool> lastOnTime = Rxn<bool>();
final Rxn<Duration> lastLateBy = Rxn<Duration>();
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<String?> 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<Map<String, String>> _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<bool> 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 = <String, dynamic>{
'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<bool> 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 = <String, dynamic>{
'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<void> setNotificationSent(bool value) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('notificationSent', value);
} catch (_) {}
}
// ---------------- Geofencing helpers ----------------
Future<int> _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<bool> _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<Map<String, String>> _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<void> _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<void> _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<Map<String, String>> _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<Map<String, String>> _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<double?> _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<void> _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<double> _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<bool> 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 = <String, dynamic>{
'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<bool> 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<String, String> 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 = <String, dynamic>{
'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<bool> 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 = <String, dynamic>{
'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<bool> 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 = <String, dynamic>{
'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<bool> 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 = <String, dynamic>{
'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<bool> 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 = <String, dynamic>{
'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<bool> 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<Map<String, dynamic>> 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<void> 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');
}
}
}