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

96 lines
4.5 KiB
Dart

/// ─────────────────────────────────────────────────────────────────────────
/// THE OPERATING DAY
///
/// Activity is a **shift log**, not an archive. What the rider needs from it is
/// "what have I done today" — and today ends when his day ends, not when a
/// timer somewhere fires.
///
/// ── Why this is a filter and not a cleanup job ──
///
/// The obvious build is a scheduled purge: at midnight, delete yesterday. It
/// is also the one that cannot work on a phone. The app is closed at midnight
/// far more often than it is open; a timer that must fire at 23:59 to keep the
/// screen correct is a timer that will not fire, and the rider opens Activity
/// at six the next morning to yesterday's finished work presented as today's.
/// Worse, a purge is destructive on a schedule nothing observes — one bad
/// timezone assumption and it takes the current shift with it.
///
/// So nothing is deleted on a clock. Every record carries the service day it
/// belongs to, and the screen asks for **today's**. When the date rolls over,
/// yesterday stops matching; it does not need to be removed, and it is still
/// there for anything that legitimately wants history. Closing the app, force
/// quitting it, flying through a timezone or leaving it open across midnight
/// all produce the same answer, because the answer is computed at read time.
///
/// ── Local, deliberately ──
///
/// A rider's day is the day where he is standing. `DateTime.now()` is already
/// local, and both stores stamp their records with the local calendar date at
/// the moment of writing — so a record written at 23:50 belongs to that day
/// and a record written ten minutes later belongs to the next, which is what
/// the rider would say too. Nothing here converts to UTC: doing so would move
/// the boundary to 05:30 local in IST and split every evening shift in half.
/// ─────────────────────────────────────────────────────────────────────────
abstract final class ServiceDay {
/// `2026-08-21` for [at], in the local calendar. The same shape both stores
/// write, so a stamp and a query can be compared as strings.
static String stamp(DateTime at) =>
'${at.year.toString().padLeft(4, '0')}-'
'${at.month.toString().padLeft(2, '0')}-'
'${at.day.toString().padLeft(2, '0')}';
/// The service day the rider is in right now.
static String get today => stamp(DateTime.now());
/// The stamps a record carries, newest-meaning first.
static const List<String> dayKeys = [
'completedday',
'skippedday',
'serviceday',
];
/// The timestamps to fall back on when a row carries no day stamp — an API
/// row, say, which has never been through either local store.
static const List<String> timeKeys = [
'completedat',
'skippedat',
'deliveredat',
'updatedat',
'createdat',
];
/// The service day [row] belongs to, or `''` when it carries nothing usable.
///
/// Empty is a real answer and callers must decide what it means for them —
/// see [belongsToToday], which keeps such a row rather than dropping it.
static String of(Map<String, dynamic> row) {
for (final k in dayKeys) {
final v = (row[k] ?? '').toString().trim();
if (v.length >= 10) return v.substring(0, 10);
}
for (final k in timeKeys) {
final raw = (row[k] ?? '').toString().trim();
if (raw.isEmpty) continue;
final t = DateTime.tryParse(raw);
if (t != null) return stamp(t.isUtc ? t.toLocal() : t);
}
return '';
}
/// Whether [row] belongs to the service day in progress.
///
/// ── A row with no date is kept, and kept quietly ──
///
/// It is on this screen because this session produced it or the API returned
/// it for this rider; the missing field is a data-quality problem, not
/// evidence that the work happened yesterday. Dropping it would silently
/// lose a rider's completed stop, and labelling it — the old `Undated` badge
/// — puts an internal defect on a screen he is supposed to read at a glance
/// and can do nothing about. So it stays, in today, unmarked.
static bool belongsToToday(Map<String, dynamic> row, {String? now}) {
final day = of(row);
if (day.isEmpty) return true;
return day == (now ?? today);
}
}