Session expiry, arrival geofence guard, multi-destination stops
Three fixes found by running the app on a real handset against production. 1. An expired token left the app looking signed in and unable to work. MilerApi.onUnauthorized was declared and called on every 401 but never assigned, so the token was dropped and nothing else happened: the profile stayed on disk, logged_out stayed false, and the rider saw his own name over a dashboard whose every call returned 401. He reads that as "no work today". The teardown now lives in endSession() and both ways out of a session — the Log out button and the 401 path — use it. 2. Arrived was written locally even when the rider was not there. updateArrivedStatus answers false for three different things and the caller treated all of them as "the write did not land", which is only true of one. A geofence refusal and a server refusal now stop the rung and hand back the reason; a dead network still advances, as it should. 3. A multi-destination customer pickup collapsed onto one stop. GET /miler/bookings returns a row per destination once collected, all with the same bookingid and reference. Every local store keys on that id, so the accepted store deduped two of three drops away and their consignment ids were unrecoverable. orderid is now the stop key; bookingreference stays the booking's name. Cards show "Stop 2 of 3" and the receiver's own name and number rather than the sender's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
This commit is contained in:
@@ -136,7 +136,6 @@ class PickupCard extends StatelessWidget {
|
||||
], 'Address not available');
|
||||
final String orderId = _val(['orderid']);
|
||||
final String notes = _val(['notes', 'Notes']);
|
||||
final String phone = _val(['pickupcontactno']);
|
||||
|
||||
final int deliverQty = deliveryParcelCount(item);
|
||||
final int collectQty = pickupParcelCount(item);
|
||||
@@ -157,15 +156,54 @@ class PickupCard extends StatelessWidget {
|
||||
// The receiver names the far end whenever the rider is carrying it there —
|
||||
// whether that is a milk round (line-level) or a hyperlocal parcel the
|
||||
// backend routed direct (per-order).
|
||||
// Asked of the ROW first. On a meal tenant this static answered true for
|
||||
// every card on the screen, so a CX customer pickup sitting next to a
|
||||
// kitchen load was drawn as though the rider were already carrying it to a
|
||||
// receiver. [WorkPolicy] reads the row's own `next_action` /
|
||||
// `pickup_source_type` and only falls back to the line where the row is
|
||||
// silent — which keeps every existing payload rendering exactly as it does
|
||||
// today.
|
||||
final bool carryingToCustomer =
|
||||
ServiceProfile.active.deliversToCustomer || leg.isCustomer;
|
||||
WorkPolicy.deliversToCustomer(item) || leg.isCustomer;
|
||||
final bool pickupIsSource = carryingToCustomer && source.isNotEmpty;
|
||||
final String pickupName = pickupIsSource ? source : customer;
|
||||
final String dropAddress = _val(['dropaddress', 'DropAddress']);
|
||||
final String dropName = pickupIsSource
|
||||
// ── Who is actually at the far end ──
|
||||
//
|
||||
// `pickupcustomer` is the booking's customer, which on a parcel booking is
|
||||
// the **sender** — the person the rider collected FROM. Using it to name the
|
||||
// drop put the sender's name over the receiver's door, and on a
|
||||
// multi-destination pickup it put the same name over all three of them.
|
||||
// The receiver is carried per destination and is the honest answer whenever
|
||||
// the payload has one.
|
||||
final String recipient = _val(['recipientname']);
|
||||
final String dropName = recipient.isNotEmpty
|
||||
? recipient
|
||||
: pickupIsSource
|
||||
? customer
|
||||
: (dropAddress.isEmpty ? '' : 'Collection centre');
|
||||
|
||||
// ── The Call button has to dial the door he is standing at ──
|
||||
//
|
||||
// `pickupcontactno` is the booking's customer — the SENDER — and it was
|
||||
// what this button dialled on every card, delivery legs included. So a
|
||||
// rider at the receiver's gate pressed Call and rang the man he had
|
||||
// collected from that morning, in another city; on a three-drop pickup he
|
||||
// rang him at all three gates. The receiver's own number is carried per
|
||||
// destination and is the right one to ring whenever the rider is on his way
|
||||
// to that receiver — the same test the name above uses, so the card cannot
|
||||
// show one person and phone another.
|
||||
final String recipientPhone = _val(['recipientphone']);
|
||||
final String phone = (recipient.isNotEmpty && recipientPhone.isNotEmpty)
|
||||
? recipientPhone
|
||||
: _val(['pickupcontactno']);
|
||||
|
||||
/// `Stop 2 of 3` on a customer pickup with several doors, `''` otherwise.
|
||||
/// Sits with the leg eyebrow because it answers the same question — what
|
||||
/// kind of stop is this — and because three cards that differ only in their
|
||||
/// address are three chances to deliver the wrong bag.
|
||||
final String stopLabel = MilkRun.stopLabel(item);
|
||||
|
||||
/// True once the rider is carrying it: the drop becomes the destination and
|
||||
/// takes the top of the card.
|
||||
// ── The immediate destination leads; never a hub leg's receiver ──
|
||||
@@ -179,6 +217,46 @@ class PickupCard extends StatelessWidget {
|
||||
(pickupIsSource || leg.isCustomer) &&
|
||||
(dropName.isNotEmpty || dropAddress.isNotEmpty);
|
||||
|
||||
// ── On a base leg the BASE leads ──
|
||||
//
|
||||
// The card was right to refuse the receiver — he is in another district and
|
||||
// leading with him is the navigation mistake the rule above exists to
|
||||
// prevent. But refusing the receiver only left `pickupName`, so a handover
|
||||
// card read:
|
||||
//
|
||||
// BASE HANDOVER
|
||||
// Anitha R
|
||||
// Gandhipuram
|
||||
//
|
||||
// …the customer he collected from an hour ago, under a heading telling him
|
||||
// to go to a base. The place he is actually going appeared nowhere on the
|
||||
// card and only on the sheet's slider, after he had already set off. On a
|
||||
// round with two bases that is a parcel left in the wrong building.
|
||||
//
|
||||
// `next_hub` rides on the row (the backend sends all six fields), so the
|
||||
// destination is known here. A base with no coordinates still leads with
|
||||
// its name — it is a caption rather than a destination, and the fence
|
||||
// refuses the handover anyway with a sentence pointing at the office.
|
||||
final HandoverHub? handoverBase = leg.isHub
|
||||
? (HandoverHub.from(item['next_hub'] ?? item['nexthub']) ??
|
||||
PickupLocations.baseFor(
|
||||
item['next_hub_id'] ?? item['nexthubid'] ?? item['hubid'],
|
||||
))
|
||||
: null;
|
||||
final bool baseLeads = handoverBase != null;
|
||||
|
||||
/// What the card is headed with, in one place so the name and the address
|
||||
/// below it can never describe two different places.
|
||||
final String headlineName = baseLeads
|
||||
? handoverBase.name
|
||||
: (dropLeads ? dropName : pickupName);
|
||||
final String headlineAddress = baseLeads
|
||||
? [
|
||||
handoverBase.address,
|
||||
handoverBase.pincode,
|
||||
].where((v) => v.trim().isNotEmpty).join(', ')
|
||||
: '';
|
||||
|
||||
// ── No accent stripe any more ──
|
||||
//
|
||||
// The card carried a 4px coloured bar down its left edge, on the argument
|
||||
@@ -261,9 +339,15 @@ class PickupCard extends StatelessWidget {
|
||||
// has to say which before the rider reads the place.
|
||||
// Drawn only once the parcel is actually his; an
|
||||
// uncollected stop has no leg yet.
|
||||
if (leg.leg.eyebrow.isNotEmpty) ...[
|
||||
if (leg.leg.eyebrow.isNotEmpty ||
|
||||
stopLabel.isNotEmpty) ...[
|
||||
Text(
|
||||
leg.leg.eyebrow,
|
||||
[
|
||||
if (leg.leg.eyebrow.isNotEmpty)
|
||||
leg.leg.eyebrow,
|
||||
if (stopLabel.isNotEmpty)
|
||||
stopLabel.toUpperCase(),
|
||||
].join(' · '),
|
||||
style: TextStyle(
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
@@ -279,9 +363,7 @@ class PickupCard extends StatelessWidget {
|
||||
SizedBox(height: 3.h),
|
||||
],
|
||||
Text(
|
||||
(dropLeads ? dropName : pickupName).isEmpty
|
||||
? 'Not named'
|
||||
: (dropLeads ? dropName : pickupName),
|
||||
headlineName.isEmpty ? 'Not named' : headlineName,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
@@ -312,6 +394,17 @@ class PickupCard extends StatelessWidget {
|
||||
// lost — see [areaOf].
|
||||
Text(
|
||||
() {
|
||||
// A base is read at a gate, so it gets its street
|
||||
// and pincode rather than the locality summary the
|
||||
// other legs use — "Peelamedu" is not enough to
|
||||
// find a loading bay by.
|
||||
if (baseLeads) {
|
||||
// Empty rather than repeating the name: a base
|
||||
// with no address on the row has nothing more
|
||||
// to say here, and printing "Salem Base" twice
|
||||
// reads as a rendering fault.
|
||||
return headlineAddress;
|
||||
}
|
||||
final full = dropLeads
|
||||
? (dropAddress.isEmpty
|
||||
? address
|
||||
@@ -1560,7 +1653,7 @@ String _destinationName(Map<String, dynamic> item) {
|
||||
String _destinationAddress(Map<String, dynamic> item) {
|
||||
final area = areaOf(
|
||||
item,
|
||||
preferDrop: ServiceProfile.active.deliversToCustomer,
|
||||
preferDrop: WorkPolicy.deliversToCustomer(item),
|
||||
);
|
||||
if (area.isNotEmpty) return area;
|
||||
|
||||
@@ -1569,6 +1662,6 @@ String _destinationAddress(Map<String, dynamic> item) {
|
||||
final drop = (item['dropaddress'] ?? item['DropAddress'] ?? '')
|
||||
.toString()
|
||||
.trim();
|
||||
if (ServiceProfile.active.deliversToCustomer && drop.isNotEmpty) return drop;
|
||||
if (WorkPolicy.deliversToCustomer(item) && drop.isNotEmpty) return drop;
|
||||
return (item['pickupaddress'] ?? '').toString().trim();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user