/// ───────────────────────────────────────────────────────────────────────── /// 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 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 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 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 row, {String? now}) { final day = of(row); if (day.isEmpty) return true; return day == (now ?? today); } }