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>
271 lines
11 KiB
Dart
271 lines
11 KiB
Dart
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// 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;
|
|
}
|