import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../data/models.dart'; import '../../state/app_scope.dart'; import '../format.dart'; import '../tokens.dart'; import '../widgets/buttons.dart'; import '../widgets/cards.dart'; import '../widgets/chrome.dart'; import '../widgets/misc.dart'; import '../widgets/route_rail.dart'; import 'booking/booking_routes.dart'; import 'booking/send_screen.dart'; import 'shell_screen.dart'; import 'sheets/place_search_sheet.dart'; import 'tracking_screen.dart'; /// Home — one question, one dominant action, and whatever is live right now. /// Nothing else competes; history belongs to the Orders tab. class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context) { final app = AppScope.of(context); final active = app.activeBookings.isEmpty ? null : app.activeBookings.first; final firstRun = app.orders.isEmpty; // The pickup point is already confirmed on this screen, so booking starts // at the destination — two taps from here to a reserved slot. void startBooking() { AppScope.read(context).startBooking(); Navigator.of(context).push( bookingRoute(BookingRoutes.send, (_) => const SendScreen()), ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ DmBrandHeader( eyebrow: 'Welcome, ${_firstName(app.customer?.name)}', title: firstRun ? 'Send your first parcel' : 'Send a parcel', action: InkResponse( onTap: () => ShellScreen.of(context)?.goToTab(2), radius: 26, child: DmAvatar(initials: initialsOf(app.customer?.name), onBrand: true), ), // The address is the one fact every booking starts from, so it sits // in the header as a live value the customer can change in place — // not as the first question of the flow. child: DmHeaderTrack( leading: const DmLiveDot(size: 6, color: Colors.white), trailing: const Icon( LucideIcons.chevronDown, size: 16, color: DmColors.onBrand3, ), onTap: () async { final place = await showPlaceSearchSheet(context); if (place != null && context.mounted) app.setPickup(place); }, child: Text( app.pickup?.title ?? 'Set a pickup address', maxLines: 1, overflow: TextOverflow.ellipsis, style: DmText.cardTitle.copyWith(color: Colors.white), ), ), ), Expanded( child: RefreshIndicator( color: DmColors.brand, backgroundColor: DmColors.surface, onRefresh: () async { await Future.wait([app.detectPickupLocation(), app.refreshOrders()]); }, child: ListView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(DmSpace.pad, 16, DmSpace.pad, 28), children: [ _BookCta(onTap: startBooking), if (active != null) ...[ const DmMicroHead('Happening now'), _ActiveCard(booking: active), ], if (firstRun) const _HowItWorks(), ], ), ), ), ], ); } static String _firstName(String? name) { final first = (name ?? '').trim().split(RegExp(r'\s+')).first; return first.isEmpty ? 'there' : first; } } /// The screen's one action, as a filled crimson card. /// /// It was a 68pt bar reading BOOK in tracked capitals — a word with no object, /// which on first run told a new customer nothing about what the app does. The /// card says what will happen ("a Miler collects from your door in a window you /// choose") and puts the action inside it, so the promise and the button are /// one object rather than a shout followed by an explanation elsewhere. class _BookCta extends StatelessWidget { const _BookCta({required this.onTap}); final VoidCallback onTap; @override Widget build(BuildContext context) { return DmCard( tone: DmCardTone.fill, children: [ DmCardCell( padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Book a pickup', style: DmText.heading.copyWith(fontSize: 19, color: Colors.white), ), const SizedBox(height: 4), Text( 'A Miler collects from your door in a window you choose.', style: DmText.small.copyWith(color: DmColors.onBrand2), ), const SizedBox(height: 14), // White on crimson: inside a filled card the primary action has // to invert, or it disappears into its own background. _WhiteButton(label: 'Start booking', onTap: onTap), ], ), ), ], ); } } class _WhiteButton extends StatefulWidget { const _WhiteButton({required this.label, required this.onTap}); final String label; final VoidCallback onTap; @override State<_WhiteButton> createState() => _WhiteButtonState(); } class _WhiteButtonState extends State<_WhiteButton> { bool _down = false; @override Widget build(BuildContext context) { return Semantics( button: true, label: widget.label, child: GestureDetector( behavior: HitTestBehavior.opaque, onTapDown: (_) => setState(() => _down = true), onTapUp: (_) => setState(() => _down = false), onTapCancel: () => setState(() => _down = false), onTap: widget.onTap, child: AnimatedContainer( duration: DmMotion.fast, curve: DmMotion.ease, // A minimum, not a height: at large accessibility text the label // needs the extra line, and a fixed 46 would clip it. constraints: const BoxConstraints(minHeight: 46), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: _down ? DmColors.brandSoft : Colors.white, borderRadius: DmRadius.all(DmRadius.sm), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( child: Text( widget.label, textAlign: TextAlign.center, style: DmText.button.copyWith(fontSize: 15, color: DmColors.brand), ), ), const SizedBox(width: 9), const Icon(LucideIcons.arrowRight, size: 18, color: DmColors.brand), ], ), ), ), ); } } /// Shown on first run only: three numbered rows that match the booking flow /// exactly, so the first send has no surprises in it. class _HowItWorks extends StatelessWidget { const _HowItWorks(); static const _steps = [ ('Tell us where it goes', 'City, address and recipient'), ('Pick a two-hour window', 'With live Miler availability'), ('Hand it over and track', 'Door to hub to doorstep, with proof'), ]; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const DmMicroHead('How it works'), DmCard( children: [ for (var i = 0; i < _steps.length; i++) DmCardRow( leading: DmTile(label: '${i + 1}', tone: DmTileTone.brand), title: _steps[i].$1, subtitle: _steps[i].$2, ), ], ), ], ); } } class _ActiveCard extends StatelessWidget { const _ActiveCard({required this.booking}); final Booking booking; @override Widget build(BuildContext context) { final app = AppScope.read(context); final slot = app.slotById(booking.slotId); final beforeHandover = booking.stage.index <= JourneyStage.pickedUp.index; final windowLabel = slot == null ? 'Pickup scheduled' : '${slot.day} · ${slot.window}'; final courierLine = switch (booking.stage) { JourneyStage.booked => 'Matching the nearest Miler', JourneyStage.assigned when booking.miler != null => '${booking.miler!.name} will collect your parcel', JourneyStage.onTheWay when booking.miler != null => '${booking.miler!.name} is on route' '${booking.milerDistanceKm == null ? '' : ' · ${booking.milerDistanceKm} km away'}', JourneyStage.arrived when booking.miler != null => '${booking.miler!.name} is at your door', _ => booking.isMultiDestination ? 'To ${booking.destinations.length} destinations' : 'To ${booking.destination.label}', }; return DmCard( onTap: () { app.openTracking(booking.reference); Navigator.of(context).push( bookingRoute(BookingRoutes.tracking, (_) => const TrackingScreen()), ); }, children: [ DmCardCell( background: DmColors.brandSoft, padding: const EdgeInsets.fromLTRB(14, 11, 14, 11), child: Row( children: [ Flexible(child: DmPill(booking.stage.milestoneLabel)), const SizedBox(width: 10), Expanded( child: Text( booking.displayId, textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis, style: DmText.mono, ), ), ], ), ), DmCardCell( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ DmRouteRail( compact: true, pickup: booking.pickup.title, drop: booking.isMultiDestination ? '${booking.destinations.length} destinations' : booking.destination.label, ), const SizedBox(height: 12), Text(courierLine, style: DmText.small), ], ), ), DmCardCell( padding: const EdgeInsets.fromLTRB(16, 12, 10, 12), child: Row( children: [ Icon( beforeHandover ? LucideIcons.clock : LucideIcons.truck, size: 15, color: DmColors.ink3, ), const SizedBox(width: 10), Expanded( child: Text( beforeHandover ? windowLabel : 'On its way', style: DmText.small, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), DmTextAction( label: 'Track', icon: LucideIcons.arrowRight, onPressed: () { app.openTracking(booking.reference); Navigator.of(context).push( bookingRoute( BookingRoutes.tracking, (_) => const TrackingScreen(), ), ); }, ), ], ), ), ], ); } }