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:
2026-08-22 05:40:35 +05:30
parent c8350563d9
commit d7348e253f
387 changed files with 80693 additions and 12272 deletions

270
lib/data/api_status.dart Normal file
View File

@@ -0,0 +1,270 @@
/// ─────────────────────────────────────────────────────────────────────────
/// THE CONTRACT'S OWN VOCABULARY
///
/// Assignment, booking and consignment statuses arrive as free-form strings and
/// were compared as string literals wherever a screen needed one. That is the
/// same mistake [StopStatus] was written to fix on the read side of the parcel
/// flow, one layer further out: case-sensitivity landmines, variant spellings,
/// and — the expensive one — **an unrecognised value silently taking the
/// success branch** because the check was `!= 'Cancelled'`.
///
/// ── Unknown is a value, not a crash and not a success ──
///
/// The backend will add statuses this build has never heard of. Every enum here
/// therefore carries an [unknown] member and parses by *exact match on a
/// normalised string*, so a new server value lands on `unknown` and the screens
/// treat it as "not something I can act on" rather than as delivered, accepted
/// or complete. `values.byName`-style lookups and `firstWhere` without an
/// `orElse` both throw; neither is used.
///
/// The raw string is kept alongside, because a status this build cannot model
/// is still something the rider and the hub can read.
/// ─────────────────────────────────────────────────────────────────────────
library;
/// `Assigned_To_Miler` / `assigned to miler` / `ASSIGNED-TO-MILER` all reduce
/// to one key, so a spelling drift on the wire is not a behaviour change here.
String _key(Object? raw) => (raw?.toString() ?? '')
.trim()
.toLowerCase()
.replaceAll(RegExp(r'[\s\-]+'), '_');
/// What the hub has done with an offer of work.
enum AssignmentStatus {
assigned,
accepted,
rejected,
reassigned,
completed,
cancelled,
unknown;
static const _byKey = <String, AssignmentStatus>{
'assigned': AssignmentStatus.assigned,
'accepted': AssignmentStatus.accepted,
'rejected': AssignmentStatus.rejected,
'reassigned': AssignmentStatus.reassigned,
'completed': AssignmentStatus.completed,
'cancelled': AssignmentStatus.cancelled,
};
static AssignmentStatus parse(Object? raw) =>
_byKey[_key(raw)] ?? AssignmentStatus.unknown;
/// The rider still owes this one a decision.
bool get needsDecision => this == AssignmentStatus.assigned;
/// He has taken it on and it is not finished.
bool get isLive => this == AssignmentStatus.accepted;
/// Nothing further will happen here. **`unknown` is deliberately not
/// settled** — a status this build cannot read must not be filed as done.
bool get isSettled =>
this == AssignmentStatus.rejected ||
this == AssignmentStatus.reassigned ||
this == AssignmentStatus.completed ||
this == AssignmentStatus.cancelled;
}
/// A pickup, from the moment it exists to the moment it becomes a consignment.
enum BookingStatus {
pendingPickup,
created,
milerAssigned,
pickupScheduled,
/// The rider is standing at the pickup address.
///
/// `POST /miler/bookings/:id/reached` writes this — but until 21 Aug 2026 it
/// wrote nothing the app could observe, so **I've arrived** appeared to do
/// nothing and the console never showed the rung. The backend now persists
/// it under this name. `At_Customer` is the older spelling seen in the wild
/// and means the same thing; both parse here.
arrivedAtPickup,
pickedUp,
convertedToConsignment,
/// ── The hyperlocal short-circuit ──
///
/// `pickup-complete` decides routing from the two pincodes: matching 3-digit
/// prefixes are hyperlocal and the parcel goes **straight to
/// `Out_for_Delivery`** instead of routing via a hub. Every DailyGrubs run is
/// hyperlocal, so this is not an edge case on that line — it is the status a
/// collected meal actually carries.
///
/// It was missing from this enum, and the cost was precise: [parse] answered
/// [unknown], which the legacy translation maps to the empty string, which
/// reads as *undecided* — so a bag already in the rider's box came back from
/// the queue looking like work he had not accepted yet. The local collected
/// record hid it on the device that did the pickup and nowhere else: a
/// restart, a reinstall or a second device showed collected orders sitting on
/// Home as pending.
outForDelivery,
/// Handed over. Terminal.
delivered,
cancelled,
unknown;
static const _byKey = <String, BookingStatus>{
'pending_pickup': BookingStatus.pendingPickup,
'created': BookingStatus.created,
'miler_assigned': BookingStatus.milerAssigned,
'pickup_scheduled': BookingStatus.pickupScheduled,
'arrived_at_pickup': BookingStatus.arrivedAtPickup,
'at_customer': BookingStatus.arrivedAtPickup,
'picked_up': BookingStatus.pickedUp,
'converted_to_consignment': BookingStatus.convertedToConsignment,
// Spelled `Out_for_Delivery` on bookings — lower-case `f`, unlike the
// consignment enum's `Out_For_Delivery`. `_key` lower-cases before lookup
// so both land here, which is deliberate: the difference is a backend
// inconsistency, not a distinction, and no caller should have to know it.
'out_for_delivery': BookingStatus.outForDelivery,
'delivered': BookingStatus.delivered,
'cancelled': BookingStatus.cancelled,
};
static BookingStatus parse(Object? raw) =>
_byKey[_key(raw)] ?? BookingStatus.unknown;
/// Collection has happened — the pickup-to-delivery boundary has been
/// crossed, server-side.
///
/// `pickup-complete` is the pivot: it converts the booking into a consignment
/// and, on a hyperlocal run, releases it for delivery in the same call. Every
/// rung from there on counts, including [delivered] — a delivered order was
/// certainly collected, and a predicate that said otherwise would put a
/// finished stop back in the pickup domain.
///
/// This is what [WorkBoundary] reads. It must never include a rung before the
/// hand-over: an acceptance is a decision about work still to be done.
bool get isCollected =>
this == BookingStatus.pickedUp ||
this == BookingStatus.convertedToConsignment ||
this == BookingStatus.outForDelivery ||
this == BookingStatus.delivered;
/// The rider still has work to do at this address.
bool get isOpen =>
this == BookingStatus.pendingPickup ||
this == BookingStatus.created ||
this == BookingStatus.milerAssigned ||
this == BookingStatus.pickupScheduled ||
this == BookingStatus.arrivedAtPickup;
/// ── Cancellation is refused once picked up ──
///
/// The server enforces it; this is the client half, so the control is not
/// offered in a state where pressing it can only fail.
bool get canCancel => isOpen;
}
/// A consignment, from the hub's point of view.
enum ConsignmentStatus {
created,
inwardedAtHub,
tripsheetLoaded,
inTransit,
outForDelivery,
delivered,
rtoInitiated,
returnedToSender,
missing,
damaged,
unknown;
static const _byKey = <String, ConsignmentStatus>{
'created': ConsignmentStatus.created,
'inwarded_at_hub': ConsignmentStatus.inwardedAtHub,
'tripsheet_loaded': ConsignmentStatus.tripsheetLoaded,
'in_transit': ConsignmentStatus.inTransit,
'out_for_delivery': ConsignmentStatus.outForDelivery,
'delivered': ConsignmentStatus.delivered,
'rto_initiated': ConsignmentStatus.rtoInitiated,
'returned_to_sender': ConsignmentStatus.returnedToSender,
'missing': ConsignmentStatus.missing,
'damaged': ConsignmentStatus.damaged,
};
static ConsignmentStatus parse(Object? raw) =>
_byKey[_key(raw)] ?? ConsignmentStatus.unknown;
/// The one state `deliver` and `skip` are legal from — anything else is a
/// 400. Offering the control elsewhere is offering a guaranteed failure.
bool get isDeliverable => this == ConsignmentStatus.outForDelivery;
/// Handed over. Only this one.
bool get isDelivered => this == ConsignmentStatus.delivered;
/// Going back, or gone. Not failures the rider caused, and not states he can
/// work out of on this screen.
bool get isReturning =>
this == ConsignmentStatus.rtoInitiated ||
this == ConsignmentStatus.returnedToSender;
/// Something is wrong with the parcel itself and the hub owns it now.
bool get isException =>
this == ConsignmentStatus.missing || this == ConsignmentStatus.damaged;
/// Nothing further happens on the rider's phone. **`unknown` is excluded** —
/// see the note at the top of this file.
bool get isClosed => isDelivered || isReturning || isException;
}
/// What the rider is doing, as the availability endpoint understands it.
///
/// `Break`, not `On_Break`: the obvious guess is the wrong one, and it is the
/// value the server validates against.
enum RiderAvailability {
offline,
available,
assigned,
onPickup,
atCustomer,
pickedUp,
onDelivery,
onBreak,
blocked,
unknown;
static const _byKey = <String, RiderAvailability>{
'offline': RiderAvailability.offline,
'available': RiderAvailability.available,
'assigned': RiderAvailability.assigned,
'on_pickup': RiderAvailability.onPickup,
'at_customer': RiderAvailability.atCustomer,
'picked_up': RiderAvailability.pickedUp,
'on_delivery': RiderAvailability.onDelivery,
'break': RiderAvailability.onBreak,
'blocked': RiderAvailability.blocked,
};
static RiderAvailability parse(Object? raw) =>
_byKey[_key(raw)] ?? RiderAvailability.unknown;
/// The exact string this value is sent back as. Named separately from the
/// Dart member so `onBreak` can carry the wire's `Break` without the enum
/// having a member called `break`, which is a keyword.
String get wire => switch (this) {
RiderAvailability.offline => 'Offline',
RiderAvailability.available => 'Available',
RiderAvailability.assigned => 'Assigned',
RiderAvailability.onPickup => 'On_Pickup',
RiderAvailability.atCustomer => 'At_Customer',
RiderAvailability.pickedUp => 'Picked_Up',
RiderAvailability.onDelivery => 'On_Delivery',
RiderAvailability.onBreak => 'Break',
RiderAvailability.blocked => 'Blocked',
// Never sent. A value this build cannot model must not be echoed back to
// the server as though it were understood.
RiderAvailability.unknown => 'Offline',
};
/// On duty in any sense — anything but offline, blocked, or unreadable.
bool get isWorking =>
this != RiderAvailability.offline &&
this != RiderAvailability.blocked &&
this != RiderAvailability.unknown;
}