Files
Xpress-rider/lib/views/Dashboard/Cart/cartpage.dart

996 lines
42 KiB
Dart

part of '../deliveries/deliveries.dart';
// -------------------------------------------------------------------------
// CART PAGE (Active Deliveries)
// -------------------------------------------------------------------------
class Cartpage extends StatefulWidget {
const Cartpage({super.key});
@override
State<Cartpage> createState() => _CartpageState();
}
class _CartpageState extends State<Cartpage> with AutomaticKeepAliveClientMixin {
final DeliveryProvider _provider = DeliveryProvider();
final CreateDeliveryLogProvider _deliveryLogProvider = CreateDeliveryLogProvider();
List<Map<String, dynamic>> _activeDeliveries = <Map<String, dynamic>>[];
StreamSubscription<void>? _pollerSubscription;
bool _fetching = false;
final Map<String, Timer> _deliveryTimers = <String, Timer>{};
final Map<String, Map<String, dynamic>> _deliveryBasePayload = <String, Map<String, dynamic>>{};
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_fetchActive();
_startPolling();
}
@override
void dispose() {
_pollerSubscription?.cancel();
_stopAllTimers();
super.dispose();
}
void _startPolling() {
_pollerSubscription?.cancel();
_pollerSubscription = Stream.periodic(
const Duration(seconds: 10),
(_) {},
).asyncMap((_) async {
if (!_fetching && mounted) {
await _fetchActive();
}
}).listen(
(_) {},
onError: (error) {
debugPrint('[CART][STREAM ERROR] $error');
},
cancelOnError: false,
);
}
Future<void> _fetchActive() async {
if (_fetching) return;
_fetching = true;
try {
final prefs = await SharedPreferences.getInstance();
final userId = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
if (userId == 0) {
debugPrint('[CART] No user ID found');
_fetching = false;
return;
}
// Get current date in YYYY-MM-DD format
final now = DateTime.now();
final today = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
// Hardcoded API endpoint for cart page only: v2/deliveries/getdeliveries
final bool isLive = ApiConstants.mainRoute == 'live';
final baseUrl = isLive
? 'https://jupiter.nearle.app/live/api/v2/deliveries/getdeliveries'
: 'https://jupiter.nearle.app/dev/api/v2/deliveries/getdeliveries';
final uri = Uri.parse(baseUrl).replace(queryParameters: {
'userid': userId.toString(),
'fromdate': today,
'todate': today,
't': DateTime.now().millisecondsSinceEpoch.toString(),
});
debugPrint('[CART] Fetching from: $uri');
// Fetch deliveries from API directly using http
// Hardcoded endpoint for cart page: v2/deliveries/getdeliveries
final httpClient = http.Client();
List<dynamic> items = [];
try {
final response = await httpClient.get(uri);
if (response.statusCode >= 200 && response.statusCode < 300) {
final decoded = json.decode(response.body);
final data = decoded is Map<String, dynamic>
? (decoded['details'] ?? decoded['data'] ?? decoded)
: decoded;
items = data is List
? data
: (data is Map && data['items'] is List ? data['items'] as List : []);
} else {
debugPrint('[CART] API error: ${response.statusCode}');
}
} catch (e) {
debugPrint('[CART] Error fetching from API: $e');
} finally {
httpClient.close();
}
debugPrint('[CART] Raw API items count: ${items.length}');
// Debug: Print all order statuses to see what we're getting
for (final item in items) {
if (item is Map<String, dynamic>) {
final orderId = (item['orderid'] ?? '').toString();
final status = (item['orderstatus'] ?? '').toString();
debugPrint('[CART] Order $orderId has status: "$status" (raw: ${item['orderstatus']})');
}
}
// Filter for ACTIVE status only
final activeOrders = items
.whereType<Map<String, dynamic>>()
.where((order) {
final status = (order['orderstatus']?.toString().toLowerCase() ?? '').trim();
final isActive = status == 'active';
if (isActive) {
debugPrint('[CART] ✅ Found active order: ${order['orderid']}');
} else {
debugPrint('[CART] ❌ Order ${order['orderid']} has status: "$status" (not active)');
}
return isActive;
})
.toList();
debugPrint('[CART] Found ${activeOrders.length} active deliveries out of ${items.length} total');
// Sort by order ID or step number if available
activeOrders.sort((a, b) {
final stepA = (a['step'] ?? a['Step'] ?? 0).toString();
final stepB = (b['step'] ?? b['Step'] ?? 0).toString();
final stepAInt = int.tryParse(stepA) ?? 0;
final stepBInt = int.tryParse(stepB) ?? 0;
if (stepAInt != stepBInt) return stepAInt.compareTo(stepBInt);
final orderIdA = (a['orderid'] ?? '').toString();
final orderIdB = (b['orderid'] ?? '').toString();
return orderIdA.compareTo(orderIdB);
});
if (mounted) {
setState(() {
_activeDeliveries = activeOrders;
});
}
// Get all active order IDs
final activeOrderIds = activeOrders
.map((o) => (o['orderid'] ?? '').toString())
.where((id) => id.isNotEmpty)
.toSet();
// ✅ CRITICAL: Start/restart timers for ALL active deliveries
// This ensures every active delivery posts logs every 30 seconds
for (final order in activeOrders) {
final orderId = (order['orderid'] ?? '').toString();
if (orderId.isEmpty) {
debugPrint('[CART] ⚠️ Skipping order with empty orderId');
continue;
}
// Always restart timer to ensure it's running (handles edge cases)
if (_deliveryTimers.containsKey(orderId)) {
debugPrint('[CART] 🔄 Restarting timer for active delivery: $orderId');
_deliveryTimers[orderId]?.cancel();
_deliveryTimers.remove(orderId);
// Also clear base payload to force reload
_deliveryBasePayload.remove(orderId);
}
debugPrint('[CART] ▶️ Starting timer for active delivery: $orderId');
await _startDeliveryPosting(order);
debugPrint('[CART] ✅ Timer started successfully for: $orderId');
}
// ✅ Stop timers for deliveries that are no longer active
final timersToStop = _deliveryTimers.keys
.where((id) => !activeOrderIds.contains(id))
.toList();
for (final id in timersToStop) {
debugPrint('[CART] Stopping timer for orderId: $id (no longer active)');
_stopDeliveryPosting(id);
}
debugPrint('[CART] Active timers: ${_deliveryTimers.keys.toList()}');
} catch (e) {
debugPrint('[CART] Error fetching active deliveries: $e');
} finally {
_fetching = false;
}
}
Future<void> _startDeliveryPosting(Map<String, dynamic> order) async {
final orderId = (order['orderid'] ?? '').toString();
if (orderId.isEmpty) {
debugPrint('[CART][DELIVERYLOG] ⚠️ Cannot start timer: empty orderId');
return;
}
// Safety check: If timer already exists, cancel it first (shouldn't happen after cleanup above)
if (_deliveryTimers.containsKey(orderId)) {
debugPrint('[CART][DELIVERYLOG] ⚠️ Timer already exists for $orderId, canceling old one');
_deliveryTimers[orderId]?.cancel();
_deliveryTimers.remove(orderId);
}
debugPrint('[CART][DELIVERYLOG] 🚀 Starting 30-second timer for orderId: $orderId');
// Get starttime from SharedPreferences (saved when order became active via updateActiveStatus)
// If not found, use activetime from order data, or current time as fallback
String startTime = '';
try {
final prefs = await SharedPreferences.getInstance();
final deliveryId = (order['deliveryid'] ?? 0).toString();
// Method 1: Get from SharedPreferences (saved when order became active)
startTime = prefs.getString('delivery_starttime_$deliveryId') ?? '';
if (startTime.isNotEmpty) {
debugPrint('[CART][DELIVERYLOG] ✅ Loaded starttime from SharedPreferences: $startTime');
}
// Method 2: Fallback - try to get from order data (starttime field)
if (startTime.isEmpty) {
startTime = (order['starttime'] ?? order['startTime'] ?? '').toString();
if (startTime.isNotEmpty) {
debugPrint('[CART][DELIVERYLOG] ✅ Loaded starttime from order data: $startTime');
}
}
// Method 3: Fallback - try activetime from order data
if (startTime.isEmpty) {
final activetime = (order['activetime'] ?? order['activTime'] ?? '').toString();
if (activetime.isNotEmpty) {
startTime = activetime;
debugPrint('[CART][DELIVERYLOG] ✅ Loaded starttime from activetime: $startTime');
}
}
// Method 4: Last fallback - current time (shouldn't happen if updateActiveStatus was called)
if (startTime.isEmpty) {
final now = DateTime.now();
startTime = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
debugPrint('[CART][DELIVERYLOG] ⚠️ Using current time as starttime fallback: $startTime');
}
} catch (e) {
debugPrint('[CART][DELIVERYLOG] ❌ Error getting starttime: $e');
// Set a fallback starttime even on error
final now = DateTime.now();
startTime = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
}
// CRITICAL: Ensure starttime is never empty
if (startTime.isEmpty) {
final now = DateTime.now();
startTime = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
debugPrint('[CART][DELIVERYLOG] ⚠️ Final fallback: starttime was empty, using: $startTime');
}
debugPrint('[CART][DELIVERYLOG] 📝 Final starttime for orderId $orderId: $startTime');
// Create base payload with starttime
final base = <String, dynamic>{
'logid': 0,
'tenantid': order['tenantid'] ?? 0,
'partnerid': order['partnerid'] ?? 0,
'locationid': order['locationid'] ?? 0,
'orderheaderid': order['orderheaderid'] ?? 0,
'deliveryid': order['deliveryid'] ?? 0,
'userid': order['userid'] ?? 0,
'orderid': orderId,
'orderstatus': 'active',
'starttime': startTime, // Include starttime in base payload
};
_deliveryBasePayload[orderId] = base;
// Save to SharedPreferences for persistence
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('deliverylog_${orderId}_tenantid', (base['tenantid'] ?? 0).toString());
await prefs.setString('deliverylog_${orderId}_partnerid', (base['partnerid'] ?? 0).toString());
await prefs.setString('deliverylog_${orderId}_locationid', (base['locationid'] ?? 0).toString());
await prefs.setString('deliverylog_${orderId}_orderheaderid', (base['orderheaderid'] ?? 0).toString());
await prefs.setString('deliverylog_${orderId}_deliveryid', (base['deliveryid'] ?? 0).toString());
await prefs.setString('deliverylog_${orderId}_userid', (base['userid'] ?? 0).toString());
await prefs.setString('deliverylog_${orderId}_orderid', orderId);
await prefs.setString('deliverylog_${orderId}_orderstatus', 'active');
await prefs.setString('deliverylog_${orderId}_starttime', startTime); // Save starttime
} catch (e) {
debugPrint('[CART][DELIVERYLOG] Error saving payload: $e');
}
// Post once immediately (don't await - let it run in background)
_postDeliveryLog(orderId, order);
debugPrint('[CART][DELIVERYLOG] 📤 Posted initial log for orderId: $orderId');
// Then every 30 seconds - CRITICAL: This ensures logs are posted every 30 seconds
final timer = Timer.periodic(const Duration(seconds: 30), (t) {
debugPrint('[CART][DELIVERYLOG] ⏰ Timer tick for orderId: $orderId (30 seconds elapsed)');
_postDeliveryLog(orderId, order);
});
_deliveryTimers[orderId] = timer;
debugPrint('[CART][DELIVERYLOG] ✅ Timer registered for orderId: $orderId (will post every 30 seconds)');
}
void _postDeliveryLog(String orderId, Map<String, dynamic> order) {
if (!mounted) {
debugPrint('[CART][DELIVERYLOG][POST] Widget disposed, skipping');
return;
}
debugPrint('[CART][DELIVERYLOG][POST] ⏰ Posting log for orderId: $orderId at ${DateTime.now()}');
// Use Future.microtask to ensure the async operation runs independently
Future.microtask(() => _performPost(orderId));
}
Future<void> _performPost(String orderId) async {
try {
debugPrint('[CART][DELIVERYLOG][POST] 🔄 Starting _performPost for orderId: $orderId');
Map<String, dynamic>? base = _deliveryBasePayload[orderId];
if (base == null) {
debugPrint('[CART][DELIVERYLOG][POST] Base is null, loading from SharedPreferences');
try {
final prefs = await SharedPreferences.getInstance();
if (!mounted) {
debugPrint('[CART][DELIVERYLOG][POST] Widget unmounted after prefs load');
return;
}
base = {
'logid': 0,
'tenantid': int.tryParse(prefs.getString('deliverylog_${orderId}_tenantid') ?? '0') ?? 0,
'partnerid': int.tryParse(prefs.getString('deliverylog_${orderId}_partnerid') ?? '0') ?? 0,
'locationid': int.tryParse(prefs.getString('deliverylog_${orderId}_locationid') ?? '0') ?? 0,
'orderheaderid': int.tryParse(prefs.getString('deliverylog_${orderId}_orderheaderid') ?? '0') ?? 0,
'deliveryid': int.tryParse(prefs.getString('deliverylog_${orderId}_deliveryid') ?? '0') ?? 0,
'userid': int.tryParse(prefs.getString('deliverylog_${orderId}_userid') ?? '0') ?? 0,
'orderid': prefs.getString('deliverylog_${orderId}_orderid') ?? orderId,
'orderstatus': prefs.getString('deliverylog_${orderId}_orderstatus') ?? 'active',
'starttime': prefs.getString('deliverylog_${orderId}_starttime') ?? '', // Load starttime
};
debugPrint('[CART][DELIVERYLOG][POST] Base loaded from prefs: $base');
} catch (e) {
debugPrint('[CART][DELIVERYLOG][POST] Error loading base: $e');
return;
}
}
// At this point, base is guaranteed to be non-null (either from cache or created above)
final basePayload = base; // Flow analysis ensures base is non-null here
debugPrint('[CART][DELIVERYLOG][POST] Getting coordinates...');
// CRITICAL: Get coordinates with retry logic - NEVER post with null or '0' coordinates
final coords = await _getValidCoordinates().timeout(
const Duration(seconds: 10), // Increased timeout to allow retries
onTimeout: () {
debugPrint('[CART][DELIVERYLOG][POST] ❌ Coordinate timeout after retries');
return null;
},
);
if (!mounted) {
debugPrint('[CART][DELIVERYLOG][POST] Widget unmounted after coords');
return;
}
// CRITICAL: Validate coordinates - NEVER post with null, '0', or invalid coordinates
if (coords == null || coords.$1.isEmpty || coords.$2.isEmpty ||
coords.$1 == '0' || coords.$2 == '0') {
debugPrint('[CART][DELIVERYLOG][POST] ❌ SKIPPING POST: Invalid coordinates (lat=${coords?.$1 ?? 'null'}, lng=${coords?.$2 ?? 'null'})');
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ Will retry on next timer tick (30 seconds)');
return; // Skip this post - don't send invalid coordinates
}
// Validate coordinate ranges
final latDouble = double.tryParse(coords.$1) ?? 0.0;
final lngDouble = double.tryParse(coords.$2) ?? 0.0;
if (latDouble == 0 || lngDouble == 0 ||
latDouble.abs() > 90 || lngDouble.abs() > 180) {
debugPrint('[CART][DELIVERYLOG][POST] ❌ SKIPPING POST: Invalid coordinate ranges (lat=$latDouble, lng=$lngDouble)');
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ Will retry on next timer tick (30 seconds)');
return; // Skip this post - don't send invalid coordinates
}
debugPrint('[CART][DELIVERYLOG][POST] ✅ Valid coordinates: lat=${coords.$1}, lng=${coords.$2}');
// Cumulative KM is tracked exclusively by LiveTrackingService (high-frequency, every 3s).
// Do not accumulate here to avoid race conditions with concurrent writers.
final now = DateTime.now();
final logdate = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
// CRITICAL: Ensure starttime is always included in payload
final starttimeValue = basePayload['starttime']?.toString() ?? '';
if (starttimeValue.isEmpty) {
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ WARNING: starttime is empty in basePayload, using fallback');
}
// CRITICAL: Use validated coordinates - guaranteed to be non-null and valid at this point
final payload = {
...basePayload,
'logdate': logdate,
'latitude': coords.$1, // Guaranteed non-null and valid
'longitude': coords.$2, // Guaranteed non-null and valid
'starttime': starttimeValue.isNotEmpty ? starttimeValue : '', // CRITICAL: Always include starttime
};
// Validate payload has all required fields
final requiredFields = ['tenantid', 'partnerid', 'locationid', 'orderheaderid', 'deliveryid', 'userid', 'orderid', 'orderstatus', 'starttime'];
final missingFields = requiredFields.where((field) => payload[field] == null || payload[field] == '').toList();
if (missingFields.isNotEmpty) {
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ WARNING: Missing fields in payload: $missingFields');
}
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createDeliveryLogLive
: ApiConstants.createDeliveryLogDev;
debugPrint('[CART][DELIVERYLOG][POST] 📤 Sending to API: $url');
debugPrint('[CART][DELIVERYLOG][POST] 📦 Payload: $payload');
debugPrint('[CART][DELIVERYLOG][POST] ✅ starttime in payload: "${payload['starttime']}"');
await _deliveryLogProvider
.createDeliveryLog(url, payload)
.timeout(
const Duration(seconds: 8),
onTimeout: () {
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ API timeout for orderId: $orderId');
throw TimeoutException('API timeout', const Duration(seconds: 8));
},
);
debugPrint('[CART][DELIVERYLOG][POST] ✅ SUCCESS for orderId: $orderId at ${DateTime.now()}');
} catch (e, stackTrace) {
debugPrint('[CART][DELIVERYLOG][POST] ❌ ERROR for orderId: $orderId - $e');
debugPrint('[CART][DELIVERYLOG][POST] Stack trace: $stackTrace');
}
}
Future<(String lat, String lng)?> _getValidCoordinates({int retryCount = 0}) async {
const maxRetries = 3;
try {
final bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
debugPrint('[CART][COORDS] Location service disabled, trying last known position');
final lastPos = await Geolocator.getLastKnownPosition();
if (lastPos != null && lastPos.latitude != 0 && lastPos.longitude != 0) {
final lat = lastPos.latitude.toString();
final lng = lastPos.longitude.toString();
debugPrint('[CART][COORDS] ✅ Using last known position: $lat, $lng');
return (lat, lng);
}
// Retry if we haven't exceeded max retries
if (retryCount < maxRetries) {
await Future.delayed(const Duration(milliseconds: 500));
return _getValidCoordinates(retryCount: retryCount + 1);
}
return null;
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.deniedForever ||
permission == LocationPermission.denied) {
debugPrint('[CART][COORDS] Permission denied, trying last known position');
final lastPos = await Geolocator.getLastKnownPosition();
if (lastPos != null && lastPos.latitude != 0 && lastPos.longitude != 0) {
final lat = lastPos.latitude.toString();
final lng = lastPos.longitude.toString();
debugPrint('[CART][COORDS] ✅ Using last known position: $lat, $lng');
return (lat, lng);
}
// Retry if we haven't exceeded max retries
if (retryCount < maxRetries) {
await Future.delayed(const Duration(milliseconds: 500));
return _getValidCoordinates(retryCount: retryCount + 1);
}
return null;
}
Position? position;
try {
// Try to get current position with higher accuracy
position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high, // Changed to high for better accuracy
timeLimit: Duration(seconds: 8), // Increased timeout
),
).timeout(const Duration(seconds: 8));
} catch (e) {
debugPrint('[CART][COORDS] Timeout getting current position: $e, trying last known');
position = await Geolocator.getLastKnownPosition();
}
if (position != null && position.latitude != 0 && position.longitude != 0) {
final lat = position.latitude.toString();
final lng = position.longitude.toString();
// Validate coordinates are within valid GPS ranges
final latDouble = double.tryParse(lat) ?? 0.0;
final lngDouble = double.tryParse(lng) ?? 0.0;
if (latDouble.abs() <= 90 && lngDouble.abs() <= 180) {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('last_lat', lat);
await prefs.setString('last_lng', lng);
} catch (_) {}
debugPrint('[CART][COORDS] ✅ Got valid coordinates: $lat, $lng');
return (lat, lng);
} else {
debugPrint('[CART][COORDS] ⚠️ Invalid coordinate ranges: $lat, $lng');
}
}
// Fallback to SharedPreferences cached coordinates
try {
final prefs = await SharedPreferences.getInstance();
final lat = (prefs.getString('last_lat') ?? '').trim();
final lng = (prefs.getString('last_lng') ?? '').trim();
if (lat.isNotEmpty && lng.isNotEmpty && lat != '0' && lng != '0') {
final latDouble = double.tryParse(lat) ?? 0.0;
final lngDouble = double.tryParse(lng) ?? 0.0;
if (latDouble != 0 && lngDouble != 0 && latDouble.abs() <= 90 && lngDouble.abs() <= 180) {
debugPrint('[CART][COORDS] ✅ Using cached coordinates: $lat, $lng');
return (lat, lng);
}
}
} catch (_) {}
// Retry if we haven't exceeded max retries
if (retryCount < maxRetries) {
debugPrint('[CART][COORDS] ⚠️ Retry ${retryCount + 1}/$maxRetries to get coordinates');
await Future.delayed(const Duration(milliseconds: 500));
return _getValidCoordinates(retryCount: retryCount + 1);
}
debugPrint('[CART][COORDS] ❌ Failed to get valid coordinates after $maxRetries retries');
return null;
} catch (e) {
debugPrint('[CART][COORDS] ❌ Error getting coordinates: $e');
// Retry if we haven't exceeded max retries
if (retryCount < maxRetries) {
await Future.delayed(const Duration(milliseconds: 500));
return _getValidCoordinates(retryCount: retryCount + 1);
}
return null;
}
}
void _stopDeliveryPosting(String orderId) {
_deliveryTimers[orderId]?.cancel();
_deliveryTimers.remove(orderId);
_deliveryBasePayload.remove(orderId);
debugPrint('[CART][DELIVERYLOG] Stopped timer for orderId: $orderId');
}
void _stopAllTimers() {
for (final timer in _deliveryTimers.values) {
timer.cancel();
}
_deliveryTimers.clear();
_deliveryBasePayload.clear();
debugPrint('[CART][DELIVERYLOG] Stopped all timers');
}
// Method to stop delivery posting for a specific order (called when order is completed)
void stopDeliveryPostingForOrder(String orderId) {
_stopDeliveryPosting(orderId);
// Refresh the list to remove completed orders
if (mounted) {
_fetchActive();
}
}
double _parseD(dynamic v) {
if (v == null) return 0.0;
if (v is num) return v.toDouble();
return double.tryParse(v.toString()) ?? 0.0;
}
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
const double R = 6371.0;
final double dLat = _toRadians(lat2 - lat1);
final double dLon = _toRadians(lon2 - lon1);
final double a = math.sin(dLat / 2) * math.sin(dLat / 2) +
math.cos(_toRadians(lat1)) *
math.cos(_toRadians(lat2)) *
math.sin(dLon / 2) *
math.sin(dLon / 2);
final double c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
return R * c;
}
double _toRadians(double degrees) {
return degrees * math.pi / 180.0;
}
String _distanceKmDisplay(Map<String, dynamic> m) {
// Try API distance first
final String apiKmsStr = (m['actualkms'] ?? m['distance'] ?? '0').toString();
final double apiKms = double.tryParse(apiKmsStr) ?? 0.0;
if (apiKms > 0) {
return apiKms < 10 ? apiKmsStr : apiKms.toStringAsFixed(0);
}
// Try rider location to delivery location
final double rLat = _parseD(m['riderslat']);
final double rLon = _parseD(m['riderslon']);
final double dLat = _parseD(m['droplat'] ?? m['deliverylat']);
final double dLon = _parseD(m['droplon'] ?? m['deliverylong']);
if (rLat != 0 && rLon != 0 && dLat != 0 && dLon != 0) {
final double km = _haversineKm(rLat, rLon, dLat, dLon);
return km.toStringAsFixed(km < 10 ? 1 : 0);
}
// Fallback: pickup to delivery
final double pLat = _parseD(m['pickuplat']);
final double pLon = _parseD(m['pickuplon']);
if (pLat != 0 && pLon != 0 && dLat != 0 && dLon != 0) {
final double km = _haversineKm(pLat, pLon, dLat, dLon);
return km.toStringAsFixed(km < 10 ? 1 : 0);
}
return '0';
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
backgroundColor: Colors.grey.shade200,
appBar: AppBar(
backgroundColor: Colors.grey.shade200,
elevation: 0,
centerTitle: false,
toolbarHeight: 70,
title: Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
"Active Deliveries",
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
color: Colors.black,
),
),
),
bottom: const PreferredSize(
preferredSize: Size.fromHeight(1),
child: Divider(height: 1, color: Colors.grey),
),
),
body: SafeArea(
child: _activeDeliveries.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 80),
Transform.translate(
offset: const Offset(0, -11),
child: Image.asset(
"assets/images/Nearle Bike.png",
errorBuilder: (c, e, s) => const Icon(
Icons.delivery_dining,
size: 120,
color: Colors.grey,
),
),
),
const SizedBox(height: 16),
Transform.translate(
offset: const Offset(0, -11),
child: Text(
"No Active Deliveries",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
fontFamily: FontConstants.fontFamily,
color: Colors.grey,
),
),
),
],
),
)
: RefreshIndicator(
onRefresh: _fetchActive,
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 20),
itemCount: _activeDeliveries.length,
itemBuilder: (context, index) {
final item = _activeDeliveries[index];
final customerName = (item['deliverycustomer'] ?? item['customer'] ?? 'Customer').toString();
final address = (item['deliveryaddress'] ?? item['address'] ?? 'Address not available').toString();
final storeName = (item['pickupcustomer'] ?? item['store'] ?? 'Store').toString();
final orderId = (item['orderid'] ?? '').toString();
final distance = _distanceKmDisplay(item);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// -------------------------------------------
// TOP CUSTOMER DETAILS
// -------------------------------------------
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
Container(
width: 12,
height: 12,
decoration: const BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
),
Container(
width: 2,
height: 30,
color: Colors.grey.shade300,
),
Container(
width: 12,
height: 12,
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
),
],
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Transform.translate(
offset: const Offset(0, -3),
child: Text(
customerName,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
color: Colors.black,
fontFamily: FontConstants.fontFamily,
),
),
),
const SizedBox(height: 8),
Transform.translate(
offset: const Offset(0, 8),
child: Text(
address,
style: TextStyle(
fontSize: 18,
color: Colors.black87,
fontFamily: FontConstants.fontFamily,
),
),
),
const SizedBox(height: 8),
Text(
'Distance: $distance km',
style: TextStyle(
fontSize: 16,
color: Colors.blueGrey,
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
InkWell(
onTap: () async {
// Just launch dialer; PiP is handled only from navigation screen
final phone =
(item['deliverycontactno'] ?? '').toString();
final bool success = await launchPhoneDialer(
phone.isNotEmpty ? phone : '9876543210',
);
if (!success && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Could not launch dialer'),
),
);
}
},
child: Image.asset(
'assets/images/phone-call .png',
height: 27,
width: 27,
errorBuilder: (c, e, s) =>
const Icon(Icons.phone, size: 27, color: Colors.green),
),
),
],
),
const SizedBox(height: 8),
const Divider(),
const SizedBox(height: 8),
// -------------------------------------------
// STORE DETAILS
// -------------------------------------------
Row(
children: [
Image.asset(
'assets/images/shoppingbag.png',
height: 32,
width: 32,
errorBuilder: (c, e, s) => const Icon(
Icons.shopping_bag,
size: 32,
color: Colors.orange,
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
storeName,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
color: Colors.black,
fontFamily: FontConstants.fontFamily,
),
),
InkWell(
onTap: () async {
_showMyOptionsSheet(
context,
item,
null, // No parent state for cart page
);
// Refresh cart page after skip (order will no longer be active)
if (mounted) {
await Future.delayed(const Duration(seconds: 1));
_fetchActive();
}
},
child: Transform.translate(
offset: const Offset(0, -5),
child: Text(
'Skip>>',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
color: ColorConstants.primaryColor,
),
),
),
),
],
),
Text(
"Order ID: #$orderId",
style: TextStyle(
fontSize: 18,
color: Colors.black54,
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
],
),
const SizedBox(height: 15),
// -------------------------------------------
// SLIDER BUTTON
// -------------------------------------------
SliderButton(
properties: SliderButtonProperties(
height: 50,
buttonSize: 45,
width: MediaQuery.of(context).size.width - 56,
backgroundColor: ColorConstants.primaryColor,
dismissThresholds: 0.90,
action: () async {
await Future.delayed(const Duration(milliseconds: 400));
if (!context.mounted) return false;
// Navigate to delivery map screen (same as deliveries page)
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => _DeliveryMapScreen(
delivery: item,
parentState: null, // No parent state for cart page
),
),
);
// Refresh cart page when returning from map screen
// (in case delivery was completed/cancelled)
if (mounted) {
await Future.delayed(const Duration(milliseconds: 500));
_fetchActive();
}
return false;
},
label: const Text(
'Slide to start Delivery',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
icon: ClipOval(
child: Material(
color: Colors.white,
child: SizedBox(
width: 45,
height: 45,
child: Center(
child: Text(
'${index + 1}',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
),
),
),
),
),
],
),
),
);
},
),
),
),
);
}
}