Files

1032 lines
38 KiB
Dart

// 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<String, dynamic> 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<String> cancelOptions = [
'Customer refused',
'Not Reachable',
'Switched Off',
'Incorrect Location',
];
final List<String> 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<void>, 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<Color>(
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<String, dynamic> 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<void> _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<void> _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<dynamic> 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<void> _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 = <String>[];
// NOTE: We rely on the explicitly constructed keys above to remove entries.
// Iterating via entry values isn't supported with Map<String, String> 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)',
);
}
}
}