device infos
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user