Miler rider app: surface system, visible design language, backend lifecycle
Design system - MilerSurface ladder (canvas → working → raised → floating) with MilerPanel as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1. - Visible vocabulary applied across Home, Deliveries, Activity, Account and the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x), canvas wells for anything that opens, small filled tags for shelf labels, demoted placeholders. Recorded in DESIGN_SYSTEM.md §6. - One icon family: 222 Material glyphs migrated to Lucide; none left outside lib/xpress. - Colour semantics corrected: amber only for what is genuinely owed, brand red reserved for the live stop, disabled primaries go neutral rather than pale. Data and lifecycle - lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart makes admin sequence the single ordering authority; service_day.dart, and stop_area.dart rewritten against live Coimbatore addresses (digit-token stripping, city stoplist, street suffixes, stammer collapse). - countLabel states the load once, in bags. Testing - 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity, sheets and verify, with test/failures/ now gitignored (diff debris). - New pins: home_gutter_test, stop_area_test, plus updated structural bounds. Note: this commit also carries pre-existing working-tree deletions that were present before this work (API_SPEC.md, README.md, demo test fixtures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
|
||||
/// Canonical lifecycle status for a booking/stop, parsed from the backend's
|
||||
/// free-form `orderstatus` string.
|
||||
///
|
||||
@@ -16,6 +22,32 @@ enum StopStatus {
|
||||
active,
|
||||
arrived,
|
||||
picked,
|
||||
|
||||
/// ── The milk run's second half ──
|
||||
///
|
||||
/// [picked] is where a logistics stop ends: the parcel is collected and it
|
||||
/// becomes the hub's problem. A milk run does not end there — collection is
|
||||
/// the middle of the day, and the rider still has to carry the load to the
|
||||
/// customers.
|
||||
///
|
||||
/// So these three rungs exist for the line that has them, and only that line
|
||||
/// puts a stop into them. Keeping them in the same enum is what lets one
|
||||
/// `stopStatusOf` test serve both halves of the day; keeping them *distinct
|
||||
/// from* [picked] and [arrived] is what stops a collected crate being
|
||||
/// reported as fifteen delivered lunches ninety minutes early.
|
||||
///
|
||||
/// The load is released and the rider is driving his round.
|
||||
outForDelivery,
|
||||
|
||||
/// At a customer's door — not at a source. The distinction matters: the
|
||||
/// pickup [arrived] and this one are different places, different actions and
|
||||
/// different next steps.
|
||||
deliveryArrived,
|
||||
|
||||
/// Handed over. Terminal for a milk-run stop, the way [picked] is terminal
|
||||
/// for a logistics one.
|
||||
delivered,
|
||||
|
||||
skipped,
|
||||
cancelled,
|
||||
rejected,
|
||||
@@ -44,6 +76,21 @@ StopStatus stopStatusFromRaw(dynamic raw) {
|
||||
case 'pickuped':
|
||||
case 'pickedup':
|
||||
return StopStatus.picked;
|
||||
// The milk run's delivery rungs. `out_for_delivery` is also the backend's
|
||||
// own consignment status, spelled its way, so a stop stamped from either
|
||||
// side folds to the same state.
|
||||
case 'outfordelivery':
|
||||
case 'out_for_delivery':
|
||||
case 'out for delivery':
|
||||
case 'delivering':
|
||||
return StopStatus.outForDelivery;
|
||||
case 'deliveryarrived':
|
||||
case 'delivery_arrived':
|
||||
case 'at customer':
|
||||
case 'at_customer':
|
||||
return StopStatus.deliveryArrived;
|
||||
case 'delivered':
|
||||
return StopStatus.delivered;
|
||||
case 'skipped':
|
||||
return StopStatus.skipped;
|
||||
case 'cancelled':
|
||||
@@ -67,11 +114,53 @@ extension StopStatusX on StopStatus {
|
||||
bool get isActive => this == StopStatus.active;
|
||||
bool get isRejected => this == StopStatus.rejected;
|
||||
|
||||
/// A completed pickup — picked up or cancelled. (Matches the long-standing
|
||||
/// `!= 'picked' && != 'picked up' && != 'cancelled'` filter used to build the
|
||||
/// "next stops" and remaining-pickups lists.)
|
||||
bool get isFinishedPickup =>
|
||||
this == StopStatus.picked || this == StopStatus.cancelled;
|
||||
/// On the customer round: released, at a door, or handed over.
|
||||
bool get isDeliveryLeg =>
|
||||
this == StopStatus.outForDelivery ||
|
||||
this == StopStatus.deliveryArrived ||
|
||||
this == StopStatus.delivered;
|
||||
|
||||
/// Handed to the customer. Terminal on a milk run.
|
||||
bool get isDelivered => this == StopStatus.delivered;
|
||||
|
||||
/// ── "Is this stop finished?" is a question about the LINE ──
|
||||
///
|
||||
/// This is the distinction that broke the milk run, so it is worth being
|
||||
/// exact about.
|
||||
///
|
||||
/// On **logistics**, collecting the parcel *is* the job: [picked] is the end,
|
||||
/// the booking becomes a consignment, and the hub takes it from there.
|
||||
///
|
||||
/// On a **milk run**, [picked] is the *middle of the morning*. The rider is
|
||||
/// holding fifteen lunches and has not delivered one of them. His day ends at
|
||||
/// [delivered], one customer at a time.
|
||||
///
|
||||
/// One boolean answered both, and it answered "picked = finished". So a
|
||||
/// milk-run order collected at a kitchen was dropped from the deliveries list
|
||||
/// as completed work and filed on Activity as history — before the rider had
|
||||
/// left the counter. He collected five lunches and watched them disappear
|
||||
/// into his own history.
|
||||
///
|
||||
/// Read this, not [isTerminal], anywhere the question is "should this stop
|
||||
/// still be worked today?".
|
||||
bool get isWorkComplete => ServiceProfile.active.deliversToCustomer
|
||||
? (this == StopStatus.delivered || this == StopStatus.cancelled)
|
||||
: (this == StopStatus.picked ||
|
||||
this == StopStatus.delivered ||
|
||||
this == StopStatus.cancelled);
|
||||
|
||||
/// Every state that ends a stop on *some* line, without asking which.
|
||||
///
|
||||
/// Only for code that must not depend on the active profile — a pure store
|
||||
/// helper, say. Screens want [isWorkComplete].
|
||||
bool get isTerminal =>
|
||||
this == StopStatus.picked ||
|
||||
this == StopStatus.delivered ||
|
||||
this == StopStatus.cancelled;
|
||||
|
||||
/// The old name for [isWorkComplete], kept because ~8 call sites read it and
|
||||
/// they all want the line-aware answer.
|
||||
bool get isFinishedPickup => isWorkComplete;
|
||||
|
||||
/// Still awaiting the rider's acceptance — belongs on Home, not Bookings.
|
||||
bool get isPending =>
|
||||
@@ -94,12 +183,68 @@ extension StopStatusX on StopStatus {
|
||||
StopStatus.accepted => 'Accepted',
|
||||
StopStatus.active => 'In progress',
|
||||
StopStatus.arrived => 'At the stop',
|
||||
StopStatus.picked => 'Completed',
|
||||
StopStatus.picked => 'Picked up',
|
||||
StopStatus.outForDelivery => 'Out for delivery',
|
||||
StopStatus.deliveryArrived => 'At the customer',
|
||||
StopStatus.delivered => 'Delivered',
|
||||
StopStatus.skipped => 'Skipped',
|
||||
StopStatus.cancelled => 'Cancelled',
|
||||
StopStatus.rejected => 'Rejected',
|
||||
StopStatus.unknown => 'Active',
|
||||
};
|
||||
|
||||
/// The colour that name is drawn in.
|
||||
///
|
||||
/// Next to [label] for the same reason the labels are here: a status cannot
|
||||
/// be added without somebody deciding both what it is called and how loud it
|
||||
/// is. Semantic, not decorative — green means the rider is done with it, red
|
||||
/// means it needs him now, grey means it is waiting on somebody else.
|
||||
Color get color => switch (this) {
|
||||
StopStatus.newStop || StopStatus.assigned => ColorConstants.secondaryText,
|
||||
StopStatus.accepted => ColorConstants.acceptGreen,
|
||||
StopStatus.active || StopStatus.arrived => ColorConstants.pickupAccent,
|
||||
StopStatus.picked => ColorConstants.acceptGreen,
|
||||
// ── Loud, but in the leg's own colour ──
|
||||
//
|
||||
// These wore the brand red, and on the stop sheet that word sat between a
|
||||
// blue leg disc and a blue "what to do here" chip, one line above a red
|
||||
// Navigate button — a status dressed as an action, on a screen that codes
|
||||
// its delivery leg blue everywhere else. The round is still drawn loudly;
|
||||
// it is drawn in the ink that already means *delivery leg*, which is the
|
||||
// same rule that keeps the pickup-leg states on the pickup accent above.
|
||||
StopStatus.outForDelivery ||
|
||||
StopStatus.deliveryArrived => ColorConstants.deliveryAccent,
|
||||
StopStatus.delivered => ColorConstants.acceptGreen,
|
||||
StopStatus.skipped => ColorConstants.warning,
|
||||
StopStatus.cancelled || StopStatus.rejected => ColorConstants.errorRed,
|
||||
StopStatus.unknown => ColorConstants.secondaryText,
|
||||
};
|
||||
|
||||
/// A glyph for the same state, so the tag never depends on colour alone —
|
||||
/// these are read in sunlight, through a scratched screen, at a gate.
|
||||
IconData get icon => switch (this) {
|
||||
StopStatus.newStop || StopStatus.assigned => LucideIcons.clock,
|
||||
StopStatus.accepted => LucideIcons.circleCheck,
|
||||
StopStatus.active => LucideIcons.bike,
|
||||
StopStatus.arrived => LucideIcons.mapPin,
|
||||
StopStatus.picked => LucideIcons.package,
|
||||
StopStatus.outForDelivery => LucideIcons.truck,
|
||||
StopStatus.deliveryArrived => LucideIcons.mapPin,
|
||||
// ── Delivered is settled, not decorated ──
|
||||
//
|
||||
// `verified_rounded` is a starburst badge — the shape Material reserves
|
||||
// for *verified account*, and the loudest glyph in the set. Down a column
|
||||
// of finished stops it made every completed delivery look like an award.
|
||||
// A package with a tick on it says the same thing about the same object,
|
||||
// quietly, and it is the mark every delivery app in the world uses.
|
||||
StopStatus.delivered => LucideIcons.packageCheck,
|
||||
// Skipped must not read as a variant of delivered. A circle with a stroke
|
||||
// through it is the "attempted, did not happen" mark; `replay` promised a
|
||||
// retry the rider may not actually be able to make.
|
||||
StopStatus.skipped => LucideIcons.circleSlash,
|
||||
StopStatus.cancelled || StopStatus.rejected => LucideIcons.ban,
|
||||
StopStatus.unknown => LucideIcons.circle,
|
||||
};
|
||||
}
|
||||
|
||||
/// Canonical protocol verbs sent to the status-change API (the *write* side).
|
||||
@@ -110,4 +255,17 @@ class OrderAction {
|
||||
static const String picked = 'PICKED';
|
||||
static const String rejected = 'REJECTED';
|
||||
static const String cancelled = 'CANCELLED';
|
||||
|
||||
/// The milk run's second half. These are *client* verbs — they name what the
|
||||
/// rider did, and each one maps to a real backend call rather than to a
|
||||
/// status string the API would not recognise:
|
||||
///
|
||||
/// startDelivery → POST /miler/deliveries/start
|
||||
/// deliveryArrived → (local; the round has no per-stop arrival route)
|
||||
/// delivered → POST /miler/consignments/:id/deliver
|
||||
///
|
||||
/// See the mapping table in `MilkRun`.
|
||||
static const String startDelivery = 'START_DELIVERY';
|
||||
static const String deliveryArrived = 'DELIVERY_ARRIVED';
|
||||
static const String delivered = 'DELIVERED';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user