From 92e704a1b42caedc8e9820e30c47b710e78ec9ed Mon Sep 17 00:00:00 2001 From: Sneha Date: Wed, 1 Apr 2026 12:15:54 +0530 Subject: [PATCH] changes --- lib/orders/all_orders.dart | 92 +++- lib/orders/assign_driver.dart | 109 +++- lib/orders/delivery_updates.dart | 920 ++++++++++++++++++++++++++++--- lib/orders/orders_model.dart | 2 + 4 files changed, 1018 insertions(+), 105 deletions(-) diff --git a/lib/orders/all_orders.dart b/lib/orders/all_orders.dart index 7299246..d8302d3 100644 --- a/lib/orders/all_orders.dart +++ b/lib/orders/all_orders.dart @@ -649,6 +649,21 @@ class _OrderCardState extends State{ return "completed"; } + /// delivery flow statuses + if( + s=='in_progress' || + s=='pickup_started' || + s=='start_loading' || + s=='loading_completed' || + s=='out_for_delivery' || + s=='arrived' || + s=='unloading_started' || + s=='unloading_stopped' || + s=='payment_pending' + ){ + return s.replaceAll('_',' '); + } + return s; } @@ -656,13 +671,17 @@ class _OrderCardState extends State{ /// ACTION RULES bool get canAssign{ - if(isExpired){ + if(orderDay == null){ return false; } - String s = - widget.order.status.toLowerCase(); + if(orderDay!.isBefore(today)){ + return false; + } + String s = widget.order.status.toLowerCase(); + + /// only before delivery starts return s=='advance_paid' || s=='accepted'; @@ -670,25 +689,49 @@ class _OrderCardState extends State{ bool get canCancel{ - if(isExpired){ + if(orderDay == null){ return false; } - String s = - widget.order.status.toLowerCase(); + if(orderDay!.isBefore(today)){ + return false; + } + + String s = widget.order.status.toLowerCase(); return s=='advance_paid' || - s=='accepted'; + s=='accepted' || + s=='in_progress' || + s=='pickup_started' || + s=='start_loading' || + s=='loading_completed' || + s=='out_for_delivery' || + s=='arrived' || + s=='payment_pending'; } bool get canTrack{ - if(isExpired){ + if(orderDay == null){ return false; } - return normalizedStatus=='in_progress'; + if(orderDay!.isBefore(today)){ + return false; + } + + String s = widget.order.status.toLowerCase(); + + return s=='in_progress' || + s=='pickup_started' || + s=='start_loading' || + s=='loading_completed' || + s=='out_for_delivery' || + s=='arrived' || + s=='unloading_started' || + s=='unloading_stopped' || + s=='payment_pending'; } @@ -746,7 +789,15 @@ class _OrderCardState extends State{ case "completed": return Color(0XFFC4E8C3); - case "in_progress": + case "in progress": + case "pickup started": + case "start loading": + case "loading completed": + case "out for delivery": + case "arrived": + case "unloading started": + case "unloading stopped": + case "payment pending": return Color(0XFFC9DFFE); case "cancelled": @@ -759,7 +810,7 @@ class _OrderCardState extends State{ return Color(0XFFFDF3D3); default: - return Colors.grey; + return Colors.grey.shade200; } @@ -776,7 +827,15 @@ class _OrderCardState extends State{ case "completed": return Color(0XFF0A9E04); - case "in_progress": + case "in progress": + case "pickup started": + case "start loading": + case "loading completed": + case "out for delivery": + case "arrived": + case "unloading started": + case "unloading stopped": + case "payment pending": return Color(0XFF1D7AFC); case "cancelled": @@ -789,7 +848,7 @@ class _OrderCardState extends State{ return Color(0XFFD0AE3C); default: - return Colors.grey; + return Colors.black87; } @@ -1043,7 +1102,7 @@ class _OrderCardState extends State{ /// ASSIGN CANCEL Visibility( - visible:canAssign || canCancel, + visible:canAssign, child:Row( @@ -1205,8 +1264,9 @@ class _OrderCardState extends State{ MaterialPageRoute( builder:(context)=> - AssignDriverScreen( - order:widget.order + DeliveryUpdatesPage( + orderId:widget.order.dbId, + initialStatus:widget.order.status ) ) diff --git a/lib/orders/assign_driver.dart b/lib/orders/assign_driver.dart index cb2809b..7af2fd8 100644 --- a/lib/orders/assign_driver.dart +++ b/lib/orders/assign_driver.dart @@ -5,6 +5,8 @@ import 'package:supplier_new/common/settings.dart'; import 'package:supplier_new/resources/source_loctaions_model.dart'; import '../resources/drivers_model.dart'; import '../resources/tankers_model.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; class AssignDriverScreen extends StatefulWidget { var order; @@ -1018,6 +1020,42 @@ class _AssignDriverScreenState extends State { ); } + void openDistanceMap() { + + double supplierLat = + double.tryParse(AppSettings.supplierLatitude.toString()) ?? 0; + + double supplierLng = + double.tryParse(AppSettings.supplierLongitude.toString()) ?? 0; + + double userLat = + double.tryParse(widget.order.lat.toString()) ?? 0; + + double userLng = + double.tryParse(widget.order.lng.toString()) ?? 0; + + String googleUrl = + "https://www.google.com/maps/dir/?api=1" + "&origin=$supplierLat,$supplierLng" + "&destination=$userLat,$userLng" + "&travelmode=driving"; + + launchUrl(Uri.parse(googleUrl)); + + } + + void callDriver(String phone){ + + if(phone.isEmpty){ + return; + } + + launchUrl( + Uri.parse("tel:$phone") + ); + + } + @override Widget build(BuildContext context) { return Scaffold( @@ -1145,11 +1183,78 @@ class _AssignDriverScreenState extends State { ], ), const Spacer(), - Text( + /* Text( widget.order.distanceInKm.toString() + 'Km', style: fontTextStyle( 12, const Color(0XFF939495), FontWeight.w400), - ), + ),*/ + Row( + children:[ + + Text( + "${widget.order.distanceInKm} Km", + style: fontTextStyle( + 12, + Color(0XFF939495), + FontWeight.w400 + ), + ), + + /*SizedBox(width:4), + + Text( + "(${((widget.order.distanceInKm)*1000).toInt()} m)", + style: fontTextStyle( + 10, + Color(0XFF8270DB), + FontWeight.w500 + ), + ), +*/ + SizedBox(width:6), + + GestureDetector( + + onTap:openDistanceMap, + + child:Image.asset( + + 'images/maps.png', + + height:18, + + width:18, + + color:Color(0XFF8270DB), + + ), + + ), + SizedBox(width:6), + GestureDetector( + + onTap:(){ + + callDriver(widget.order.phone); + + }, + + child:Image.asset( + + 'images/phone_icon.png', + + height:20, + + width:20, + + color:Color(0XFF8270DB), + + ), + + ), + + ], + ) ], ), ), diff --git a/lib/orders/delivery_updates.dart b/lib/orders/delivery_updates.dart index 4febc60..a2e6cf9 100644 --- a/lib/orders/delivery_updates.dart +++ b/lib/orders/delivery_updates.dart @@ -1,131 +1,877 @@ +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}); + + const DeliveryUpdatesPage({ + super.key, + required this.orderId, + required this.initialStatus, + }); @override State createState() => _DeliveryUpdatesPageState(); } class _DeliveryUpdatesPageState extends State { - List statuses = [ - "Tanker reached source", - "Water filling started", - "Water filling completed", - "Tanker started to customer location", - "Offloading water started", - "Offloading water completed", - "Payment completed", - "Delivery completed" - ]; - - int currentStep = 0; + 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(); - // Example: Get live updates from backend (MQTT, WebSocket, Firestore, etc.) - _simulateStatusUpdates(); + currentStatus = _beautifyStatus(widget.initialStatus); + _loadTracking(showLoader: true); + + _timer = Timer.periodic(const Duration(seconds: 5), (_) { + _loadTracking(showLoader: false); + }); } - void _simulateStatusUpdates() async { - // ⚡ This is just simulation — replace with your listener - for (int i = 0; i < statuses.length; i++) { - await Future.delayed(const Duration(seconds: 3)); + @override + void dispose() { + _timer?.cancel(); + _mapController?.dispose(); + super.dispose(); + } + + Future _loadTracking({bool showLoader = false}) async { + if (showLoader && mounted) { setState(() { - currentStep = i; + 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; + }); + } + } } - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text("Track Order"), - backgroundColor: const Color(0XFF0A9E04), + 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, + ), ), - body: Padding( - padding: const EdgeInsets.all(16.0), + }; + } + + 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: [ - Expanded( - child: ListView.builder( - itemCount: statuses.length, - itemBuilder: (context, index) { - final isCompleted = index <= currentStep; + Container( + width: 44, + height: 5, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(20), + ), + ), + const SizedBox(height: 16), - return Row( + 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: [ - Column( - children: [ - // Circle with tanker icon or checkmark - Container( - width: 30, - height: 30, - decoration: BoxDecoration( - color: isCompleted ? Colors.green : Colors.grey[300], - shape: BoxShape.circle, - ), - child: Center( - child: index == currentStep - ? const Icon(Icons.local_shipping) - : Icon( - isCompleted - ? Icons.check - : Icons.circle_outlined, - size: 16, - color: isCompleted ? Colors.white : Colors.grey, - ), - ), - ), - if (index != statuses.length - 1) - Container( - width: 4, - height: 50, - color: index < currentStep ? Colors.green : Colors.grey[300], - ), - ], + Text( + driverName.isNotEmpty ? driverName : 'Driver not assigned', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Color(0XFF2D2E30), + ), ), - const SizedBox(width: 12), - Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 4), - child: Text( - statuses[index], - style: TextStyle( - fontSize: 14, - fontWeight: isCompleted ? FontWeight.w600 : FontWeight.w400, - color: isCompleted ? Colors.black : Colors.grey[600], - ), - ), + 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, + ), + ), + ], + ), + ), + ) + ], ), - // 🔸 Current status shown at bottom + const SizedBox(height: 16), + Container( width: double.infinity, - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: Colors.green.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), + color: const Color(0XFFF7F7FB), + borderRadius: BorderRadius.circular(18), ), - child: Text( - "Current Status: ${statuses[currentStep]}", - style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + 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(), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/orders/orders_model.dart b/lib/orders/orders_model.dart index 6eb7779..6eadcbc 100644 --- a/lib/orders/orders_model.dart +++ b/lib/orders/orders_model.dart @@ -5,6 +5,7 @@ import 'package:geolocator/geolocator.dart'; class OrdersModel { String building_name = ''; + String phone = ''; String address = ''; String type_of_water = ''; String capacity = ''; @@ -32,6 +33,7 @@ class OrdersModel { OrdersModel rtvm = new OrdersModel(); rtvm.building_name = json['buildingName'] ?? ''; + rtvm.phone = json['customerPhone'] ?? ''; rtvm.dbId = json['_id']?? ''; rtvm.address = json['address'] ?? ''; rtvm.type_of_water = json['typeofwater'] ?? '';