Files
Xpress-rider/lib/views/Dashboard/home/homepage.dart

3086 lines
131 KiB
Dart

// 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<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 Homepage extends StatefulWidget {
const Homepage({super.key});
@override
State<Homepage> createState() => _HomepageState();
}
class _HomepageState extends State<Homepage>
with AutomaticKeepAliveClientMixin {
bool isOnline = true;
String _userName = '';
String _shiftStart = '';
String _shiftEnd = '';
final DeliveryProvider _delivery = DeliveryProvider();
final DeliveryController _deliveryController = Get.find<DeliveryController>();
// Core data - using Map with orderId as key for stable tracking
Map<String, Map<String, dynamic>> _ordersMap = {};
List<String> _orderIds = []; // Maintain order
StreamSubscription<void>? _pollerSubscription;
Duration _currentPollInterval = const Duration(seconds: 0);
bool _isToggling = false;
// Selection tracking
Map<String, bool> _selectedOrders = {};
bool isAllSelected = false;
String _lastQueuesJson = '';
bool _loadedOnlineFromPrefs = false;
bool _isFetchingQueues = false;
final Map<String, bool> _deliveryRunning = <String, bool>{};
final Map<String, Timer> _deliveryTimers = <String, Timer>{};
final Map<String, Map<String, dynamic>> _deliveryBasePayload =
<String, Map<String, dynamic>>{};
final CreateDeliveryLogProvider _deliveryLogProvider =
CreateDeliveryLogProvider();
// Prevent optimistic status flips; disable row while updating
final Map<String, bool> _statusBusy = <String, bool>{};
Timer? _locationTimer;
final Set<String> _hiddenOrderIds = <String>{};
// Active delivery tracking
List<Map<String, dynamic>> _activeDeliveries = <Map<String, dynamic>>[];
@override
void dispose() {
// Stop any active delivery posting timers
final keys = List<String>.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<String, dynamic> 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<bool> _isNearPickupLocation(Map<String, dynamic> 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<Map<String, bool>> _checkProximityForOrders(
List<String> orderIds,
) async {
final Map<String, bool> 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<void> _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<Duration> _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<ProfileController>()) {
final pc = Get.find<ProfileController>();
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<void> _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<Placemark> 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<void> _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<void> _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<void> _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<void> _loadName() async {
String name = '';
try {
if (Get.isRegistered<ProfileController>()) {
final pc = Get.find<ProfileController>();
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<LogController>()) {
Get.find<LogController>().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<bool> _handleOnlineToggle(bool online) async {
try {
if (!Get.isRegistered<RiderLogController>()) return false;
final ctl = Get.find<RiderLogController>();
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<void> _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<Map<String, dynamic>>().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<String, Map<String, dynamic>> newOrdersMap = {};
final List<String> newOrderIds = [];
for (final order in validOrders) {
final orderId = (order['orderid'] ?? '').toString();
if (orderId.isEmpty) continue;
newOrdersMap[orderId] = order;
newOrderIds.add(orderId);
}
final Map<String, bool> 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<void> _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<dynamic> 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<String, dynamic>
? (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<Map<String, dynamic>>().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<void> _navigateToActiveDelivery(Map<String, dynamic> 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<void> _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<String, bool>.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<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'),
),
],
),
);
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<String, dynamic> it) async {
final orderId = (it['orderid'] ?? '').toString();
if (orderId.isEmpty) return;
final base = <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': 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<void> _postDeliveryLog(String orderId) async {
try {
Map<String, dynamic>? 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<String, dynamic>? 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<GoogleMapController> 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<LatLng> polylineCoords = [];
final Set<Polyline> 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,
),
),
],
),
),
);
},
);
}