Initial commit
This commit is contained in:
362
lib/features/tracking/screens/live_tracking_screen.dart
Normal file
362
lib/features/tracking/screens/live_tracking_screen.dart
Normal file
@@ -0,0 +1,362 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:doormile/shared/models/models.dart';
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
import 'package:doormile/core/theme/app_theme.dart';
|
||||
import 'package:doormile/core/widgets/common.dart';
|
||||
|
||||
class LiveTrackingScreen extends StatelessWidget {
|
||||
final Order? order;
|
||||
const LiveTrackingScreen({super.key, this.order});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final id = order?.id ?? 'DM-CHN-BLR-7741';
|
||||
final eta = order?.eta ?? '2:30 PM';
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surface,
|
||||
body: Stack(
|
||||
children: [
|
||||
// Faux map
|
||||
Positioned.fill(
|
||||
child: CustomPaint(painter: _MapPainter()),
|
||||
),
|
||||
// Top bar
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
_circleBtn(Icons.arrow_back, () => Navigator.pop(context)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text('Live Tracking',
|
||||
style: AppTheme.headlineSm.copyWith(color: AppColors.primary),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
StatusChip(
|
||||
label: 'EV Priority',
|
||||
icon: Icons.eco,
|
||||
color: AppColors.tertiary,
|
||||
bg: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom detail sheet
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Color(0x1A000000), blurRadius: 24, offset: Offset(0, -6)),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('PARCEL ID',
|
||||
style: AppTheme.caption.copyWith(letterSpacing: 1)),
|
||||
Text(id,
|
||||
style: AppTheme.headlineSm
|
||||
.copyWith(fontWeight: FontWeight.w700)),
|
||||
],
|
||||
),
|
||||
),
|
||||
StatusChip(
|
||||
label: 'In transit',
|
||||
color: AppColors.tertiary,
|
||||
bg: AppColors.tertiaryFixed.withValues(alpha: 0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('ETA $eta',
|
||||
style: AppTheme.labelMd.copyWith(color: AppColors.primary)),
|
||||
const SizedBox(height: 16),
|
||||
const _MileStepper(activeIndex: 2),
|
||||
const SizedBox(height: 16),
|
||||
_riderCard(context),
|
||||
const SizedBox(height: 12),
|
||||
const EcoBanner(
|
||||
text: 'Eco-Friendly Journey · saving 1.2kg of CO₂ on this delivery.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _circleBtn(IconData icon, VoidCallback onTap) {
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
shape: const CircleBorder(),
|
||||
elevation: 2,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
customBorder: const CircleBorder(),
|
||||
child: SizedBox(
|
||||
width: 40, height: 40, child: Icon(icon, color: AppColors.primary, size: 22)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _riderCard(BuildContext context) {
|
||||
return AppCard(
|
||||
color: AppColors.surfaceContainerLow,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: AppColors.primaryFixed,
|
||||
child: Icon(Icons.person, color: AppColors.primary, size: 28),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Rahul Sharma',
|
||||
style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)),
|
||||
Row(
|
||||
children: [
|
||||
StatusChip(
|
||||
label: 'EV',
|
||||
icon: Icons.bolt,
|
||||
color: AppColors.tertiary,
|
||||
bg: AppColors.tertiaryFixed.withValues(alpha: 0.5)),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text('KA-01-EV-4412',
|
||||
style: AppTheme.caption, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('★ 4.9',
|
||||
style: AppTheme.labelMd.copyWith(color: AppColors.warning)),
|
||||
Text('4821 trips', style: AppTheme.caption),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
final uri = Uri(scheme: 'tel', path: '+919876543210');
|
||||
try {
|
||||
await launchUrl(uri);
|
||||
} catch (e) {
|
||||
if (context.mounted) _toast(context, 'Could not launch dialer.');
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.call, size: 18),
|
||||
label: const Text('Call Rider'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primary,
|
||||
side: const BorderSide(color: AppColors.primary),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => Navigator.pushNamed(context, '/chat', arguments: 'Rahul Sharma'),
|
||||
icon: const Icon(Icons.chat_bubble_outline, size: 18),
|
||||
label: const Text('Chat'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _toast(BuildContext context, String msg) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(
|
||||
content: Text(msg),
|
||||
backgroundColor: AppColors.inverseSurface,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class _MileStepper extends StatelessWidget {
|
||||
final int activeIndex;
|
||||
const _MileStepper({required this.activeIndex});
|
||||
|
||||
static const _stops = [
|
||||
('Booked', AppColors.primary),
|
||||
('First Mile', AppColors.firstMile),
|
||||
('Mid Mile', AppColors.midMile),
|
||||
('Last Mile', AppColors.lastMile),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: List.generate(_stops.length, (i) {
|
||||
final done = i <= activeIndex;
|
||||
final stop = _stops[i];
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 3,
|
||||
color: i == 0
|
||||
? Colors.transparent
|
||||
: (i <= activeIndex ? stop.$2 : AppColors.surfaceContainerHigh),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: done ? stop.$2 : AppColors.surfaceContainerHigh,
|
||||
border: Border.all(
|
||||
color: done ? stop.$2 : AppColors.outlineVariant, width: 2),
|
||||
),
|
||||
child: done
|
||||
? const Icon(Icons.check, size: 8, color: Colors.white)
|
||||
: null,
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 3,
|
||||
color: i == _stops.length - 1
|
||||
? Colors.transparent
|
||||
: (i < activeIndex ? _stops[i + 1].$2 : AppColors.surfaceContainerHigh),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(stop.$1,
|
||||
style: AppTheme.caption.copyWith(
|
||||
color: done ? stop.$2 : AppColors.onSurfaceVariant,
|
||||
fontWeight: i == activeIndex ? FontWeight.w700 : FontWeight.w500,
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Lightweight stylised street map with a dashed delivery route.
|
||||
class _MapPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final bg = Paint()..color = const Color(0xFFEDEAE9);
|
||||
canvas.drawRect(Offset.zero & size, bg);
|
||||
|
||||
final road = Paint()
|
||||
..color = Colors.white
|
||||
..strokeWidth = 10;
|
||||
final thin = Paint()
|
||||
..color = const Color(0xFFE0DCDB)
|
||||
..strokeWidth = 4;
|
||||
|
||||
for (double x = 30; x < size.width; x += 70) {
|
||||
canvas.drawLine(Offset(x, 0), Offset(x, size.height), thin);
|
||||
}
|
||||
for (double y = 80; y < size.height; y += 70) {
|
||||
canvas.drawLine(Offset(0, y), Offset(size.width, y), thin);
|
||||
}
|
||||
canvas.drawLine(Offset(0, size.height * 0.35),
|
||||
Offset(size.width, size.height * 0.42), road);
|
||||
canvas.drawLine(Offset(size.width * 0.4, 0),
|
||||
Offset(size.width * 0.5, size.height * 0.7), road);
|
||||
|
||||
// Dashed route
|
||||
final routePaint = Paint()
|
||||
..color = AppColors.primary
|
||||
..strokeWidth = 4
|
||||
..style = PaintingStyle.stroke;
|
||||
final path = Path()
|
||||
..moveTo(size.width * 0.2, size.height * 0.6)
|
||||
..quadraticBezierTo(size.width * 0.35, size.height * 0.3,
|
||||
size.width * 0.55, size.height * 0.35)
|
||||
..quadraticBezierTo(size.width * 0.75, size.height * 0.4,
|
||||
size.width * 0.7, size.height * 0.18);
|
||||
_drawDashed(canvas, path, routePaint);
|
||||
|
||||
// Origin & destination markers
|
||||
final dot = Paint()..color = AppColors.primary;
|
||||
canvas.drawCircle(Offset(size.width * 0.2, size.height * 0.6), 7, dot);
|
||||
canvas.drawCircle(Offset(size.width * 0.2, size.height * 0.6), 3,
|
||||
Paint()..color = Colors.white);
|
||||
canvas.drawCircle(Offset(size.width * 0.7, size.height * 0.18), 9,
|
||||
Paint()..color = AppColors.primary);
|
||||
// Vehicle marker (mid route)
|
||||
final mid = Offset(size.width * 0.55, size.height * 0.35);
|
||||
canvas.drawCircle(mid, 16, Paint()..color = AppColors.primary.withValues(alpha: 0.2));
|
||||
canvas.drawCircle(mid, 9, Paint()..color = AppColors.tertiary);
|
||||
}
|
||||
|
||||
void _drawDashed(Canvas canvas, Path path, Paint paint) {
|
||||
const dash = 8.0;
|
||||
const gap = 6.0;
|
||||
for (final metric in path.computeMetrics()) {
|
||||
double dist = 0;
|
||||
while (dist < metric.length) {
|
||||
canvas.drawPath(
|
||||
metric.extractPath(dist, dist + dash), paint);
|
||||
dist += dash + gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
278
lib/features/tracking/screens/my_orders_screen.dart
Normal file
278
lib/features/tracking/screens/my_orders_screen.dart
Normal file
@@ -0,0 +1,278 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:doormile/main.dart';
|
||||
import 'package:doormile/shared/models/models.dart';
|
||||
import 'package:doormile/shared/state/app_state.dart';
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
import 'package:doormile/core/theme/app_theme.dart';
|
||||
import 'package:doormile/core/widgets/common.dart';
|
||||
|
||||
class MyOrdersScreen extends StatefulWidget {
|
||||
/// When embedded in the bottom-nav shell we hide the back button.
|
||||
final bool embedded;
|
||||
const MyOrdersScreen({super.key, this.embedded = false});
|
||||
|
||||
@override
|
||||
State<MyOrdersScreen> createState() => _MyOrdersScreenState();
|
||||
}
|
||||
|
||||
class _MyOrdersScreenState extends State<MyOrdersScreen> {
|
||||
final _filters = const ['All', 'Active', 'Delivered', 'Cancelled'];
|
||||
int _filter = 0;
|
||||
Timer? _pollingTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<AppState>().fetchOrders();
|
||||
});
|
||||
|
||||
// Start polling every 10 seconds for live status updates
|
||||
_pollingTimer = Timer.periodic(const Duration(seconds: 10), (_) {
|
||||
if (mounted) {
|
||||
context.read<AppState>().fetchOrders();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollingTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.watch<AppState>();
|
||||
final orders = state.orders;
|
||||
final filtered = switch (_filter) {
|
||||
1 => orders.where((o) => o.status == OrderStatus.active).toList(),
|
||||
2 => orders.where((o) => o.status == OrderStatus.delivered).toList(),
|
||||
3 => orders.where((o) => o.status == OrderStatus.cancelled).toList(),
|
||||
_ => orders,
|
||||
};
|
||||
|
||||
return AppBackground(
|
||||
child: Column(
|
||||
children: [
|
||||
ImmersiveHeader(
|
||||
title: 'My Parcels',
|
||||
subtitle: 'Track all your deliveries',
|
||||
icon: Icons.inventory_2_rounded,
|
||||
showBack: !widget.embedded,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 44,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: _filters.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, i) {
|
||||
final active = i == _filter;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _filter = i),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: active ? AppColors.primary : AppColors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(_filters[i],
|
||||
style: AppTheme.labelMd.copyWith(
|
||||
color: active ? Colors.white : AppColors.onSurfaceVariant)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: state.isLoadingOrders
|
||||
? const Center(child: CircularProgressIndicator(color: AppColors.primary))
|
||||
: filtered.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColors.primary.withValues(alpha: 0.1), AppColors.primary.withValues(alpha: 0.25)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primary.withValues(alpha: 0.15),
|
||||
blurRadius: 24,
|
||||
spreadRadius: 4,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 68,
|
||||
height: 68,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.inventory_2_rounded,
|
||||
size: 34,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text('No parcels found',
|
||||
style: AppTheme.headlineSm.copyWith(color: AppColors.onSurface, fontWeight: FontWeight.w700)),
|
||||
const SizedBox(height: 8),
|
||||
Text('You have no active bookings in this category.',
|
||||
style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)),
|
||||
const SizedBox(height: 48), // Padding to lift it slightly above true center
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
itemCount: filtered.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 14),
|
||||
itemBuilder: (context, i) => _OrderCard(order: filtered[i]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OrderCard extends StatelessWidget {
|
||||
final Order order;
|
||||
const _OrderCard({required this.order});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final active = order.status == OrderStatus.active;
|
||||
return AppCard(
|
||||
onTap: () => Navigator.pushNamed(
|
||||
context,
|
||||
active ? Routes.liveTracking : Routes.shipmentJourney,
|
||||
arguments: order,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Order ID', style: AppTheme.caption),
|
||||
Text('#${order.id}',
|
||||
style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)),
|
||||
],
|
||||
),
|
||||
),
|
||||
StatusChip(
|
||||
label: order.status.label, color: order.status.color, bg: order.status.bg),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
// Route mini timeline
|
||||
_routeRow(Icons.radio_button_unchecked, order.fromLabel, order.fromSub,
|
||||
AppColors.primary, dashed: true),
|
||||
_routeRow(Icons.location_on, order.toLabel, order.toSub, AppColors.onSurfaceVariant),
|
||||
if (active) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text('Journey Progress',
|
||||
style: AppTheme.caption.copyWith(color: AppColors.primary),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text('${(order.progress * 3).toStringAsFixed(1)} / 3.0 Miles',
|
||||
style: AppTheme.caption),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
JourneyProgressBar(value: order.progress),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
if (order.ecoRoute) ...[
|
||||
const Icon(Icons.eco, size: 14, color: AppColors.tertiary),
|
||||
const SizedBox(width: 4),
|
||||
Text('CO₂ saved: ${order.co2Saved} (EV)',
|
||||
style: AppTheme.caption.copyWith(color: AppColors.tertiary)),
|
||||
],
|
||||
],
|
||||
),
|
||||
const Divider(height: 22, color: AppColors.surfaceContainer),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(order.date,
|
||||
style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)),
|
||||
Text('₹${order.amount.toStringAsFixed(2)}',
|
||||
style: AppTheme.headlineSm.copyWith(fontWeight: FontWeight.w700)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _routeRow(IconData icon, String title, String sub, Color color,
|
||||
{bool dashed = false}) {
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: color),
|
||||
if (dashed)
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: 1.5,
|
||||
margin: const EdgeInsets.symmetric(vertical: 2),
|
||||
color: AppColors.outlineVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: dashed ? 10 : 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)),
|
||||
Text(sub, style: AppTheme.caption),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
319
lib/features/tracking/screens/shipment_journey_screen.dart
Normal file
319
lib/features/tracking/screens/shipment_journey_screen.dart
Normal file
@@ -0,0 +1,319 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:doormile/shared/models/models.dart';
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
import 'package:doormile/core/theme/app_theme.dart';
|
||||
import 'package:doormile/core/widgets/common.dart';
|
||||
import 'package:doormile/core/widgets/logo.dart';
|
||||
|
||||
class ShipmentJourneyScreen extends StatelessWidget {
|
||||
final Order? order;
|
||||
const ShipmentJourneyScreen({super.key, this.order});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final steps = const [
|
||||
JourneyStep(
|
||||
title: 'First Mile',
|
||||
subtitle: 'Picked up from Guindy Hub, Chennai\nAssigned to EV-Fleet #TN01-EV-4421',
|
||||
time: '7 Jun, 10:45 AM',
|
||||
icon: Icons.inventory_2,
|
||||
color: AppColors.firstMile,
|
||||
done: true,
|
||||
),
|
||||
JourneyStep(
|
||||
title: 'Mid Mile · IN TRANSIT',
|
||||
subtitle:
|
||||
'Departed Chennai Regional Hub\nEnroute to Nelamangala Distribution Center',
|
||||
time: 'Active Now',
|
||||
icon: Icons.local_shipping,
|
||||
color: AppColors.midMile,
|
||||
active: true,
|
||||
note:
|
||||
'AI REROUTE · Avoided AH45 congestion near Krishnagiri. Diverted to optimized arterial route.',
|
||||
),
|
||||
JourneyStep(
|
||||
title: 'Last Mile',
|
||||
subtitle: 'Out for Delivery\nTo: Indiranagar, Bengaluru Hub',
|
||||
time: 'Expected 9 Jun',
|
||||
icon: Icons.home_filled,
|
||||
color: AppColors.lastMile,
|
||||
),
|
||||
];
|
||||
|
||||
final logs = const [
|
||||
ActivityLog('Security Check Complete', 'Chennai Regional Center · 08 Jun 14:12',
|
||||
Icons.verified_user_outlined),
|
||||
ActivityLog('Sorted for Destination', 'Chennai Regional Center · 08 Jun 11:30',
|
||||
Icons.alt_route),
|
||||
ActivityLog('Arrived at Sorting Hub', 'Chennai Hub A · 07 Jun 18:45',
|
||||
Icons.warehouse_outlined),
|
||||
ActivityLog('Shipment Created', 'Electronic City Office · 07 Jun 09:15',
|
||||
Icons.add_box_outlined),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surface,
|
||||
appBar: AppBar(
|
||||
leading: BackButton(color: AppColors.primary, onPressed: () => Navigator.pop(context)),
|
||||
title: const DoormileWordmark(markSize: 32, fontSize: 22),
|
||||
actions: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 16),
|
||||
child: Icon(Icons.location_on_outlined, color: AppColors.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||
children: [
|
||||
// Summary card
|
||||
AppCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Shipment Contents', style: AppTheme.caption),
|
||||
Text('Documents & electronics · 3.2 kg',
|
||||
style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)),
|
||||
],
|
||||
),
|
||||
),
|
||||
StatusChip(
|
||||
label: 'EV EXPRESS',
|
||||
icon: Icons.eco,
|
||||
color: AppColors.tertiary,
|
||||
bg: AppColors.tertiaryFixed.withValues(alpha: 0.6),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
_city('MAA', 'Chennai'),
|
||||
const Expanded(
|
||||
child: Icon(Icons.bolt, color: AppColors.primary),
|
||||
),
|
||||
_city('BLR', 'Bengaluru', end: true),
|
||||
],
|
||||
),
|
||||
const Divider(height: 28, color: AppColors.surfaceContainer),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.schedule, size: 18, color: AppColors.onSurfaceVariant),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Estimated Arrival', style: AppTheme.caption),
|
||||
Text('9 Jun 2:30 PM',
|
||||
style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)),
|
||||
],
|
||||
),
|
||||
),
|
||||
PrimaryButton(
|
||||
label: 'Track Live',
|
||||
height: 44,
|
||||
onPressed: () => Navigator.pushNamed(context, '/tracking/live',
|
||||
arguments: order),
|
||||
).constrainedTo(120),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// AI insight banner
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.tertiaryFixed.withValues(alpha: 0.35),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.tertiary.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.psychology_alt, color: AppColors.tertiary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text('MileTruth AI Insight · Rerouted via Hosur bypass — ETA protected despite traffic.',
|
||||
style: AppTheme.bodyMd
|
||||
.copyWith(color: AppColors.tertiary, height: 1.35)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text('Journey Timeline', style: AppTheme.headlineMd),
|
||||
const SizedBox(height: 12),
|
||||
...List.generate(steps.length, (i) =>
|
||||
_TimelineTile(step: steps[i], isLast: i == steps.length - 1)),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Activity Log', style: AppTheme.headlineMd),
|
||||
Text('All Updates',
|
||||
style: AppTheme.labelMd.copyWith(color: AppColors.primary)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AppCard(
|
||||
child: Column(
|
||||
children: [
|
||||
for (int i = 0; i < logs.length; i++) ...[
|
||||
Row(
|
||||
children: [
|
||||
IconBadge(
|
||||
icon: logs[i].icon,
|
||||
bg: AppColors.surfaceContainer,
|
||||
color: AppColors.onSurfaceVariant,
|
||||
size: 38,
|
||||
iconSize: 18,
|
||||
circle: true,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(logs[i].title,
|
||||
style: AppTheme.labelMd
|
||||
.copyWith(fontWeight: FontWeight.w700)),
|
||||
Text(logs[i].subtitle, style: AppTheme.caption),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (i < logs.length - 1)
|
||||
const Divider(height: 22, color: AppColors.surfaceContainer),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _city(String code, String name, {bool end = false}) {
|
||||
return Column(
|
||||
crossAxisAlignment: end ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(code, style: AppTheme.headlineMd.copyWith(color: AppColors.primary)),
|
||||
Text(name, style: AppTheme.caption),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimelineTile extends StatelessWidget {
|
||||
final JourneyStep step;
|
||||
final bool isLast;
|
||||
const _TimelineTile({required this.step, required this.isLast});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: step.done || step.active
|
||||
? step.color.withValues(alpha: 0.15)
|
||||
: AppColors.surfaceContainer,
|
||||
shape: BoxShape.circle,
|
||||
border: step.active
|
||||
? Border.all(color: step.color, width: 2)
|
||||
: null,
|
||||
),
|
||||
child: Icon(step.icon,
|
||||
color: step.done || step.active ? step.color : AppColors.outline,
|
||||
size: 20),
|
||||
),
|
||||
if (!isLast)
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: 2.5,
|
||||
color: step.done ? step.color : AppColors.surfaceContainerHigh,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(step.title,
|
||||
style: AppTheme.labelMd.copyWith(
|
||||
color: step.color, fontWeight: FontWeight.w700)),
|
||||
),
|
||||
Text(step.time,
|
||||
style: AppTheme.caption.copyWith(
|
||||
color: step.active
|
||||
? AppColors.tertiary
|
||||
: AppColors.onSurfaceVariant,
|
||||
fontWeight:
|
||||
step.active ? FontWeight.w700 : FontWeight.w400)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(step.subtitle,
|
||||
style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)),
|
||||
if (step.note != null) ...[
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warning.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: AppColors.warning.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded,
|
||||
size: 16, color: AppColors.warning),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(step.note!,
|
||||
style: AppTheme.caption.copyWith(
|
||||
color: AppColors.warning, height: 1.4)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on Widget {
|
||||
Widget constrainedTo(double width) =>
|
||||
ConstrainedBox(constraints: BoxConstraints(maxWidth: width), child: this);
|
||||
}
|
||||
Reference in New Issue
Block a user