Files
doormile_milderapp/lib/data/stop_contact.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

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