Files
doormile_milderapp/lib/providers/pickup/pickup_provider.dart
Thiru-tenext d7348e253f 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>
2026-08-22 05:40:35 +05:30

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.';
}