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>
272 lines
11 KiB
Dart
272 lines
11 KiB
Dart
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.
|
|
///
|
|
/// Status used to be compared as raw string literals in ~90 places, with
|
|
/// case-sensitivity landmines (`'active'` vs `'ACTIVE'`) and variant spellings
|
|
/// (`'picked'` / `'picked up'` / `'pickuped'`). This normalizes all of that in
|
|
/// one place so filters agree everywhere.
|
|
///
|
|
/// NOTE: this is the *read* side. The status-change API still sends its own
|
|
/// protocol verbs (`ACCEPTED`/`ARRIVED`/`PICKED`/`REJECTED`/`CANCELLED`) — see
|
|
/// [OrderAction] for those canonical write values.
|
|
enum StopStatus {
|
|
newStop,
|
|
assigned,
|
|
accepted,
|
|
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,
|
|
unknown,
|
|
}
|
|
|
|
/// Normalizes any raw `orderstatus` value (case-insensitive, trimmed, variant
|
|
/// spellings folded) into a [StopStatus].
|
|
StopStatus stopStatusFromRaw(dynamic raw) {
|
|
final s = (raw?.toString() ?? '').trim().toLowerCase();
|
|
switch (s) {
|
|
case 'new':
|
|
return StopStatus.newStop;
|
|
case 'assigned':
|
|
case 'miler_assigned':
|
|
case 'pending':
|
|
return StopStatus.assigned;
|
|
case 'accepted':
|
|
return StopStatus.accepted;
|
|
case 'active':
|
|
return StopStatus.active;
|
|
case 'arrived':
|
|
return StopStatus.arrived;
|
|
case 'picked':
|
|
case 'picked up':
|
|
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':
|
|
case 'canceled':
|
|
return StopStatus.cancelled;
|
|
case 'rejected':
|
|
return StopStatus.rejected;
|
|
default:
|
|
return StopStatus.unknown;
|
|
}
|
|
}
|
|
|
|
/// Parse straight from a booking map's `orderstatus`.
|
|
StopStatus stopStatusOf(Map<String, dynamic> booking) =>
|
|
stopStatusFromRaw(booking['orderstatus']);
|
|
|
|
extension StopStatusX on StopStatus {
|
|
bool get isPicked => this == StopStatus.picked;
|
|
bool get isCancelled => this == StopStatus.cancelled;
|
|
bool get isSkipped => this == StopStatus.skipped;
|
|
bool get isActive => this == StopStatus.active;
|
|
bool get isRejected => this == StopStatus.rejected;
|
|
|
|
/// 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 =>
|
|
this == StopStatus.newStop || this == StopStatus.assigned;
|
|
|
|
/// What this state is *called*, in the rider's words.
|
|
///
|
|
/// The status vocabulary had been re-invented per widget: the live banner had
|
|
/// its own switch over raw `orderstatus` strings returning "At pickup" and
|
|
/// "In progress", the trip card wrote 'Accepted' as a literal, and the two
|
|
/// disagreed about the same backend state. A rider moving between Home, the
|
|
/// banner and Bookings was reading three vocabularies for one lifecycle and
|
|
/// re-learning the app at each stop.
|
|
///
|
|
/// One list, here, next to the parser that produces the states — so a new
|
|
/// status cannot be added without someone deciding what to call it.
|
|
String get label => switch (this) {
|
|
StopStatus.newStop => 'New',
|
|
StopStatus.assigned => 'Assigned',
|
|
StopStatus.accepted => 'Accepted',
|
|
StopStatus.active => 'In progress',
|
|
StopStatus.arrived => 'At the stop',
|
|
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).
|
|
class OrderAction {
|
|
static const String accept = 'ACCEPT';
|
|
static const String accepted = 'ACCEPTED';
|
|
static const String arrived = 'ARRIVED';
|
|
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';
|
|
}
|