Miler rider app: surface system, visible design language, backend lifecycle
Design system - MilerSurface ladder (canvas → working → raised → floating) with MilerPanel as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1. - Visible vocabulary applied across Home, Deliveries, Activity, Account and the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x), canvas wells for anything that opens, small filled tags for shelf labels, demoted placeholders. Recorded in DESIGN_SYSTEM.md §6. - One icon family: 222 Material glyphs migrated to Lucide; none left outside lib/xpress. - Colour semantics corrected: amber only for what is genuinely owed, brand red reserved for the live stop, disabled primaries go neutral rather than pale. Data and lifecycle - lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart makes admin sequence the single ordering authority; service_day.dart, and stop_area.dart rewritten against live Coimbatore addresses (digit-token stripping, city stoplist, street suffixes, stammer collapse). - countLabel states the load once, in bags. Testing - 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity, sheets and verify, with test/failures/ now gitignored (diff debris). - New pins: home_gutter_test, stop_area_test, plus updated structural bounds. Note: this commit also carries pre-existing working-tree deletions that were present before this work (API_SPEC.md, README.md, demo test fixtures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:miler/data/api_config.dart';
|
||||
|
||||
import 'package:miler/data/miler_api.dart';
|
||||
|
||||
/// Resolves a BOOKING id into the BOOKING ASSIGNMENT id that the accept/reject
|
||||
/// endpoints key on.
|
||||
@@ -21,6 +19,27 @@ class AssignmentLookup {
|
||||
|
||||
/// bookingid (as string) -> bookingassignmentid
|
||||
static final Map<String, int> _cache = <String, int>{};
|
||||
|
||||
/// bookingid (as string) -> the hub's solved stop number.
|
||||
///
|
||||
/// ── Why the sequence lives here and not on the booking ──
|
||||
///
|
||||
/// `step` is written onto **`bookingassignments`**, not onto the booking: the
|
||||
/// hub's batch-assign endpoint sends each affected rider's whole active set to
|
||||
/// the route optimizer and writes the returned road-network order back onto
|
||||
/// the assignment rows. So `GET /miler/bookings` — the app's one read of the
|
||||
/// day — cannot carry it, and this endpoint is the only place it exists.
|
||||
///
|
||||
/// It was being thrown away. This class fetched the assignment rows for their
|
||||
/// ids and dropped everything else, so `Trip.sortStops` — which documents
|
||||
/// `step` as authoritative and says the app must never second-guess the
|
||||
/// admin's order — never saw one and fell back to booked time on every run.
|
||||
/// The rider was choosing his own order while the hub believed it had solved
|
||||
/// one for him.
|
||||
///
|
||||
/// `step: 0` means *not sequenced*, never *first*, and is not stored.
|
||||
static final Map<String, int> _steps = <String, int>{};
|
||||
|
||||
static DateTime? _fetchedAt;
|
||||
|
||||
/// Assignments change whenever the hub assigns work, so the map goes stale
|
||||
@@ -41,9 +60,22 @@ class AssignmentLookup {
|
||||
|
||||
static void invalidate() {
|
||||
_cache.clear();
|
||||
_steps.clear();
|
||||
_fetchedAt = null;
|
||||
}
|
||||
|
||||
/// The hub's stop order, by booking id, refreshed on the same TTL as the ids.
|
||||
///
|
||||
/// Best-effort by contract: sequencing is a separate service and the flow doc
|
||||
/// is explicit that it being down must leave bookings *assigned but
|
||||
/// unordered* rather than undo anything. An empty map therefore means "no
|
||||
/// solved order available", which the sort reads as "fall back to booked
|
||||
/// time" — not as "every stop is step 0".
|
||||
static Future<Map<String, int>> steps() async {
|
||||
if (_isStale) await _refresh();
|
||||
return Map<String, int>.unmodifiable(_steps);
|
||||
}
|
||||
|
||||
/// The assignment id for [bookingId], or null when the backend has no
|
||||
/// assignment row for it (or the call fails).
|
||||
///
|
||||
@@ -67,22 +99,21 @@ class AssignmentLookup {
|
||||
|
||||
static Future<void> _refresh() async {
|
||||
try {
|
||||
final uri = Uri.parse(ApiConfig.url('/miler/assignments'));
|
||||
final res = await http
|
||||
.get(uri, headers: await ApiConfig.authHeaders())
|
||||
.timeout(const Duration(seconds: 15));
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
debugPrint('[ASSIGNMENTS] fetch failed: HTTP ${res.statusCode}');
|
||||
// Through [MilerApi], not a hand-rolled `http.get`. It was the latter,
|
||||
// which meant this one call carried its own header building, its own
|
||||
// envelope unwrapping and its own idea of what a 2xx is — and, because it
|
||||
// bypassed `MilerApi.client`, it was the only request in the app that a
|
||||
// test could not stub, so the suite made real network calls to the
|
||||
// production API while checking a repository.
|
||||
final res = await MilerApi.assignments();
|
||||
if (!res.ok) {
|
||||
debugPrint('[ASSIGNMENTS] fetch failed: HTTP ${res.status}');
|
||||
return;
|
||||
}
|
||||
|
||||
final decoded = json.decode(res.body);
|
||||
dynamic data = decoded;
|
||||
if (decoded is Map) {
|
||||
data = decoded['data'] ?? decoded['details'] ?? decoded['assignments'];
|
||||
}
|
||||
if (data is! List) {
|
||||
debugPrint('[ASSIGNMENTS] unexpected payload: ${data.runtimeType}');
|
||||
final List<dynamic> data = res.list;
|
||||
if (data.isEmpty && res.data is! List) {
|
||||
debugPrint('[ASSIGNMENTS] no assignment rows in the response');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,9 +121,14 @@ class AssignmentLookup {
|
||||
_cache
|
||||
..clear()
|
||||
..addAll(next);
|
||||
final nextSteps = buildStepIndex(data);
|
||||
_steps
|
||||
..clear()
|
||||
..addAll(nextSteps);
|
||||
_fetchedAt = DateTime.now();
|
||||
debugPrint(
|
||||
'[ASSIGNMENTS] cached ${_cache.length} booking->assignment ids',
|
||||
'[ASSIGNMENTS] cached ${_cache.length} booking->assignment ids, '
|
||||
'${_steps.length} sequenced',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[ASSIGNMENTS] fetch error: $e');
|
||||
@@ -131,6 +167,44 @@ class AssignmentLookup {
|
||||
return index;
|
||||
}
|
||||
|
||||
/// Collapses the assignment rows into one bookingid -> step entry.
|
||||
///
|
||||
/// Same newest-first, actionable-wins rule as [buildIndex] — a booking that
|
||||
/// was assigned, rejected and reassigned must take the live assignment's
|
||||
/// sequence, not the rejected corpse's — so the two indexes cannot describe
|
||||
/// different assignment rows for the same booking.
|
||||
@visibleForTesting
|
||||
static Map<String, int> buildStepIndex(List<dynamic> rows) {
|
||||
final index = <String, int>{};
|
||||
final tookActionable = <String>{};
|
||||
|
||||
for (final row in rows.whereType<Map>()) {
|
||||
final bookingKey = row['bookingid']?.toString().trim() ?? '';
|
||||
if (bookingKey.isEmpty) continue;
|
||||
|
||||
final step = _asInt(row['step']) ?? 0;
|
||||
final status = (row['assignmentstatus'] ?? '').toString().trim();
|
||||
final isActionable = _actionable.contains(status);
|
||||
|
||||
final seen =
|
||||
index.containsKey(bookingKey) || tookActionable.contains(bookingKey);
|
||||
if (seen && !(isActionable && !tookActionable.contains(bookingKey))) {
|
||||
continue;
|
||||
}
|
||||
if (isActionable) tookActionable.add(bookingKey);
|
||||
|
||||
// 0 is "not sequenced". Storing it would make an unsequenced stop look
|
||||
// like it had been solved into position zero.
|
||||
if (step > 0) {
|
||||
index[bookingKey] = step;
|
||||
} else {
|
||||
index.remove(bookingKey);
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
static int? _asInt(dynamic v) {
|
||||
if (v is int) return v;
|
||||
if (v is num) return v.toInt();
|
||||
|
||||
Reference in New Issue
Block a user