device infos

This commit is contained in:
2026-08-28 15:07:30 +05:30
parent 5723d373b2
commit 69f4f3e909
84 changed files with 4337 additions and 2339 deletions

View File

@@ -254,8 +254,17 @@ class Trip {
///
/// Returns `this` unchanged when nothing has been collected yet, so the
/// common case allocates nothing.
/// ── Declined work is not outstanding work ──
///
/// [rejectedIds] is the set the rider's own decision writes, and it has to be
/// passed in because a decline is **local first**: the stop stays in the
/// payload reading `assigned` until the backend catches up, so
/// `stopStatusOf` cannot see it and the figures counted a stop the rider had
/// refused. He declined four of eight and the card still measured the
/// kilometres to all eight.
Trip outstanding({
Set<String> collectedIds = const <String>{},
Set<String> rejectedIds = const <String>{},
double? originLat,
double? originLng,
}) {
@@ -263,7 +272,9 @@ class Trip {
for (final s in stops)
if (!WorkBoundary.pickupComplete(s, collectedIds: collectedIds) &&
!stopStatusOf(s).isCancelled &&
!stopStatusOf(s).isSkipped)
!stopStatusOf(s).isSkipped &&
!stopStatusOf(s).isRejected &&
!rejectedIds.contains((s['orderid'] ?? '').toString()))
s,
];
if (left.length == stops.length) return this;
@@ -300,6 +311,35 @@ class Trip {
'${RouteMetricsHelper.formatClock(e)}';
}
/// The slot as a **clock range**, or `''` when it has none.
///
/// ── Why a day part is not printed at clock position ──
///
/// [slotLabel] answers "what is this trip called" and names a whole day part
/// as *Morning* / *Afternoon* / *Evening*, which is right for a heading.
/// It is wrong wherever the app is printing a live figure.
///
/// Two things happen when it lands there. The word is a **label pretending to
/// be data**: it sits where "3h 10m left" sits, in the corner a rider checks
/// for how much time he has, and answers with a noun he cannot act on. And it
/// is redundant twice over — the trip is already numbered `Trip 1`, and the
/// number *is* the day part, so `Trip 1 · Morning` says one thing twice.
///
/// This became visible when trips started being placed by day part at all:
/// before that they carried no window, [slotLabel] fell through to "Today's
/// route", and the same call sites printed nothing. So the fix is not to undo
/// the placement — it is to stop reading a heading as a measurement.
///
/// Returns a range only when the hub sent a real one: `10:00 AM – 1:00 PM`.
String get slotClockLabel {
final s = slotStart;
final e = slotEnd;
if (s == null || e == null) return '';
if (DayPart.spans(s, e)) return '';
return '${RouteMetricsHelper.formatClock(s)} – '
'${RouteMetricsHelper.formatClock(e)}';
}
/// True when the slot window has already closed.
bool isExpired([DateTime? now]) {
final e = slotEnd;
@@ -410,15 +450,47 @@ class Trip {
DateTime? get _earliestDue {
DateTime? best;
for (final s in stops) {
final due = _parseTimestamp(
s['expected_pickup_time'] ?? s['expectedpickuptime'] ?? s['eta'],
);
final due = dayPartTimeOf(s);
if (due == null) continue;
if (best == null || due.isBefore(best)) best = due;
}
return best;
}
/// The clock that decides which part of the day a stop belongs to.
///
/// ── Due time first, assignment time second ──
///
/// The right answer is when the work is *due*: an order to be collected at
/// 10am is morning work whenever the hub happened to raise it. That is
/// `expected_pickup_time`, and where the backend sends it nothing here
/// changes.
///
/// Where it does not — which is the live Doormile contract today — the day
/// part had nothing to read at all, and every stop fell into the undated
/// `day:` bucket. One trip, no day part, and [TripSlots.slots] put it in the
/// first free slot: **everything was Trip 1, all day**. A rider given an
/// evening run opened Home to find it filed under the morning.
///
/// So the fallback is when the work reached him — `assignedat`, merged onto
/// the row by [WorkRepository] from the assignment ledger. It is the hub's
/// own act as the phone observed it, and it is what dispatch means on the
/// telephone: work handed over in the morning is the morning run. Approximate
/// where a due time would be exact, and exactly right for the common case,
/// which is a slot assigned at the top of the slot it is for.
///
/// `eta` is read last and usually answers nothing — on the v1 adapter it is
/// a duration, not a clock, and [_parseTimestamp] rejects it rather than
/// inventing a 15th of the month out of "15 min".
static DateTime? dayPartTimeOf(Map<String, dynamic> stop) =>
_parseTimestamp(
stop['expected_pickup_time'] ??
stop['expectedpickuptime'] ??
stop['assignedat'] ??
stop['assignedon'] ??
stop['eta'],
);
/// Address components every stop on this trip shares — city, state,
/// pincode and so on. Cached per build by the caller.
List<String> get sharedAddressTail => commonAddressTail([
@@ -663,11 +735,11 @@ class Trip {
from = slotFrom;
to = slotTo;
} else {
final due = _parseTimestamp(
stop['expected_pickup_time'] ??
stop['expectedpickuptime'] ??
stop['eta'],
);
// Due time where the hub sends one, and the moment the work reached
// the rider where it does not — see [Trip.dayPartTimeOf]. Without the
// second half every stop landed in the undated bucket below and the
// whole day rendered as Trip 1.
final due = dayPartTimeOf(stop);
if (due != null) {
// Morning, afternoon or evening — see [DayPart]. Not a rolling
// window: two stops due an hour apart are on the same run, and the