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> deliveries; final Map preservedStepNumbers; const MultiCustomerMapScreen({ super.key, required this.deliveries, this.preservedStepNumbers = const {}, }); @override State createState() => _MultiCustomerMapScreenState(); } class _MultiCustomerMapScreenState extends State { GoogleMapController? mapController; Set markers = {}; Set polylines = {}; LatLng? currentLocation; bool _isLoading = true; bool _mapReady = false; final PolylinePoints _polylinePoints = PolylinePoints( apiKey: 'AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q', ); final Map> _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 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 order) { final deliveryId = (order['deliveryid'] ?? '').toString(); final orderId = (order['orderid'] ?? '').toString(); return deliveryId.isNotEmpty ? 'delivery_$deliveryId' : 'order_$orderId'; } int _getPreservedOrCurrentStep(Map order) { final orderKey = _getOrderKey(order); if (widget.preservedStepNumbers.containsKey(orderKey)) { return widget.preservedStepNumbers[orderKey]!; } final currentStep = _getStepNumber(order); return currentStep; } Future _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 _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 _createDeliveryMarkers() async { Set 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 customerName = (delivery['deliverycustomer'] ?? 'Customer ${i + 1}') .toString(); 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 _createCircularMarkerBitmap( displayStep, isSkipped: isSkipped, isNext: isNext, ); tempMarkers.add( Marker( markerId: MarkerId('delivery_$orderId'), position: LatLng(lat, lon), icon: icon, infoWindow: InfoWindow( title: isSkipped ? 'Step $displayStep: $customerName (SKIPPED)' : 'Step $displayStep: $customerName', snippet: 'Order #$orderId', ), onTap: () { _showCustomerDetailsSheet(delivery, displayStep); }, ), ); stepPoints.add(_StepPoint(displayStep, LatLng(lat, lon))); } if (currentLocation != null) { tempMarkers.add( Marker( markerId: const MarkerId('current_location'), position: currentLocation!, icon: BitmapDescriptor.defaultMarkerWithHue( BitmapDescriptor.hueAzure, ), infoWindow: const InfoWindow(title: 'You are here'), ), ); stepPoints.insert(0, _StepPoint(0, currentLocation!)); } stepPoints.sort((a, b) => a.step.compareTo(b.step)); Set newPolylines = {}; int segmentIndex = 0; for (int i = 0; i < stepPoints.length - 1; i++) { final start = stepPoints[i].position; final end = stepPoints[i + 1].position; try { // ignore: deprecated_member_use final request = PolylineRequest( origin: PointLatLng(start.latitude, start.longitude), destination: PointLatLng(end.latitude, end.longitude), mode: TravelMode.driving, ); final result = await _polylinePoints.getRouteBetweenCoordinates( request: request, ); if (result.points.isNotEmpty) { final routePoints = result.points .map((p) => LatLng(p.latitude, p.longitude)) .toList(); newPolylines.add( Polyline( polylineId: PolylineId('segment_$segmentIndex'), points: routePoints, width: 6, color: Colors.blue, startCap: Cap.roundCap, endCap: Cap.roundCap, jointType: JointType.round, geodesic: true, ), ); segmentIndex++; } else { newPolylines.add( Polyline( polylineId: PolylineId('segment_fallback_$segmentIndex'), points: [start, end], width: 4, color: Colors.blue.shade200, ), ); segmentIndex++; } } catch (e) { debugPrint('[CUSTOMER_MAP] Directions error for segment $i: $e'); newPolylines.add( Polyline( polylineId: PolylineId('segment_error_$segmentIndex'), points: [start, end], width: 4, color: Colors.blue.shade200, ), ); segmentIndex++; } } if (mounted) { setState(() { markers = tempMarkers; polylines = newPolylines; }); } await Future.delayed(const Duration(milliseconds: 200)); _fitBoundsToMarkersAndPolylines(); } Future _fitBoundsToMarkersAndPolylines() async { if (mapController == null) return; double minLat = double.infinity; double maxLat = -double.infinity; double minLng = double.infinity; double maxLng = -double.infinity; bool hasPoint = false; for (final m in markers) { final pos = m.position; 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 poly in polylines) { for (final pos in poly.points) { 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; } } if (!hasPoint) return; final bounds = LatLngBounds( southwest: LatLng(minLat, minLng), northeast: LatLng(maxLat, maxLng), ); WidgetsBinding.instance.addPostFrameCallback((_) async { try { await mapController!.animateCamera( CameraUpdate.newLatLngBounds(bounds, 80), ); } catch (e) { Future.delayed(const Duration(milliseconds: 300), () async { try { await mapController!.animateCamera( CameraUpdate.newLatLngBounds(bounds, 80), ); } catch (e) { debugPrint('[CUSTOMER_MAP] Retry failed: $e'); } }); } }); } Future _createCircularMarkerBitmap( int number, { bool isSkipped = false, bool isNext = false, }) async { const double size = 70; final pictureRecorder = ui.PictureRecorder(); final canvas = Canvas(pictureRecorder); final center = Offset(size / 2, size / 2); final paint = Paint() ..color = isSkipped ? Colors.orange : ColorConstants.primaryColor; canvas.drawCircle(center, 15, paint); final border = Paint() ..color = isSkipped ? Colors.orange.shade900 : Colors.white ..style = PaintingStyle.stroke ..strokeWidth = isSkipped ? 4 : 3; canvas.drawCircle(center, 15, border); final textPainter = TextPainter( text: TextSpan( text: number.toString(), style: const TextStyle( fontSize: 19, color: Colors.white, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, ); textPainter.layout(); textPainter.paint( canvas, Offset( center.dx - textPainter.width / 2, center.dy - textPainter.height / 2, ), ); // Draw "NEXT" indicator (road sign with down arrow) on top if (isNext) { // Draw road sign background (rectangle) final signPaint = Paint() ..color = Colors.green ..style = PaintingStyle.fill; final signRect = RRect.fromRectAndRadius( Rect.fromCenter(center: Offset(size / 2, 8), width: 45, height: 30), const Radius.circular(4), ); canvas.drawRRect(signRect, signPaint); // Draw border final signBorder = Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = 1.5; canvas.drawRRect(signRect, signBorder); // Draw "NEXT" text final nextTextPainter = TextPainter( text: const TextSpan( text: 'NEXT', style: TextStyle( fontSize: 12, color: Colors.white, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, ); nextTextPainter.layout(); nextTextPainter.paint( canvas, Offset(size / 2 - nextTextPainter.width / 2, 4), ); // Draw down arrow final arrowPath = Path(); arrowPath.moveTo(size / 2, 18); arrowPath.lineTo(size / 2 - 4, 24); arrowPath.lineTo(size / 2 + 4, 24); arrowPath.close(); final arrowPaint = Paint() ..color = Colors.white ..style = PaintingStyle.fill; canvas.drawPath(arrowPath, arrowPaint); } final img = await pictureRecorder.endRecording().toImage( size.toInt(), size.toInt(), ); final data = await img.toByteData(format: ui.ImageByteFormat.png); return BitmapDescriptor.bytes(data!.buffer.asUint8List()); } void _showCustomerDetailsSheet( Map 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()) : GoogleMap( initialCameraPosition: initialCameraPosition, markers: markers, polylines: polylines, myLocationEnabled: true, myLocationButtonEnabled: true, onMapCreated: (controller) { mapController = controller; _mapReady = true; WidgetsBinding.instance.addPostFrameCallback((_) { Future.delayed(const Duration(milliseconds: 150), () { if (_mapReady) _fitBoundsToMarkersAndPolylines(); }); }); }, ), ); } }