3086 lines
131 KiB
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|