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

@@ -64,12 +64,13 @@ import 'package:miler/views/Dashboard/pickups/collect_payment.dart';
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
import 'package:miler/widget/Bottom_page.dart';
import 'package:miler/data/meal_run_mock.dart';
import 'package:miler/data/bag_manifest.dart';
import 'package:miler/data/order_manifest.dart';
import 'package:miler/data/milk_run.dart';
import 'package:miler/data/order_events.dart';
import 'package:miler/data/work_domain.dart';
import 'package:miler/data/work_repository.dart';
import 'package:miler/data/pickup_locations.dart';
import 'package:miler/data/service_day.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/views/helpers/constants/miler_type.dart';
import 'package:miler/views/Dashboard/home/pickup_preview_sheet.dart';
@@ -241,9 +242,22 @@ class _HomepageState extends State<Homepage>
Set<String> _closedIds = <String>{};
Set<String> _rejectedIds = <String>{};
/// Notices when the day underneath the screen has moved on.
///
/// [_fetchQueues] filters against `ServiceDay.today` *as read at fetch time*,
/// so the route on screen is only ever as fresh as the last fetch. See
/// [_rolloverCheck].
final ServiceDayRollover _day = ServiceDayRollover();
/// Fires at the next local midnight. See [_armRollover].
Timer? _rollover;
/// Which trip tab is open.
int _selectedTripIndex = 0;
/// True once the rider has picked a tab himself. See [_settleTripTab].
bool _tripTabChosen = false;
/// Stops the rider has ticked, for "accept just these".
Set<String> _selectedStopIds = <String>{};
@@ -258,6 +272,7 @@ class _HomepageState extends State<Homepage>
}
_pollerSubscription?.cancel();
_locationTimer?.cancel();
_rollover?.cancel();
_httpClient.close();
super.dispose();
}
@@ -271,6 +286,7 @@ class _HomepageState extends State<Homepage>
_ensureLocationPermission();
_ensureCameraPermission();
_startPolling();
_armRollover();
// What the counters are actually called, joined on the location id the
// booking already carries. One request, cached, and every failure resolves
// to "use the row's own name" — see [PickupLocations].
@@ -971,6 +987,11 @@ class _HomepageState extends State<Homepage>
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
// Before the early return below: a resume inside the same foreground state
// still has to notice a midnight that passed while the phone was in the
// rider's pocket, and that is the case the timer cannot cover — both
// platforms suspend timers in the background.
if (state == AppLifecycleState.resumed) _rolloverCheck();
final bool foreground = state == AppLifecycleState.resumed;
if (foreground == _pollForeground) return;
_pollForeground = foreground;
@@ -1175,7 +1196,15 @@ class _HomepageState extends State<Homepage>
// are still on the rider's route — their parcels are still to be
// collected and their kilometres still to be ridden. The route brief
// must count them or it understates the day.
final acceptedStops = await getAcceptedBookings();
// Through the day filter as well. The accepted store is deliberately
// *not* pruned to today — it is "what have I taken on", and an id set
// that forgets a stop would flip that stop back to pending under the
// rider's thumb — but the route brief above the run is a statement about
// **this** day's kilometres and parcels. Yesterday's accepted-and-unclosed
// booking belongs in the set and not in that total.
//
// Only the rows are filtered; `acceptedIds` above is left whole.
final acceptedStops = _todayOnly(await getAcceptedBookings());
if (mounted && acceptedStops.length != _acceptedStops.length) {
setState(() => _acceptedStops = acceptedStops);
} else {
@@ -1192,7 +1221,9 @@ class _HomepageState extends State<Homepage>
//
// Accepted stops still also appear on Bookings — that is where he WORKS
// them. Home is where he watches the trip fill up.
final allStops = items.whereType<Map<String, dynamic>>().toList();
final allStops = _todayOnly(
items.whereType<Map<String, dynamic>>().toList(),
);
// ── The skip store is the truth about what is parked ──
//
@@ -1311,6 +1342,8 @@ class _HomepageState extends State<Homepage>
}
}
});
// After the state is applied, because the slots are derived from it.
_settleTripTab();
} catch (e) {
debugPrint('[FETCH_QUEUES] Error: $e');
if (mounted) setState(() => _fetchFailed = true);
@@ -1322,6 +1355,108 @@ class _HomepageState extends State<Homepage>
}
}
/// Opens Home on a tab that has work on it.
///
/// ── Why this became necessary ──
///
/// The tab index is a **day part**, not a position: slot 0 is the morning
/// whether or not the rider has morning work. That is the right model, and
/// while nothing could place a trip by day part it cost nothing — every trip
/// fell into the undated bucket and landed on Trip 1, so Trip 1 was where the
/// work was.
///
/// Now that [Trip.dayPartTimeOf] can place them, a rider whose whole day is
/// an afternoon run has his work under Trip 2 — correctly — and Home opened
/// on Trip 1 and told him his hub had assigned him nothing. The fix for one
/// screen must not hand him a worse version of the same lie.
///
/// So the landing tab is the first slot that actually holds stops. Three
/// things keep it from fighting the rider:
///
/// • It gives up the moment he taps a tab himself — [_tripTabChosen]. An
/// empty Trip 3 he chose deliberately is an answer he asked for.
/// • It gives up the moment the tab it is on has work, so it lands once and
/// then stops moving under him.
/// • It never *hides* a slot. The tabs still show all three, empty ones
/// included, because "the day has room left in it" is a fact he plans
/// around.
void _settleTripTab() {
if (_tripTabChosen || !mounted) return;
final trips = _trips;
final here = trips.tripAt(_selectedTripIndex);
if (here != null && here.stops.isNotEmpty) return;
final slots = trips.slots;
for (var i = 0; i < slots.length; i++) {
final t = slots[i];
if (t == null || t.stops.isEmpty) continue;
if (i != _selectedTripIndex) {
setState(() {
_selectedTripIndex = i;
_selectedStopIds.clear();
});
}
return;
}
}
/// ── Home is today's work, and only today's ──
///
/// The rule, and the reasoning behind every part of it, is
/// [ServiceDay.onlyToday] — shared with the Deliveries queue, because the two
/// screens showing the rider's live work must not disagree about which day it
/// is. What is local here is only *which* day: the one the screen is
/// currently showing, adopted so the midnight check below compares against
/// the pixels rather than reading the wall clock twice.
List<Map<String, dynamic>> _todayOnly(List<Map<String, dynamic>> stops) {
_day.rollIfNeeded();
return ServiceDay.onlyToday(stops, now: _day.day, where: 'HOME');
}
/// ── The day ending under a screen that is already open ──
///
/// [ServiceDay] answers "which day is this row" at read time, which is right
/// for the data and not sufficient for the screen: [_fetchQueues] filters
/// against the day *as read when it ran*. A rider who leaves Home open
/// through 23:59, or backgrounds the app on it and picks the phone up at six,
/// is looking at a route filtered against a day that has ended.
///
/// Two triggers, because a phone gives no single reliable one — a timer to
/// the next local midnight for the app left in the foreground, and resume for
/// every other case. Both land in [_rolloverCheck], which is idempotent.
/// The same arrangement `ActivityPage` uses, for the same reason.
void _armRollover() {
_rollover?.cancel();
_rollover = Timer(_day.untilNextDay, _rolloverCheck);
}
/// Turns Home over when the service day has moved on.
///
/// Yesterday's stops are cleared **before** the refetch, not after it. A new
/// day starts empty even with no signal, because the alternative is a rider
/// in a dead spot at six in the morning reading yesterday's route as today's
/// assignment — the exact failure this exists to stop. Anything that really
/// is the new day's comes straight back from the fetch.
void _rolloverCheck() {
if (!mounted) return;
_armRollover();
if (!_day.rollIfNeeded()) return;
setState(() {
_allTripStops = <Map<String, dynamic>>[];
_ordersMap = <String, Map<String, dynamic>>{};
_orderIds = <String>[];
_selectedOrders = <String, bool>{};
_selectedStopIds = <String>{};
_acceptedStops = <Map<String, dynamic>>[];
isAllSelected = false;
// The fetch short-circuits on an unchanged payload, and the payload has
// not changed — only the day it is read against has. Clearing the key is
// what lets the new day's answer through.
_lastQueuesJson = '';
});
_fetchQueues();
}
/// See [HomepageArrivalPrecedence]. Kept as a one-line forwarder so the call
/// site reads the same as it did while the rule itself is reachable from a
/// test — a precedence contract that can only be exercised by building the
@@ -1388,14 +1523,21 @@ class _HomepageState extends State<Homepage>
...await getSkippedOrderIds(),
};
final activeOrders = itemsV2.whereType<Map<String, dynamic>>().where((
booking,
) {
final status = (booking['orderstatus']?.toString().toLowerCase() ?? '')
.trim();
if (status != 'active') return false;
return !closedIds.contains((booking['orderid'] ?? '').toString());
}).toList();
// ── Through the same day filter as the queue ──
//
// These are merged into the trip by [_tripStops], and a row this call
// carries that the queue does not is **appended** — so an unfinished
// stop still reporting `active` from a shift two days ago walked past
// [_todayOnly] and back onto Home through the side door, wearing a LIVE
// mark. One day, one rule, however the row arrived.
final activeOrders = _todayOnly(
itemsV2.whereType<Map<String, dynamic>>().where((booking) {
final status =
(booking['orderstatus']?.toString().toLowerCase() ?? '').trim();
if (status != 'active') return false;
return !closedIds.contains((booking['orderid'] ?? '').toString());
}).toList(),
);
if (mounted) {
setState(() {
@@ -2048,7 +2190,7 @@ class _HomepageState extends State<Homepage>
stage: rung,
placeName: place,
count: ids.length,
lines: BagManifest.forGroup(batch),
lines: OrderManifest.forGroup(batch),
onConfirm: (outcome) async {
setState(() => _busyStopIds.addAll(ids));
try {
@@ -2069,9 +2211,9 @@ class _HomepageState extends State<Homepage>
}
if (refusal != null) return refusal;
if (rung == StopStatus.arrived) {
await saveBagLabels({
for (final line in BagManifest.forGroup(loaded))
line.orderId: line.bag,
await saveOrderLabels({
for (final line in OrderManifest.forGroup(loaded))
line.orderId: line.label,
});
}
if (missing.isNotEmpty) {
@@ -2230,8 +2372,8 @@ class _HomepageState extends State<Homepage>
count: ids.length,
// Named, with the bag each one travels in — this is the list he checks
// against the shelf in front of him. One order, one bag; see
// [BagManifest].
lines: BagManifest.forGroup(stops),
// [OrderManifest].
lines: OrderManifest.forGroup(stops),
onConfirm: (outcome) async {
setState(() => _busyStopIds.addAll(ids));
try {
@@ -2259,11 +2401,11 @@ class _HomepageState extends State<Homepage>
// Joe is Bag 1 of the five that left this counter. Deriving that
// later from whatever list is on screen breaks it the moment one is
// delivered, so it is written down at the one moment it is true and
// read back unchanged for the rest of the round. See [BagManifest].
// read back unchanged for the rest of the round. See [OrderManifest].
if (action == SelectionAction.pickUp) {
await saveBagLabels({
for (final line in BagManifest.forGroup(loaded))
line.orderId: line.bag,
await saveOrderLabels({
for (final line in OrderManifest.forGroup(loaded))
line.orderId: line.label,
});
}
if (missing.isNotEmpty) {
@@ -3738,6 +3880,9 @@ class _HomepageState extends State<Homepage>
// Trip 1 must not be counted by the bar while Trip 2
// is on screen. See [_selectedPendingIds].
onSelect: (index) {
// His choice from here on, even if it is an empty
// slot — see [_settleTripTab].
_tripTabChosen = true;
if (index == _selectedTripIndex) return;
setState(() {
_selectedTripIndex = index;