// ignore_for_file: invalid_return_type_for_catch_error part of 'deliveries.dart'; // ignore_for_file: unused_element_parameter // Imports should be in deliveries.dart // import 'dart:io'; // import 'package:image_picker/image_picker.dart'; // import 'package:shared_preferences/shared_preferences.dart'; // ------------------------------------------------------------------------- // DELIVERY BOTTOM SHEET (Delivered/Cancel with payment options) // ------------------------------------------------------------------------- class _DeliveryBottomSheet extends StatefulWidget { final Map delivery; final _MyDeliveriesState? parentState; const _DeliveryBottomSheet({ super.key, required this.delivery, this.parentState, }); @override State<_DeliveryBottomSheet> createState() => _DeliveryBottomSheetState(); } class _DeliveryBottomSheetState extends State<_DeliveryBottomSheet> { String selectedOption = ''; bool isDeliveredSelected = true; bool showOptions = false; bool showSlider = true; bool isLoading = false; bool isProcessing = false; // Prevents any action until alert is dismissed DateTime? lastFailureTime; // Track last failure to prevent duplicate alerts final List cancelOptions = [ 'Customer refused', 'Not Reachable', 'Switched Off', 'Incorrect Location', ]; final List deliveredOptions = [ 'Pay Later', 'Cash Payment', 'QR Payment', ]; @override void initState() { super.initState(); _resetSheetState(); } void _resetSheetState() { selectedOption = ''; showOptions = false; showSlider = true; isDeliveredSelected = true; isProcessing = false; lastFailureTime = null; } bool _hasCollection() { final raw = widget.delivery['collectionamt']; if (raw == null) return false; if (raw is num) return raw > 0; final parsed = double.tryParse(raw.toString()) ?? 0.0; return parsed > 0; } double _getCollectionAmount() { final raw = widget.delivery['collectionamt']; if (raw == null) return 0.0; if (raw is num) return raw.toDouble(); return double.tryParse(raw.toString()) ?? 0.0; } double _getDeliveryAmount() { final raw = widget.delivery['deliveryamt']; if (raw == null) return 0.0; if (raw is num) return raw.toDouble(); return double.tryParse(raw.toString()) ?? 0.0; } @override Widget build(BuildContext context) { // For delivered orders with collection amount, show custom payment choices final options = isDeliveredSelected ? (_hasCollection() ? [ 'Cash (₹${_getCollectionAmount().toStringAsFixed(0)})', 'UPI (₹${_getCollectionAmount().toStringAsFixed(0)})', 'Not Collected', ] : deliveredOptions) : cancelOptions; final Color confirmColor = isDeliveredSelected ? ColorConstants.primaryColor : Colors.red; return Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: SafeArea( top: false, child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Expanded( child: Text( 'Update Delivery Status', style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), InkWell( onTap: () { if (!showSlider && showOptions) { setState(() => _resetSheetState()); } else { Navigator.pop(context); } }, child: const Icon( Icons.cancel, color: Colors.red, size: 30, ), ), ], ), const SizedBox(height: 10), const Divider(), if (!showOptions && showSlider) ...[ _buildCustomerInfo(), const SizedBox(height: 10), ], if (showSlider) Slidable( key: ValueKey(widget.delivery['deliveryid']), startActionPane: ActionPane( motion: const ScrollMotion(), extentRatio: 0.9, // Allocate 90% of width for the 3 actions to prevent truncation children: [ SlidableAction( borderRadius: BorderRadius.circular(6), onPressed: (_) { setState(() { isDeliveredSelected = true; selectedOption = ''; showOptions = true; showSlider = false; isProcessing = false; // Reset when switching lastFailureTime = null; // Clear failure cache }); }, backgroundColor: Colors.green, foregroundColor: Colors.white, icon: Icons.check_circle, label: 'Delivered', ), SlidableAction( borderRadius: BorderRadius.circular(6), onPressed: (_) { setState(() { isDeliveredSelected = false; selectedOption = ''; showOptions = true; showSlider = false; isProcessing = false; // Reset when switching lastFailureTime = null; // Clear failure cache }); }, backgroundColor: Colors.red, foregroundColor: Colors.white, icon: Icons.cancel, label: 'Cancelled', ), SlidableAction( borderRadius: BorderRadius.circular(6), onPressed: (c) async { // Resolve parent state reliably final resolvedParentState = widget.parentState ?? context .findAncestorStateOfType<_MyDeliveriesState>(); // Open skip sheet on TOP of this sheet (safer context usage) // We await it so we know when it closes await _showMyOptionsSheet( context, widget.delivery, resolvedParentState, ); // Close this update sheet after skip sheet is done // Pass 'true' to indicate that an action *might* have been taken // We could potentially return result from _showMyOptionsSheet but currently it returns void // The skip sheet handles the logic, so we can assume if we return here we are done. // Actually, we should only return true if the skip sheet *actually* skipped. // Since _showMyOptionsSheet returns Future, we can't know for sure. // BUT, since the user interacted with the skip sheet and we are closing this sheet, // passing 'true' is safer to trigger a refresh check upstream. if (mounted) { Navigator.pop(context, true); } }, backgroundColor: Colors.orange, foregroundColor: Colors.white, icon: Icons.skip_next, label: 'Skip', ), ], ), child: Container( height: 65, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(6), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.swipe, color: Colors.black), SizedBox(width: 10), Text( 'Slide to Update Delivery', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black, ), ), ], ), ), ), const SizedBox(height: 5), if (showOptions) ...[ ListView( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: [ ...options.map((option) { final bool isSelected = option == selectedOption; return GestureDetector( onTap: () { if (isLoading || isProcessing) return; setState(() { selectedOption = option; lastFailureTime = null; // Clear on new selection }); }, child: Container( margin: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.symmetric( vertical: 12, horizontal: 16, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: Colors.white, border: Border.all( color: isSelected ? confirmColor : Colors.grey.shade300, width: isSelected ? 2 : 1, ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( option, style: TextStyle( fontSize: 18, color: isSelected ? confirmColor : Colors.black, fontFamily: FontConstants.fontFamily, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ), Icon( isSelected ? Icons.check_circle : Icons.radio_button_unchecked, color: isSelected ? confirmColor : Colors.grey, ), ], ), ), ); }), ], ), ], const SizedBox(height: 15), // Slider enabled when a menu option is chosen. if (selectedOption.isNotEmpty) SliderButton( properties: SliderButtonProperties( height: 55, buttonSize: 50, width: MediaQuery.of(context).size.width - 40, backgroundColor: confirmColor, dismissThresholds: 0.90, /// ACTION action: () async { // Prevent action if already processing if (isLoading || isProcessing) { debugPrint( '[SLIDER] ⚠️ Already processing, ignoring slide action', ); return false; } // _handleConfirm manages isLoading and isProcessing internally await _handleConfirm(); return false; // required by slider_button_lite }, /// LABEL label: Text( isDeliveredSelected ? "Slide to Confirm Delivery" : "Slide to Confirm Cancellation", textAlign: TextAlign.left, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white, ), ), /// SLIDER BUTTON (Icon or Loader) icon: ClipOval( child: Material( color: Colors.white, child: SizedBox( width: 50, height: 50, child: Center( child: isLoading ? SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 3, valueColor: AlwaysStoppedAnimation( confirmColor, ), ), ) : Icon( isDeliveredSelected ? Icons.check : Icons.close, size: 28, color: confirmColor, ), ), ), ), ), ), ), ], ), ), ), ); } Widget _buildCustomerInfo() { final pickupCustomer = (widget.delivery['pickupcustomer'] ?? '') .toString() .trim(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInfoRow( 'Name:', widget.delivery['deliverycustomer']?.toString() ?? 'Customer', ), const SizedBox(height: 10), if (pickupCustomer.isNotEmpty) ...[ _buildInfoRow('Kitchen:', pickupCustomer), const SizedBox(height: 10), ], _buildInfoRow( 'Address:', widget.delivery['deliveryaddress']?.toString() ?? 'Address not available', isExpanded: true, ), const SizedBox(height: 10), _buildInfoRow( 'Order ID:', '#${widget.delivery['orderid']?.toString() ?? ''}', ), ], ); } Widget _buildInfoRow(String title, String value, {bool isExpanded = false}) { final textWidget = Text( value, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 18, fontFamily: FontConstants.fontFamily, ), ); return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '$title ', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 18, fontFamily: FontConstants.fontFamily, ), ), const SizedBox(width: 10), isExpanded ? Expanded(child: textWidget) : Flexible(child: textWidget), ], ); } bool _wasSkippedDelivery( Map delivery, _MyDeliveriesState? parentState, ) { final status = (delivery['orderstatus'] ?? delivery['status'] ?? delivery['orderStatus'] ?? '') .toString() .toLowerCase(); if (status == 'skipped') return true; if (parentState == null) return false; final key = parentState._getOrderKey(delivery); return parentState._skippedOrdersCache.containsKey(key); } Future _handleConfirm() async { // CRITICAL: Prevent any re-entry during processing or alert display if (isLoading || isProcessing) { debugPrint( '[CONFIRM] ⚠️ Already processing or alert showing, ignoring tap', ); return; } // Check if we recently had a failure (within last 4 seconds) // This prevents cached/duplicate alerts from showing if (lastFailureTime != null) { final timeSinceLastFailure = DateTime.now().difference(lastFailureTime!); if (timeSinceLastFailure.inSeconds < 4) { debugPrint( '[CONFIRM] ⚠️ Too soon after last failure (${timeSinceLastFailure.inSeconds}s), ignoring tap', ); return; } } final d = widget.delivery; debugPrint('[CONFIRM] ===== DELIVERY INFO ====='); debugPrint('[CONFIRM] deliveryid: ${d['deliveryid']}'); debugPrint('[CONFIRM] orderid: ${d['orderid']}'); debugPrint('[CONFIRM] orderstatus: ${d['orderstatus']}'); final parentState = widget.parentState ?? context.findAncestorStateOfType<_MyDeliveriesState>(); final orderId = (d['orderid'] ?? '').toString(); final deliveryId = (d['deliveryid'] ?? '').toString(); final bool wasSkipped = _wasSkippedDelivery(d, parentState); if (parentState != null) { final key = parentState._getOrderKey(d); debugPrint('[CONFIRM] Generated key: $key'); debugPrint( '[CONFIRM] Is in cache: ${parentState._skippedOrdersCache.containsKey(key)}', ); debugPrint( '[CONFIRM] All cache keys: ${parentState._skippedOrdersCache.keys.toList()}', ); } debugPrint('[CONFIRM] wasSkipped=$wasSkipped'); debugPrint('[CONFIRM] =========================='); // Set BOTH flags to block all interactions if (mounted) { setState(() { isLoading = true; isProcessing = true; }); } try { final dc = Get.put(DeliveriesController(), permanent: true); final int deliveryIdInt = int.tryParse('${d['deliveryid'] ?? 0}') ?? 0; final int orderHeaderId = int.tryParse('${d['orderheaderid'] ?? 0}') ?? 0; final int deliveryLocationId = int.tryParse('${d['deliverylocationid'] ?? 0}') ?? 0; bool apiSuccess = false; if (isDeliveredSelected) { // Delivery amount should come from API (deliveryamt), not from the // selected payment option text (Cash/UPI), to avoid conflicts with // collection amounts. double deliveryAmount = _getDeliveryAmount(); // Fallback: if API did not send deliveryamt, keep old behavior of // parsing a numeric value from the selected option (if any). if (deliveryAmount == 0.0 && selectedOption.isNotEmpty) { final m = RegExp(r'[0-9]+(?:\.[0-9]+)?').firstMatch(selectedOption); if (m != null) { deliveryAmount = double.tryParse(m.group(0)!) ?? 0.0; } } // Collection amounts from API (if any) final double apiCollectionAmt = _getCollectionAmount(); double collectionAmtToSend = 0.0; double collectedAmtToSend = 0.0; int collectionStatusToSend = 0; // 1 = cash, 2 = UPI, 3 = not collected if (apiCollectionAmt > 0) { collectionAmtToSend = apiCollectionAmt; final lower = selectedOption.toLowerCase(); if (lower.startsWith('cash')) { collectedAmtToSend = apiCollectionAmt; collectionStatusToSend = 1; // Cash } else if (lower.startsWith('upi')) { collectedAmtToSend = apiCollectionAmt; collectionStatusToSend = 2; // UPI } else { collectedAmtToSend = 0.0; collectionStatusToSend = 3; // Not Collected } } double parseD(dynamic v) { if (v == null) return 0.0; if (v is num) return v.toDouble(); return double.tryParse(v.toString()) ?? 0.0; } final pickupLat = parseD(d['pickuplat'] ?? d['PickupLat'] ?? 0); final pickupLng = parseD(d['pickuplon'] ?? d['PickupLon'] ?? 0); final deliveryLat = parseD( d['droplat'] ?? d['DropLat'] ?? d['deliverylat'] ?? 0, ); final deliveryLng = parseD( d['droplon'] ?? d['DropLon'] ?? d['deliverylong'] ?? 0, ); // Start fetching location in background while user takes photo final locationFuture = Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, timeLimit: Duration(seconds: 5), ), ).then((p) => p).catchError((e) { debugPrint('[DELIVER] Error getting rider location: $e'); return null; }); debugPrint( '[DELIVER][CONFIRM] deliveryId=$deliveryIdInt orderHeaderId=$orderHeaderId deliveryLocationId=$deliveryLocationId deliveryAmount=$deliveryAmount notes="$selectedOption"', ); // TRIGGER CAMERA - Proof of Delivery // We do this concurrently with location fetching to save time String? proofImageUrl; try { final ImagePicker picker = ImagePicker(); // Optimize image: Resize to max 1024x1024 and 50% quality for faster upload final XFile? photo = await picker.pickImage( source: ImageSource.camera, imageQuality: 50, maxWidth: 1024, maxHeight: 1024, ); if (photo == null) { // User cancelled camera, abort delivery confirmation if (mounted) { setState(() { isLoading = false; isProcessing = false; }); } debugPrint('[CONFIRM] Camera cancelled by user'); return; } // Upload Proof // By now, location fetching should be nearly done final prefs = await SharedPreferences.getInstance(); final userId = prefs.getInt('userid') ?? 0; if (userId > 0) { proofImageUrl = await dc.uploadProofImage( File(photo.path), 'delivered', userId, deliveryIdInt, ); } } catch (e) { debugPrint('[CONFIRM] Camera/Upload error: $e'); } // Now wait for location if it hasn't finished yet String riderLatStr = '0'; String riderLngStr = '0'; final position = await locationFuture; riderLatStr = position.latitude.toStringAsFixed(6); riderLngStr = position.longitude.toStringAsFixed(6); // Validate coordinates before API call final double riderLat = double.tryParse(riderLatStr) ?? 0.0; final double riderLng = double.tryParse(riderLngStr) ?? 0.0; final bool hasValidRiderLocation = riderLat != 0 && riderLng != 0 && riderLat.abs() <= 90 && riderLng.abs() <= 180; final bool hasValidDeliveryLocation = deliveryLat != 0 && deliveryLng != 0 && deliveryLat.abs() <= 90 && deliveryLng.abs() <= 180; if (!hasValidRiderLocation || !hasValidDeliveryLocation) { debugPrint( '[DELIVER][CONFIRM] ⚠️ Invalid coordinates - Rider: ($riderLat, $riderLng), Delivery: ($deliveryLat, $deliveryLng)', ); if (mounted) { setState(() { isLoading = false; }); } // Show error alert Get.snackbar( 'Location Error', 'Unable to get your location. Please ensure GPS is enabled and try again.', backgroundColor: Colors.red, colorText: Colors.white, duration: const Duration(seconds: 4), snackPosition: SnackPosition.TOP, isDismissible: true, ); lastFailureTime = DateTime.now(); await Future.delayed(const Duration(seconds: 4)); if (mounted) { setState(() { isProcessing = false; }); } return; } if (proofImageUrl == null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Failed to upload proof image. Please try again.')), ); setState(() { isLoading = false; isProcessing = false; }); } return; } apiSuccess = await dc.updateDeliveredStatus( deliveryId: deliveryIdInt, orderHeaderId: orderHeaderId, deliveryLocationId: deliveryLocationId, smsDelivery: 0, ridersLat: riderLatStr, ridersLng: riderLngStr, pickupLat: pickupLat.toStringAsFixed(6), pickupLng: pickupLng.toStringAsFixed(6), deliveryLat: deliveryLat.toStringAsFixed(6), deliveryLng: deliveryLng.toStringAsFixed(6), notes: selectedOption, deliveryAmount: deliveryAmount, collectionAmount: collectionAmtToSend, collectedAmount: collectedAmtToSend, collectionStatus: collectionStatusToSend, wasSkipped: wasSkipped, orderId: orderId, // ✅ Pass orderId for bonus points logic proofImage: proofImageUrl, ); debugPrint('[DELIVER][CONFIRM] result=$apiSuccess'); if (apiSuccess) { // On success, cache and clean up before navigating to done screen await _cacheDeliveredCancelled( deliveryId, orderId, isDelivered: true, ); await _cleanupAfterCompletion( orderId, deliveryId, parentState, isCancelled: false, ); if (!mounted) return; if (!mounted) return; Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => DeliveriesDone( bonusPoints: dc.lastBonusPoints.value, ), ), ); return; } } else { double parseD(dynamic v) { if (v == null) return 0.0; if (v is num) return v.toDouble(); return double.tryParse(v.toString()) ?? 0.0; } final pickupLat = parseD(d['pickuplat'] ?? d['PickupLat'] ?? 0); final pickupLng = parseD(d['pickuplon'] ?? d['PickupLon'] ?? 0); String riderLatStr = '0'; String riderLngStr = '0'; try { final position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, timeLimit: Duration(seconds: 5), ), ); riderLatStr = position.latitude.toStringAsFixed(6); riderLngStr = position.longitude.toStringAsFixed(6); } catch (e) { debugPrint('[CANCEL] Error getting rider location: $e'); } // Validate coordinates before API call final double riderLat = double.tryParse(riderLatStr) ?? 0.0; final double riderLng = double.tryParse(riderLngStr) ?? 0.0; final bool hasValidRiderLocation = riderLat != 0 && riderLng != 0 && riderLat.abs() <= 90 && riderLng.abs() <= 180; // For cancellation, we need delivery location from the delivery data final double cancelDeliveryLat = parseD( d['droplat'] ?? d['DropLat'] ?? d['deliverylat'] ?? 0, ); final double cancelDeliveryLng = parseD( d['droplon'] ?? d['DropLon'] ?? d['deliverylong'] ?? 0, ); final bool hasValidDeliveryLocation = cancelDeliveryLat != 0 && cancelDeliveryLng != 0 && cancelDeliveryLat.abs() <= 90 && cancelDeliveryLng.abs() <= 180; if (!hasValidRiderLocation || !hasValidDeliveryLocation) { debugPrint( '[CANCEL][CONFIRM] ⚠️ Invalid coordinates - Rider: ($riderLat, $riderLng), Delivery: ($cancelDeliveryLat, $cancelDeliveryLng)', ); if (mounted) { setState(() { isLoading = false; }); } // Show error alert Get.snackbar( 'Location Error', 'Unable to get your location. Please ensure GPS is enabled and try again.', backgroundColor: Colors.red, colorText: Colors.white, duration: const Duration(seconds: 4), snackPosition: SnackPosition.TOP, isDismissible: true, ); lastFailureTime = DateTime.now(); await Future.delayed(const Duration(seconds: 4)); if (mounted) { setState(() { isProcessing = false; }); } return; } apiSuccess = await dc.updateCancelledStatus( deliveryId: deliveryIdInt, orderHeaderId: orderHeaderId, ridersLat: riderLatStr, ridersLng: riderLngStr, pickupLat: pickupLat.toStringAsFixed(6), pickupLng: pickupLng.toStringAsFixed(6), deliveryLat: cancelDeliveryLat.toStringAsFixed(6), deliveryLng: cancelDeliveryLng.toStringAsFixed(6), notes: selectedOption, wasSkipped: wasSkipped, ); debugPrint('[CANCEL][CONFIRM] result=$apiSuccess'); if (apiSuccess) { // On success, cache and clean up before navigating to done screen await _cacheDeliveredCancelled( deliveryId, orderId, isDelivered: false, ); await _cleanupAfterCompletion( orderId, deliveryId, parentState, isCancelled: true, ); if (!mounted) return; Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => const DeliveriesDone(isCancelled: true), ), ); return; } } // If API failed, record the failure time to prevent duplicate alerts lastFailureTime = DateTime.now(); debugPrint( '[CONFIRM] ⚠️ API failed at $lastFailureTime, waiting for alert to dismiss...', ); // Stop the loading spinner immediately if (mounted) { setState(() { isLoading = false; }); } // Wait for geofence/error snackbar to show and be dismissed // The controller's _checkGeofence already shows the snackbar, so we just wait // Don't show duplicate fallback error - controller already shows error message await Future.delayed(const Duration(seconds: 3)); // Now re-enable the button if (mounted) { setState(() { isProcessing = false; }); debugPrint('[CONFIRM] ✅ Button re-enabled after alert dismissal'); } } catch (e) { debugPrint('[CONFIRM] ❌ Exception during confirm: $e'); // Record failure time lastFailureTime = DateTime.now(); // Stop loading spinner if (mounted) { setState(() { isLoading = false; }); } // Wait for potential error alert to dismiss (4 seconds) await Future.delayed(const Duration(seconds: 4)); // Re-enable button if (mounted) { setState(() { isProcessing = false; }); debugPrint('[CONFIRM] ✅ Button re-enabled after exception handling'); } } } Future _cacheDeliveredCancelled( String deliveryId, String orderId, { required bool isDelivered, }) async { try { final prefs = await SharedPreferences.getInstance(); final userId = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0; if (userId > 0) { final key = 'recently_delivered_cancelled_$userId'; final deliveredCancelledJson = prefs.getString(key); List deliveredCancelledList = []; if (deliveredCancelledJson != null && deliveredCancelledJson.isNotEmpty) { deliveredCancelledList = jsonDecode(deliveredCancelledJson); } deliveredCancelledList.add({ 'deliveryId': deliveryId, 'orderId': orderId, 'timestamp': DateTime.now().toIso8601String(), 'type': isDelivered ? 'delivered' : 'cancelled', }); await prefs.setString(key, jsonEncode(deliveredCancelledList)); debugPrint( '[${isDelivered ? 'DELIVER' : 'CANCEL'}] ✅ Stored order in SharedPreferences: deliveryId=$deliveryId, orderId=$orderId', ); } } catch (e) { debugPrint( '[${isDelivered ? 'DELIVER' : 'CANCEL'}] Error storing order in SharedPreferences: $e', ); } } Future _cleanupAfterCompletion( String orderId, String deliveryId, _MyDeliveriesState? parentState, { required bool isCancelled, }) async { final label = isCancelled ? 'CANCEL' : 'DELIVER'; if (orderId.isEmpty) { debugPrint('[$label] ⚠️ Empty orderId, skipping cleanup'); return; } if (parentState == null) { debugPrint( '[$label] ⚠️ parentState null; relying on cache cleanup via SharedPreferences', ); // Try to find and stop timer from cart page if it exists // This is a workaround - ideally we'd pass a callback try { // The cart page will handle timer cleanup on next refresh debugPrint('[$label] Cart page will cleanup timer on next refresh'); } catch (_) {} return; } parentState._stopDeliveryPosting(orderId); debugPrint('[$label] Stopped timer for completed delivery: $orderId'); final deliveryKey = 'delivery_$deliveryId'; final orderKey = 'order_$orderId'; final actualKey = parentState._getOrderKey(widget.delivery); bool cacheUpdated = false; void removeKey(String key, String reason) { if (parentState._skippedOrdersCache.remove(key) != null) { parentState._skippedOrderTimestamps.remove(key); cacheUpdated = true; debugPrint('[$label] ✅ Removed from cache ($reason): $key'); } } removeKey(deliveryKey, 'delivery key'); removeKey(orderKey, 'order key'); removeKey(actualKey, 'actual key'); if (!cacheUpdated) { final keysToRemove = []; // NOTE: We rely on the explicitly constructed keys above to remove entries. // Iterating via entry values isn't supported with Map cache. // If items were not found by keys above, they remain until valid keys are used. for (final key in keysToRemove) { parentState._skippedOrdersCache.remove(key); parentState._skippedOrderTimestamps.remove(key); cacheUpdated = true; debugPrint('[$label] ✅ Removed from cache (matched by ID): $key'); } } if (cacheUpdated) { await parentState._saveSkippedOrdersCache(); debugPrint( '[$label] ✅ Cache saved. Remaining: ${parentState._skippedOrdersCache.length}', ); } else { debugPrint( '[$label] ⚠️ Order not found in skipped cache (cleanup will run on next fetch)', ); } } }