production

This commit is contained in:
2026-08-28 11:13:15 +05:30
parent d7348e253f
commit 5723d373b2
162 changed files with 17924 additions and 7026 deletions

View File

@@ -5,6 +5,7 @@ import 'package:miler/views/Dashboard/pickups/route_metrics.dart';
import 'package:miler/data/route_order.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/Models/stop_status.dart';
import 'package:miler/data/work_domain.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// A TRIP — one slot's worth of work, as a single unit.
@@ -105,6 +106,16 @@ enum DayPart {
///
/// Trip 2 is the afternoon even on a day with no morning work — the number is
/// the day part, so the rider's "Trip 2" and the hub's are the same run.
///
/// ── This split is the client's, and the backend has confirmed it ──
///
/// Asked directly (24 Aug 2026): *does the console own Trip 1/2/3?* Answer:
/// **no — there is no `tripid` or `slotid` backend-side.** The day-part
/// grouping is therefore documented client behaviour rather than a guess
/// standing in for a field that exists, and the app is not waiting on one.
///
/// If that ever changes, this is the class to delete: the ids would arrive on
/// the booking row and the split would stop being derived at all.
extension TripSlots on List<Trip> {
/// The day laid out as slots: index 0 is the morning, 1 the afternoon, 2 the
/// evening, and an empty slot is a part of the day with no work in it.
@@ -212,6 +223,61 @@ class Trip {
int get stopCount => stops.length;
/// The same trip, recounted over **only the work still owed on Home**.
///
/// ── Why the brief was reading the wrong number ──
///
/// The four figures in [TripBriefStrip] — duration, distance, parcels,
/// payment — were computed once, over every stop the slot ever held, and
/// never moved again. The headline above them says `20 stops left` and
/// counts down as the rider works; the figures under it kept describing the
/// morning he started with.
///
/// So a rider with five collections to go read `≈4h 15m · 29.0 km · 25
/// parcels` — the whole day, including the twenty stops already in his box
/// and gone to Deliveries. The one question the card exists to answer, *what
/// is left in front of me*, was the one thing on it that was stale.
///
/// ── Where the line is ──
///
/// [WorkBoundary.pickupComplete] — the same boundary Home and Deliveries
/// already split on, so a stop cannot be counted here and owned there. Once
/// the collection is done the stop belongs to the delivery leg and is not
/// this card's business. Skipped and cancelled work is dropped for the same
/// reason: it is a record now, not a stop to plan around.
///
/// [origin] is the point the round trip is measured from — the rider's own
/// position, which is what the page passes to [Trip.fromStops] as the hub.
/// Passing it keeps the recount on the same footing as the original
/// measurement; omitting it would silently fall back to per-stop kilometres
/// and read as a distance that changed for no reason.
///
/// Returns `this` unchanged when nothing has been collected yet, so the
/// common case allocates nothing.
Trip outstanding({
Set<String> collectedIds = const <String>{},
double? originLat,
double? originLng,
}) {
final left = <Map<String, dynamic>>[
for (final s in stops)
if (!WorkBoundary.pickupComplete(s, collectedIds: collectedIds) &&
!stopStatusOf(s).isCancelled &&
!stopStatusOf(s).isSkipped)
s,
];
if (left.length == stops.length) return this;
return Trip.fromStops(
id: id,
stops: left,
slotStart: slotStart,
slotEnd: slotEnd,
hubLat: originLat,
hubLng: originLng,
);
}
/// The number the rider is actually planning around: ride time plus every
/// minute spent standing at a door.
Duration get totalDuration => travelDuration + serviceDuration;
@@ -847,6 +913,24 @@ enum StopState {
/// [ServiceProfile.handoffAt] and the collected store.
collected,
/// He is **at** the stop and has said so — the arrival is recorded, the
/// pickup is not yet made.
///
/// ── Why this is not [active] ──
///
/// It was. `stopStateOf` folded `arrived` and `active` into one value, so the
/// Home row had no arrived to render and the live chip printed the literal
/// word **Active** the moment a rider slid *confirm arrival*. He was told his
/// arrival had not registered, when in fact it had: the rung moved, the API
/// fired, the store was written, and the one word on screen said something
/// else.
///
/// They are different facts. Arrived is a rung on the pickup ladder that the
/// rider puts the stop on. Active is an operational statement about which
/// stop is being worked — see `_activePickupOrderId` — and it is the word the
/// backend reserves for a delivery under way.
arrived,
/// The rider is physically on this stop right now.
active,
@@ -874,12 +958,35 @@ extension StopStateX on StopState {
bool get isCommitted =>
this == StopState.accepted ||
this == StopState.collected ||
this == StopState.active ||
this.isAtSource ||
this == StopState.skipped;
/// In the rider's hands right now.
bool get isCollected => this == StopState.collected;
/// He is **at** the source and the pickup is not yet made.
///
/// ── Why this is a predicate and not a comparison ──
///
/// [StopState.arrived] and [StopState.active] were one value. Splitting them
/// apart is what finally let the row say *Arrived* instead of *Active* — and
/// it silently changed the answer at every site that had written
/// `== StopState.active` to mean **is he standing at the counter**. There
/// were a dozen, and each of them began answering `false` for the one rung
/// whose entire point is that he is standing there:
///
/// • Home's visibility filter — so a stop *left the screen* the moment he
/// marked it arrived, and Deliveries would not take it either because
/// arrival is pre-pickup. The stop was in neither place.
/// • the selection bar's action, which offered *Accept* for a stop he was
/// already at
/// • the card's own **Mark as picked** button, which stopped being built
/// • which group opens by default, and which one counts as live
///
/// One question asked in one place, so the next rung added to the ladder
/// cannot quietly repeat it.
bool get isAtSource => this == StopState.arrived || this == StopState.active;
/// What this state is called on a card.
///
/// ── Two vocabularies, one state machine ──
@@ -899,6 +1006,7 @@ extension StopStateX on StopState {
StopState.pending => 'Awaiting your decision',
StopState.accepted => 'Accepted',
StopState.collected => 'Collected',
StopState.arrived => 'Arrived',
StopState.active => 'In progress',
StopState.done => 'Completed',
StopState.skipped => 'Skipped',
@@ -908,6 +1016,7 @@ extension StopStateX on StopState {
StopState.pending => 'Pending',
StopState.accepted => 'Accepted',
StopState.collected => 'Collected',
StopState.arrived => 'Arrived',
StopState.active => 'Out for delivery',
StopState.done => 'Delivered',
StopState.skipped => 'Skipped',
@@ -946,8 +1055,11 @@ StopState stopStateOf(
return StopState.done;
}
// 2. The rider is physically on the stop — also not a local decision.
if (raw == 'active' || raw == 'arrived') return StopState.active;
// 2. He is at the stop — also not a local decision.
//
// Two values, not one. Folding them lost the arrival: see [StopState.arrived].
if (raw == 'arrived') return StopState.arrived;
if (raw == 'active') return StopState.active;
if (raw == 'skipped') return StopState.skipped;
// 2b. Released for delivery — which `pickup-complete` does by itself on