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>
106 lines
4.8 KiB
Dart
106 lines
4.8 KiB
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.
|
|
///
|
|
/// ── What this used to be ──
|
|
///
|
|
/// Three near-identical methods — `getPickupQueues`, `getCurrentPickups`,
|
|
/// `getPickupQueuesPicked` — each with its own legacy URL, its own
|
|
/// `fromdate`/`todate`/`userid` query and its own hand-rolled envelope
|
|
/// unwrapping, and each opening with `if (ApiConfig.useNewApi) return
|
|
/// _getBookingsNew(...)`. All three legacy paths hit different endpoints on a
|
|
/// backend that is no longer deployed, and all three new paths were the same
|
|
/// call. What is left is that one call.
|
|
///
|
|
/// The three names survive because three call sites read differently — Home
|
|
/// wants the queue, Bookings wants the accepted work — even though the request
|
|
/// is identical and the filtering happens client-side. Collapsing them into one
|
|
/// method is a separate change to the screens, not to this file.
|
|
class PickupProvider {
|
|
/// `GET /miler/bookings` → the list, mapped into the legacy stop shape the
|
|
/// cards read.
|
|
///
|
|
/// 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 {
|
|
// 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 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.
|
|
Future<List<dynamic>> getPickupQueues({String? orderstatus}) =>
|
|
_bookings(status: orderstatus);
|
|
|
|
/// The stops he is working now.
|
|
Future<List<dynamic>> getCurrentPickups() => _bookings();
|
|
|
|
/// 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.';
|
|
}
|