You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
877 lines
25 KiB
877 lines
25 KiB
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<DeliveryUpdatesPage> createState() => _DeliveryUpdatesPageState();
|
|
}
|
|
|
|
class _DeliveryUpdatesPageState extends State<DeliveryUpdatesPage> {
|
|
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();
|
|
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<void> _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<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),
|
|
),
|
|
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(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |