import 'package:flutter/material.dart'; import '../../data/models.dart'; import '../format.dart'; import '../tokens.dart'; import 'buttons.dart'; import 'cards.dart'; /// The customer's journey: seven milestones, not nine backend states. /// /// Operational detail — "Miler on the way", "Order created" — is shown as /// context beneath the milestone that is currently happening, so a transition /// informs without becoming another permanent row. /// /// ── Newest first, and only as many as matter ── /// /// The list used to run oldest-to-newest, full height, every time: seven rows /// of which four were grey placeholders for things that have not happened. On /// a screen whose job is "where is my parcel right now", the answer was at the /// bottom, below the scroll. It now leads with what just happened and, when /// [maxRows] is set, keeps the rest behind one line — the same compression the /// design applies to the live tracking card. class DmMilestones extends StatelessWidget { const DmMilestones({ super.key, required this.booking, this.stage, this.title = 'Recent updates', this.maxRows, this.onExpand, }); final Booking booking; /// Journey to show. Defaults to the pickup's own stage; a destination order /// passes its own. final JourneyStage? stage; final String title; /// Show at most this many rows, newest first. Null shows the whole journey. final int? maxRows; /// Tapped from the "All n stages" action. Null hides the action. final VoidCallback? onExpand; @override Widget build(BuildContext context) { final current = stage ?? booking.stage; final currentMilestone = CustomerMilestone.of(current); final milestones = CustomerMilestone.values; final currentIndex = currentMilestone.index; // Newest first: the current milestone, then everything already done, then // — only if there is room — what is still to come. final ordered = [ for (var i = currentIndex; i >= 0; i--) i, for (var i = currentIndex + 1; i < milestones.length; i++) i, ]; final shown = maxRows == null ? ordered : ordered.take(maxRows!).toList(); return DmCard( children: [ DmCardCell( padding: const EdgeInsets.fromLTRB(16, 15, 16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( title.toUpperCase(), style: DmText.eyebrow.copyWith(color: DmColors.brand), ), ), if (onExpand != null && shown.length < ordered.length) DmTextAction( label: 'All ${milestones.length} stages', onPressed: onExpand, minHeight: 24, ) else Text( 'Step ${currentIndex + 1} of ${milestones.length}', style: DmText.monoSm, ), ], ), const SizedBox(height: 14), for (var n = 0; n < shown.length; n++) _MilestoneRow( milestone: milestones[shown[n]], done: shown[n] < currentIndex, isCurrent: shown[n] == currentIndex, last: n == shown.length - 1, time: shown[n] <= currentIndex ? relativeTime(booking.timeOf(milestones[shown[n]].firstStage)) : '', detail: shown[n] == currentIndex ? _detailFor(current) : null, ), ], ), ), ], ); } /// One line of operational context for the stage actually in progress. String? _detailFor(JourneyStage current) => switch (current) { JourneyStage.booked => 'Matching the nearest available Miler', JourneyStage.assigned => booking.miler == null ? null : '${booking.miler!.name} accepted this pickup', JourneyStage.onTheWay => booking.milerDistanceKm == null ? 'Miler on the way' : 'Miler on the way · ${booking.milerDistanceKm} km away, ' 'about ${booking.milerEtaMinutes} min', JourneyStage.arrived => 'Miler arrived at your door · verification OTP ready', JourneyStage.pickedUp => 'Weighing and sealing your packages', JourneyStage.orderCreated => booking.isMultiDestination ? 'Order created for each destination · tracking issued' : 'Order created · tracking number issued', JourneyStage.inTransit => 'Moving through our network', JourneyStage.outForDelivery => 'Out with the delivery Miler', JourneyStage.delivered => null, }; } class _MilestoneRow extends StatelessWidget { const _MilestoneRow({ required this.milestone, required this.done, required this.isCurrent, required this.last, required this.time, this.detail, }); final CustomerMilestone milestone; final bool done; final bool isCurrent; final bool last; final String time; final String? detail; @override Widget build(BuildContext context) { final upcoming = !done && !isCurrent; return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 12, child: Column( children: [ const SizedBox(height: 3), _Marker(done: done, isCurrent: isCurrent), if (!last) Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Container(width: 1.5, color: DmColors.border), ), ), ], ), ), const SizedBox(width: 14), Expanded( child: Padding( padding: EdgeInsets.only(bottom: last ? 0 : 15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( milestone.label, style: upcoming ? DmText.small.copyWith(color: DmColors.ink3) : DmText.cardTitle.copyWith(fontSize: 14.5), ), ), if (time.isNotEmpty) ...[ const SizedBox(width: 10), Text(time, style: DmText.monoSm), ], ], ), if (detail != null) ...[ const SizedBox(height: 4), Text(detail!, style: DmText.small), ], ], ), ), ), ], ), ); } } class _Marker extends StatelessWidget { const _Marker({required this.done, required this.isCurrent}); final bool done; final bool isCurrent; @override Widget build(BuildContext context) { if (isCurrent) { return Container( width: 12, height: 12, decoration: const BoxDecoration( color: DmColors.brand, shape: BoxShape.circle, boxShadow: [ BoxShadow(color: DmColors.brandSoft, spreadRadius: 4), ], ), ); } return Container( width: 12, height: 12, decoration: BoxDecoration( color: done ? DmColors.brand : DmColors.border, shape: BoxShape.circle, ), ); } }