// ignore_for_file: unuse, unused_element, duplicate_ignore, unnecessary_cast library; import 'dart:async'; import 'dart:convert'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_polyline_points/flutter_polyline_points.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:lottie/lottie.dart' hide Marker; import 'package:nearle/views/helpers/constants/Colorconstants.dart'; import 'package:nearle/views/helpers/constants/Font_constant.dart'; import 'package:slider_button_lite/feature/presentation/slider_button/slider.dart'; import 'package:slider_button_lite/feature/presentation/slider_button/slider_button_prop.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:nearle/providers/delivery/delivery_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:nearle/controllers/deliveries_controller.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:nearle/widget/Bottom_page.dart'; import 'package:circular_countdown_timer/circular_countdown_timer.dart'; import 'dart:math' as math; import 'package:nearle/controllers/riderlog.dart'; import 'package:nearle/providers/deliverylog/deliverylog_provider.dart'; import 'package:nearle/views/helpers/constants/apiconstants.dart'; import 'dart:io' show Platform, File; import 'package:image_picker/image_picker.dart'; import 'package:flutter/services.dart'; import 'package:floating/floating.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:http/http.dart' as http; import 'package:scratcher/scratcher.dart'; import 'package:confetti/confetti.dart'; part 'card.dart'; part 'map.dart'; part 'nav.dart'; part 'pip.dart'; part 'sheet.dart'; part 'map_btn.dart'; part 'multi_map.dart'; part 'done.dart'; part 'skip_sheet.dart'; part '../Cart/cartpage.dart'; /// Helper function to launch phone dialer - works in both debug and release builds /// In release builds, canLaunchUrl may fail due to R8/ProGuard, so we always try to launch Future launchPhoneDialer(String phoneNumber) async { try { // Sanitize phone number: keep only digits and '+' final phone = phoneNumber.replaceAll(RegExp(r'[^\d+]'), ''); if (phone.isEmpty) { debugPrint( '[PHONE] Empty phone number after sanitization, skipping dial', ); return false; } final Uri uri = Uri(scheme: 'tel', path: phone); // Try canLaunchUrl first (works in debug, may fail in release) bool canLaunch = false; try { canLaunch = await canLaunchUrl(uri); debugPrint('[PHONE] canLaunchUrl result: $canLaunch'); } catch (e) { debugPrint('[PHONE] canLaunchUrl check failed (common in release): $e'); // Continue anyway - launch might still work } // Always attempt to launch, even if canLaunchUrl returned false // Using LaunchMode.platformDefault is often safer for system intents like dialing try { final launched = await launchUrl(uri, mode: LaunchMode.platformDefault); if (launched) { debugPrint('[PHONE] Successfully launched dialer for: $phone'); return true; } else { debugPrint('[PHONE] launchUrl returned false for: $phone'); } } catch (e) { debugPrint('[PHONE] Failed to launch dialer: $e'); } return false; } catch (e) { debugPrint('[PHONE] Error in launchPhoneDialer: $e'); return false; } } class MyDeliveries extends StatefulWidget { const MyDeliveries({super.key}); @override State createState() => _MyDeliveriesState(); // ✅ Public static method to navigate to delivery map screen from outside (e.g., home page) static Future navigateToDeliveryMap( BuildContext context, Map delivery, ) async { await Navigator.push( context, MaterialPageRoute( builder: (context) => _RiderNavigationScreen( delivery: delivery, parentState: null, // No parent state when called from outside ), ), ); } } class _MyDeliveriesState extends State with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { final DeliveryProvider _provider = DeliveryProvider(); final CreateDeliveryLogProvider _deliveryLogProvider = CreateDeliveryLogProvider(); List> _picked = >[]; List> _activeDeliveries = >[]; // Track active deliveries for banner StreamSubscription? _pollerSubscription; bool _fetching = false; Position? _currentLocation; final Map _deliveryTimers = {}; final Map> _deliveryBasePayload = >{}; // Preserve original step numbers so they don't change when deliveries are completed final Map _preservedStepNumbers = {}; String? _activeDeliveryOrderId; // Cache for skipped orders final Map _skippedOrdersCache = {}; final Map _skippedOrderTimestamps = {}; Future _saveSkippedOrdersCache() async { // Implementation can be empty if we rely on API now, // or strictly local. For now, we'll keep it simple or empty // to satisfy the interface expected by child widgets. // If child widgets call this, they expect it to exist. } @override bool get wantKeepAlive => true; 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; } int _getStepNumber(Map order) { final dynamic raw = order['step'] ?? order['Step']; final int step = raw == null ? 0 : (raw is num ? raw.toInt() : int.tryParse(raw.toString()) ?? 0); return step; } String _getOrderKey(Map order) { // Use deliveryId as primary key, fallback to orderId final deliveryId = (order['deliveryid'] ?? '').toString(); final orderId = (order['orderid'] ?? '').toString(); return deliveryId.isNotEmpty ? 'delivery_$deliveryId' : 'order_$orderId'; } int _getPreservedOrCurrentStep(Map order) { final orderKey = _getOrderKey(order); // If we have a preserved step number, use it; otherwise use current API step if (_preservedStepNumbers.containsKey(orderKey)) { return _preservedStepNumbers[orderKey]!; } final currentStep = _getStepNumber(order); // Preserve the step number if it's valid (> 0) if (currentStep > 0) { _preservedStepNumbers[orderKey] = currentStep; } return currentStep; } String _distanceKmDisplay(Map m) { final String apiKmsStr = (m['kms'] ?? '').toString().trim(); final double apiKms = double.tryParse(apiKmsStr) ?? 0.0; if (apiKms > 0) { return apiKms < 10 ? apiKmsStr : apiKms.toStringAsFixed(0); } 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); } 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'; } double _calculateDistanceFromCurrent(Map order) { if (_currentLocation == null) return double.infinity; final double dropLat = _parseD(order['droplat'] ?? order['deliverylat']); final double dropLon = _parseD(order['droplon'] ?? order['deliverylong']); if (dropLat == 0 || dropLon == 0) return double.infinity; return _haversineKm( _currentLocation!.latitude, _currentLocation!.longitude, dropLat, dropLon, ); } List> _sortOrders(List> orders) { if (orders.isEmpty) return orders; // Use preserved step numbers for sorting to maintain original order final ordersWithStep = orders .where((o) => _getPreservedOrCurrentStep(o) > 0) .toList(); final ordersWithoutStep = orders .where((o) => _getPreservedOrCurrentStep(o) == 0) .toList(); ordersWithStep.sort( (a, b) => _getPreservedOrCurrentStep( a, ).compareTo(_getPreservedOrCurrentStep(b)), ); if (_currentLocation != null && ordersWithoutStep.isNotEmpty) { ordersWithoutStep.sort((a, b) { final distA = _calculateDistanceFromCurrent(a); final distB = _calculateDistanceFromCurrent(b); return distA.compareTo(distB); }); } return [...ordersWithStep, ...ordersWithoutStep]; } int _getDisplayStepNumber(List> allOrders, int index) { final order = allOrders[index]; // Use preserved step number if available, otherwise get current step final step = _getPreservedOrCurrentStep(order); if (step > 0) return step; // For orders without step numbers, calculate based on orders that have step numbers final ordersWithStep = allOrders .where((o) => _getPreservedOrCurrentStep(o) > 0) .length; return ordersWithStep + (index - ordersWithStep) + 1; } @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); WakelockPlus.enable(); // Listen for external refresh triggers try { final dc = Get.isRegistered() ? Get.find() : Get.put(DeliveriesController()); ever(dc.refreshTrigger, (_) { if (mounted) { debugPrint('[MYDELIVERIES] External trigger -> Refreshing picked orders'); _fetchPicked(); } }); } catch (_) {} // Run these in parallel _restoreActiveDelivery(); _initializeLocation(); // Don't block _fetchPicked(); _startPollingStream(); } Future _restoreActiveDelivery() async { try { final prefs = await SharedPreferences.getInstance(); final savedActiveId = prefs.getString('active_delivery_order_id'); if (savedActiveId != null && savedActiveId.isNotEmpty) { _activeDeliveryOrderId = savedActiveId; debugPrint( '[MYDELIVERIES] Restored active delivery orderId: $savedActiveId', ); } } catch (e) { debugPrint('[MYDELIVERIES] Error restoring active delivery: $e'); } } void _startPollingStream() { _pollerSubscription?.cancel(); _pollerSubscription = Stream.periodic(const Duration(seconds: 3), (_) {}) .asyncMap((_) async { if (mounted && !_fetching) { await _fetchPicked(); } }) .listen( (_) {}, // Success handler onError: (error) { // Handle errors gracefully without crashing debugPrint('[MYDELIVERIES][STREAM ERROR] $error'); }, cancelOnError: false, // Continue even on errors ); } Future _initializeLocation() async { try { // 1. Try Last Known Position (Instant & Preferred) try { final lastPos = await Geolocator.getLastKnownPosition(); if (lastPos != null && mounted) { setState(() { _currentLocation = lastPos; }); // If we have a cached location, don't block waiting for fresh GPS // We can let the background stream update it later return; } } catch (_) {} // 2. Try Current Position (Optimized) final bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) return; LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.deniedForever || permission == LocationPermission.denied) { return; } // Single attempt with balanced accuracy/timeout try { final position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, timeLimit: Duration(seconds: 3), ), ); if (mounted) { setState(() => _currentLocation = position); } } catch (_) { // Fallback to low accuracy if high fails try { final position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.low, timeLimit: Duration(seconds: 2), ), ); if (mounted) { setState(() => _currentLocation = position); } } catch (_) {} } } catch (e) { debugPrint('[MYDELIVERIES] Error initializing location: $e'); } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _pollerSubscription?.cancel(); for (final timer in _deliveryTimers.values) { timer.cancel(); } _deliveryTimers.clear(); _deliveryBasePayload.clear(); // Persist current live deliveries state for other screens SharedPreferences.getInstance() .then((p) => p.setBool('has_live_deliveries', _picked.isNotEmpty)) // ignore: body_might_complete_normally_catch_error .catchError((_) {}); WakelockPlus.disable(); super.dispose(); } bool _shallowMapEquals(Map a, Map b) { if (identical(a, b)) return true; if (a.length != b.length) return false; for (final entry in a.entries) { if (b[entry.key] != entry.value) { return false; } } return true; } // Helper to check if two lists of orders are effectively equal // This prevents unnecessary rebuilds when polling bool _areOrdersEqual( List> oldList, List> newList, ) { if (oldList.length != newList.length) return false; for (int i = 0; i < oldList.length; i++) { final oldItem = oldList[i]; final newItem = newList[i]; // Compare critical fields that affect UI or logic if (oldItem['orderid'] != newItem['orderid']) return false; if (oldItem['orderstatus'] != newItem['orderstatus']) return false; if (oldItem['step'] != newItem['step']) return false; // Compare location data (important for map updates) if (oldItem['riderslat'] != newItem['riderslat']) return false; if (oldItem['riderslon'] != newItem['riderslon']) return false; if (oldItem['deliverylat'] != newItem['deliverylat']) return false; if (oldItem['deliverylong'] != newItem['deliverylong']) return false; // Compare notes/instructions if they might change if (oldItem['notes'] != newItem['notes']) return false; } return true; } // Manually mark skipped to trigger immediate refresh void markOrderAsSkipped(Map order, String reason) { if (!mounted) return; debugPrint('[MYDELIVERIES] Mark skipped -> Refreshing from API...'); // We don't update local state manually anymore, we trust the API to return the 'skipped' status. // Just trigger a fetch. _fetchPicked(); } // ✅ Check if there's an active delivery (blocks all actions) bool _hasActiveDelivery() { return _activeDeliveries.isNotEmpty; } // Previously showed a blocking dialog; now we just disable conflicting buttons in the UI. void _showActiveDeliveryBlockMessage() {} Future startDelivery(Map item) async { if (!mounted) return; // ✅ BLOCK: Check if there's already an active delivery (and this isn't it) final currentOrderId = (item['orderid'] ?? '').toString(); final activeOrderIds = _activeDeliveries .map((d) => (d['orderid'] ?? '').toString()) .where((id) => id.isNotEmpty) .toSet(); // Allow if this IS the active delivery, block if there's a different active delivery if (_hasActiveDelivery() && !activeOrderIds.contains(currentOrderId)) { // Just ignore taps on other cards when a different active delivery exists. return; } // Verify status is updated to ACTIVE before navigating final dc = Get.put(DeliveriesController()); final dId = int.tryParse((item['deliveryid'] ?? 0).toString()) ?? 0; final ohId = int.tryParse((item['orderheaderid'] ?? 0).toString()) ?? 0; if (dId > 0 && ohId > 0) { // We don't block navigation on failure, but we try to update // This ensures 'starttime' is generated and saved await dc.updateActiveStatus( deliveryId: dId, orderHeaderId: ohId, orderId: (item['orderid'] ?? '').toString(), ); } await Navigator.push( context, MaterialPageRoute( builder: (context) => _RiderNavigationScreen(delivery: item, parentState: this), ), ); } Future resumeDelivery(Map item) async { // ✅ BLOCK: Check if there's already an active delivery (and this isn't it) final currentOrderId = (item['orderid'] ?? '').toString(); final activeOrderIds = _activeDeliveries .map((d) => (d['orderid'] ?? '').toString()) .where((id) => id.isNotEmpty) .toSet(); // Allow if this IS the active delivery, block if there's a different active delivery if (_hasActiveDelivery() && !activeOrderIds.contains(currentOrderId)) { // Ignore resume taps when some other delivery is active. return; } // For now, resume behaves same as start (navigates to nav screen) // You can add specific resume logic here if needed (e.g. un-skip) await startDelivery(item); } Future _fetchPicked() async { if (_fetching) return; _fetching = true; try { final prefs = await SharedPreferences.getInstance(); final userId = prefs.getInt('userId') ?? prefs.getInt('userid') ?? 0; // ✅ CRITICAL: Fetch from v3 API (picked orders) AND v2 API (all statuses) IN PARALLEL // Define V2 fetcher function Future> fetchV2() async { try { final now = DateTime.now(); final today = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; 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(), }, ); final httpClient = http.Client(); 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 ? (decoded['details'] ?? decoded['data'] ?? decoded) : decoded; return data is List ? data : (data is Map && data['items'] is List ? data['items'] as List : []); } } finally { httpClient.close(); } } catch (e) { debugPrint('[MYDELIVERIES] Error fetching from v2 API: $e'); } return []; } // Execute in parallel final results = await Future.wait([ _provider.getDeliveryQueuesPicked(live: true, userid: userId), fetchV2(), ]); final itemsV3 = results[0] as List; final itemsV2 = results[1] as List; // Merge v3 orders (picked) with v2 orders (all statuses) final Map> mergedOrders = {}; // First add v3 orders (picked) for (final order in itemsV3.whereType>()) { final key = _getOrderKey(order); mergedOrders[key] = order; } // Then add/update with v2 orders - V2 STATUS TAKES PRECEDENCE for (final order in itemsV2.whereType>()) { final key = _getOrderKey(order); final existing = mergedOrders[key]; if (existing == null) { mergedOrders[key] = order; } else { // If it exists in V3 (picked) but V2 says something else, trust V2. // Especially for 'skipped', 'delivered', 'cancelled'. mergedOrders[key] = { ...existing, ...order, // Overwrite with V2 data }; } } final items = mergedOrders.values.toList(); debugPrint('[MYDELIVERIES] Merged items count: ${items.length}'); final dedupedList = items; // Already deduped by key map // Preserve step numbers for existing orders for (final order in dedupedList) { final orderKey = _getOrderKey(order); final currentStep = _getStepNumber(order); if (currentStep > 0 && !_preservedStepNumbers.containsKey(orderKey)) { _preservedStepNumbers[orderKey] = currentStep; } } // Sort the list final sortedList = _sortOrders(dedupedList); // Filter out ACTIVE orders from top list (they go to banner) final listForTop = sortedList.where((o) { final status = (o['orderstatus']?.toString().toLowerCase() ?? '') .trim(); return status != 'active'; }).toList(); final skippedInFinal = listForTop.where((m) { final status = (m['orderstatus']?.toString().toLowerCase() ?? '') .trim(); return status == 'skipped'; }).length; debugPrint( '[MYDELIVERIES] Final top list: ${listForTop.length} (skipped: $skippedInFinal)', ); // Check if data has actually changed before rebuilding // This avoids unnecessary setState calls during polling final bool hasChanged = !_areOrdersEqual(_picked, listForTop); if (hasChanged) { if (mounted) { setState(() { _picked = listForTop; }); debugPrint( '[MYDELIVERIES] 🔄 UI Updated: ${listForTop.length} orders ($skippedInFinal skipped)', ); } } else { // No changes } // Get all order IDs from the current list final currentOrderIds = sortedList .map((o) => (o['orderid'] ?? '').toString()) .where((id) => id.isNotEmpty) .toSet(); // ✅ Find all ACTIVE deliveries and ensure timers are running final activeOrders = sortedList.where((o) { final status = (o['orderstatus']?.toString().toLowerCase() ?? '') .trim(); return status == 'active'; }).toList(); debugPrint( '[MYDELIVERIES] Found ${activeOrders.length} active deliveries', ); // ✅ CRITICAL: Only set has_live_deliveries to true if there are ACTIVE orders (status = "active") // Don't set it to true for "picked" or other statuses - only for "active" try { final hasActiveOrders = activeOrders.isNotEmpty; await prefs.setBool('has_live_deliveries', hasActiveOrders); debugPrint( '[MYDELIVERIES] Set has_live_deliveries: $hasActiveOrders (${activeOrders.length} active orders)', ); // Clear active_delivery_order_id if no active deliveries if (!hasActiveOrders) { final activeOrderId = prefs.getString('active_delivery_order_id'); if (activeOrderId != null && activeOrderId.isNotEmpty) { debugPrint( '[MYDELIVERIES] Clearing stale active_delivery_order_id: $activeOrderId (no active deliveries)', ); await prefs.remove('active_delivery_order_id'); } } } catch (_) {} // Update active deliveries list for banner display if (mounted) { setState(() { _activeDeliveries = activeOrders; }); } // Get set of active order IDs final activeOrderIds = activeOrders .map((o) => (o['orderid'] ?? '').toString()) .where((id) => id.isNotEmpty) .toSet(); // ✅ CRITICAL: Start timers ONLY for active deliveries that don't have one yet // This ensures logs are posted every 30 seconds WITHOUT restarting timers on every fetch for (final order in activeOrders) { final orderId = (order['orderid'] ?? '').toString(); if (orderId.isEmpty) continue; // ✅ ONLY start timer if it doesn't already exist // This prevents restarting timers on every _fetchPicked() call (which happens frequently) if (!_deliveryTimers.containsKey(orderId)) { debugPrint( '[MYDELIVERIES] Active delivery: $orderId - Starting delivery log posting (timer not found)', ); // ✅ RE-ENABLED: Delivery logs now posted from deliveries page with robust validation await _startDeliveryPosting(order); } else { debugPrint( '[MYDELIVERIES] Active delivery: $orderId - Timer already running, skipping restart', ); } _activeDeliveryOrderId = orderId; // Track the active delivery // Persist active delivery ID so it survives app restarts try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('active_delivery_order_id', orderId); } catch (e) { debugPrint('[MYDELIVERIES] Error saving active delivery ID: $e'); } } // ✅ Stop timers ONLY for deliveries that are no longer active or in the list final timersToStop = _deliveryTimers.keys .where( (id) => !activeOrderIds.contains(id) || !currentOrderIds.contains(id), ) .toList(); for (final id in timersToStop) { debugPrint( '[MYDELIVERIES] Stopping timer for orderId: $id (no longer active)', ); _stopDeliveryPosting(id); } debugPrint( '[MYDELIVERIES] Active timers: ${_deliveryTimers.keys.toList()}', ); debugPrint( '[MYDELIVERIES] Active delivery orderId: $_activeDeliveryOrderId', ); } catch (e) { debugPrint('[MYDELIVERIES] Error fetching picked orders: $e'); } finally { _fetching = false; } } // ignore: unused_element Map _createBasePayload(Map it) { return { 'logid': 0, 'tenantid': it['tenantid'] ?? 0, 'partnerid': it['partnerid'] ?? 0, 'locationid': it['locationid'] ?? 0, 'orderheaderid': it['orderheaderid'] ?? 0, 'deliveryid': it['deliveryid'] ?? 0, 'userid': it['userid'] ?? 0, 'orderid': (it['orderid'] ?? '').toString(), 'orderstatus': 'active', }; } // Stop posting logs for a specific delivery void _stopDeliveryPosting(String orderId) { final timer = _deliveryTimers.remove(orderId); if (timer != null) { timer.cancel(); debugPrint('[DELIVERYLOG] Stopped posting logs for orderId: $orderId'); } else { debugPrint('[DELIVERYLOG] No timer to stop for orderId: $orderId'); } } Future<(String lat, String lng)?> _getValidCoordinates({ int retryCount = 0, }) async { const maxRetries = 3; try { final bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { debugPrint( '[DELIVERIES][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( '[DELIVERIES][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( '[DELIVERIES][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( '[DELIVERIES][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( '[DELIVERIES][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( '[DELIVERIES][COORDS] ✅ Got valid coordinates: $lat, $lng', ); return (lat, lng); } else { debugPrint( '[DELIVERIES][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( '[DELIVERIES][COORDS] ✅ Using cached coordinates: $lat, $lng', ); return (lat, lng); } } } catch (_) {} // Retry if we haven't exceeded max retries if (retryCount < maxRetries) { debugPrint( '[DELIVERIES][COORDS] ⚠️ Retry ${retryCount + 1}/$maxRetries to get coordinates', ); await Future.delayed(const Duration(milliseconds: 500)); return _getValidCoordinates(retryCount: retryCount + 1); } debugPrint( '[DELIVERIES][COORDS] ❌ Failed to get valid coordinates after $maxRetries retries', ); return null; } catch (e) { debugPrint('[DELIVERIES][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; } } Future _startDeliveryPosting(Map order) async { final orderId = (order['orderid'] ?? '').toString(); if (orderId.isEmpty) { debugPrint( '[DELIVERIES][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( '[DELIVERIES][DELIVERYLOG] ⚠️ Timer already exists for $orderId, canceling old one', ); _deliveryTimers[orderId]?.cancel(); _deliveryTimers.remove(orderId); } debugPrint( '[DELIVERIES][DELIVERYLOG] 🚀 Starting 30-second timer for orderId: $orderId', ); debugPrint( '[DELIVERIES][DELIVERYLOG] 🚀 Starting 30-second timer for orderId: $orderId', ); // REMOVED: Do not reset cumulative distance here. // It is already reset in DeliveriesController.updateActiveStatus when the status actually changes. // Resetting here causes data loss if the app is restarted while a delivery is in progress. // START FOREGROUND SERVICE PROTECTION // Ensure the RiderLogController knows we are active so the Foreground Service stays alive // This protects THIS timer from being killed by the OS try { if (Get.isRegistered()) { final riderLog = Get.find(); debugPrint( '[DELIVERIES][DELIVERYLOG] 🛡️ Activating Foreground Service via RiderLogController...', ); // Force 30s interval to match delivery logging riderLog.startAutoCreateLoginLoop(seconds: 30); } } catch (e) { debugPrint('[DELIVERIES][DELIVERYLOG] ⚠️ Could not start Foreground Service: $e'); } final deliveryId = (order['deliveryid'] ?? 0).toString(); // 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(); // Method 1: Get from SharedPreferences (saved when order became active) startTime = prefs.getString('delivery_starttime_$deliveryId') ?? ''; if (startTime.isNotEmpty) { debugPrint( '[DELIVERIES][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( '[DELIVERIES][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( '[DELIVERIES][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( '[DELIVERIES][DELIVERYLOG] ⚠️ Using current time as starttime fallback: $startTime', ); } } catch (e) { debugPrint('[DELIVERIES][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( '[DELIVERIES][DELIVERYLOG] ⚠️ Final fallback: starttime was empty, using: $startTime', ); } debugPrint( '[DELIVERIES][DELIVERYLOG] 📝 Final starttime for orderId $orderId: $startTime', ); // Create base payload with starttime final base = { '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('[DELIVERIES][DELIVERYLOG] Error saving payload: $e'); } // Post once immediately (don't await - let it run in background) _postDeliveryLog(orderId, order); debugPrint( '[DELIVERIES][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( '[DELIVERIES][DELIVERYLOG] ⏰ Timer tick for orderId: $orderId (30 seconds elapsed)', ); _postDeliveryLog(orderId, order); }); _deliveryTimers[orderId] = timer; debugPrint( '[DELIVERIES][DELIVERYLOG] ✅ Timer registered for orderId: $orderId (will post every 30 seconds)', ); } // Reset cumulative distance when order becomes active Future _resetCumulativeDistance(String deliveryId) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.remove('delivery_tracking_${deliveryId}_lastLat'); await prefs.remove('delivery_tracking_${deliveryId}_lastLng'); await prefs.remove('delivery_tracking_${deliveryId}_cumulativeKm'); debugPrint( '[DELIVERIES] 🧹 Reset cumulative distance tracking for deliveryId: $deliveryId', ); } catch (e) { debugPrint('[DELIVERIES] Error resetting cumulative distance: $e'); } } void _postDeliveryLog(String orderId, Map order) { if (!mounted) { debugPrint('[DELIVERIES][DELIVERYLOG][POST] Widget disposed, skipping'); return; } debugPrint( '[DELIVERIES][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 _performPost(String orderId) async { try { debugPrint( '[DELIVERIES][DELIVERYLOG][POST] 🔄 Starting _performPost for orderId: $orderId', ); Map? base = _deliveryBasePayload[orderId]; if (base == null) { debugPrint( '[DELIVERIES][DELIVERYLOG][POST] Base is null, loading from SharedPreferences', ); try { final prefs = await SharedPreferences.getInstance(); if (!mounted) { debugPrint( '[DELIVERIES][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( '[DELIVERIES][DELIVERYLOG][POST] Base loaded from prefs: $base', ); } catch (e) { debugPrint('[DELIVERIES][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('[DELIVERIES][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( '[DELIVERIES][DELIVERYLOG][POST] ❌ Coordinate timeout after retries', ); return null; }, ); if (!mounted) { debugPrint( '[DELIVERIES][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( '[DELIVERIES][DELIVERYLOG][POST] ❌ SKIPPING POST: Invalid coordinates (lat=${coords?.$1 ?? 'null'}, lng=${coords?.$2 ?? 'null'})', ); debugPrint( '[DELIVERIES][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( '[DELIVERIES][DELIVERYLOG][POST] ❌ SKIPPING POST: Invalid coordinate ranges (lat=$latDouble, lng=$lngDouble)', ); debugPrint( '[DELIVERIES][DELIVERYLOG][POST] ⚠️ Will retry on next timer tick (30 seconds)', ); return; // Skip this post - don't send invalid coordinates } debugPrint( '[DELIVERIES][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( '[DELIVERIES][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( '[DELIVERIES][DELIVERYLOG][POST] ⚠️ WARNING: Missing fields in payload: $missingFields', ); } final url = ApiConstants.mainRoute == 'live' ? ApiConstants.createDeliveryLogLive : ApiConstants.createDeliveryLogDev; debugPrint('[DELIVERIES][DELIVERYLOG][POST] 📤 Sending to API: $url'); debugPrint('[DELIVERIES][DELIVERYLOG][POST] 📦 Payload: $payload'); debugPrint( '[DELIVERIES][DELIVERYLOG][POST] ✅ starttime in payload: "${payload['starttime']}"', ); await _deliveryLogProvider .createDeliveryLog(url, payload) .timeout( const Duration(seconds: 8), onTimeout: () { debugPrint( '[DELIVERIES][DELIVERYLOG][POST] ⚠️ API timeout for orderId: $orderId', ); throw TimeoutException('API timeout', const Duration(seconds: 8)); }, ); debugPrint( '[DELIVERIES][DELIVERYLOG][POST] ✅ SUCCESS for orderId: $orderId at ${DateTime.now()}', ); } catch (e, stackTrace) { debugPrint( '[DELIVERIES][DELIVERYLOG][POST] ❌ ERROR for orderId: $orderId - $e', ); debugPrint('[DELIVERIES][DELIVERYLOG][POST] Stack trace: $stackTrace'); } } @override Widget build(BuildContext context) { super.build(context); return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) async { if (didPop) return; if (_picked.isNotEmpty) { final confirm = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Pending deliveries'), content: const Text( 'Are you sure you want to close? There are deliveries pending.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('No'), ), TextButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Yes'), ), ], ), ); if (confirm == true && context.mounted) { Navigator.pop(context); } } else { Navigator.pop(context); } }, child: SafeArea( child: Scaffold( backgroundColor: Colors.grey.shade200, appBar: AppBar( backgroundColor: Colors.grey.shade200, elevation: 0, centerTitle: false, toolbarHeight: 70, // Same as your summary page titleSpacing: 0, title: Padding( padding: const EdgeInsets.only(left: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Transform.translate( offset: Offset(0, 6), child: Text( "DELIVERIES", style: TextStyle( fontSize: FontConstants.xxxLarge(context).sp, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, color: Colors.black87, ), ), ), /// Map view row on right side Transform.translate( offset: Offset(-20, 2), child: MapViewRow( deliveries: _picked, preservedStepNumbers: _preservedStepNumbers, ), ), ], ), ), /// Divider under AppBar (same as summary page) bottom: PreferredSize( preferredSize: Size.fromHeight(1), child: Divider(color: Colors.grey, height: 1), ), ), body: Stack( children: [ _picked.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 80), Transform.translate( offset: Offset(0, -1.5), 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: Offset(0, -1.5), child: Text( "No Deliveries at the moment", textAlign: TextAlign.center, style: TextStyle( fontSize: FontConstants.xxxLarge(context), fontFamily: FontConstants.fontFamily, color: Colors.grey.shade500, ), ), ), ], ), ) : RefreshIndicator( onRefresh: _fetchPicked, child: ListView.builder( padding: EdgeInsets.only( left: 2, right: 2, top: 8, bottom: _activeDeliveries.isNotEmpty ? 80 : 8, // Add bottom padding for banner ), itemCount: _picked.length, itemBuilder: (context, index) { final item = _picked[index]; final int displayStep = _getDisplayStepNumber( _picked, index, ); final String distanceStr = _distanceKmDisplay(item); final String orderId = (item['orderid'] ?? '') .toString(); final String status = (item['orderstatus']?.toString().toLowerCase() ?? '') .trim(); // Logic for enabling cards: // 1. Skipped orders are ALWAYS enabled (so they can be un-skipped or completed) // 2. The first non-skipped, non-cancelled, non-delivered order is enabled // 3. All other orders are disabled final bool isSkipped = status == 'skipped'; final bool isCancelledOrDelivered = status == 'cancelled' || status == 'delivered'; final bool hasActive = _hasActiveDelivery(); // Don't show cancelled or delivered orders if (isCancelledOrDelivered) { return const SizedBox.shrink(); } // Skipped orders: only enable when there is NO other active delivery if (isSkipped && !isCancelledOrDelivered) { return DeliveryCard( key: ValueKey('delivery_$orderId'), item: item, displayStep: displayStep, distanceStr: distanceStr, enabled: !hasActive, isSkipped: true, ); } // Find the first non-skipped order to enable int firstEnabledIndex = -1; for (int i = 0; i < _picked.length; i++) { final orderStatus = (_picked[i]['orderstatus'] ?.toString() .toLowerCase() ?? '') .trim(); if (orderStatus != 'skipped' && orderStatus != 'cancelled' && orderStatus != 'delivered') { firstEnabledIndex = i; break; } } // Enable if this is the first available non-skipped order // AND there is no active delivery running. final bool enabled = !hasActive && firstEnabledIndex >= 0 && index == firstEnabledIndex; return DeliveryCard( key: ValueKey('delivery_$orderId'), item: item, displayStep: displayStep, distanceStr: distanceStr, enabled: enabled, isSkipped: false, ); }, ), ), // Bottom banner for active deliveries (Swiggy/Zomato style) if (_activeDeliveries.isNotEmpty) Positioned( left: 0, right: 0, bottom: 0, child: _ActiveDeliveryBanner( activeDeliveries: _activeDeliveries, onTap: (delivery) async { await startDelivery(delivery); // Refresh after returning from map screen if (mounted) { await Future.delayed(const Duration(milliseconds: 500)); _fetchPicked(); } }, ), ), ], ), ), ), ); } } // ------------------------------------------------------------------------- // ACTIVE DELIVERY BANNER (Swiggy/Zomato style) // ------------------------------------------------------------------------- class _ActiveDeliveryBanner extends StatelessWidget { final List> activeDeliveries; final Function(Map) onTap; const _ActiveDeliveryBanner({ required this.activeDeliveries, required this.onTap, }); String _getDeliveryAddress(Map delivery) { final address = delivery['deliveryaddress'] ?? delivery['DeliveryAddress'] ?? delivery['address'] ?? ''; if (address.toString().length > 40) { return '${address.toString().substring(0, 40)}...'; } return address.toString(); } @override Widget build(BuildContext context) { if (activeDeliveries.isEmpty) { return const SizedBox.shrink(); } // Show first active delivery (or show count if multiple) final delivery = activeDeliveries.first; final count = activeDeliveries.length; return Container( margin: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.green, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.2), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, child: InkWell( onTap: () => onTap(delivery), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row( children: [ // Active indicator icon Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.two_wheeler, color: Colors.white, size: 24, ), ), const SizedBox(width: 12), // Delivery info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Text( count > 1 ? '$count Active Deliveries' : 'Active Delivery', style: TextStyle( color: Colors.white, fontSize: 19, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), if (count > 1) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(12), ), child: Text( '$count', style: TextStyle( color: Colors.white, fontSize: FontConstants.small(context).sp, fontWeight: FontWeight.bold, ), ), ), ], ], ), const SizedBox(height: 4), Text( _getDeliveryAddress(delivery), style: TextStyle( color: Colors.white.withValues(alpha: 0.9), fontSize: 17, fontFamily: FontConstants.fontFamily, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), // Arrow icon const Icon( Icons.arrow_forward_ios, color: Colors.white, size: 20, ), ], ), ), ), ), ); } }