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>
96 lines
4.5 KiB
Dart
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);
|
|
}
|
|
}
|