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>
90 lines
3.6 KiB
Dart
90 lines
3.6 KiB
Dart
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// WHO THE RIDER IS RINGING
|
|
///
|
|
/// One number on a screen is not one fact. `Call` beside a kitchen's name and
|
|
/// `Call` beside a customer's name are different promises, and a rider at a
|
|
/// door who reaches the kitchen instead has lost the two minutes the control
|
|
/// existed to save him.
|
|
///
|
|
/// ── What the payload actually carries ──
|
|
///
|
|
/// Verified against production 21 Aug 2026: `GET /miler/bookings` returns
|
|
/// exactly **one** phone field, `customerphone`, which the adapter stores as
|
|
/// `pickupcontactno` — a key named after the leg it was first read on rather
|
|
/// than after whose number it is. There is no separate drop or receiver
|
|
/// contact anywhere in the contract.
|
|
///
|
|
/// So on today's data there is only one number to dial and it belongs to the
|
|
/// booking's customer: the sender on a logistics collection, the subscriber on
|
|
/// a meal delivery. Dialling it is correct on both legs. What was **not**
|
|
/// correct is the app announcing it as "Call customer" while the rider is
|
|
/// standing at a kitchen counter.
|
|
///
|
|
/// This file therefore does two things: it prefers a genuine drop contact the
|
|
/// moment the backend ships one — the precedence is written now so that day
|
|
/// needs no archaeology — and it names who is being called, per leg, so the
|
|
/// label can never drift from the number again.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
library;
|
|
|
|
/// A number to dial, and who answers it.
|
|
class StopContact {
|
|
const StopContact(this.number, this.who);
|
|
|
|
/// Empty when the stop carries no usable number. Callers omit the control
|
|
/// rather than showing one that cannot dial.
|
|
final String number;
|
|
|
|
/// `the kitchen` / `the customer` — used to build the accessible label and
|
|
/// any visible caption, so both are produced from one decision.
|
|
final String who;
|
|
|
|
bool get isEmpty => number.isEmpty;
|
|
bool get isNotEmpty => number.isNotEmpty;
|
|
|
|
/// `Call the customer`.
|
|
String get action => 'Call $who';
|
|
|
|
/// Resolves the contact for the leg being worked.
|
|
///
|
|
/// [delivery] is the leg, not the line: a milk run's collection at a kitchen
|
|
/// is a pickup leg even though the line delivers to customers. Ask
|
|
/// `MilkRun.navigatesToCustomer` for it rather than deriving it here — one
|
|
/// answer to "which leg is this", used by the map, the sheet and this.
|
|
static StopContact forLeg(
|
|
Map<String, dynamic> stop, {
|
|
required bool delivery,
|
|
}) {
|
|
String read(List<String> keys) {
|
|
for (final k in keys) {
|
|
final v = (stop[k] ?? '').toString().trim();
|
|
if (v.isNotEmpty && v != 'null' && v != '0') return v;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
// Written for the contract that exists *and* the one that is coming: a
|
|
// drop contact wins on a delivery leg the moment one is present, and until
|
|
// then the single number the payload carries is used on both.
|
|
const dropKeys = [
|
|
'dropcontactno',
|
|
'DropContactNo',
|
|
'deliverycontactno',
|
|
'receiverphone',
|
|
'dropphone',
|
|
];
|
|
const pickupKeys = [
|
|
'pickupcontactno',
|
|
'PickupContactNo',
|
|
'contactno',
|
|
'customerphone',
|
|
];
|
|
|
|
final number = delivery
|
|
? read([...dropKeys, ...pickupKeys])
|
|
: read([...pickupKeys, ...dropKeys]);
|
|
|
|
return StopContact(number, delivery ? 'the customer' : 'the pickup');
|
|
}
|
|
}
|