Files
Xpress-rider/lib/views/Dashboard/deliveries/multi_map.dart
2026-08-12 10:50:45 +05:30

744 lines
25 KiB
Dart

part of 'deliveries.dart';
class _StepPoint {
final int step;
final LatLng position;
_StepPoint(this.step, this.position);
}
// -------------------------------------------------------------------------
// MULTI CUSTOMER MAP (Shows all deliveries with real step numbers)
// -------------------------------------------------------------------------
class MultiCustomerMapScreen extends StatefulWidget {
final List<Map<String, dynamic>> deliveries;
final Map<String, int> preservedStepNumbers;
const MultiCustomerMapScreen({
super.key,
required this.deliveries,
this.preservedStepNumbers = const {},
});
@override
State<MultiCustomerMapScreen> createState() => _MultiCustomerMapScreenState();
}
class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
NearleMapController? mapController;
List<NearleMapMarker> markers = const [];
List<NearleMapPolyline> polylines = const [];
LatLng? currentLocation;
bool _isLoading = true;
final Map<String, Map<String, dynamic>> _deliveryMap = {};
@override
void initState() {
super.initState();
if (widget.deliveries.isNotEmpty) {
final firstDelivery = widget.deliveries.first;
final dropLat = _parseD(
firstDelivery['droplat'] ?? firstDelivery['deliverylat'] ?? 0,
);
final dropLon = _parseD(
firstDelivery['droplon'] ?? firstDelivery['deliverylong'] ?? 0,
);
if (dropLat != 0 && dropLon != 0) {
currentLocation = LatLng(dropLat, dropLon);
} else {
currentLocation = const LatLng(11.018356, 77.012596);
}
} else {
currentLocation = const LatLng(11.018356, 77.012596);
}
_loadMapData();
}
double _parseD(dynamic v) {
if (v == null) return 0.0;
if (v is num) return v.toDouble();
return double.tryParse(v.toString()) ?? 0.0;
}
int _getStepNumber(Map<String, dynamic> order) {
final dynamic raw = order['step'] ?? order['Step'];
final int step = raw == null
? 0
: (raw is num ? raw.toInt() : int.tryParse(raw.toString()) ?? 0);
return step;
}
String _getOrderKey(Map<String, dynamic> order) {
final deliveryId = (order['deliveryid'] ?? '').toString();
final orderId = (order['orderid'] ?? '').toString();
return deliveryId.isNotEmpty ? 'delivery_$deliveryId' : 'order_$orderId';
}
int _getPreservedOrCurrentStep(Map<String, dynamic> order) {
final orderKey = _getOrderKey(order);
if (widget.preservedStepNumbers.containsKey(orderKey)) {
return widget.preservedStepNumbers[orderKey]!;
}
final currentStep = _getStepNumber(order);
return currentStep;
}
Future<void> _loadMapData() async {
try {
if (mounted) {
setState(() {
_isLoading = false;
});
}
_getCurrentLocation().then((_) {
if (mounted) {
_createDeliveryMarkers();
}
});
} catch (e) {
debugPrint('[CUSTOMER_MAP] Error loading map data: $e');
if (mounted) {
setState(() => _isLoading = false);
}
}
}
Future<void> _getCurrentLocation() async {
try {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
final lastPos = await Geolocator.getLastKnownPosition();
if (lastPos != null && mounted) {
setState(() {
currentLocation = LatLng(lastPos.latitude, lastPos.longitude);
});
}
return;
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
final lastPos = await Geolocator.getLastKnownPosition();
if (lastPos != null && mounted) {
setState(() {
currentLocation = LatLng(lastPos.latitude, lastPos.longitude);
});
}
return;
}
}
Position? position;
try {
position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.medium,
distanceFilter: 0,
timeLimit: Duration(seconds: 5),
),
);
} catch (e) {
debugPrint(
'[CUSTOMER_MAP] Timeout getting location, using last known: $e',
);
position = await Geolocator.getLastKnownPosition();
}
final safePosition = position;
if (safePosition != null && mounted) {
setState(() {
currentLocation = LatLng(
safePosition.latitude,
safePosition.longitude,
);
});
}
} catch (e) {
debugPrint('[CUSTOMER_MAP] Error getting location: $e');
try {
final lastPos = await Geolocator.getLastKnownPosition();
if (lastPos != null && mounted) {
setState(() {
currentLocation = LatLng(lastPos.latitude, lastPos.longitude);
});
}
} catch (_) {}
}
}
int? _getNextDeliveryStep() {
// Find the first non-skipped delivery (next delivery from user location)
for (int i = 0; i < widget.deliveries.length; i++) {
final delivery = widget.deliveries[i];
final status = (delivery['orderstatus']?.toString().toLowerCase() ?? '')
.trim();
if (status != 'skipped') {
final stepNumber = _getPreservedOrCurrentStep(delivery);
if (stepNumber > 0) {
return stepNumber;
} else {
// Calculate display step for orders without step
final ordersWithStepBefore = widget.deliveries
.sublist(0, i)
.where((o) => _getPreservedOrCurrentStep(o) > 0)
.length;
final totalOrdersWithStep = widget.deliveries
.where((o) => _getPreservedOrCurrentStep(o) > 0)
.length;
return totalOrdersWithStep + (i - ordersWithStepBefore) + 1;
}
}
}
return null;
}
Future<void> _createDeliveryMarkers() async {
final List<NearleMapMarker> tempMarkers = [];
List<_StepPoint> stepPoints = [];
_deliveryMap.clear();
final nextStep = _getNextDeliveryStep();
for (int i = 0; i < widget.deliveries.length; i++) {
final delivery = widget.deliveries[i];
final stepNumber = _getPreservedOrCurrentStep(delivery);
int displayStep;
if (stepNumber > 0) {
displayStep = stepNumber;
} else {
final ordersWithStepBefore = widget.deliveries
.sublist(0, i)
.where((o) => _getPreservedOrCurrentStep(o) > 0)
.length;
final totalOrdersWithStep = widget.deliveries
.where((o) => _getPreservedOrCurrentStep(o) > 0)
.length;
displayStep = totalOrdersWithStep + (i - ordersWithStepBefore) + 1;
}
final double lat = _parseD(
delivery['droplat'] ?? delivery['deliverylat'],
);
final double lon = _parseD(
delivery['droplon'] ?? delivery['deliverylong'],
);
if (lat == 0 || lon == 0) continue;
final orderId = (delivery['orderid'] ?? '').toString();
final status = (delivery['orderstatus']?.toString().toLowerCase() ?? '')
.trim();
final isSkipped = status == 'skipped';
final isNext = displayStep == nextStep;
// Store delivery data for dialog
_deliveryMap['delivery_$orderId'] = delivery;
final icon = await MapMarkerIcons.numberedCircle(
displayStep,
primaryColor: ColorConstants.primaryColor,
isSkipped: isSkipped,
isNext: isNext,
);
tempMarkers.add(
NearleMapMarker(
id: 'delivery_$orderId',
position: LatLng(lat, lon),
icon: icon,
anchor: MarkerAnchor.center,
onTap: () {
_showCustomerDetailsSheet(delivery, displayStep);
},
),
);
stepPoints.add(_StepPoint(displayStep, LatLng(lat, lon)));
}
if (currentLocation != null) {
tempMarkers.add(
NearleMapMarker(
id: 'current_location',
position: currentLocation!,
icon: await MapMarkerIcons.dot(Colors.blueAccent),
anchor: MarkerAnchor.center,
),
);
stepPoints.insert(0, _StepPoint(0, currentLocation!));
}
stepPoints.sort((a, b) => a.step.compareTo(b.step));
final newPolylines = await _buildRoutePolylines(stepPoints);
if (mounted) {
setState(() {
markers = tempMarkers;
polylines = newPolylines;
});
}
await Future.delayed(const Duration(milliseconds: 200));
_fitBoundsToMarkersAndPolylines();
}
/// Builds the route through every stop in one Directions call.
///
/// This used to issue one request per consecutive pair of stops, so opening
/// this screen with N stops cost N billable requests. OSRM takes every stop
/// as a coordinate in one request, so the whole route is a single call.
Future<List<NearleMapPolyline>> _buildRoutePolylines(
List<_StepPoint> stepPoints,
) async {
if (stepPoints.length < 2) return const [];
final route = await RoutingService.getRoute(
origin: stepPoints.first.position,
destination: stepPoints.last.position,
waypoints: stepPoints
.sublist(1, stepPoints.length - 1)
.map((s) => s.position)
.toList(),
);
if (route != null && route.points.isNotEmpty) {
return [
NearleMapPolyline(
id: 'route',
points: route.points,
width: 6,
color: Colors.blue,
),
];
}
debugPrint('[CUSTOMER_MAP] Routing unavailable, drawing direct segments');
return [
NearleMapPolyline(
id: 'route_fallback',
points: stepPoints.map((s) => s.position).toList(),
width: 4,
color: Colors.blue.shade200,
),
];
}
Future<void> _fitBoundsToMarkersAndPolylines() async {
final controller = mapController;
if (controller == null) return;
double minLat = double.infinity;
double maxLat = -double.infinity;
double minLng = double.infinity;
double maxLng = -double.infinity;
bool hasPoint = false;
void include(LatLng pos) {
minLat = math.min(minLat, pos.latitude);
maxLat = math.max(maxLat, pos.latitude);
minLng = math.min(minLng, pos.longitude);
maxLng = math.max(maxLng, pos.longitude);
hasPoint = true;
}
for (final m in markers) {
include(m.position);
}
for (final poly in polylines) {
for (final pos in poly.points) {
include(pos);
}
}
if (!hasPoint) return;
await controller.fitBounds(
LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
),
padding: 80,
);
}
void _showCustomerDetailsSheet(
Map<String, dynamic> delivery,
int stepNumber,
) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: true,
enableDrag: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (BuildContext context) {
final customerName = (delivery['deliverycustomer'] ?? 'Customer')
.toString();
final address = (delivery['deliveryaddress'] ?? 'Address not available')
.toString();
final phone = (delivery['deliverycontactno'] ?? '').toString();
final orderId = (delivery['orderid'] ?? '').toString();
final status = (delivery['orderstatus']?.toString().toLowerCase() ?? '')
.trim();
final isSkipped = status == 'skipped';
return SafeArea(
top: false,
left: false,
right: false,
bottom: true,
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Container(
padding: const EdgeInsets.all(24),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with title and close icon
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Customer Details',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
color: ColorConstants.primaryColor,
),
),
),
InkWell(
onTap: () => Navigator.of(context).pop(),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
),
child: const Icon(
Icons.cancel,
size: 34,
color: Colors.red,
),
),
),
],
),
const Divider(thickness: 1.5),
// Step number and status
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: isSkipped
? Colors.orange
: ColorConstants.primaryColor,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Step $stepNumber',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
if (isSkipped) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'SKIPPED',
style: TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
],
),
const SizedBox(height: 16),
// Customer name
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.person, size: 20, color: Colors.grey),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Customer Name',
style: TextStyle(
fontSize: 16,
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
customerName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.grey,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Delivery address
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icons.location_on,
size: 20,
color: Colors.grey,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Delivery Address',
style: TextStyle(
fontSize: 16,
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
address,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.grey,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Phone number
if (phone.isNotEmpty)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.phone, size: 20, color: Colors.grey),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Contact Number',
style: TextStyle(
fontSize: 16,
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
phone,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.grey,
),
),
],
),
),
],
),
const SizedBox(height: 20),
// Order ID
Text(
'Order ID: $orderId',
style: const TextStyle(
fontSize: 16,
color: Colors.black,
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 20),
// Call button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () async {
// Close bottom sheet first
Navigator.of(context).pop();
// Small delay to ensure bottom sheet is closed
await Future.delayed(
const Duration(milliseconds: 300),
);
// Then make the call
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'),
),
);
}
},
icon: const Icon(
Icons.phone,
color: Colors.white,
size: 20,
),
label: const Text(
'Call',
style: TextStyle(
color: Colors.white,
fontSize: 21,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(height: 8),
],
),
),
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
LatLng initialTarget;
double initialZoom = 13;
if (currentLocation != null) {
initialTarget = currentLocation!;
} else if (widget.deliveries.isNotEmpty) {
final firstDelivery = widget.deliveries.first;
final lat = _parseD(
firstDelivery['droplat'] ?? firstDelivery['deliverylat'],
);
final lon = _parseD(
firstDelivery['droplon'] ?? firstDelivery['deliverylong'],
);
initialTarget = (lat != 0 && lon != 0)
? LatLng(lat, lon)
: const LatLng(11.0168, 76.9558);
} else {
initialTarget = const LatLng(11.0168, 76.9558);
}
final initialCameraPosition = CameraPosition(
target: initialTarget,
zoom: initialZoom,
);
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(70),
child: SafeArea(
bottom: false,
child: AppBar(
automaticallyImplyLeading: false,
backgroundColor: ColorConstants.primaryColor,
elevation: 0,
toolbarHeight: 80,
leadingWidth: double.infinity,
leading: Row(
children: [
IconButton(
icon: const Icon(
Icons.arrow_back_ios,
color: Colors.white,
size: 26,
),
onPressed: () => Navigator.pop(context),
),
const Text(
'Delivery Route',
style: TextStyle(
fontSize: 26,
color: Colors.white,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
],
),
centerTitle: false,
),
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: NearleMap(
initialCameraPosition: initialCameraPosition,
markers: markers,
polylines: polylines,
myLocationEnabled: true,
onMapReady: (controller) {
mapController = controller;
_fitBoundsToMarkersAndPolylines();
},
),
);
}
}