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> _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>>(:final value) => value, // A real empty day. The screens render their own empty state from this. LoadEmpty>>() => const [], // No endpoint for this line of work — neither waiting nor retrying helps, // so it must not arrive as an empty day. See [LineNotServedException]. LoadUnavailable>>() => throw const LineNotServedException(), LoadFailure>>(: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>>() => throw Exception( 'Still loading', ), }; } /// Everything the hub has put in front of this rider today. Future> getPickupQueues({String? orderstatus}) => _bookings(status: orderstatus); /// The stops he is working now. Future> getCurrentPickups() => _bookings(); /// The stops he has accepted. Future> 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.'; }