Files
doormile_milderapp/lib/xpress/views/Dashboard/Cart/cartpage.dart
Thiru-tenext d7348e253f Miler rider app: surface system, visible design language, backend lifecycle
Design system
- MilerSurface ladder (canvas → working → raised → floating) with MilerPanel
  as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1.
- Visible vocabulary applied across Home, Deliveries, Activity, Account and
  the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x),
  canvas wells for anything that opens, small filled tags for shelf labels,
  demoted placeholders. Recorded in DESIGN_SYSTEM.md §6.
- One icon family: 222 Material glyphs migrated to Lucide; none left outside
  lib/xpress.
- Colour semantics corrected: amber only for what is genuinely owed, brand red
  reserved for the live stop, disabled primaries go neutral rather than pale.

Data and lifecycle
- lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart
  makes admin sequence the single ordering authority; service_day.dart, and
  stop_area.dart rewritten against live Coimbatore addresses (digit-token
  stripping, city stoplist, street suffixes, stammer collapse).
- countLabel states the load once, in bags.

Testing
- 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity,
  sheets and verify, with test/failures/ now gitignored (diff debris).
- New pins: home_gutter_test, stop_area_test, plus updated structural bounds.

Note: this commit also carries pre-existing working-tree deletions that were
present before this work (API_SPEC.md, README.md, demo test fixtures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 05:40:35 +05:30

1200 lines
46 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')}';
// One Doormile route now — see [DeliveryApi].
final baseUrl = DeliveryApi.currentDeliveries;
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/Doormile 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,
),
),
),
),
),
),
),
),
],
),
),
);
},
),
),
),
);
}
}