Files
Xpress-rider/lib/views/Dashboard/deliveries/deliveries.dart

1763 lines
63 KiB
Dart

// 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<bool> 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<MyDeliveries> createState() => _MyDeliveriesState();
// ✅ Public static method to navigate to delivery map screen from outside (e.g., home page)
static Future<void> navigateToDeliveryMap(
BuildContext context,
Map<String, dynamic> 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<MyDeliveries>
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
final DeliveryProvider _provider = DeliveryProvider();
final CreateDeliveryLogProvider _deliveryLogProvider =
CreateDeliveryLogProvider();
List<Map<String, dynamic>> _picked = <Map<String, dynamic>>[];
List<Map<String, dynamic>> _activeDeliveries =
<Map<String, dynamic>>[]; // Track active deliveries for banner
StreamSubscription<void>? _pollerSubscription;
bool _fetching = false;
Position? _currentLocation;
final Map<String, Timer> _deliveryTimers = <String, Timer>{};
final Map<String, Map<String, dynamic>> _deliveryBasePayload =
<String, Map<String, dynamic>>{};
// Preserve original step numbers so they don't change when deliveries are completed
final Map<String, int> _preservedStepNumbers = <String, int>{};
String? _activeDeliveryOrderId;
// Cache for skipped orders
final Map<String, String> _skippedOrdersCache = {};
final Map<String, int> _skippedOrderTimestamps = {};
Future<void> _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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<Map<String, dynamic>> _sortOrders(List<Map<String, dynamic>> 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<Map<String, dynamic>> 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<DeliveriesController>()
? Get.find<DeliveriesController>()
: 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<void> _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<void> _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<String, dynamic> a, Map<String, dynamic> 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<Map<String, dynamic>> oldList,
List<Map<String, dynamic>> 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<String, dynamic> 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<void> startDelivery(Map<String, dynamic> 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<void> resumeDelivery(Map<String, dynamic> 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<void> _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<List<dynamic>> 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<String, dynamic>
? (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<dynamic>;
final itemsV2 = results[1] as List<dynamic>;
// Merge v3 orders (picked) with v2 orders (all statuses)
final Map<String, Map<String, dynamic>> mergedOrders = {};
// First add v3 orders (picked)
for (final order in itemsV3.whereType<Map<String, dynamic>>()) {
final key = _getOrderKey(order);
mergedOrders[key] = order;
}
// Then add/update with v2 orders - V2 STATUS TAKES PRECEDENCE
for (final order in itemsV2.whereType<Map<String, dynamic>>()) {
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<String, dynamic> _createBasePayload(Map<String, dynamic> it) {
return <String, dynamic>{
'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<void> _startDeliveryPosting(Map<String, dynamic> 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<RiderLogController>()) {
final riderLog = Get.find<RiderLogController>();
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 = <String, dynamic>{
'logid': 0,
'tenantid': order['tenantid'] ?? 0,
'partnerid': order['partnerid'] ?? 0,
'locationid': order['locationid'] ?? 0,
'orderheaderid': order['orderheaderid'] ?? 0,
'deliveryid': order['deliveryid'] ?? 0,
'userid': order['userid'] ?? 0,
'orderid': orderId,
'orderstatus': 'active',
'starttime': startTime, // Include starttime in base payload
};
_deliveryBasePayload[orderId] = base;
// Save to SharedPreferences for persistence
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(
'deliverylog_${orderId}_tenantid',
(base['tenantid'] ?? 0).toString(),
);
await prefs.setString(
'deliverylog_${orderId}_partnerid',
(base['partnerid'] ?? 0).toString(),
);
await prefs.setString(
'deliverylog_${orderId}_locationid',
(base['locationid'] ?? 0).toString(),
);
await prefs.setString(
'deliverylog_${orderId}_orderheaderid',
(base['orderheaderid'] ?? 0).toString(),
);
await prefs.setString(
'deliverylog_${orderId}_deliveryid',
(base['deliveryid'] ?? 0).toString(),
);
await prefs.setString(
'deliverylog_${orderId}_userid',
(base['userid'] ?? 0).toString(),
);
await prefs.setString('deliverylog_${orderId}_orderid', orderId);
await prefs.setString('deliverylog_${orderId}_orderstatus', 'active');
await prefs.setString(
'deliverylog_${orderId}_starttime',
startTime,
); // Save starttime
} catch (e) {
debugPrint('[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<void> _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<String, dynamic> 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<void> _performPost(String orderId) async {
try {
debugPrint(
'[DELIVERIES][DELIVERYLOG][POST] 🔄 Starting _performPost for orderId: $orderId',
);
Map<String, dynamic>? 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<bool>(
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<Map<String, dynamic>> activeDeliveries;
final Function(Map<String, dynamic>) onTap;
const _ActiveDeliveryBanner({
required this.activeDeliveries,
required this.onTap,
});
String _getDeliveryAddress(Map<String, dynamic> 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,
),
],
),
),
),
),
);
}
}