part of '../deliveries/deliveries.dart'; // ------------------------------------------------------------------------- // CART PAGE (Active Deliveries) // ------------------------------------------------------------------------- class Cartpage extends StatefulWidget { const Cartpage({super.key}); @override State createState() => _CartpageState(); } class _CartpageState extends State with AutomaticKeepAliveClientMixin { final DeliveryProvider _provider = DeliveryProvider(); final CreateDeliveryLogProvider _deliveryLogProvider = CreateDeliveryLogProvider(); List> _activeDeliveries = >[]; StreamSubscription? _pollerSubscription; bool _fetching = false; final Map _deliveryTimers = {}; final Map> _deliveryBasePayload = >{}; @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 _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 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 ? (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) { 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>() .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 _startDeliveryPosting(Map 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 = { '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 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 _performPost(String orderId) async { try { debugPrint('[CART][DELIVERYLOG][POST] 🔄 Starting _performPost for orderId: $orderId'); Map? 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 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, ), ), ), ), ), ), ), ), ], ), ), ); }, ), ), ), ); } }