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