miler map

This commit is contained in:
2026-09-09 12:55:23 +05:30
parent 074cc0eccf
commit 127fa062ed
143 changed files with 4315 additions and 2291 deletions

View File

@@ -69,6 +69,7 @@ Future<void> migrateLegacyStores() async {
_kArrivedOrderIdsKeyBase,
_kCollectedOrderIdsKeyBase,
_kConsignmentIdsKeyBase,
_kPivotNextActionKeyBase,
_kOutForDeliveryKeyBase,
_kNotLoadedKeyBase,
_kOrderLabelsKeyBase,
@@ -115,6 +116,7 @@ Future<void> clearScopedStores() async {
_kArrivedOrderIdsKeyBase,
_kCollectedOrderIdsKeyBase,
_kConsignmentIdsKeyBase,
_kPivotNextActionKeyBase,
_kOutForDeliveryKeyBase,
_kNotLoadedKeyBase,
_kOrderLabelsKeyBase,
@@ -714,6 +716,89 @@ Future<void> forgetConsignmentIds(List<String> orderIds) async {
);
}
// ─────────────────────────────────────────────────────────────────────────
// THE PIVOT'S ROUTING ANSWER — which way this parcel went at pickup-complete
//
// `next_action` is the server naming the next leg: `start_delivery` for a
// parcel this rider delivers himself, `inward_at_hub` for one that enters the
// network. It is returned **once**, by the pivot, and it was being logged and
// thrown away — the only trace of it was `lastPivotNextAction`, a single
// in-memory string on a controller, which is not per order and does not
// survive a rebuild.
//
// So a hub-routed parcel was indistinguishable from a hyperlocal one on the
// very next poll, and `NextLegResolver` had nothing to fall back on when the
// consignment read `Created` — the state that cannot decide anything by itself.
//
// ── This is continuity, not authority ──
//
// It is rung 4 of four, below every live server reading, deliberately: it says
// what the server decided *at the pickup*, and if the server has since said
// something different the server wins. See [NextLegResolver] for the full
// precedence table.
//
// ── It should not have to exist ──
//
// The right home for this fact is the queue row. `GET /miler/bookings` does not
// carry `next_action`, which is the logged backend gap this store works around.
// When that field lands, `NextLegResolver` prefers it automatically and this
// becomes a cache nothing reads.
const String _kPivotNextActionKeyBase = 'pivot_next_action_by_order';
/// Records the `next_action` the pivot returned for [orderId].
///
/// Ignores an empty action rather than storing a blank: an absent instruction
/// and a recorded "no instruction" are different facts, and only the first is
/// true when the server said nothing.
Future<void> rememberPivotNextAction(String orderId, String action) async {
final id = orderId.trim();
final value = action.trim().toLowerCase();
if (id.isEmpty || value.isEmpty) return;
final prefs = await SharedPreferences.getInstance();
final map = await getPivotNextActions();
map[id] = value;
await prefs.setString(
await _scopedKey(_kPivotNextActionKeyBase),
jsonEncode(map),
);
debugPrint('[NEXTLEG] recorded $id → $value');
}
/// Every pivot instruction this device recorded, by order id.
Future<Map<String, String>> getPivotNextActions() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(await _scopedKey(_kPivotNextActionKeyBase));
if (raw == null || raw.isEmpty) return <String, String>{};
try {
final decoded = jsonDecode(raw);
if (decoded is! Map) return <String, String>{};
return {
for (final e in decoded.entries)
e.key.toString(): (e.value?.toString() ?? '').toLowerCase(),
}..removeWhere((_, v) => v.isEmpty);
} catch (_) {
return <String, String>{};
}
}
/// Drops instructions for orders that are finished, so the map does not grow
/// for the life of the install.
Future<void> forgetPivotNextActions(List<String> orderIds) async {
if (orderIds.isEmpty) return;
final prefs = await SharedPreferences.getInstance();
final map = await getPivotNextActions();
var changed = false;
for (final id in orderIds) {
if (map.remove(id) != null) changed = true;
}
if (changed) {
await prefs.setString(
await _scopedKey(_kPivotNextActionKeyBase),
jsonEncode(map),
);
}
}
/// Clears ids once they are delivered — or once a short pick says they were
/// never in the box to begin with. Without this the set grows for the life of
/// the install and yesterday's run keeps today's cards off Home.