// ignore_for_file: unused_element, unused_import import 'package:flutter/material.dart'; import 'dart:convert'; import 'package:flutter_polyline_points/flutter_polyline_points.dart'; import 'dart:async'; import 'package:image_picker/image_picker.dart'; import 'dart:io'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:nearle/providers/delivery/delivery_provider.dart'; import 'package:nearle/providers/deliverylog/deliverylog_provider.dart'; import 'package:nearle/views/Dashboard/orders/orderstaus_button.dart'; import 'package:nearle/views/helpers/constants/apiconstants.dart'; import 'package:nearle/views/helpers/constants/Colorconstants.dart'; import 'package:nearle/views/helpers/constants/Font_constant.dart'; import 'package:nearle/controllers/profile_controller.dart'; import 'package:nearle/controllers/riderlog.dart'; import 'package:nearle/controllers/logcontroller.dart'; import 'package:nearle/controllers/delivery.dart'; import 'package:nearle/controllers/deliveries_controller.dart'; import 'package:geolocator/geolocator.dart'; import 'package:slide_to_submit_button/slide_to_submit_button.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:geocoding/geocoding.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:floating/floating.dart'; import 'dart:math' as math; import 'package:http/http.dart' as http; import 'package:nearle/views/Dashboard/home/homepage_banner.dart'; import 'package:nearle/views/Dashboard/deliveries/deliveries.dart' as deliveries; import 'package:nearle/background/live_tracking_service.dart'; /// Helper function to launch phone dialer - works in both debug and release builds /// In release builds, canLaunchUrl may fail due to R8/ProGuard, so we always try to launch Future launchPhoneDialer(String phoneNumber) async { try { // Sanitize phone number: keep only digits and '+' final phone = phoneNumber.replaceAll(RegExp(r'[^\d+]'), ''); if (phone.isEmpty) { debugPrint( '[PHONE] Empty phone number after sanitization, skipping dial', ); return false; } final Uri uri = Uri(scheme: 'tel', path: phone); // Try canLaunchUrl first (works in debug, may fail in release) bool canLaunch = false; try { canLaunch = await canLaunchUrl(uri); debugPrint('[PHONE] canLaunchUrl result: $canLaunch'); } catch (e) { debugPrint('[PHONE] canLaunchUrl check failed (common in release): $e'); // Continue anyway - launch might still work } // Always attempt to launch, even if canLaunchUrl returned false // Using LaunchMode.platformDefault is often safer for system intents like dialing try { final launched = await launchUrl(uri, mode: LaunchMode.platformDefault); if (launched) { debugPrint('[PHONE] Successfully launched dialer for: $phone'); return true; } else { debugPrint('[PHONE] launchUrl returned false for: $phone'); } } catch (e) { debugPrint('[PHONE] Failed to launch dialer: $e'); } return false; } catch (e) { debugPrint('[PHONE] Error in launchPhoneDialer: $e'); return false; } } class Homepage extends StatefulWidget { const Homepage({super.key}); @override State createState() => _HomepageState(); } class _HomepageState extends State with AutomaticKeepAliveClientMixin { bool isOnline = true; String _userName = ''; String _shiftStart = ''; String _shiftEnd = ''; final DeliveryProvider _delivery = DeliveryProvider(); final DeliveryController _deliveryController = Get.find(); // Core data - using Map with orderId as key for stable tracking Map> _ordersMap = {}; List _orderIds = []; // Maintain order StreamSubscription? _pollerSubscription; Duration _currentPollInterval = const Duration(seconds: 0); bool _isToggling = false; // Selection tracking Map _selectedOrders = {}; bool isAllSelected = false; String _lastQueuesJson = ''; bool _loadedOnlineFromPrefs = false; bool _isFetchingQueues = false; final Map _deliveryRunning = {}; final Map _deliveryTimers = {}; final Map> _deliveryBasePayload = >{}; final CreateDeliveryLogProvider _deliveryLogProvider = CreateDeliveryLogProvider(); // Prevent optimistic status flips; disable row while updating final Map _statusBusy = {}; Timer? _locationTimer; final Set _hiddenOrderIds = {}; // Active delivery tracking List> _activeDeliveries = >[]; @override void dispose() { // Stop any active delivery posting timers final keys = List.from(_deliveryTimers.keys); for (final k in keys) { _stopDeliveryPosting(k); } _pollerSubscription?.cancel(); _locationTimer?.cancel(); super.dispose(); } void _stopDeliveryPosting(String orderId) { _deliveryTimers[orderId]?.cancel(); _deliveryTimers.remove(orderId); } // Helper to get order status from API data String _getStatusFromOrder(Map order) { final apiStatus = (order['orderstatus']?.toString().toLowerCase() ?? '') .trim(); if (apiStatus == 'accepted') return 'ACCEPTED'; if (apiStatus == 'arrived') return 'ARRIVED'; if (apiStatus == 'picked') return 'PICKED'; return 'ACCEPT'; // Default for pending/new orders } // Parse double safely double _parseDouble(dynamic v) { if (v == null) return 0.0; if (v is num) return v.toDouble(); return double.tryParse(v.toString()) ?? 0.0; } // Temporarily disabled: always allow (prox check will be re-enabled later) Future _isNearPickupLocation(Map order) async { try { final pickupLat = _parseDouble(order['pickuplat'] ?? order['PickupLat']); final pickupLng = _parseDouble(order['pickuplon'] ?? order['PickupLon']); if (pickupLat == 0 || pickupLng == 0) { debugPrint('[PROXIMITY] Invalid pickup coordinates, skipping check.'); return true; // Assume near if coordinates are missing } final riderLoc = await _getValidCoordinates(); if (riderLoc == null) { debugPrint('[PROXIMITY] Could not get rider location, skipping check.'); return true; // Assume near if location fails (don't block) } final double riderLat = double.tryParse(riderLoc.$1) ?? 0; final double riderLng = double.tryParse(riderLoc.$2) ?? 0; if (riderLat == 0 || riderLng == 0) { return true; } final double distanceInMeters = Geolocator.distanceBetween( riderLat, riderLng, pickupLat, pickupLng, ); debugPrint( '[PROXIMITY] Distance: ${distanceInMeters.toStringAsFixed(2)}m (Threshold: 500m)', ); if (distanceInMeters > 500) { return false; } return true; } catch (e) { debugPrint('[PROXIMITY] Error checking proximity: $e'); return true; // Fail safe } } // Check proximity for multiple orders Future> _checkProximityForOrders( List orderIds, ) async { final Map results = {}; for (final orderId in orderIds) { final order = _ordersMap[orderId]; if (order == null) { results[orderId] = false; continue; } results[orderId] = await _isNearPickupLocation(order); } return results; } // Show proximity warning dialog Future _showProximityWarning( BuildContext context, { String? specificMessage, }) async { return showDialog( context: context, barrierDismissible: true, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: Row( children: [ const Icon(Icons.location_off, color: Colors.red, size: 28), const SizedBox(width: 12), Expanded( child: Text( 'Not at Pickup Location', style: TextStyle( fontSize: FontConstants.xLarge(context), fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), ), ], ), content: Text( specificMessage ?? 'You must be within 500 meters of the pickup location to mark this order as arrived. Please move closer and try again.', style: TextStyle( fontSize: FontConstants.regular(context), fontFamily: FontConstants.fontFamily, ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), style: TextButton.styleFrom( foregroundColor: ColorConstants.primaryColor, ), child: Text( 'OK', style: TextStyle( fontSize: FontConstants.regular(context), fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), ), ], ), ); } Future<(String lat, String lng)?> _getValidCoordinates() async { try { await _ensureLocationPermission(); Position? pos; try { // Use lower accuracy with a shorter timeout for faster loading pos = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.low, timeLimit: const Duration(seconds: 3), ).timeout(const Duration(seconds: 3)); } catch (_) { pos = await Geolocator.getLastKnownPosition(); } if (pos != null) { final lat = pos.latitude.toStringAsFixed(6); final lng = pos.longitude.toStringAsFixed(6); try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('last_lat', lat); await prefs.setString('last_lng', lng); } catch (_) {} return (lat, lng); } 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') { return (lat, lng); } } catch (_) {} return null; } catch (_) { return null; } } Future _getLogInterval() async { try { final prefs = await SharedPreferences.getInstance(); final secs = prefs.getInt('logseconds'); final int interval = (secs != null && secs > 0) ? secs : 30; return Duration(seconds: interval); } catch (_) { return const Duration(seconds: 30); } } @override void initState() { super.initState(); // Run these in parallel for faster loading _loadName(); _ensureLocationPermission(); // Don't block on this _ensureCameraPermission(); // Request camera permission at startup _startPolling(); // Fetch queues immediately, don't wait for location WidgetsBinding.instance.addPostFrameCallback((_) { _fetchQueues(); }); try { if (Get.isRegistered()) { final pc = Get.find(); ever(pc.userName, (_) { _loadName(); }); } } catch (_) {} } void _showLocationBottomSheet() { showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) => Padding( padding: EdgeInsets.only( left: 20, right: 20, top: 20, bottom: MediaQuery.of(ctx).viewInsets.bottom + 20, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const Spacer(), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(ctx).pop(), ), ], ), const Icon(Icons.location_off, size: 80, color: Colors.red), const SizedBox(height: 16), Text( "Location is turned off", style: TextStyle( fontSize: FontConstants.xxLarge(context), fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), const SizedBox(height: 12), Text( "Please turn on location to receive orders and track delivery.", textAlign: TextAlign.center, style: TextStyle( fontSize: FontConstants.regular(context), fontFamily: FontConstants.fontFamily, ), ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () async { Navigator.of(ctx).pop(); await Geolocator.openLocationSettings(); }, style: ElevatedButton.styleFrom( backgroundColor: ColorConstants.primaryColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), ), child: Text( "Turn On Location", style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), ), ), ], ), ), ); } Future _getAndUpdateCurrentLocation() async { bool serviceEnabled; LocationPermission permission; serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { _showLocationBottomSheet(); return; } permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { debugPrint("Location permissions are denied."); return; } } if (permission == LocationPermission.deniedForever) { debugPrint("Location permissions are permanently denied."); return; } Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, ); // ✅ OPTIMIZATION: Only geocode if moved significantly (> 100m) // Reverse geocoding is expensive and slow if (_lastGeocodedPosition != null) { final dist = Geolocator.distanceBetween( _lastGeocodedPosition!.latitude, _lastGeocodedPosition!.longitude, position.latitude, position.longitude, ); if (dist < 100) { // Hasn't moved enough to change address, skip return; } } try { List placemarks = await placemarkFromCoordinates( position.latitude, position.longitude, ); if (placemarks.isNotEmpty) { _lastGeocodedPosition = position; // Save for next check Placemark place = placemarks.first; String fullAddress = "${place.name}, ${place.subLocality}, ${place.locality}, ${place.administrativeArea}, ${place.country}, ${place.postalCode}"; String city = place.locality ?? ''; String state = place.administrativeArea ?? ''; String suburb = place.subLocality ?? ''; await _updateProfile( address: fullAddress, latitude: position.latitude, longitude: position.longitude, city: city, state: state, suburb: suburb, ); } } catch (e) { debugPrint("Error geocoding location: $e"); } } Future _updateProfile({ required String address, required double latitude, required double longitude, required String city, required String state, required String suburb, }) async { debugPrint( 'Profile update → $address | $latitude,$longitude | $city,$state,$suburb', ); } void _startLocationUpdates() { _locationTimer?.cancel(); _locationTimer = Timer.periodic(const Duration(seconds: 30), (_) { _getAndUpdateCurrentLocation(); }); } Future _ensureLocationPermission() async { try { final prefs = await SharedPreferences.getInstance(); bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { // Try last known position first final lastPos = await Geolocator.getLastKnownPosition(); if (lastPos != null) { await prefs.setString('last_lat', lastPos.latitude.toString()); await prefs.setString('last_lng', lastPos.longitude.toString()); } // Open settings in background, don't wait Geolocator.openLocationSettings(); return; } LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.deniedForever || permission == LocationPermission.denied) { // Use last known position if available final lastPos = await Geolocator.getLastKnownPosition(); if (lastPos != null) { await prefs.setString('last_lat', lastPos.latitude.toString()); await prefs.setString('last_lng', lastPos.longitude.toString()); } return; } // Use medium accuracy with timeout for faster initialization Position? pos; try { pos = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.medium, // Changed from high timeLimit: const Duration(seconds: 5), // Add timeout ).timeout(const Duration(seconds: 5)); } catch (e) { debugPrint('[HOMEPAGE] Timeout getting location, using last known: $e'); pos = await Geolocator.getLastKnownPosition(); } if (pos != null) { await prefs.setString('last_lat', pos.latitude.toString()); await prefs.setString('last_lng', pos.longitude.toString()); } } catch (e) { debugPrint('[HOMEPAGE] Error ensuring location permission: $e'); // Try to save last known position as fallback try { final lastPos = await Geolocator.getLastKnownPosition(); if (lastPos != null) { final prefs = await SharedPreferences.getInstance(); await prefs.setString('last_lat', lastPos.latitude.toString()); await prefs.setString('last_lng', lastPos.longitude.toString()); } } catch (_) {} } } Future _ensureCameraPermission() async { try { final status = await Permission.camera.status; if (status.isDenied) { // We haven't asked yet or user denied previously but not forever await Permission.camera.request(); } else if (status.isPermanentlyDenied) { // User denied forever, we might want to show a dialog or snackbar // asking them to go to settings, but for startup let's just leave it // so we don't annoy them every time if they really don't want to. debugPrint('[HOMEPAGE] Camera permission permanently denied'); } } catch (e) { debugPrint('[HOMEPAGE] Error ensuring camera permission: $e'); } } void _confirmOnlineOffline() { if (_isToggling) return; final bool newStatus = !isOnline; if (!mounted) return; showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (BuildContext ctx) { return StatefulBuilder( builder: (ctx2, setSheetState) { return SafeArea( top: false, left: false, right: false, bottom: true, child: SingleChildScrollView( child: Padding( padding: EdgeInsets.only( left: 20, right: 20, top: 20, bottom: MediaQuery.of(ctx2).viewInsets.bottom + 20, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const Spacer(), IconButton( icon: const Icon( Icons.cancel, color: Colors.red, size: 32, ), onPressed: () { if (Navigator.of(ctx2).canPop()) { Navigator.of(ctx2).pop(); } }, ), ], ), Image.asset( 'assets/images/Nearle Bike.png', height: 140, errorBuilder: (c, e, s) => const SizedBox.shrink(), ), const SizedBox(height: 16), Text( newStatus ? "Go Online?" : "Go Offline?", style: TextStyle( fontSize: FontConstants.xxxLarge(context), fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), const SizedBox(height: 16), Text( newStatus ? "You will start receiving orders." : "You will stop receiving orders.", style: TextStyle( fontSize: FontConstants.large(context), fontFamily: FontConstants.fontFamily, ), ), const SizedBox(height: 24), SizedBox( width: MediaQuery.of(context).size.width - 40, child: SlideToSubmit.custom( height: 55, sliderWidth: 40, padding: const EdgeInsets.all(8), backgroundDecoration: BoxDecoration( color: newStatus ? ColorConstants.primaryColor.withOpacity(0.50) : Colors.red.withOpacity(0.50), borderRadius: BorderRadius.circular(40), ), foregroundDecoration: BoxDecoration( color: newStatus ? ColorConstants.primaryColor : Colors.red, borderRadius: BorderRadius.circular(999), ), slider: Center( child: ClipOval( child: Container( height: 40, width: 40, color: Colors.white, // or any background color padding: EdgeInsets.all(8), child: const Icon( Icons.arrow_forward_ios, size: 28, color: Colors.black, ), ), ), ), hint: const Align( alignment: Alignment.centerRight, child: Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: AnimatedSlideArrow( arrowImage: AssetImage( 'assets/arrow_right.png', package: 'slide_to_submit_button', ), ), ), ), onSubmit: (controller) async { if (!mounted) return; if (_isToggling) return; if (newStatus == false) { // Check for any pending queues or live (picked) deliveries bool hasPending = _ordersMap.isNotEmpty || _deliveryRunning.values.any((v) => v); try { final prefs = await SharedPreferences.getInstance(); final bool hasLive = prefs.getBool('has_live_deliveries') ?? false; hasPending = hasPending || hasLive; } catch (_) {} if (hasPending) { try { await showDialog( context: context, builder: (dCtx) => AlertDialog( title: const Text('Pending orders'), content: const Text( 'Please complete all orders before going offline.', ), actions: [ TextButton( onPressed: () => Navigator.of(dCtx).pop(), child: const Text('OK'), ), ], ), ); } catch (_) {} try { controller.reset(); } catch (_) {} if (Navigator.of(ctx2).canPop()) { Navigator.of(ctx2).pop(); } return; } } _isToggling = true; showDialog( context: context, barrierDismissible: false, builder: (dialogCtx) { return const Center( child: CircularProgressIndicator(), ); }, ); bool ok = false; try { ok = await _handleOnlineToggle( newStatus, ).timeout(const Duration(seconds: 30)); } catch (_) { ok = false; } finally { if (mounted && Navigator.of(context).canPop()) { Navigator.of(context).pop(); } _isToggling = false; } if (!mounted) return; setState(() { isOnline = ok ? newStatus : isOnline; }); try { final prefs = await SharedPreferences.getInstance(); await prefs.setBool('online', isOnline); } catch (_) {} if (mounted) { Get.snackbar( ok ? (newStatus ? 'Online' : 'Offline') : 'Error', ok ? (newStatus ? 'You are now online.' : 'You are now offline.') : 'Something went wrong. Please try again.', backgroundColor: Colors.black.withOpacity(0.6), colorText: Colors.white, snackPosition: SnackPosition.BOTTOM, margin: const EdgeInsets.all(12), borderRadius: 12, duration: const Duration(seconds: 2), ); } if (ok && newStatus) { _startLocationUpdates(); LiveTrackingService().startTracking(); _fetchQueues(); _startPolling(); } else if (!newStatus) { _locationTimer?.cancel(); LiveTrackingService().stopTracking(); _fetchQueues(); _startPolling(); } try { controller.reset(); } catch (_) {} if (Navigator.of(ctx2).canPop()) { Navigator.of(ctx2).pop(); } }, ), ), ], ), ), ), ); }, ); }, ); } Future _loadName() async { String name = ''; try { if (Get.isRegistered()) { final pc = Get.find(); final val = pc.userName.value; if (val.toString().trim().isNotEmpty) { name = val.toString().trim(); } } } catch (_) {} try { final prefs = await SharedPreferences.getInstance(); if (name.isEmpty) { name = (prefs.getString('user_name') ?? '').trim(); } _shiftStart = (prefs.getString('starttime') ?? '').toString(); _shiftEnd = (prefs.getString('endtime') ?? '').toString(); final savedOnline = prefs.getBool('online'); final int onduty = prefs.getInt('onduty') ?? -1; if (!mounted) return; setState(() { _userName = name; if (!_loadedOnlineFromPrefs) { if (onduty == 1) { isOnline = true; prefs.setBool('online', true); // Ensure foreground logging service is running when already on duty try { if (Get.isRegistered()) { Get.find().startLogging(); } } catch (_) {} } else if (onduty == 0) { isOnline = false; prefs.setBool('online', false); } else { isOnline = savedOnline ?? true; } _loadedOnlineFromPrefs = true; } }); } catch (_) { if (!mounted) return; setState(() { _userName = name; isOnline = true; }); } } Future _handleOnlineToggle(bool online) async { try { if (!Get.isRegistered()) return false; final ctl = Get.find(); final prefs = await SharedPreferences.getInstance(); final userId = prefs.getInt('userId') ?? prefs.getInt('userid'); if (userId == null) return false; const String lat = '0'; const String lng = '0'; if (online == true) { debugPrint('[BREAK] Ending break... lat=$lat lng=$lng'); final ok = await ctl .endBreakAuto(latitude: lat, longitude: lng) .timeout(const Duration(seconds: 12), onTimeout: () => false); debugPrint('[BREAK] endBreakAuto -> $ok'); final dutyOk = await ctl.setOnDuty(true); debugPrint('[ONDUTY] setOnDuty(true) -> $dutyOk'); await prefs.setBool('online', dutyOk); } else { debugPrint('[BREAK] Starting break... lat=$lat lng=$lng'); final ok = await ctl .startBreakAuto(latitude: lat, longitude: lng) .timeout(const Duration(seconds: 12), onTimeout: () => false); debugPrint('[BREAK] startBreakAuto -> $ok'); final dutyOk = await ctl.setOnDuty(false); debugPrint('[ONDUTY] setOnDuty(false) -> $dutyOk'); await prefs.setBool('online', dutyOk ? false : true); } return true; } catch (_) { return false; } } void _startPolling() { final interval = isOnline ? const Duration(seconds: 5) : const Duration(seconds: 20); if (_pollerSubscription != null && _currentPollInterval == interval) { return; } _pollerSubscription?.cancel(); _currentPollInterval = interval; // Use Stream.periodic instead of Timer.periodic for better resource management _pollerSubscription = Stream.periodic(interval, (_) {}) .asyncMap((_) async { if (mounted && !_isFetchingQueues) { await _fetchQueues(); } }) .listen( (_) {}, // Success handler onError: (error) { // Handle errors gracefully without crashing debugPrint('[HOMEPAGE][STREAM ERROR] $error'); }, cancelOnError: false, // Continue even on errors ); } Future _fetchQueues() async { if (_isFetchingQueues) return; try { _isFetchingQueues = true; final prefs = await SharedPreferences.getInstance(); final userId = prefs.getInt('userid'); if (userId == null) { if (!mounted) return; setState(() { _ordersMap.clear(); _orderIds.clear(); _selectedOrders.clear(); isAllSelected = false; }); return; } final items = await _delivery.getDeliveryQueues( live: true, userid: userId, ); if (!mounted) return; // ✅ Also fetch active deliveries from v2 API (same as deliveries page) await _fetchActiveDeliveries(userId); final validOrders = items.whereType>().where((m) { final status = (m['orderstatus']?.toString().toLowerCase() ?? '') .trim(); final oid = (m['orderid'] ?? '').toString(); if (oid.isNotEmpty && _hiddenOrderIds.contains(oid)) return false; // Exclude active orders from main list (they show in banner) if (status == 'active') return false; return status != 'picked'; }).toList(); if (validOrders.isNotEmpty) { final delivery = validOrders.first; await _deliveryController.saveFromQueueItem(delivery); } final newJson = jsonEncode(validOrders); if (newJson == _lastQueuesJson) { return; } _lastQueuesJson = newJson; final Map> newOrdersMap = {}; final List newOrderIds = []; for (final order in validOrders) { final orderId = (order['orderid'] ?? '').toString(); if (orderId.isEmpty) continue; newOrdersMap[orderId] = order; newOrderIds.add(orderId); } final Map newSelection = {}; for (final orderId in newOrderIds) { newSelection[orderId] = _selectedOrders[orderId] ?? false; } setState(() { _ordersMap = newOrdersMap; _orderIds = newOrderIds; _selectedOrders = newSelection; if (_selectedOrders.isEmpty) { isAllSelected = false; } else { isAllSelected = _selectedOrders.values.every((selected) => selected); } for (final orderId in _orderIds) { if (!_deliveryRunning.containsKey(orderId)) { _deliveryRunning[orderId] = false; } } }); } catch (e) { debugPrint('[FETCH_QUEUES] Error: $e'); } finally { _isFetchingQueues = false; } } // Persistent client to avoid creating new connections constantly static final http.Client _httpClient = http.Client(); Position? _lastGeocodedPosition; // Fetch active deliveries from v2 API (same as deliveries page) Future _fetchActiveDeliveries(int userId) async { try { // Get current date in YYYY-MM-DD format final now = DateTime.now(); final today = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; // Hardcoded API endpoint for active deliveries: v2/deliveries/getdeliveries final bool isLive = ApiConstants.mainRoute == 'live'; final baseUrl = isLive ? 'https://jupiter.nearle.app/live/api/v2/deliveries/getdeliveries' : 'https://jupiter.nearle.app/dev/api/v2/deliveries/getdeliveries'; final uri = Uri.parse(baseUrl).replace( queryParameters: { 'userid': userId.toString(), 'fromdate': today, 'todate': today, 't': DateTime.now().millisecondsSinceEpoch.toString(), }, ); // Fetch deliveries from v2 API directly using REUSED http client List itemsV2 = []; try { final response = await _httpClient.get(uri); // Use persistent client if (response.statusCode >= 200 && response.statusCode < 300) { final decoded = json.decode(response.body); final data = decoded is Map ? (decoded['details'] ?? decoded['data'] ?? decoded) : decoded; itemsV2 = data is List ? data : (data is Map && data['items'] is List ? data['items'] as List : []); } else { debugPrint('[HOMEPAGE] v2 API error: ${response.statusCode}'); } } catch (e) { debugPrint('[HOMEPAGE] Error fetching from v2 API: $e'); } // Do NOT close client here // Filter for ACTIVE status from v2 API final activeOrders = itemsV2.whereType>().where(( order, ) { final status = (order['orderstatus']?.toString().toLowerCase() ?? '') .trim(); final isActive = status == 'active'; if (isActive) { debugPrint( '[HOMEPAGE] ✅ Found active order from v2: ${order['orderid']}', ); } return isActive; }).toList(); debugPrint( '[HOMEPAGE] Found ${activeOrders.length} active deliveries from v2 API', ); if (mounted) { setState(() { _activeDeliveries = activeOrders; }); } } catch (e) { debugPrint('[HOMEPAGE] Error fetching active deliveries: $e'); } } PreferredSizeWidget _buildHeader(double width) { final height = MediaQuery.of(context).size.height; final w = width; // Responsive multipliers copied from lib/homepage.dart final titleSize = w * 0.06; // Hi, Rider final shiftSize = w * 0.04; // Shift : 10 AM to 6 PM final switchWidth = w * 0.32; // Status button width final switchHeight = height * 0.045; return PreferredSize( preferredSize: Size.fromHeight(height * 0.115), child: SafeArea( top: true, bottom: false, child: AppBar( automaticallyImplyLeading: false, backgroundColor: ColorConstants.primaryColor, elevation: 0, toolbarHeight: height * 0.115, // responsive toolbar height title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Hi, ${_userName.isNotEmpty ? _userName.split(' ').first : "Rider"}!", style: TextStyle( fontSize: titleSize, // responsive fontWeight: FontWeight.bold, color: Colors.white, fontFamily: FontConstants.fontFamily, ), ), SizedBox(height: height * 0.012), Text( (() { final s = _shiftStart.trim(); final e = _shiftEnd.trim(); if (s.isEmpty || e.isEmpty) return "Shift : -"; return "Shift : $s to $e"; })(), style: TextStyle( color: Colors.white, fontSize: shiftSize, // responsive fontFamily: FontConstants.fontFamily, ), ), ], ), actions: [ Padding( padding: EdgeInsets.only(right: w * 0.03), child: GestureDetector( onTap: _hasActiveDelivery() ? null : _confirmOnlineOffline, child: Container( width: switchWidth, // responsive height: switchHeight, // responsive decoration: BoxDecoration( color: isOnline ? Colors.green : Colors.red, borderRadius: BorderRadius.circular(switchHeight * 0.9), ), child: Stack( children: [ Align( alignment: isOnline ? Alignment.centerLeft : Alignment.centerRight, child: Padding( padding: EdgeInsets.symmetric(horizontal: w * 0.04), child: Text( isOnline ? "ONLINE" : "OFFLINE", style: TextStyle( color: Colors.white, fontSize: w * 0.038, // responsive fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), ), ), Align( alignment: isOnline ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.all(2), width: switchHeight * 0.78, height: switchHeight * 0.78, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), child: Center( child: Image.asset( 'assets/images/onlineoffline.png', width: switchHeight * 0.45, height: switchHeight * 0.45, errorBuilder: (c, e, s) => const SizedBox.shrink(), ), ), ), ), ], ), ), ), ), ], ), ), ); } // ✅ 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() {} // Navigate to active delivery map screen directly (same as deliveries page) Future _navigateToActiveDelivery(Map delivery) async { if (!mounted) return; debugPrint( '[HOMEPAGE] Navigate to active delivery map: ${delivery['orderid']}', ); // Navigate directly to delivery map screen (same as deliveries page) await deliveries.MyDeliveries.navigateToDeliveryMap(context, delivery); // Refresh after returning from map screen if (mounted) { await Future.delayed(const Duration(milliseconds: 500)); final prefs = await SharedPreferences.getInstance(); final userId = prefs.getInt('userid'); if (userId != null) { await _fetchQueues(); } } } String _getBulkButtonLabel() { final selectedOrderIds = _selectedOrders.entries .where((e) => e.value) .map((e) => e.key) .toList(); if (selectedOrderIds.isEmpty) return "Accept Orders"; final statuses = selectedOrderIds .map((id) => _getStatusFromOrder(_ordersMap[id] ?? {})) .toSet(); if (statuses.length == 1 && statuses.first == 'ACCEPT') { return "Accept Orders"; } if (statuses.length == 1 && statuses.first == 'ACCEPTED') { return "Move to Arrived"; } if (statuses.length == 1 && statuses.first == 'ARRIVED') { return "Move to Picked"; } if (statuses.contains('ACCEPT')) { return "Accept Orders"; } else if (statuses.contains('ACCEPTED')) { return "Move to Arrived"; } else { return "Move to Picked"; } } Future _handleBulkAction() async { final dc = Get.put(DeliveriesController(), permanent: true); final selectedOrderIds = _selectedOrders.entries .where((e) => e.value) .map((e) => e.key) .toList(); if (selectedOrderIds.isEmpty) return; final statuses = selectedOrderIds .map((id) => _getStatusFromOrder(_ordersMap[id] ?? {})) .toSet(); String targetStatus; if (statuses.length == 1 && statuses.first == 'ACCEPT') { targetStatus = 'ACCEPTED'; } else if (statuses.length == 1 && statuses.first == 'ACCEPTED') { targetStatus = 'ARRIVED'; } else if (statuses.length == 1 && statuses.first == 'ARRIVED') { targetStatus = 'PICKED'; } else { targetStatus = 'ACCEPTED'; } // ✅ PROXIMITY CHECK FOR ARRIVED STATUS if (targetStatus == 'ARRIVED') { debugPrint( '[BULK] Checking proximity for ${selectedOrderIds.length} orders...', ); // Quick proximity check (no loading dialog since it's fast) final proximityResults = await _checkProximityForOrders(selectedOrderIds) .timeout( const Duration(seconds: 3), onTimeout: () { debugPrint('[BULK] Proximity check timeout, allowing all'); // Return all true on timeout return Map.fromEntries( selectedOrderIds.map((id) => MapEntry(id, true)), ); }, ); // Find orders that are NOT near pickup final farOrders = proximityResults.entries .where((e) => !e.value) .map((e) => e.key) .toList(); if (farOrders.isNotEmpty) { // Show error for all orders that are too far if (mounted) { final message = farOrders.length == selectedOrderIds.length ? 'You must be within 500 meters of the pickup location(s) to mark orders as arrived. Please move closer and try again.' : 'Some selected orders are too far from their pickup locations. Please move closer or deselect those orders.'; await _showProximityWarning(context, specificMessage: message); } return; // Don't proceed with the update } debugPrint('[BULK] All orders within 500m of pickup. Proceeding...'); } debugPrint( '[BULK] Moving ${selectedOrderIds.length} orders to $targetStatus', ); // TRIGGER CAMERA FOR BULK PICKED XFile? bulkPhoto; if (targetStatus == 'PICKED') { try { final ImagePicker picker = ImagePicker(); bulkPhoto = await picker.pickImage( source: ImageSource.camera, imageQuality: 50, ); if (bulkPhoto == null) return; // Abort if cancelled } catch (e) { debugPrint('[BULK] Camera error: $e'); return; } } // Show minimal loading indicator (only for bulk operations) showDialog( context: context, barrierDismissible: false, barrierColor: Colors.black.withOpacity(0.3), builder: (ctx) => const Center(child: CircularProgressIndicator()), ); try { // Upload Bulk Proof if available String? bulkProofImageUrl; if (bulkPhoto != null) { try { final prefs = await SharedPreferences.getInstance(); final userId = int.tryParse(prefs.getString('userid') ?? '0') ?? 0; if (userId > 0) { bulkProofImageUrl = await dc.uploadProofImage( File(bulkPhoto.path), 'picked', userId, 0, // 0 for bulk upload ); } } catch (e) { debugPrint('[BULK] Upload error: $e'); } } for (final orderId in selectedOrderIds) { final order = _ordersMap[orderId]; if (order == null) continue; final deliveryId = int.tryParse('${order['deliveryid'] ?? 0}') ?? 0; final orderHeaderId = int.tryParse('${order['orderheaderid'] ?? 0}') ?? 0; final pickupLocationId = int.tryParse('${order['pickuplocationid'] ?? 0}') ?? 0; bool success = false; if (targetStatus == 'ACCEPTED') { debugPrint('[BULK] Accepting order $orderId'); success = await dc .updateAcceptedStatus( deliveryId: deliveryId, orderHeaderId: orderHeaderId, ) .timeout(const Duration(seconds: 20), onTimeout: () => false); } else if (targetStatus == 'ARRIVED') { debugPrint('[BULK] Arriving order $orderId'); final pickupLat = _parseDouble( order['pickuplat'] ?? order['PickupLat'] ?? 0, ); final pickupLng = _parseDouble( order['pickuplon'] ?? order['PickupLon'] ?? 0, ); final riderLoc = await _getValidCoordinates(); success = await dc .updateArrivedStatus( deliveryId: deliveryId, orderHeaderId: orderHeaderId, pickupLat: pickupLat.toString(), pickupLng: pickupLng.toString(), ridersLat: riderLoc?.$1 ?? '0', ridersLng: riderLoc?.$2 ?? '0', ) .timeout(const Duration(seconds: 20), onTimeout: () => false); } else if (targetStatus == 'PICKED') { debugPrint('[BULK] Picking order $orderId'); // Get pickup coordinates and rider location for distance calculation final pickupLat = _parseDouble( order['pickuplat'] ?? order['PickupLat'] ?? 0, ); final pickupLng = _parseDouble( order['pickuplon'] ?? order['PickupLon'] ?? 0, ); final riderLoc = await _getValidCoordinates(); final riderLatStr = riderLoc?.$1 ?? '0'; final riderLngStr = riderLoc?.$2 ?? '0'; success = await dc .updatePickedStatus( deliveryId: deliveryId, orderHeaderId: orderHeaderId, pickupLocationId: pickupLocationId, ridersLat: riderLatStr, ridersLng: riderLngStr, pickupLat: pickupLat.toStringAsFixed(6), pickupLng: pickupLng.toStringAsFixed(6), proofImage: bulkProofImageUrl, ) .timeout(const Duration(seconds: 20), onTimeout: () => false); if (success) { _hiddenOrderIds.add(orderId); } } if (!success) { debugPrint('[BULK] Failed to update order $orderId'); } // Reduced delay between requests await Future.delayed(const Duration(milliseconds: 50)); } setState(() { _selectedOrders.clear(); isAllSelected = false; }); await _fetchQueues(); } catch (e) { debugPrint('[BULK] Error: $e'); } finally { if (mounted && Navigator.of(context).canPop()) { Navigator.of(context).pop(); } } } @override Widget build(BuildContext context) { super.build(context); final selectedCount = _selectedOrders.values.where((s) => s).length; return WillPopScope( onWillPop: () async { try { final prefs = await SharedPreferences.getInstance(); final hasLive = prefs.getBool('has_live_deliveries') ?? false; if (hasLive) { final confirm = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Pending deliveries'), content: const Text( 'Are you sure you want to close? There are deliveries pending.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('No'), ), TextButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Yes'), ), ], ), ); return confirm == true; } } catch (_) {} return true; }, child: SafeArea( top: true, bottom: true, left: true, right: true, child: Scaffold( backgroundColor: Colors.grey.shade200, appBar: _buildHeader(MediaQuery.of(context).size.width), body: Stack( children: [ Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( padding: const EdgeInsets.all(14.0), child: Text( "Orders Pending", style: TextStyle( fontSize: 21, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), ), Padding( padding: const EdgeInsets.all(4.0), child: Row( children: [ Transform.translate( offset: Offset(-10, 1), child: Text( "Select all", style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), ), Transform.translate( offset: Offset(-10, 0), child: Transform.scale( scale: 1.3, child: Checkbox( value: isAllSelected, activeColor: ColorConstants.primaryColor, onChanged: _orderIds.isEmpty || _hasActiveDelivery() ? null : (value) { setState(() { isAllSelected = value ?? false; for (final orderId in _orderIds) { _selectedOrders[orderId] = isAllSelected; } }); }, ), ), ), ], ), ), ], ), Divider(color: Colors.grey.shade400, thickness: 1, height: 1), Expanded( child: _orderIds.isEmpty ? Transform.translate( offset: const Offset(0, -4), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset("assets/images/Nearle Bike.png"), const SizedBox(height: 16), Text( "No Orders at the moment", textAlign: TextAlign.center, style: TextStyle( fontSize: FontConstants.xxxLarge(context), fontFamily: FontConstants.fontFamily, color: Colors.grey.shade500, ), ), ], ), ), ) : ListView.builder( physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics(), ), itemCount: _orderIds.length, itemBuilder: (context, index) { final orderId = _orderIds[index]; final item = _ordersMap[orderId] ?? {}; final title = (item['deliverycustomer'] ?? 'Senthil Stores, RS Puram') .toString(); final address = (item['deliveryaddress'] ?? '23, 2nd street, Race course') .toString(); final tenant = (item['tenantname'] ?? '') .toString(); final pickupCustomer = (item['pickupcustomer'] ?? '') .toString() .trim(); final bool hasPickupCustomer = pickupCustomer.isNotEmpty; final String primaryStoreName = hasPickupCustomer ? pickupCustomer : tenant; final String secondaryStoreName = hasPickupCustomer ? tenant : ''; final int quantity = int.tryParse( '${item['Quantity'] ?? item['quantity'] ?? 0}', ) ?? 0; final isSelected = _selectedOrders[orderId] ?? false; final currentStatus = _getStatusFromOrder(item); return Container( key: ValueKey('order_$orderId'), margin: const EdgeInsets.symmetric( horizontal: 14, vertical: 8, ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 6, offset: const Offset(0, 3), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( children: [ Container( width: 12, height: 12, decoration: const BoxDecoration( color: Colors.orange, shape: BoxShape.circle, ), ), Container( width: 2, height: 30, color: Colors.grey.shade300, ), Container( width: 12, height: 12, decoration: const BoxDecoration( color: Colors.green, shape: BoxShape.circle, ), ), ], ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Transform.translate( offset: const Offset( 0, -5, ), child: Text( title, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 18, color: Colors.black, fontFamily: FontConstants .fontFamily, ), ), ), const SizedBox(height: 18), Transform.translate( offset: const Offset( 0, -1, ), child: Text( address, style: TextStyle( fontSize: 18, color: Colors.black87, fontFamily: FontConstants .fontFamily, ), ), ), const SizedBox(height: 6), if (quantity > 0) Text( 'Quantity: $quantity', style: TextStyle( fontSize: 19, color: Colors .blueGrey .shade900, fontFamily: FontConstants .fontFamily, fontWeight: FontWeight.bold, ), ), ], ), ), GestureDetector( onTap: _hasActiveDelivery() ? null : () { setState(() { _selectedOrders[orderId] = !isSelected; isAllSelected = _selectedOrders .values .every( (s) => s, ); }); }, child: Container( padding: const EdgeInsets.all( 4, ), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: ColorConstants .primaryColor, width: 2, ), ), child: AnimatedContainer( duration: const Duration( milliseconds: 200, ), width: 20, height: 20, decoration: BoxDecoration( color: isSelected ? ColorConstants .primaryColor : Colors.white, shape: BoxShape.circle, ), ), ), ), ], ), const SizedBox(height: 10), LayoutBuilder( builder: (context, constraints) => Row( children: List.generate( (constraints.maxWidth / 6) .floor(), (index) => const Expanded( child: Padding( padding: EdgeInsets.symmetric( horizontal: 1.5, ), child: Divider( color: Colors.grey, thickness: 1, height: 1, ), ), ), ), ), ), const SizedBox(height: 10), SafeArea( top: false, left: false, right: false, bottom: true, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Image.asset( "assets/images/shoppingbag.png", height: 32, width: 32, ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment .start, children: [ Column( crossAxisAlignment: CrossAxisAlignment .start, children: [ Text( primaryStoreName, style: TextStyle( fontWeight: FontWeight .w600, fontSize: 16, color: Colors.black, fontFamily: FontConstants .fontFamily, ), ), if (secondaryStoreName .isNotEmpty) ...[ const SizedBox( height: 2, ), Text( secondaryStoreName, style: TextStyle( fontSize: 16, color: Colors .blueGrey .shade700, fontFamily: FontConstants .fontFamily, fontWeight: FontWeight .w500, ), ), ], ], ), Text( "Order ID: #$orderId", style: TextStyle( fontSize: 18, color: Colors.black54, fontFamily: FontConstants .fontFamily, ), ), ], ), ), InkWell( onTap: () async { // Just launch dialer, no PiP final phone = (item['pickupcontactno'] ?? '') .toString(); final bool success = await launchPhoneDialer( phone.isNotEmpty ? phone : '9876543210', ); if (!success && context.mounted) { ScaffoldMessenger.of( context, ).showSnackBar( const SnackBar( content: Text( 'Could not launch dialer', ), ), ); } }, child: Image.asset( "assets/images/phone-call .png", height: 27, width: 27, errorBuilder: (c, e, s) => const Icon( Icons.phone, size: 27, color: Colors.green, ), ), ), SizedBox(width: 13), InkWell( onTap: () { _showmapDetailsSheet( context, item, ); }, child: Row( children: [ Image.asset( "assets/images/map.png", height: 26, width: 26, ), const SizedBox(width: 15), InkWell( onTap: () { _showProductDetailsSheet( context, ); }, child: Image.asset( "assets/images/information-point.png", height: 27, width: 27, ), ), ], ), ), ], ), ), ], ), ), // Status buttons row Row( children: [ Expanded( child: OrderStatusRow( key: ValueKey( 'status_${orderId}_$currentStatus', ), currentStatus: currentStatus, enabled: selectedCount == 1 && isSelected && !(_statusBusy[orderId] ?? false), onStatusChange: ( newStatus, { String? notes, String? proofImagePath, }) async { debugPrint( '[SINGLE] Order $orderId: $currentStatus -> $newStatus', ); // ✅ PROXIMITY CHECK FOR ARRIVED STATUS (SINGLE ORDER) // ✅ PROXIMITY CHECK DELEGATED TO CONTROLLER // We removed the local check here because the controller // already performs a robust geofence check with better error handling. if (_statusBusy[orderId] == true) { return false; } if (mounted) { setState(() { _statusBusy[orderId] = true; }); } final dc = Get.put( DeliveriesController(), permanent: true, ); final deliveryId = int.tryParse( '${item['deliveryid'] ?? 0}', ) ?? 0; final orderHeaderId = int.tryParse( '${item['orderheaderid'] ?? 0}', ) ?? 0; final pickupLocationId = int.tryParse( '${item['pickuplocationid'] ?? 0}', ) ?? 0; bool success = false; try { if (newStatus == 'ACCEPTED') { debugPrint( '[SINGLE] updateAcceptedStatus deliveryId=$deliveryId', ); success = await dc .updateAcceptedStatus( deliveryId: deliveryId, orderHeaderId: orderHeaderId, ) .timeout( const Duration( seconds: 30, ), onTimeout: () => false, ); } else if (newStatus == 'ARRIVED') { debugPrint( '[SINGLE] updateArrivedStatus deliveryId=$deliveryId', ); final pickupLat = _parseDouble( item['pickuplat'] ?? item['PickupLat'] ?? 0, ); final pickupLng = _parseDouble( item['pickuplon'] ?? item['PickupLon'] ?? 0, ); final riderLoc = await _getValidCoordinates(); success = await dc .updateArrivedStatus( deliveryId: deliveryId, orderHeaderId: orderHeaderId, pickupLat: pickupLat .toString(), pickupLng: pickupLng .toString(), ridersLat: riderLoc?.$1 ?? '0', ridersLng: riderLoc?.$2 ?? '0', ) .timeout( const Duration( seconds: 30, ), onTimeout: () => false, ); } else if (newStatus == 'PICKED') { debugPrint( '[SINGLE] updatePickedStatus deliveryId=$deliveryId', ); final pickupLat = _parseDouble( item['pickuplat'] ?? item['PickupLat'] ?? 0, ); // PROOF OF DELIVERY UPLOAD String? proofImageUrl; if (proofImagePath != null) { try { final prefs = await SharedPreferences.getInstance(); // Safely retrieve 'userid' which might be stored as int or String final rawUserId = prefs.get('userid'); final userId = int.tryParse( '${rawUserId ?? 0}', ) ?? 0; if (userId > 0) { proofImageUrl = await dc .uploadProofImage( File( proofImagePath, ), 'picked', userId, deliveryId, ); debugPrint( '[SINGLE] Picked Proof URL: $proofImageUrl'); } } catch (e) { debugPrint( 'Error uploading proof image: $e', ); } } final pickupLng = _parseDouble( item['pickuplon'] ?? item['PickupLon'] ?? 0, ); final riderLoc = await _getValidCoordinates(); final riderLatStr = riderLoc?.$1 ?? '0'; final riderLngStr = riderLoc?.$2 ?? '0'; success = await dc .updatePickedStatus( deliveryId: deliveryId, orderHeaderId: orderHeaderId, pickupLocationId: pickupLocationId, ridersLat: riderLatStr, ridersLng: riderLngStr, pickupLat: pickupLat .toStringAsFixed( 6, ), pickupLng: pickupLng .toStringAsFixed( 6, ), proofImage: proofImageUrl, ) .timeout( const Duration( seconds: 30, ), onTimeout: () => false, ); if (success) { // Signal Deliveries page to refresh immediately dc.triggerRefresh(); _startDeliveryPosting( item, ); _hiddenOrderIds.add( orderId, ); } } else if (newStatus == 'REJECTED') { debugPrint( '[SINGLE] updateRejectedStatus deliveryId=$deliveryId notes=${notes ?? ''}', ); success = await dc .updateRejectedStatus( deliveryId: deliveryId, orderHeaderId: orderHeaderId, notes: notes ?? '', ) .timeout( const Duration( seconds: 30, ), onTimeout: () => false, ); } if (success) { if (newStatus == 'REJECTED' || newStatus == 'PICKED') { if (mounted) { setState(() { _hiddenOrderIds.add(orderId); _ordersMap.remove(orderId); _orderIds.remove(orderId); _selectedOrders.remove(orderId); _statusBusy.remove(orderId); }); } await _fetchQueues(); if (mounted) { if (newStatus == 'REJECTED') { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Order rejected.')), ); } else if (newStatus == 'PICKED') { // Optional: Confirm move to next tab // ScaffoldMessenger.of(context).showSnackBar( // const SnackBar( // content: Text('Order moved to Deliveries.'), // duration: Duration(seconds: 1), // ), // ); } } } else { if (mounted) { setState(() { final order = _ordersMap[orderId]; if (order != null) { order['orderstatus'] = newStatus.toLowerCase(); } _statusBusy[orderId] = false; }); } await _fetchQueues(); if (mounted && newStatus == 'ARRIVED') { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Arrived status updated.')), ); } } } else { debugPrint( '[SINGLE] API call failed for order $orderId', ); if (mounted) { setState(() { _statusBusy[orderId] = false; }); ScaffoldMessenger.of( context, ).showSnackBar( const SnackBar( content: Text( 'Failed to update status. Please try again.', ), ), ); } } } catch (e) { debugPrint( '[SINGLE] Error updating order $orderId: $e', ); if (mounted) { setState(() { _statusBusy[orderId] = false; }); ScaffoldMessenger.of( context, ).showSnackBar( const SnackBar( content: Text( 'Error updating status.', ), ), ); } } return success; }, ), ), ], ), ], ), ); }, ), ), // Bulk action button if (selectedCount > 1) Container( width: double.infinity, padding: const EdgeInsets.all(12), child: ElevatedButton( onPressed: _hasActiveDelivery() ? null : _handleBulkAction, style: ElevatedButton.styleFrom( backgroundColor: ColorConstants.primaryColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), ), child: Text( _getBulkButtonLabel(), style: TextStyle( fontSize: FontConstants.xLarge(context), fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), ), ), ], ), // Bottom banner for active deliveries (Swiggy/Zomato style) _activeDeliveries.isNotEmpty ? Positioned( left: 0, right: 0, bottom: 0, child: ActiveDeliveryBanner( activeDeliveries: _activeDeliveries, onTap: (delivery) async { await _navigateToActiveDelivery(delivery); // Refresh after returning from map screen if (mounted) { await Future.delayed( const Duration(milliseconds: 500), ); final prefs = await SharedPreferences.getInstance(); final userId = prefs.getInt('userid'); if (userId != null) { await _fetchQueues(); } } }, ), ) : const SizedBox.shrink(), ], ), ), ), ); } void _startDeliveryPosting(Map it) async { final orderId = (it['orderid'] ?? '').toString(); if (orderId.isEmpty) return; final base = { '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': orderId, 'orderstatus': 'picked', }; _deliveryBasePayload[orderId] = base; 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', (base['orderstatus'] ?? 'picked').toString(), ); } catch (_) {} await _postDeliveryLog(orderId); final interval = await _getLogInterval(); _deliveryTimers[orderId]?.cancel(); _deliveryTimers[orderId] = Timer.periodic(interval, (_) { _postDeliveryLog(orderId); }); } Future _postDeliveryLog(String orderId) async { try { Map? base = _deliveryBasePayload[orderId]; if (base == null) { try { final prefs = await SharedPreferences.getInstance(); 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') ?? 'picked', }; } catch (_) {} } if (base == null) return; final coords = await _getValidCoordinates(); 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')}'; final payload = { ...base, 'logdate': logdate, 'latitude': (coords?.$1 ?? '0'), 'longitude': (coords?.$2 ?? '0'), }; final url = ApiConstants.mainRoute == 'live' ? ApiConstants.createDeliveryLogLive : ApiConstants.createDeliveryLogDev; debugPrint('[DELIVERYLOG][POST] URL: $url'); debugPrint('[DELIVERYLOG][POST] Body: $payload'); await _deliveryLogProvider.createDeliveryLog(url, payload); } catch (e) { debugPrint('[DELIVERYLOG][POST] error: $e'); } } @override bool get wantKeepAlive => true; } // Map details bottom sheet void _showmapDetailsSheet( BuildContext context, Map? item, ) async { // ⭐ STEP 1 — Show instant loader bottom sheet showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (_) { return SizedBox( height: 180, child: Center(child: CircularProgressIndicator()), ); }, ); // Give time to open loader smoothly await Future.delayed(const Duration(milliseconds: 10)); // ⭐ STEP 2 — Now load heavy work (your full code) 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 String pickupLatStr = (item?['pickuplat'] ?? item?['PickupLat'] ?? '') .toString(); final String pickupLngStr = (item?['pickuplon'] ?? item?['PickupLon'] ?? '') .toString(); // ignore: unused_local_variable final String dropLatStr = (item?['droplat'] ?? item?['DropLat'] ?? item?['deliverylat'] ?? '') .toString(); // ignore: unused_local_variable final String dropLngStr = (item?['droplon'] ?? item?['DropLon'] ?? item?['deliverylong'] ?? '') .toString(); final String orderId = (item?['orderid'] ?? '').toString(); final String statusStr = (item?['orderstatus'] ?? '').toString(); final String pickupAddress = (item?['Pickupaddress'] ?? item?['pickuplocation'] ?? '').toString(); // ignore: unused_local_variable final String dropAddress = (item?['deliveryaddress'] ?? item?['deliverylocation'] ?? '').toString(); double pickupLat = parseD(pickupLatStr); double pickupLng = parseD(pickupLngStr); bool hasPickup = pickupLat != 0 && pickupLng != 0; final Completer mapController = Completer(); double riderLat = 0.0; double riderLng = 0.0; bool hasRiderLocation = false; try { Position? pos; try { pos = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, timeLimit: const Duration(seconds: 8), ); } catch (e) { pos = await Geolocator.getLastKnownPosition(); } if (pos != null) { riderLat = pos.latitude; riderLng = pos.longitude; hasRiderLocation = true; } } catch (e) {} double distanceMeters = 0.0; String distanceKm = '0.00'; if (hasRiderLocation && hasPickup) { distanceMeters = Geolocator.distanceBetween( riderLat, riderLng, pickupLat, pickupLng, ); distanceKm = (distanceMeters / 1000).toStringAsFixed(2); } final bool hasAny = hasRiderLocation && hasPickup; final List polylineCoords = []; final Set polylines = {}; const String googleAPIKey = "AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q"; if (hasRiderLocation && hasPickup) { final PolylinePoints polylinePoints = PolylinePoints(apiKey: googleAPIKey); final PolylineResult result = await polylinePoints .getRouteBetweenCoordinates( request: PolylineRequest( origin: PointLatLng(riderLat, riderLng), destination: PointLatLng(pickupLat, pickupLng), mode: TravelMode.driving, ), ); if (result.points.isNotEmpty) { for (var point in result.points) { polylineCoords.add(LatLng(point.latitude, point.longitude)); } polylines.add( Polyline( polylineId: const PolylineId("route"), color: Colors.blueAccent, width: 5, points: polylineCoords, ), ); } } // ⭐ STEP 3 — Close loader Navigator.pop(context); // ⭐ STEP 4 — Open your FULL ORIGINAL bottom sheet (no UI changed) showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) { return Container( margin: const EdgeInsets.only(top: 50), decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.vertical(top: Radius.circular(25)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), blurRadius: 15, offset: const Offset(0, -5), ), ], ), child: Padding( padding: EdgeInsets.only( left: 20, right: 20, top: 10, bottom: MediaQuery.of(context).viewInsets.bottom + 20, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 50, height: 5, margin: const EdgeInsets.only(bottom: 15), decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(20), ), ), // ⭐ ALL YOUR ORIGINAL UI BELOW (unchanged) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Trip Map", style: TextStyle( fontSize: FontConstants.xxLarge(context), fontWeight: FontWeight.bold, color: Colors.grey.shade900, fontFamily: FontConstants.fontFamily, ), ), const SizedBox(height: 4), Text( orderId.isNotEmpty ? 'Order: #$orderId' : 'Order: -', style: TextStyle(color: Colors.grey.shade700), ), ], ), ), InkWell( onTap: () => Navigator.of(context).pop(), child: const Icon( Icons.cancel_rounded, size: 36, color: Colors.red, ), ), ], ), const SizedBox(height: 5), if (hasAny) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade200), ), child: Row( children: [ Transform.translate( offset: Offset(0, -18), child: const Icon( Icons.location_on, color: Colors.red, size: 22, ), ), const SizedBox(width: 8), Expanded( child: Text( hasPickup ? "Pickup: ${pickupAddress.isNotEmpty ? pickupAddress : 'Your current location'}" : 'Pickup: Not available', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, fontFamily: FontConstants.fontFamily, ), ), ), ], ), ), const SizedBox(height: 5), if (distanceKm != '0.00') Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.blue.shade200), ), child: Row( children: [ const Icon( Icons.navigation, color: Colors.blue, size: 22, ), const SizedBox(width: 8), Expanded( child: Text( "Distance to Pickup: $distanceKm km", style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, fontFamily: FontConstants.fontFamily, color: Colors.blue.shade700, ), ), ), ], ), ), if (distanceKm != '0.00') const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular(15), child: Container( height: 280, decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade200), ), child: GoogleMap( initialCameraPosition: CameraPosition( target: hasRiderLocation && hasPickup ? LatLng( (riderLat + pickupLat) / 2, (riderLng + pickupLng) / 2, ) : hasPickup ? LatLng(pickupLat, pickupLng) : hasRiderLocation ? LatLng(riderLat, riderLng) : const LatLng(0, 0), zoom: hasRiderLocation && hasPickup ? 13 : 15, ), markers: { if (hasRiderLocation) Marker( markerId: const MarkerId("rider"), position: LatLng(riderLat, riderLng), infoWindow: const InfoWindow( title: "Your Location", ), icon: BitmapDescriptor.defaultMarkerWithHue( BitmapDescriptor.hueAzure, ), ), if (hasPickup) Marker( markerId: const MarkerId("pickup"), position: LatLng(pickupLat, pickupLng), infoWindow: const InfoWindow( title: "Pickup Location", ), icon: BitmapDescriptor.defaultMarkerWithHue( BitmapDescriptor.hueRed, ), ), }, polylines: polylines, zoomGesturesEnabled: true, scrollGesturesEnabled: true, tiltGesturesEnabled: true, rotateGesturesEnabled: true, zoomControlsEnabled: false, myLocationButtonEnabled: false, onMapCreated: (GoogleMapController controller) async { mapController.complete(controller); if (hasRiderLocation && hasPickup) { final bounds = LatLngBounds( southwest: LatLng( riderLat < pickupLat ? riderLat : pickupLat, riderLng < pickupLng ? riderLng : pickupLng, ), northeast: LatLng( riderLat > pickupLat ? riderLat : pickupLat, riderLng > pickupLng ? riderLng : pickupLng, ), ); await controller.animateCamera( CameraUpdate.newLatLngBounds(bounds, 80), ); } }, ), ), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(15), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(15), border: Border.all(color: Colors.grey.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Order Details", style: TextStyle( fontSize: 17, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, color: Colors.black87, ), ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Order ID:", style: TextStyle( color: Colors.grey.shade700, fontWeight: FontWeight.w500, ), ), Text( orderId.isNotEmpty ? "#$orderId" : '-', style: const TextStyle( color: Colors.black87, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Status:", style: TextStyle( color: Colors.grey.shade700, fontWeight: FontWeight.w500, ), ), Text( statusStr.isNotEmpty ? statusStr : '-', style: TextStyle( color: Colors.orange.shade700, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Distance:", style: TextStyle( color: Colors.grey.shade700, fontWeight: FontWeight.w500, ), ), Text( "$distanceKm km", style: const TextStyle( color: Colors.black87, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Quantity:", style: TextStyle( color: Colors.grey.shade700, fontWeight: FontWeight.w500, ), ), Text( "${item?['Quantity'] ?? item?['quantity'] ?? '-'}", style: const TextStyle( color: Colors.black87, fontWeight: FontWeight.w600, ), ), ], ), ], ), ), ], ), ], ), ), ); }, ); } // Product details bottom sheet void _showProductDetailsSheet(BuildContext context) { showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (context) { return Padding( padding: EdgeInsets.only( left: 20, right: 20, top: 20, bottom: MediaQuery.of(context).viewInsets.bottom + 20, ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Product Details", style: TextStyle( fontSize: FontConstants.xxxLarge(context), fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), IconButton( icon: const Icon(Icons.cancel, color: Colors.red, size: 32), onPressed: () => Navigator.pop(context), ), ], ), const SizedBox(height: 10), Text( "No product details yet", style: TextStyle( fontSize: 18, color: Colors.grey, fontFamily: FontConstants.fontFamily, ), ), ], ), ), ); }, ); }