Files
doormile_milderapp/lib/data/bag_manifest.dart
Thiru-tenext d7348e253f 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>
2026-08-22 05:40:35 +05:30

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,
});
}