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,6 +1,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:miler/data/api_config.dart';
|
||||
import 'package:miler/data/work_repository.dart';
|
||||
import 'package:miler/data/load_state.dart';
|
||||
import 'package:miler/data/miler_api.dart';
|
||||
|
||||
/// Reads the rider's bookings.
|
||||
@@ -26,25 +26,56 @@ class PickupProvider {
|
||||
/// The mapping lives in [ApiConfig.pickupFromBooking] rather than here: it is
|
||||
/// the one place that knows how a v1 booking becomes a stop, and the same
|
||||
/// translation is needed by anything else that receives a booking.
|
||||
/// ── One request, however many screens ask ──
|
||||
///
|
||||
/// Home, Bookings, Activity and the post-delivery screen all call this, and
|
||||
/// each of them used to produce its own `GET /miler/bookings` — on mount, on
|
||||
/// tab switch, and on every poll. Four timers, four copies of the day, and
|
||||
/// four ideas of what had been accepted, reconciled only by whichever
|
||||
/// happened to refresh last.
|
||||
///
|
||||
/// They all go through [WorkRepository] now, which collapses concurrent
|
||||
/// callers into one request, keeps one copy, and drops a response that lands
|
||||
/// out of order. Nothing at the call sites changed: this still returns a list
|
||||
/// or throws, which is the contract the screens were written against.
|
||||
///
|
||||
/// The richer answer — loading, empty, offline, unavailable — is on the
|
||||
/// repository for screens that want to render it properly rather than
|
||||
/// flattening it into an exception. See [LoadState].
|
||||
Future<List<dynamic>> _bookings({String? status}) async {
|
||||
final res = await MilerApi.bookings(status: status);
|
||||
if (!res.ok) {
|
||||
throw Exception('Failed (${res.status})${res.message.isEmpty ? '' : ': ${res.message}'}');
|
||||
// A filtered read is a different question and is not the one the shared
|
||||
// copy answers. Nothing calls this with a status today; if something does,
|
||||
// it gets its own request rather than silently receiving the whole day.
|
||||
if (status != null && status.isNotEmpty) {
|
||||
final res = await MilerApi.bookings(status: status);
|
||||
if (!res.ok) {
|
||||
throw Exception(
|
||||
'Failed (${res.status})'
|
||||
'${res.message.isEmpty ? '' : ': ${res.message}'}',
|
||||
);
|
||||
}
|
||||
return ApiConfig.pickupsFromBookings(res.list);
|
||||
}
|
||||
|
||||
final mapped = ApiConfig.pickupsFromBookings(res.list);
|
||||
debugPrint('[BOOKINGS] ${res.list.length} rows → ${mapped.length} stops');
|
||||
if (mapped.isEmpty && res.list.isNotEmpty) {
|
||||
// The rows arrived and none of them survived translation, which is a
|
||||
// field-name mismatch rather than an empty day. Worth saying out loud —
|
||||
// it is the difference between "no work" and "the adapter is wrong".
|
||||
ApiConfig.logGap(
|
||||
'pickupFromBooking',
|
||||
'${res.list.length} bookings returned but none mapped — check the '
|
||||
'field names against a real payload.',
|
||||
);
|
||||
}
|
||||
return mapped;
|
||||
final state = await WorkRepository.instance.load();
|
||||
return switch (state) {
|
||||
LoadData<List<Map<String, dynamic>>>(:final value) => value,
|
||||
// A real empty day. The screens render their own empty state from this.
|
||||
LoadEmpty<List<Map<String, dynamic>>>() => const <dynamic>[],
|
||||
// No endpoint for this line of work — neither waiting nor retrying helps,
|
||||
// so it must not arrive as an empty day. See [LineNotServedException].
|
||||
LoadUnavailable<List<Map<String, dynamic>>>() =>
|
||||
throw const LineNotServedException(),
|
||||
LoadFailure<List<Map<String, dynamic>>>(:final kind, :final message) =>
|
||||
throw Exception(
|
||||
message.isEmpty ? 'Could not load your work ($kind)' : message,
|
||||
),
|
||||
// Only reachable if the repository handed back its pre-load state, which
|
||||
// it does not. Treated as a failure rather than as an empty day.
|
||||
LoadLoading<List<Map<String, dynamic>>>() => throw Exception(
|
||||
'Still loading',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Everything the hub has put in front of this rider today.
|
||||
@@ -57,3 +88,18 @@ class PickupProvider {
|
||||
/// The stops he has accepted.
|
||||
Future<List<dynamic>> getPickupQueuesPicked() => _bookings();
|
||||
}
|
||||
|
||||
/// The rider's line of work has no backend to ask.
|
||||
///
|
||||
/// Distinct from an empty day and from a failed request, because the rider's
|
||||
/// answer is different in each case: wait, retry, or "your hub has not switched
|
||||
/// this on yet". Screens catch this to render the unavailable state rather than
|
||||
/// the empty one.
|
||||
class LineNotServedException implements Exception {
|
||||
const LineNotServedException();
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'This line of work is not served by the backend yet — there is no '
|
||||
'bookings endpoint for it. See ServiceProfile.hasBookingsEndpoint.';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user