miler map
This commit is contained in:
@@ -63,7 +63,6 @@ import 'package:miler/data/consignment_state.dart';
|
||||
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/order_manifest.dart';
|
||||
import 'package:miler/data/milk_run.dart';
|
||||
import 'package:miler/data/order_events.dart';
|
||||
@@ -233,6 +232,11 @@ class _HomepageState extends State<Homepage>
|
||||
/// "picked", and `picked` is the delivery. See the collected store.
|
||||
Set<String> _collectedIds = <String>{};
|
||||
|
||||
/// `next_action` per order, as the pivot returned it. Home reads it only to
|
||||
/// count what is waiting on the other tab — the two screens must hand
|
||||
/// [WorkBoundary] the same arguments or their counts drift apart.
|
||||
Map<String, String> _pivotActions = <String, String>{};
|
||||
|
||||
/// Orders the rider has written off today — delivered, cancelled or skipped.
|
||||
///
|
||||
/// The union of the completed and skipped stores, both of which record a
|
||||
@@ -291,6 +295,9 @@ class _HomepageState extends State<Homepage>
|
||||
// booking already carries. One request, cached, and every failure resolves
|
||||
// to "use the row's own name" — see [PickupLocations].
|
||||
unawaited(PickupLocations.ensureLoaded());
|
||||
// The building the logistics run is headed by — see [RiderHub]. Read from
|
||||
// prefs the login wrote, so it costs nothing and cannot fail the fetch.
|
||||
unawaited(RiderHub.ensureLoaded());
|
||||
BottomPage.currentTab.addListener(_onTabChanged);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_fetchQueues();
|
||||
@@ -677,7 +684,7 @@ class _HomepageState extends State<Homepage>
|
||||
'Not at Pickup Location',
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xLarge(context),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: _textPrimary,
|
||||
),
|
||||
@@ -704,7 +711,7 @@ class _HomepageState extends State<Homepage>
|
||||
'OK',
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.regular(context),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
@@ -764,7 +771,7 @@ class _HomepageState extends State<Homepage>
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: _textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
content: Text(
|
||||
@@ -1180,6 +1187,7 @@ class _HomepageState extends State<Homepage>
|
||||
final acceptedIds = await getAcceptedOrderIds();
|
||||
final rejectedIds = await getRejectedOrderIds();
|
||||
final collectedIds = await getCollectedOrderIds();
|
||||
final pivotActions = await getPivotNextActions();
|
||||
// Stops parked for a return visit. They are Activity's now — see the
|
||||
// stamping loop below and `TripCard._showsOnHome`.
|
||||
final skippedIds = await getSkippedOrderIds();
|
||||
@@ -1330,6 +1338,7 @@ class _HomepageState extends State<Homepage>
|
||||
_acceptedIds = acceptedIds;
|
||||
_rejectedIds = rejectedIds;
|
||||
_collectedIds = collectedIds;
|
||||
_pivotActions = pivotActions;
|
||||
_ordersMap = newOrdersMap;
|
||||
_orderIds = newOrderIds;
|
||||
_selectedOrders = newSelection;
|
||||
@@ -1670,14 +1679,6 @@ class _HomepageState extends State<Homepage>
|
||||
}
|
||||
if (base == null) return;
|
||||
|
||||
// A telemetry log about a booking the backend has never issued is a row
|
||||
// nobody can join to anything. The meal line's stops exist only on this
|
||||
// phone until the endpoints land — see [MealRunMock].
|
||||
if (MealRunMock.active) {
|
||||
debugPrint('[MEAL_MOCK] pickup log not posted for $orderId');
|
||||
return;
|
||||
}
|
||||
|
||||
final coords = await _getValidCoordinates();
|
||||
final now = DateTime.now();
|
||||
final logdate =
|
||||
@@ -1719,7 +1720,7 @@ class _HomepageState extends State<Homepage>
|
||||
if (selectedOrderIds.isEmpty) return "Accept Bookings";
|
||||
|
||||
final statuses = selectedOrderIds
|
||||
.map((id) => _getStatusFromOrder(_ordersMap[id] ?? {}))
|
||||
.map((id) => _getStatusFromOrder(_bulkStopFor(id) ?? const {}))
|
||||
.toSet();
|
||||
|
||||
if (statuses.length == 1 && statuses.first == 'ACCEPT') {
|
||||
@@ -1794,6 +1795,7 @@ class _HomepageState extends State<Homepage>
|
||||
_tripStops,
|
||||
collectedIds: _collectedIds,
|
||||
acceptedIds: _acceptedIds,
|
||||
pivotActions: _pivotActions,
|
||||
// ── The set the other screen was passing and this one was not ──
|
||||
//
|
||||
// Both screens call [WorkBoundary.deliveryQueue], which was supposed to be
|
||||
@@ -1911,17 +1913,29 @@ class _HomepageState extends State<Homepage>
|
||||
/// The middle ground between one-at-a-time and all-at-once: on a long trip
|
||||
/// he can scan the list, tick the four addresses he knows are open, and take
|
||||
/// them in a single action instead of eight taps.
|
||||
/// ── Both filters here were keyed on the undecided list ──
|
||||
///
|
||||
/// `_ordersMap.containsKey` dropped every already-accepted id before the
|
||||
/// action ran, and `_selectedOrders` was then rebuilt over `_orderIds` —
|
||||
/// also undecided-only — so an accepted id could not survive into
|
||||
/// [_handleBulkAction] by either route. The selection bar could accept and
|
||||
/// nothing else, whatever its label said.
|
||||
///
|
||||
/// Both now resolve through [_bulkStopFor], so a ticked set that has already
|
||||
/// been accepted arrives intact and the action advances it to arrived, and
|
||||
/// then to picked. Which rung it is remains [_handleBulkAction]'s decision,
|
||||
/// read off the stops themselves.
|
||||
Future<void> _acceptSelectedStops(List<String> orderIds) async {
|
||||
final ids = orderIds.where((id) => _ordersMap.containsKey(id)).toList();
|
||||
final ids = orderIds.where((id) => _bulkStopFor(id) != null).toList();
|
||||
if (ids.isEmpty || _acceptingTripId != null) return;
|
||||
|
||||
setState(() {
|
||||
_acceptingTripId = 'selection';
|
||||
_selectedOrders = {for (final id in _orderIds) id: ids.contains(id)};
|
||||
_selectedOrders = {for (final id in ids) id: true};
|
||||
});
|
||||
|
||||
try {
|
||||
await _handleBulkAction(navigateToBookings: false);
|
||||
await _handleBulkAction();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -2331,6 +2345,24 @@ class _HomepageState extends State<Homepage>
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The stop a bulk selection refers to, wherever it currently lives.
|
||||
///
|
||||
/// ── Why [_ordersMap] on its own could only ever accept ──
|
||||
///
|
||||
/// `_ordersMap` is built from `validOrders`, which is deliberately the
|
||||
/// *undecided* stops only — so a stop leaves it the moment it is accepted.
|
||||
/// Every rung past ACCEPT looked its stop up there, got null and `continue`d,
|
||||
/// and the label is derived from the same map: a selection of accepted stops
|
||||
/// read back as `ACCEPT`, so the button said **Accept Bookings** and posted
|
||||
/// another accept. `Move to Arrived` and `Move to Picked` were unreachable
|
||||
/// from the selection bar even though [_handleBulkAction] implements both.
|
||||
///
|
||||
/// [_allTripStops] keeps every stop in every state, which is exactly what the
|
||||
/// rungs past acceptance need. On a milk run the order stays on Home to be
|
||||
/// walked accepted → arrived → picked — see [MilkRun].
|
||||
Map<String, dynamic>? _bulkStopFor(String orderId) =>
|
||||
_ordersMap[orderId] ?? _stopById(orderId);
|
||||
|
||||
/// Walks every ticked stop up one rung, in one gesture.
|
||||
///
|
||||
/// This is the shape of the job at a kitchen: five parcels come off one
|
||||
@@ -2794,7 +2826,7 @@ class _HomepageState extends State<Homepage>
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
@@ -2835,7 +2867,7 @@ class _HomepageState extends State<Homepage>
|
||||
actionLabel,
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
@@ -2854,7 +2886,27 @@ class _HomepageState extends State<Homepage>
|
||||
/// checks each stop is still accepted before touching it.
|
||||
List<Map<String, dynamic>> _lastAcceptedBatch = const [];
|
||||
|
||||
Future<void> _handleBulkAction({bool navigateToBookings = true}) async {
|
||||
/// Walks the ticked stops up one rung: ACCEPT → ACCEPTED → ARRIVED → PICKED.
|
||||
///
|
||||
/// ── Where the rider ends up is the line's decision, not the caller's ──
|
||||
///
|
||||
/// This took a `navigateToBookings` flag, defaulted to `true`, and the two
|
||||
/// branches it selected disagreed about the thing [ServiceProfile] already
|
||||
/// answers. The tick-based path passed `false` and got the profile-aware
|
||||
/// branch; the bulk action button passed nothing, got the default, and hit an
|
||||
/// unconditional `Get.offAll(BottomPage(initialIndex: 1))`.
|
||||
///
|
||||
/// On a **milk run** that is the wrong move and it broke the whole ladder.
|
||||
/// The rider accepts at a kitchen, and the order has to stay on Home to be
|
||||
/// walked to arrived and then picked — see [MilkRun]. Throwing him at the
|
||||
/// work tab on accept sent him to a tab that was correctly empty, made the
|
||||
/// accept look like it had filed his order somewhere else, and left the two
|
||||
/// rungs that follow it unreachable from where he was standing.
|
||||
///
|
||||
/// So the flag is gone. `handsOffAtCollection` is the one rule and it is read
|
||||
/// in one place: the line that hands off at *acceptance* (logistics) moves
|
||||
/// him, the line that hands off at *collection* (milk man) keeps him on Home.
|
||||
Future<void> _handleBulkAction() async {
|
||||
final selectedOrderIds = _selectedOrders.entries
|
||||
.where((e) => e.value)
|
||||
.map((e) => e.key)
|
||||
@@ -2863,7 +2915,7 @@ class _HomepageState extends State<Homepage>
|
||||
if (selectedOrderIds.isEmpty) return;
|
||||
|
||||
final statuses = selectedOrderIds
|
||||
.map((id) => _getStatusFromOrder(_ordersMap[id] ?? {}))
|
||||
.map((id) => _getStatusFromOrder(_bulkStopFor(id) ?? const {}))
|
||||
.toSet();
|
||||
|
||||
String targetStatus;
|
||||
@@ -2984,8 +3036,15 @@ class _HomepageState extends State<Homepage>
|
||||
|
||||
final List<Map<String, dynamic>> acceptedBookings = [];
|
||||
|
||||
/// Orders this batch confirmed as collected, mirrored into [_collectedIds]
|
||||
/// once the loop is done so the cards re-read their leg immediately rather
|
||||
/// than at the next poll.
|
||||
final Set<String> bulkCollected = <String>{};
|
||||
|
||||
for (final orderId in selectedOrderIds) {
|
||||
final Booking = _ordersMap[orderId];
|
||||
// Not `_ordersMap` — a stop past ACCEPT is no longer in it, and every
|
||||
// rung after the first was silently skipped here. See [_bulkStopFor].
|
||||
final Booking = _bulkStopFor(orderId);
|
||||
if (Booking == null) continue;
|
||||
|
||||
final pickupId = int.tryParse('${Booking['pickupid'] ?? 0}') ?? 0;
|
||||
@@ -3071,12 +3130,45 @@ class _HomepageState extends State<Homepage>
|
||||
if (success) {
|
||||
_startPickupPosting(Booking);
|
||||
_hiddenOrderIds.add(orderId);
|
||||
// Picked → remove from the accepted store so it stops counting as
|
||||
// an open booking on the Bookings tab, and record it as finished
|
||||
// so it lands on Activity. Both stores, or the stop falls out of
|
||||
// the app entirely.
|
||||
await removeAcceptedBookings([orderId]);
|
||||
await addCompletedBookings([Booking]);
|
||||
|
||||
// ── The write that says the load is in his hands ──
|
||||
//
|
||||
// `_advanceStop` records this rung in the collected store and
|
||||
// this branch did not, so a bag picked from the **selection bar**
|
||||
// was never marked as carried. That one omission is what put
|
||||
// **Start Pickup** on a delivery: [MilkRun.workingKind] reads
|
||||
// exactly this set to decide which leg a stop is on, and the
|
||||
// booking adapter stamps `type: pickup` on every row, so with an
|
||||
// empty set a bag already in the box still described itself as a
|
||||
// collection. The same answer feeds `toDrop` on the queue's
|
||||
// distance, so the card also measured to the kitchen he had just
|
||||
// left instead of to the customer's door — which is the "km and
|
||||
// time are wrong" half of it.
|
||||
//
|
||||
// Recorded only after the backend confirmed, same rule as accept.
|
||||
await addCollectedOrderIds([orderId]);
|
||||
bulkCollected.add(orderId);
|
||||
// One copy of the day, and it is now stale — Deliveries must not
|
||||
// render this order from a response that predates the hand-over.
|
||||
unawaited(WorkRepository.instance.invalidate());
|
||||
|
||||
// ── Picked is a hand-over on one line and a finish on the other ──
|
||||
//
|
||||
// This branch closed the booking outright. That is right for
|
||||
// **logistics**, where what he collects goes to the hub and the
|
||||
// booking's own story ends at the counter.
|
||||
//
|
||||
// On a **milk run** it is wrong and it loses the order: collection
|
||||
// is the *middle* of the day — every bag is followed by a round of
|
||||
// drops he makes himself — so filing it as completed sent it to
|
||||
// Activity instead of to Deliveries, and the delivery leg had
|
||||
// nothing to work. See [ServiceProfile.handsOffAtCollection].
|
||||
if (ServiceProfile.active.handsOffAtCollection) {
|
||||
await addAcceptedBookings([Booking]);
|
||||
} else {
|
||||
await removeAcceptedBookings([orderId]);
|
||||
await addCompletedBookings([Booking]);
|
||||
}
|
||||
// Off the arrived rung — it has been collected.
|
||||
await removeArrivedOrderIds([orderId]);
|
||||
}
|
||||
@@ -3093,6 +3185,12 @@ class _HomepageState extends State<Homepage>
|
||||
}
|
||||
}
|
||||
|
||||
// The leg every card reads is derived from this set, so it has to land
|
||||
// before the next build — not at the next poll three seconds later.
|
||||
if (bulkCollected.isNotEmpty && mounted) {
|
||||
setState(() => _collectedIds = {..._collectedIds, ...bulkCollected});
|
||||
}
|
||||
|
||||
// Persist accepted bookings so the Bookings tab can show them and Home
|
||||
// keeps them filtered out.
|
||||
if (acceptedBookings.isNotEmpty) {
|
||||
@@ -3183,7 +3281,7 @@ class _HomepageState extends State<Homepage>
|
||||
// the spot rather than at the next tick of its poller, and the switch is an
|
||||
// IndexedStack index change — the same ScaffoldMessenger, so the UNDO
|
||||
// snackbar below travels with him and is still there to be pressed.
|
||||
if (bulkSucceeded && targetStatus == 'ACCEPTED' && !navigateToBookings) {
|
||||
if (bulkSucceeded && targetStatus == 'ACCEPTED') {
|
||||
final taken = _lastAcceptedBatch;
|
||||
final n = taken.length;
|
||||
|
||||
@@ -3234,12 +3332,6 @@ class _HomepageState extends State<Homepage>
|
||||
return;
|
||||
}
|
||||
|
||||
if (bulkSucceeded && targetStatus == 'ACCEPTED' && navigateToBookings) {
|
||||
AppFeedback.successGlobal(
|
||||
'Moved to your ${ServiceProfile.active.workTabLabel}',
|
||||
);
|
||||
Get.offAll(() => const BottomPage(initialIndex: 1));
|
||||
}
|
||||
}
|
||||
|
||||
/// The whole route in one object: load, distance, time windows, shift.
|
||||
@@ -3256,6 +3348,20 @@ class _HomepageState extends State<Homepage>
|
||||
riderLng: _lastGeocodedPosition?.longitude ?? _riderLng,
|
||||
);
|
||||
|
||||
/// A trip with nothing in it, for a slot the hub has not filled.
|
||||
///
|
||||
/// Every figure the brief draws is derived from the stops, so an empty list
|
||||
/// gives `0 stops left`, a progress track at zero and an em dash under
|
||||
/// duration, distance and payment on its own — there is no "empty" variant of
|
||||
/// the brief to keep in step with the real one, because this *is* the real
|
||||
/// one with nothing in it.
|
||||
Trip _unassignedTrip() => Trip.fromStops(
|
||||
id: 'unassigned-$_selectedTripIndex',
|
||||
stops: const [],
|
||||
hubLat: _lastGeocodedPosition?.latitude ?? _riderLat,
|
||||
hubLng: _lastGeocodedPosition?.longitude ?? _riderLng,
|
||||
);
|
||||
|
||||
/// One configured [TripCard] for the selected trip.
|
||||
///
|
||||
/// Home does not render the card as a single widget any more: its summary, its
|
||||
@@ -3556,7 +3662,7 @@ class _HomepageState extends State<Homepage>
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.4,
|
||||
color: ColorConstants.slateText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
@@ -3564,13 +3670,13 @@ class _HomepageState extends State<Homepage>
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
Text(
|
||||
'Stay on duty — new orders appear here the moment '
|
||||
'your office assigns them.',
|
||||
'Stay on duty new bookings appear here the moment '
|
||||
'your office assigns you.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13.5.sp,
|
||||
height: 1.45,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ColorConstants.secondaryText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
@@ -3601,7 +3707,7 @@ class _HomepageState extends State<Homepage>
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: _textPrimary,
|
||||
),
|
||||
@@ -3624,7 +3730,7 @@ class _HomepageState extends State<Homepage>
|
||||
"Go Online",
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
@@ -3717,7 +3823,7 @@ class _HomepageState extends State<Homepage>
|
||||
'${_getBulkButtonLabel()} (${selectedCount})',
|
||||
style: TextStyle(
|
||||
fontSize: 15.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.3,
|
||||
@@ -3763,7 +3869,7 @@ class _HomepageState extends State<Homepage>
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: _textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
content: Text(
|
||||
@@ -3868,32 +3974,9 @@ class _HomepageState extends State<Homepage>
|
||||
// when it collapses, which is what a rider scrolling needs;
|
||||
// three tabs held permanently would be the same information
|
||||
// twice, in the space the work is supposed to be in.
|
||||
if (trips.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(20.w, 14.h, 20.w, 2.h),
|
||||
child: TripSelector(
|
||||
trips: trips,
|
||||
selectedIndex: _selectedTripIndex,
|
||||
acceptedIds: _acceptedIds,
|
||||
rejectedIds: _rejectedIds,
|
||||
// Selection is per-trip, so the ticks do not travel
|
||||
// with the rider between trips — a tick made on
|
||||
// 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;
|
||||
_selectedStopIds.clear();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
// The tabs are no longer a sliver of their own. They are
|
||||
// the head of the panel that holds the trip, and that panel
|
||||
// is built with the work below — see the `else` branch.
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// READING ORDER
|
||||
@@ -3989,15 +4072,119 @@ class _HomepageState extends State<Homepage>
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
// ── The panel is the page ──
|
||||
//
|
||||
// Tabs and trip used to be two slivers: a control in one
|
||||
// box, the work it governs in the next, with nothing but
|
||||
// adjacency to say they were related. The panel encloses
|
||||
// the run now — the selected tab is the top edge of the
|
||||
// brand surface the brief, the route and the action bar all
|
||||
// sit inside, so which trip is on screen is said once, by
|
||||
// the shape holding it.
|
||||
//
|
||||
// Still not pinned. The header keeps
|
||||
// `Trip 1 · 17 left · 33/50` when it collapses, which is
|
||||
// what a rider scrolling needs; a tab strip held
|
||||
// permanently would be the same information twice, in the
|
||||
// space the work is supposed to be in.
|
||||
// ── The tabs are attached to the trip's figures ──
|
||||
//
|
||||
// Tabs and brief used to be two boxes: a control in one
|
||||
// sliver, the figures it governs in the next, with nothing
|
||||
// but adjacency to say they were related. They are one
|
||||
// panel now — the selected tab is the top edge of the glass
|
||||
// the brief sits on, so which trip is on screen is said
|
||||
// once, by the shape holding it.
|
||||
//
|
||||
// The route and the action bar stay outside it, on the
|
||||
// canvas with their own white panel. A version of this put
|
||||
// the whole page inside the panel and it was too much: the
|
||||
// ground a rider reads a route off should be the quietest
|
||||
// thing on the screen, not the loudest.
|
||||
//
|
||||
// Still not pinned. The header keeps
|
||||
// `Trip 1 · 17 left · 33/50` when it collapses, which is
|
||||
// what a rider scrolling needs; a tab strip held
|
||||
// permanently would be the same information twice, in the
|
||||
// space the work is supposed to be in.
|
||||
else ...[
|
||||
SliverToBoxAdapter(
|
||||
// Air above, and the panel takes the same gutter as
|
||||
// every other layer-1 surface on Home. The inset inside
|
||||
// it is [TripSelector.contentInset], so the brief's copy
|
||||
// lands on the same vertical as `TODAY'S RUN` below.
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
MilerSurface.panelGutter,
|
||||
12.h,
|
||||
MilerSurface.panelGutter,
|
||||
0,
|
||||
),
|
||||
child: TripSelector(
|
||||
trips: trips,
|
||||
selectedIndex: _selectedTripIndex,
|
||||
acceptedIds: _acceptedIds,
|
||||
rejectedIds: _rejectedIds,
|
||||
// Selection is per-trip, so the ticks do not travel
|
||||
// with the rider between trips — a tick made on
|
||||
// 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;
|
||||
_selectedStopIds.clear();
|
||||
});
|
||||
},
|
||||
// ── An unfilled slot gets the same panel ──
|
||||
//
|
||||
// It used to get a different one: a line of its own
|
||||
// reading `Not assigned yet`, which made the panel
|
||||
// a 32pt stub under a tab twice its height, and
|
||||
// changed the shape of the screen depending on
|
||||
// whether the hub had got to this slot yet.
|
||||
//
|
||||
// The brief of an empty slot is a real brief — it
|
||||
// is just all zeroes. `0 stops left`, an empty
|
||||
// track, and an em dash under every figure that has
|
||||
// nothing to report. So the panel is the same panel
|
||||
// at the same height, and when the hub assigns the
|
||||
// trip the figures fill in where the dashes were
|
||||
// instead of the page changing shape around the
|
||||
// rider.
|
||||
body: SmoothSwap(
|
||||
child: Reveal(
|
||||
// Keyed on the trip, so switching tabs plays
|
||||
// the entrance once and the 5s poll never
|
||||
// replays it. The empty slot keys on its own
|
||||
// index for the same reason.
|
||||
key: ValueKey(
|
||||
'brief_${trip?.id ?? 'unassigned-$_selectedTripIndex'}',
|
||||
),
|
||||
// The panel above is the surface and it is the
|
||||
// brand bar's red, so the brief drops its own
|
||||
// card and inverts its ink.
|
||||
child: _tripCardFor(
|
||||
trip ?? _unassignedTrip(),
|
||||
).buildSummary(onBrand: true),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// The route and the action bar, on the canvas.
|
||||
SliverToBoxAdapter(
|
||||
child: SmoothSwap(
|
||||
// ── A slot the hub has not filled ──
|
||||
//
|
||||
// The day has work, just not on this tab. Says so
|
||||
// by name rather than reusing the whole-day empty
|
||||
// state, which would claim he has nothing to do
|
||||
// while another trip is running.
|
||||
// The day has work, just not on this tab. Says so by
|
||||
// name rather than reusing the whole-day empty state,
|
||||
// which would claim he has nothing to do while another
|
||||
// trip is running.
|
||||
child: trip == null
|
||||
? KeyedSubtree(
|
||||
key: ValueKey(
|
||||
@@ -4008,37 +4195,21 @@ class _HomepageState extends State<Homepage>
|
||||
),
|
||||
)
|
||||
: Reveal(
|
||||
// Keyed on the trip, so switching tabs plays
|
||||
// the entrance once and the 5s poll never
|
||||
// replays it.
|
||||
key: ValueKey('reveal_${trip.id}'),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
// The brief sits UNDER the tabs, not above.
|
||||
//
|
||||
// It describes one trip, and the tabs are what
|
||||
// choose that trip — so above them it was
|
||||
// answering a question the rider had not been
|
||||
// asked yet, and switching tabs changed a
|
||||
// panel he had already scrolled past. Below
|
||||
// them it reads as the selected tab's own
|
||||
// content, which is what it is, and it lands
|
||||
// inside the per-trip [Reveal] so it swaps
|
||||
// with the route it belongs to.
|
||||
_tripCardFor(trip).buildSummary(),
|
||||
// The "one thing to do now" head that
|
||||
// used to sit here is gone. It drew the
|
||||
// current place, its load and a
|
||||
// full-width **Navigate to pickup** —
|
||||
// all of which the timeline's own group
|
||||
// node below already carries, button
|
||||
// included. Two identical red buttons
|
||||
// for one destination was the single
|
||||
// largest thing on this screen; the
|
||||
// node that owns the journey owns the
|
||||
// action.
|
||||
// full-width **Navigate to pickup** — all
|
||||
// of which the timeline's own group node
|
||||
// below already carries, button included.
|
||||
// Two identical red buttons for one
|
||||
// destination was the single largest
|
||||
// thing on this screen; the node that owns
|
||||
// the journey owns the action.
|
||||
_tripCardFor(trip).buildStops(),
|
||||
_tripCardFor(trip).buildActionBar(),
|
||||
],
|
||||
@@ -4046,6 +4217,7 @@ class _HomepageState extends State<Homepage>
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// ── Room for everything that floats over this list ──
|
||||
//
|
||||
@@ -4238,7 +4410,7 @@ Widget _buildDetailRow(String label, String value) {
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: ColorConstants.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14.sp,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
@@ -4258,7 +4430,7 @@ Widget _buildDetailRow(String label, String value) {
|
||||
value.contains('Accepted')
|
||||
? primaryRed
|
||||
: ColorConstants.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14.sp,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
height: 1.35,
|
||||
@@ -4336,7 +4508,7 @@ void _showProductDetailsSheet(
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ColorConstants.secondaryText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user