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>
This commit is contained in:
95
lib/data/service_day.dart
Normal file
95
lib/data/service_day.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user