parent
fb8777b7f8
commit
92e704a1b4
@ -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<DeliveryUpdatesPage> createState() => _DeliveryUpdatesPageState();
|
||||
}
|
||||
|
||||
class _DeliveryUpdatesPageState extends State<DeliveryUpdatesPage> {
|
||||
List<String> 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<Marker> markers = {};
|
||||
Set<Polyline> polylines = {};
|
||||
|
||||
List<LatLng> 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<void> _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<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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Track Order"),
|
||||
backgroundColor: const Color(0XFF0A9E04),
|
||||
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,
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
};
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue