parent
fb8777b7f8
commit
92e704a1b4
@ -1,130 +1,876 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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 {
|
class DeliveryUpdatesPage extends StatefulWidget {
|
||||||
final String orderId;
|
final String orderId;
|
||||||
final String initialStatus;
|
final String initialStatus;
|
||||||
const DeliveryUpdatesPage({super.key, required this.orderId, required this.initialStatus});
|
|
||||||
|
const DeliveryUpdatesPage({
|
||||||
|
super.key,
|
||||||
|
required this.orderId,
|
||||||
|
required this.initialStatus,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DeliveryUpdatesPage> createState() => _DeliveryUpdatesPageState();
|
State<DeliveryUpdatesPage> createState() => _DeliveryUpdatesPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DeliveryUpdatesPageState extends State<DeliveryUpdatesPage> {
|
class _DeliveryUpdatesPageState extends State<DeliveryUpdatesPage> {
|
||||||
List<String> statuses = [
|
GoogleMapController? _mapController;
|
||||||
"Tanker reached source",
|
Timer? _timer;
|
||||||
"Water filling started",
|
|
||||||
"Water filling completed",
|
final PolylinePoints _polylinePoints = PolylinePoints();
|
||||||
"Tanker started to customer location",
|
|
||||||
"Offloading water started",
|
/// ---------------- CHANGE THIS ----------------
|
||||||
"Offloading water completed",
|
/// Put your Google Maps API key here
|
||||||
"Payment completed",
|
/// Need Directions API + Distance Matrix API enabled
|
||||||
"Delivery completed"
|
static const String googleApiKey = 'AIzaSyDJpK9RVhlBejtJu9xSGfneuTN6HOfJgSM';
|
||||||
];
|
/// --------------------------------------------
|
||||||
|
|
||||||
|
bool isLoading = true;
|
||||||
|
bool mapReady = false;
|
||||||
|
|
||||||
|
String currentStatus = '';
|
||||||
|
String etaText = '--';
|
||||||
|
String distanceText = '--';
|
||||||
|
String durationText = '--';
|
||||||
|
|
||||||
int currentStep = 0;
|
String driverName = '';
|
||||||
|
String driverPhone = '';
|
||||||
|
String tankerName = '';
|
||||||
|
String orderAddress = '';
|
||||||
|
|
||||||
|
double? driverLat;
|
||||||
|
double? driverLng;
|
||||||
|
double? destinationLat;
|
||||||
|
double? destinationLng;
|
||||||
|
|
||||||
|
Set<Marker> markers = {};
|
||||||
|
Set<Polyline> polylines = {};
|
||||||
|
|
||||||
|
List<LatLng> routePoints = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Example: Get live updates from backend (MQTT, WebSocket, Firestore, etc.)
|
currentStatus = _beautifyStatus(widget.initialStatus);
|
||||||
_simulateStatusUpdates();
|
_loadTracking(showLoader: true);
|
||||||
|
|
||||||
|
_timer = Timer.periodic(const Duration(seconds: 5), (_) {
|
||||||
|
_loadTracking(showLoader: false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _simulateStatusUpdates() async {
|
@override
|
||||||
// ⚡ This is just simulation — replace with your listener
|
void dispose() {
|
||||||
for (int i = 0; i < statuses.length; i++) {
|
_timer?.cancel();
|
||||||
await Future.delayed(const Duration(seconds: 3));
|
_mapController?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadTracking({bool showLoader = false}) async {
|
||||||
|
if (showLoader && mounted) {
|
||||||
setState(() {
|
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}');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
final decoded = jsonDecode(response.body);
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
/// Supports either:
|
||||||
appBar: AppBar(
|
/// { status:true, data:{...} }
|
||||||
title: const Text("Track Order"),
|
/// OR direct flat response
|
||||||
backgroundColor: const Color(0XFF0A9E04),
|
final data = decoded is Map && decoded['data'] is Map
|
||||||
|
? decoded['data'] as Map<String, dynamic>
|
||||||
|
: decoded as Map<String, dynamic>;
|
||||||
|
|
||||||
|
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<void> _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<void> _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<void> _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<void> _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),
|
||||||
),
|
),
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
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(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: Column(
|
||||||
itemCount: statuses.length,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
itemBuilder: (context, index) {
|
children: [
|
||||||
final isCompleted = index <= currentStep;
|
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(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
// Circle with tanker icon or checkmark
|
|
||||||
Container(
|
Container(
|
||||||
width: 30,
|
width: 20,
|
||||||
height: 30,
|
height: 20,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isCompleted ? Colors.green : Colors.grey[300],
|
color: done
|
||||||
|
? const Color(0XFF8270DB)
|
||||||
|
: Colors.grey.shade300,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: done
|
||||||
child: index == currentStep
|
? const Icon(Icons.check,
|
||||||
? const Icon(Icons.local_shipping)
|
size: 12, color: Colors.white)
|
||||||
: Icon(
|
: null,
|
||||||
isCompleted
|
|
||||||
? Icons.check
|
|
||||||
: Icons.circle_outlined,
|
|
||||||
size: 16,
|
|
||||||
color: isCompleted ? Colors.white : Colors.grey,
|
|
||||||
),
|
),
|
||||||
),
|
if (!isLast)
|
||||||
),
|
|
||||||
if (index != statuses.length - 1)
|
|
||||||
Container(
|
Container(
|
||||||
width: 4,
|
width: 2,
|
||||||
height: 50,
|
height: 28,
|
||||||
color: index < currentStep ? Colors.green : Colors.grey[300],
|
color: done
|
||||||
|
? const Color(0XFF8270DB)
|
||||||
|
: Colors.grey.shade300,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 1),
|
||||||
child: Text(
|
child: Text(
|
||||||
statuses[index],
|
steps[index],
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: isCompleted ? FontWeight.w600 : FontWeight.w400,
|
fontWeight: done ? FontWeight.w700 : FontWeight.w500,
|
||||||
color: isCompleted ? Colors.black : Colors.grey[600],
|
color: done
|
||||||
|
? const Color(0XFF2D2E30)
|
||||||
|
: const Color(0XFF8A8B8D),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 🔸 Current status shown at bottom
|
@override
|
||||||
Container(
|
Widget build(BuildContext context) {
|
||||||
width: double.infinity,
|
return Scaffold(
|
||||||
padding: const EdgeInsets.all(12),
|
backgroundColor: const Color(0XFFF2F2F2),
|
||||||
decoration: BoxDecoration(
|
appBar: AppBar(
|
||||||
color: Colors.green.withOpacity(0.1),
|
elevation: 0,
|
||||||
borderRadius: BorderRadius.circular(8),
|
backgroundColor: Colors.white,
|
||||||
|
iconTheme: const IconThemeData(color: Color(0XFF2D2E30)),
|
||||||
|
title: const Text(
|
||||||
|
'Track Delivery',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0XFF2D2E30),
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
child: Text(
|
|
||||||
"Current Status: ${statuses[currentStep]}",
|
|
||||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
body: isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
_buildTopMapCard(),
|
||||||
|
SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.40,
|
||||||
|
child: _buildBottomPanel(),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in new issue