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>
169 lines
7.5 KiB
Dart
169 lines
7.5 KiB
Dart
import 'package:miler/Models/stop_status.dart';
|
|
import 'package:miler/data/milk_run.dart';
|
|
import 'package:miler/data/service_profile.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// WHICH SCREEN OWNS AN ORDER
|
|
///
|
|
/// One rule, in one place, for the question both work screens have to answer
|
|
/// about every row of the shared day: **is this Home's or is it Deliveries'?**
|
|
///
|
|
/// ── Why it is here and not on the screens ──
|
|
///
|
|
/// It used to be answered twice. Home decided what to draw from
|
|
/// [stopStateOf] plus its own local stores; the Deliveries tab decided what to
|
|
/// list from a `where` clause inside its fetch, with a second `where` for the
|
|
/// statuses that clause was supposed to re-admit. Two expressions of one fact,
|
|
/// edited on different days, and they disagreed exactly where it hurts most:
|
|
/// **an accepted booking could be claimed by both.** It stayed on Home to be
|
|
/// collected, and it appeared on Deliveries as live work with a strip offering
|
|
/// to continue it — which opened a map, an I'VE ARRIVED and a confirmation
|
|
/// sheet for a collection the rider had not made yet.
|
|
///
|
|
/// The fix is not a third filter. It is that the classification is one function
|
|
/// on the shared data layer, above both screens, so they cannot hold different
|
|
/// opinions about where an order belongs.
|
|
///
|
|
/// ── The boundary ──
|
|
///
|
|
/// PENDING ─ accept ─▶ ACCEPTED ─ navigate ─▶ ARRIVED ─ pick up ─▶ PICKED
|
|
/// └──────────────── HOME owns all of this ──────────────┘ │
|
|
/// ▼
|
|
/// DELIVERIES owns from here on
|
|
///
|
|
/// **Picked up is the boundary**, and only the backend can move it. Accepting
|
|
/// an assignment is a decision about work still to be done; it is not evidence
|
|
/// that anything has been collected. Nothing in this file infers a completed
|
|
/// pickup from an acceptance.
|
|
///
|
|
/// ── The one knob ──
|
|
///
|
|
/// Where the boundary sits is a property of the rider's line, declared once as
|
|
/// [ServiceProfile.handoffAt], and read here rather than re-derived:
|
|
///
|
|
/// • **A kitchen line** ([HandoffPoint.collected]) hands over at collection.
|
|
/// The rider makes one trip to one counter for a stack of bags, so every
|
|
/// rung up to picked-up is Home's, in bulk, and Deliveries holds only what
|
|
/// is in his hands.
|
|
/// • **Logistics** ([HandoffPoint.accepted]) hands over at acceptance. There is
|
|
/// no counter and nothing to batch: he drives to one customer, raises the
|
|
/// shipment at the door and the collection *is* the job — so an accepted
|
|
/// booking is already the work tab's, which is where that flow lives.
|
|
///
|
|
/// Same rule, one declared knob. Not a line-name check, and not a per-screen
|
|
/// conditional. See [ServiceProfile.handoffAt].
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
enum WorkDomain {
|
|
/// Home's. Pending, accepted, on the way, arrived — everything before the
|
|
/// backend has confirmed the load is aboard.
|
|
pickup,
|
|
|
|
/// The Deliveries tab's. Past the hand-over point for this line.
|
|
delivery,
|
|
|
|
/// Neither work screen's. Finished or withdrawn — Activity's record.
|
|
closed,
|
|
}
|
|
|
|
/// The hand-over point between [WorkDomain.pickup] and [WorkDomain.delivery].
|
|
abstract final class WorkBoundary {
|
|
/// True once the **backend** says the collection is done.
|
|
///
|
|
/// Two sources, both authoritative, neither of them "the rider accepted it":
|
|
///
|
|
/// • The status on the row — `Picked_Up` / `Converted_To_Consignment`, or
|
|
/// any of the delivery rungs past them.
|
|
/// • The collected record, written by Home only after the pickup-complete
|
|
/// call has come back successful. It exists because the queue is up to one
|
|
/// poll behind: without it a stop the rider has just handed over jumps back
|
|
/// to Home for a few seconds, which reads as the confirm having failed.
|
|
///
|
|
/// It deliberately does **not** consult the accepted store. That set says a
|
|
/// decision was made, not that goods changed hands.
|
|
static bool pickupComplete(
|
|
Map<String, dynamic> stop, {
|
|
Set<String> collectedIds = const <String>{},
|
|
}) {
|
|
final status = stopStatusOf(stop);
|
|
if (status.isPicked || status.isDeliveryLeg) return true;
|
|
return collectedIds.contains(MilkRun.idOf(stop));
|
|
}
|
|
|
|
/// True when this order is finished or withdrawn, whatever screen it was on.
|
|
///
|
|
/// [StopStatus.picked] is terminal on a line that ends at the hub and is the
|
|
/// middle of the morning on one that does not — so it is asked of the line,
|
|
/// through the same knob everything else here reads. See
|
|
/// [ServiceProfile.endsAtHub].
|
|
static bool isClosed(Map<String, dynamic> stop) {
|
|
final status = stopStatusOf(stop);
|
|
if (status == StopStatus.delivered || status.isCancelled) return true;
|
|
return status.isPicked && ServiceProfile.active.endsAtHub;
|
|
}
|
|
|
|
/// Which screen owns this order.
|
|
///
|
|
/// [rejectedIds] and a server-side rejection do **not** close an order here:
|
|
/// a declined stop stays in the pickup domain because Home is where the rider
|
|
/// can change his mind about it. What it must never be is delivery work.
|
|
static WorkDomain domainOf(
|
|
Map<String, dynamic> stop, {
|
|
Set<String> collectedIds = const <String>{},
|
|
Set<String> acceptedIds = const <String>{},
|
|
}) {
|
|
if (isClosed(stop)) return WorkDomain.closed;
|
|
|
|
final handedOver = switch (ServiceProfile.active.handoffAt) {
|
|
HandoffPoint.collected => pickupComplete(
|
|
stop,
|
|
collectedIds: collectedIds,
|
|
),
|
|
// On logistics the work tab takes it at acceptance — from either source,
|
|
// because the local store leads the queue by a poll.
|
|
HandoffPoint.accepted =>
|
|
acceptedIds.contains(MilkRun.idOf(stop)) ||
|
|
stopStatusOf(stop) == StopStatus.accepted ||
|
|
stopStatusOf(stop).isActive ||
|
|
stopStatusOf(stop) == StopStatus.arrived,
|
|
};
|
|
|
|
return handedOver ? WorkDomain.delivery : WorkDomain.pickup;
|
|
}
|
|
|
|
/// Everything the Deliveries tab is allowed to hold, out of the shared day.
|
|
///
|
|
/// The tab's list, the count on Home's pill and anything else that asks "how
|
|
/// much is waiting on the other tab?" all read this, so the two screens
|
|
/// cannot report different numbers for the same day.
|
|
static List<Map<String, dynamic>> deliveryQueue(
|
|
Iterable<Map<String, dynamic>> day, {
|
|
Set<String> collectedIds = const <String>{},
|
|
Set<String> acceptedIds = const <String>{},
|
|
}) => [
|
|
for (final stop in day)
|
|
if (domainOf(
|
|
stop,
|
|
collectedIds: collectedIds,
|
|
acceptedIds: acceptedIds,
|
|
) ==
|
|
WorkDomain.delivery)
|
|
stop,
|
|
];
|
|
|
|
/// Everything Home is still responsible for, out of the shared day.
|
|
static List<Map<String, dynamic>> pickupQueue(
|
|
Iterable<Map<String, dynamic>> day, {
|
|
Set<String> collectedIds = const <String>{},
|
|
Set<String> acceptedIds = const <String>{},
|
|
}) => [
|
|
for (final stop in day)
|
|
if (domainOf(
|
|
stop,
|
|
collectedIds: collectedIds,
|
|
acceptedIds: acceptedIds,
|
|
) ==
|
|
WorkDomain.pickup)
|
|
stop,
|
|
];
|
|
}
|