import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:flutter_polyline_points/flutter_polyline_points.dart'; import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; import 'package:supplier_new/common/settings.dart'; class DeliveryUpdatesPage extends StatefulWidget { final String orderId; final String initialStatus; const DeliveryUpdatesPage({ super.key, required this.orderId, required this.initialStatus, }); @override State createState() => _DeliveryUpdatesPageState(); } class _DeliveryUpdatesPageState extends State { GoogleMapController? _mapController; Timer? _timer; final PolylinePoints _polylinePoints = PolylinePoints(); /// ---------------- CHANGE THIS ---------------- /// Put your Google Maps API key here /// Need Directions API + Distance Matrix API enabled static const String googleApiKey = 'AIzaSyDJpK9RVhlBejtJu9xSGfneuTN6HOfJgSM'; /// -------------------------------------------- bool isLoading = true; bool mapReady = false; String currentStatus = ''; String etaText = '--'; String distanceText = '--'; String durationText = '--'; String driverName = ''; String driverPhone = ''; String tankerName = ''; String orderAddress = ''; double? driverLat; double? driverLng; double? destinationLat; double? destinationLng; Set markers = {}; Set polylines = {}; List routePoints = []; @override void initState() { super.initState(); currentStatus = _beautifyStatus(widget.initialStatus); _loadTracking(showLoader: true); _timer = Timer.periodic(const Duration(seconds: 5), (_) { _loadTracking(showLoader: false); }); } @override void dispose() { _timer?.cancel(); _mapController?.dispose(); super.dispose(); } Future _loadTracking({bool showLoader = false}) async { if (showLoader && mounted) { setState(() { isLoading = true; }); } try { /// =========================== /// IMPORTANT: /// Change this endpoint path to your real backend route /// Example: /// ${AppSettings.host}trackOrder/${widget.orderId} /// ${AppSettings.host}getOrderTracking/${widget.orderId} /// =========================== final uri = Uri.parse( '${AppSettings.host}trackOrder/${widget.orderId}', ); final response = await http.get(uri); if (response.statusCode < 200 || response.statusCode >= 300) { throw Exception('Tracking API failed: ${response.statusCode}'); } final decoded = jsonDecode(response.body); /// Supports either: /// { status:true, data:{...} } /// OR direct flat response final data = decoded is Map && decoded['data'] is Map ? decoded['data'] as Map : decoded as Map; driverLat = _toDouble( data['driverLat'] ?? data['driver_lat'] ?? data['deliveryBoyLat'] ?? data['deliveryboyLat'] ?? data['lat'], ); driverLng = _toDouble( data['driverLng'] ?? data['driver_lng'] ?? data['deliveryBoyLng'] ?? data['deliveryboyLng'] ?? data['lng'] ?? data['long'], ); destinationLat = _toDouble( data['destinationLat'] ?? data['destination_lat'] ?? data['orderLat'] ?? data['customerLat'] ?? data['dropLat'], ); destinationLng = _toDouble( data['destinationLng'] ?? data['destination_lng'] ?? data['orderLng'] ?? data['customerLng'] ?? data['dropLng'], ); driverName = (data['driverName'] ?? data['delivery_agent_name'] ?? data['deliveryBoyName'] ?? '') .toString(); driverPhone = (data['driverPhone'] ?? data['delivery_agent_mobile'] ?? data['deliveryBoyPhone'] ?? '') .toString(); tankerName = (data['tankerName'] ?? data['tanker_name'] ?? '').toString(); orderAddress = (data['destinationAddress'] ?? data['displayAddress'] ?? data['address'] ?? data['customerAddress'] ?? '') .toString(); final apiStatus = (data['status'] ?? data['orderStatus'] ?? widget.initialStatus) .toString(); currentStatus = _beautifyStatus(apiStatus); final apiEta = (data['eta'] ?? data['etaText'] ?? data['estimatedTime'] ?? '') .toString() .trim(); final apiDistance = (data['distance'] ?? data['distanceText'] ?? '').toString().trim(); if (apiEta.isNotEmpty) etaText = apiEta; if (apiDistance.isNotEmpty) distanceText = apiDistance; if (driverLat != null && driverLng != null && destinationLat != null && destinationLng != null) { await _buildMarkers(); await _fetchGoogleRouteAndEta(); await _moveCameraToBounds(); } if (mounted) { setState(() { isLoading = false; }); } } catch (e) { debugPrint('Tracking error: $e'); if (mounted) { setState(() { isLoading = false; }); } } } Future _buildMarkers() async { if (driverLat == null || driverLng == null || destinationLat == null || destinationLng == null) { return; } markers = { Marker( markerId: const MarkerId('driver'), position: LatLng(driverLat!, driverLng!), infoWindow: InfoWindow( title: driverName.isNotEmpty ? driverName : 'Driver', snippet: tankerName.isNotEmpty ? tankerName : 'Live location', ), icon: BitmapDescriptor.defaultMarkerWithHue( BitmapDescriptor.hueAzure, ), ), Marker( markerId: const MarkerId('destination'), position: LatLng(destinationLat!, destinationLng!), infoWindow: InfoWindow( title: 'Destination', snippet: orderAddress.isNotEmpty ? orderAddress : 'Order location', ), icon: BitmapDescriptor.defaultMarkerWithHue( BitmapDescriptor.hueRed, ), ), }; } Future _fetchGoogleRouteAndEta() async { if (driverLat == null || driverLng == null || destinationLat == null || destinationLng == null) { return; } try { final origin = '${driverLat!},${driverLng!}'; final destination = '${destinationLat!},${destinationLng!}'; final directionsUrl = Uri.parse( 'https://maps.googleapis.com/maps/api/directions/json' '?origin=$origin' '&destination=$destination' '&departure_time=now' '&traffic_model=best_guess' '&mode=driving' '&key=$googleApiKey', ); final directionsResponse = await http.get(directionsUrl); final directionsData = jsonDecode(directionsResponse.body); if (directionsData['routes'] != null && (directionsData['routes'] as List).isNotEmpty) { final route = directionsData['routes'][0]; final overviewPolyline = route['overview_polyline']?['points']?.toString() ?? ''; final legs = route['legs'] as List?; if (legs != null && legs.isNotEmpty) { final leg = legs[0]; final distanceObj = leg['distance']; final durationObj = leg['duration']; final durationTrafficObj = leg['duration_in_traffic']; if (distanceObj != null && (distanceText == '--' || distanceText.isEmpty)) { distanceText = (distanceObj['text'] ?? '--').toString(); } if (durationObj != null) { durationText = (durationObj['text'] ?? '--').toString(); } if (durationTrafficObj != null) { etaText = (durationTrafficObj['text'] ?? '--').toString(); } else if (durationObj != null && (etaText == '--' || etaText.isEmpty)) { etaText = (durationObj['text'] ?? '--').toString(); } } final result = _polylinePoints.decodePolyline(overviewPolyline); routePoints = result .map((p) => LatLng(p.latitude, p.longitude)) .toList(); polylines = { Polyline( polylineId: const PolylineId('delivery_route'), points: routePoints, width: 5, color: const Color(0XFF8270DB), ), }; } else { _setStraightLinePolyline(); _setApproxEtaIfNeeded(); } } catch (e) { debugPrint('Directions error: $e'); _setStraightLinePolyline(); _setApproxEtaIfNeeded(); } } void _setStraightLinePolyline() { if (driverLat == null || driverLng == null || destinationLat == null || destinationLng == null) { return; } polylines = { Polyline( polylineId: const PolylineId('delivery_route_fallback'), points: [ LatLng(driverLat!, driverLng!), LatLng(destinationLat!, destinationLng!), ], width: 5, color: const Color(0XFF8270DB), ), }; } void _setApproxEtaIfNeeded() { if (driverLat == null || driverLng == null || destinationLat == null || destinationLng == null) { return; } final km = _calculateDistanceKm( driverLat!, driverLng!, destinationLat!, destinationLng!, ); if (distanceText == '--' || distanceText.isEmpty) { distanceText = '${km.toStringAsFixed(1)} km'; } /// Rough city driving estimate at 25 km/h final mins = ((km / 25) * 60).ceil().clamp(1, 999); if (etaText == '--' || etaText.isEmpty) { etaText = '$mins mins'; } if (durationText == '--' || durationText.isEmpty) { durationText = '$mins mins'; } } Future _moveCameraToBounds() async { if (!mapReady || _mapController == null) return; if (driverLat == null || driverLng == null || destinationLat == null || destinationLng == null) { return; } final southwest = LatLng( min(driverLat!, destinationLat!), min(driverLng!, destinationLng!), ); final northeast = LatLng( max(driverLat!, destinationLat!), max(driverLng!, destinationLng!), ); final bounds = LatLngBounds( southwest: southwest, northeast: northeast, ); try { await _mapController!.animateCamera( CameraUpdate.newLatLngBounds(bounds, 70), ); } catch (_) { await Future.delayed(const Duration(milliseconds: 300)); try { await _mapController!.animateCamera( CameraUpdate.newLatLngBounds(bounds, 70), ); } catch (_) {} } } double _calculateDistanceKm( double startLat, double startLng, double endLat, double endLng, ) { const double earthRadius = 6371; final dLat = _degToRad(endLat - startLat); final dLng = _degToRad(endLng - startLng); final a = sin(dLat / 2) * sin(dLat / 2) + cos(_degToRad(startLat)) * cos(_degToRad(endLat)) * sin(dLng / 2) * sin(dLng / 2); final c = 2 * atan2(sqrt(a), sqrt(1 - a)); return earthRadius * c; } double _degToRad(double deg) => deg * pi / 180; double? _toDouble(dynamic val) { if (val == null) return null; if (val is double) return val; if (val is int) return val.toDouble(); return double.tryParse(val.toString()); } String _beautifyStatus(String s) { final value = s.trim().toLowerCase(); if (value == 'advance_paid' || value == 'accepted') { return 'Pending'; } if (value == 'deliveryboy_assigned' || value == 'tanker_assigned') { return 'Assigned'; } if (value == 'delivered') { return 'Completed'; } return value .replaceAll('_', ' ') .split(' ') .map((e) => e.isEmpty ? e : '${e[0].toUpperCase()}${e.substring(1)}') .join(' '); } int _statusStepIndex() { final raw = widget.initialStatus.trim().toLowerCase(); final live = currentStatus.trim().toLowerCase().replaceAll(' ', '_'); final s = live.isNotEmpty ? live : raw; switch (s) { case 'accepted': case 'advance_paid': return 0; case 'deliveryboy_assigned': case 'tanker_assigned': return 1; case 'pickup_started': return 2; case 'start_loading': case 'loading_completed': return 3; case 'out_for_delivery': case 'arrived': return 4; case 'unloading_started': case 'unloading_stopped': case 'payment_pending': return 5; case 'delivered': return 6; default: return 0; } } Future _callDriver() async { if (driverPhone.trim().isEmpty) return; final uri = Uri.parse('tel:$driverPhone'); if (await canLaunchUrl(uri)) { await launchUrl(uri); } } Widget _buildTopMapCard() { final initialTarget = LatLng( driverLat ?? 17.385, driverLng ?? 78.486, ); return Expanded( child: Stack( children: [ GoogleMap( initialCameraPosition: CameraPosition( target: initialTarget, zoom: 14, ), myLocationButtonEnabled: false, zoomControlsEnabled: false, compassEnabled: true, trafficEnabled: true, markers: markers, polylines: polylines, onMapCreated: (controller) async { _mapController = controller; mapReady = true; await _moveCameraToBounds(); }, ), Positioned( top: 14, left: 14, right: 14, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), blurRadius: 10, offset: const Offset(0, 3), ) ], ), child: Row( children: [ _miniMetric('ETA', etaText), const SizedBox(width: 10), _miniMetric('Distance', distanceText), const SizedBox(width: 10), _miniMetric('Status', currentStatus), ], ), ), ), Positioned( right: 14, bottom: 14, child: FloatingActionButton( heroTag: 'fit_map_btn', backgroundColor: const Color(0XFF8270DB), onPressed: _moveCameraToBounds, child: const Icon(Icons.center_focus_strong, color: Colors.white), ), ), ], ), ); } Widget _miniMetric(String title, String value) { return Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), decoration: BoxDecoration( color: const Color(0XFFF7F7FB), borderRadius: BorderRadius.circular(14), ), child: Column( children: [ Text( title, style: const TextStyle( fontSize: 11, color: Color(0XFF777777), fontWeight: FontWeight.w500, ), ), const SizedBox(height: 6), Text( value.isEmpty ? '--' : value, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 12, color: Color(0XFF2D2E30), fontWeight: FontWeight.w700, ), ), ], ), ), ); } Widget _buildBottomPanel() { return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(16, 16, 16, 18), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(26)), ), child: SingleChildScrollView( child: Column( children: [ Container( width: 44, height: 5, decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(20), ), ), const SizedBox(height: 16), Row( children: [ CircleAvatar( radius: 24, backgroundColor: const Color(0XFFEEE9FF), child: const Icon( Icons.person, color: Color(0XFF8270DB), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( driverName.isNotEmpty ? driverName : 'Driver not assigned', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: Color(0XFF2D2E30), ), ), const SizedBox(height: 4), Text( tankerName.isNotEmpty ? tankerName : 'Delivery partner', style: const TextStyle( fontSize: 13, color: Color(0XFF6A6B6D), fontWeight: FontWeight.w500, ), ), ], ), ), InkWell( onTap: driverPhone.trim().isEmpty ? null : _callDriver, borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10, ), decoration: BoxDecoration( color: driverPhone.trim().isEmpty ? Colors.grey.shade300 : const Color(0XFF8270DB), borderRadius: BorderRadius.circular(16), ), child: const Row( children: [ Icon(Icons.call, color: Colors.white, size: 16), SizedBox(width: 6), Text( 'Call', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w700, ), ), ], ), ), ) ], ), const SizedBox(height: 16), Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0XFFF7F7FB), borderRadius: BorderRadius.circular(18), ), child: Column( children: [ _infoRow('Order ID', widget.orderId), const SizedBox(height: 10), _infoRow('Current Status', currentStatus), const SizedBox(height: 10), _infoRow('ETA to Destination', etaText), const SizedBox(height: 10), _infoRow('Remaining Distance', distanceText), if (orderAddress.trim().isNotEmpty) ...[ const SizedBox(height: 10), _infoRow('Destination', orderAddress), ], ], ), ), const SizedBox(height: 18), _buildTimeline(), ], ), ), ); } Widget _infoRow(String title, String value) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 130, child: Text( title, style: const TextStyle( fontSize: 13, color: Color(0XFF6B6C6E), fontWeight: FontWeight.w500, ), ), ), Expanded( child: Text( value.isEmpty ? '--' : value, style: const TextStyle( fontSize: 13, color: Color(0XFF222222), fontWeight: FontWeight.w700, ), ), ), ], ); } Widget _buildTimeline() { final currentIndex = _statusStepIndex(); final steps = [ 'Order Confirmed', 'Driver Assigned', 'Pickup Started', 'Loading', 'Out for Delivery', 'Unloading', 'Completed', ]; return Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0XFFF7F7FB), borderRadius: BorderRadius.circular(18), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Delivery Progress', style: TextStyle( fontSize: 15, color: Color(0XFF2D2E30), fontWeight: FontWeight.w700, ), ), const SizedBox(height: 14), ...List.generate(steps.length, (index) { final done = index <= currentIndex; final isLast = index == steps.length - 1; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( children: [ Container( width: 20, height: 20, decoration: BoxDecoration( color: done ? const Color(0XFF8270DB) : Colors.grey.shade300, shape: BoxShape.circle, ), child: done ? const Icon(Icons.check, size: 12, color: Colors.white) : null, ), if (!isLast) Container( width: 2, height: 28, color: done ? const Color(0XFF8270DB) : Colors.grey.shade300, ), ], ), const SizedBox(width: 12), Expanded( child: Padding( padding: const EdgeInsets.only(top: 1), child: Text( steps[index], style: TextStyle( fontSize: 14, fontWeight: done ? FontWeight.w700 : FontWeight.w500, color: done ? const Color(0XFF2D2E30) : const Color(0XFF8A8B8D), ), ), ), ), ], ); }), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0XFFF2F2F2), appBar: AppBar( elevation: 0, backgroundColor: Colors.white, iconTheme: const IconThemeData(color: Color(0XFF2D2E30)), title: const Text( 'Track Delivery', style: TextStyle( color: Color(0XFF2D2E30), fontSize: 18, fontWeight: FontWeight.w700, ), ), ), body: isLoading ? const Center(child: CircularProgressIndicator()) : Column( children: [ _buildTopMapCard(), SizedBox( height: MediaQuery.of(context).size.height * 0.40, child: _buildBottomPanel(), ), ], ), ); } }