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>
125 lines
5.2 KiB
Dart
125 lines
5.2 KiB
Dart
import 'package:miler/data/milk_run.dart';
|
|
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// ONE PICKUP ORDER = ONE BAG
|
|
///
|
|
/// The rule the whole rider workflow rests on, in one file so it cannot be
|
|
/// stated two different ways on two screens.
|
|
///
|
|
/// If a kitchen has five pickup orders, the rider is handed **five bags** — one
|
|
/// per order — and ends up with **five deliveries**, each carrying the bag it
|
|
/// arrived in:
|
|
///
|
|
/// ```
|
|
/// Order 1 → Bag 1 → Joe
|
|
/// Order 2 → Bag 2 → Arun
|
|
/// Order 3 → Bag 3 → Priya
|
|
/// ```
|
|
///
|
|
/// ── Why this is a file and not a `+ 1` at each call site ──
|
|
///
|
|
/// Every screen in the pickup half of the app has to answer "how many bags?"
|
|
/// and "which bag is this?", and every screen that answered it independently
|
|
/// answered it differently: one showed a backend `Quantity` (an order's item
|
|
/// count, not a bag count), one showed a bare `baglabel` when the payload
|
|
/// happened to carry one and nothing at all when it did not, and the kitchen
|
|
/// heading counted "meals". A rider standing at a counter comparing "5 meals"
|
|
/// on his phone with four bags on the shelf has no way to tell which of the two
|
|
/// numbers is wrong.
|
|
///
|
|
/// So the count is **derived from the orders**, always, and there is no second
|
|
/// quantity anywhere that can disagree with it.
|
|
///
|
|
/// ── What is derived, and what is never invented ──
|
|
///
|
|
/// • **The count** is `orders.length`. It cannot drift, because it is not
|
|
/// stored — a bag is what an order arrives in.
|
|
/// • **The identity** prefers the label the backend printed on the physical
|
|
/// bag ([stopBagLabel]) and falls back to the order's **position in its own
|
|
/// pickup group** — `Bag 1`, `Bag 2` — which is what a rider counting a
|
|
/// shelf actually uses.
|
|
///
|
|
/// Nothing here fabricates a crate, a tote or a quantity the backend has not
|
|
/// sent. If a future contract ever puts more than one bag on an order, this is
|
|
/// the one file that changes.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
class BagManifest {
|
|
BagManifest._();
|
|
|
|
/// One line of a pickup manifest: the order, the customer it is for, and the
|
|
/// bag it travels in.
|
|
///
|
|
/// Deliberately carries the stop itself: every caller that renders a manifest
|
|
/// also needs to act on the orders behind it, and pairing them here is what
|
|
/// stops a screen from rendering five lines and posting four ids.
|
|
static List<BagLine> forGroup(List<Map<String, dynamic>> stops) => [
|
|
for (var i = 0; i < stops.length; i++)
|
|
BagLine(
|
|
stop: stops[i],
|
|
orderId: MilkRun.idOf(stops[i]),
|
|
customer: customerOf(stops[i]),
|
|
bag: _bagFor(stops[i], i),
|
|
),
|
|
];
|
|
|
|
/// The bag one order travels in, given the group it was collected with.
|
|
///
|
|
/// [stops] must be the whole pickup group in route order — the position in it
|
|
/// *is* the bag number when the backend has not printed one.
|
|
static String bagFor(
|
|
Map<String, dynamic> stop,
|
|
List<Map<String, dynamic>> group,
|
|
) {
|
|
final id = MilkRun.idOf(stop);
|
|
final index = group.indexWhere((s) => MilkRun.idOf(s) == id);
|
|
return _bagFor(stop, index < 0 ? 0 : index);
|
|
}
|
|
|
|
static String _bagFor(Map<String, dynamic> stop, int index) {
|
|
final printed = stopBagLabel(stop);
|
|
return printed.isNotEmpty ? printed : 'Bag ${index + 1}';
|
|
}
|
|
|
|
/// `5 bags` — the load, in the unit the rider actually carries.
|
|
///
|
|
/// It used to print `5 orders · 5 bags`: the two halves of the one-bag-per-
|
|
/// order rule side by side, as a check the rider could eyeball. On a device
|
|
/// that check cost the header the fact it exists for — a real kitchen name
|
|
/// plus `13 orders · 13 bags` pushed `11 to accept` off the row, and the
|
|
/// clause that truncated was the one that changes what he does next.
|
|
///
|
|
/// One number survives, and it is the physical one: the rows underneath are
|
|
/// named `Bag 1 … Bag n` and a settled group says `n bags collected`, so
|
|
/// "bags" is the word this column already speaks. No information is lost —
|
|
/// the rule makes the counts identical — and the verifiable statement lives
|
|
/// where the verifying happens: the manifest list itself, one line per bag.
|
|
static String countLabel(int orders) {
|
|
return orders == 1 ? '1 bag' : '$orders bags';
|
|
}
|
|
|
|
/// The name the bag is going to, for a manifest line.
|
|
static String customerOf(Map<String, dynamic> stop) =>
|
|
(stop['pickupcustomer'] ??
|
|
stop['customername'] ??
|
|
stop['tenantname'] ??
|
|
'')
|
|
.toString()
|
|
.trim();
|
|
}
|
|
|
|
/// One row of a pickup manifest. See [BagManifest.forGroup].
|
|
class BagLine {
|
|
final Map<String, dynamic> stop;
|
|
final String orderId;
|
|
final String customer;
|
|
final String bag;
|
|
|
|
const BagLine({
|
|
required this.stop,
|
|
required this.orderId,
|
|
required this.customer,
|
|
required this.bag,
|
|
});
|
|
}
|