import 'package:flutter/foundation.dart'; import 'package:miler/data/mock_backend.dart'; import 'package:miler/data/service_profile.dart'; /// ───────────────────────────────────────────────────────────────────────── /// A MEAL DAY, WITH NO BACKEND BEHIND IT /// /// The meal line has no endpoints. The Doormile backend serves parcel bookings /// and consignments and knows nothing about kitchens, crates or subscribers, and /// building against invented URLs would produce a flow that compiles, demos, and /// is wrong the day a real contract arrives. /// /// So the meal line runs on this: a complete day, held in memory, walked with /// the real screens. Every status change the rider makes is recorded here /// instead of being sent, which means the whole process — accept, arrive, load /// the crate, deliver, skip, fail — can be designed, used and judged before a /// single endpoint exists. /// /// ── Why this is not the demo layer that was deleted ── /// /// A mock layer was ripped out of this app for a good reason: it wrote rows into /// the same SharedPreferences stores the real work uses, so demo stops surfaced /// on a live rider's tabs weeks later, with nothing left in the repo to explain /// them. Three rules keep this one from becoming that: /// /// 1. **It cannot reach a parcel rider.** [active] is false unless the signed-in /// profile is the meal line, and an unrecognised tenant resolves to parcel. /// There is no path from a normal login to this data. /// 2. **Its ids are self-identifying.** Every id is `MOCK-…`, which is exactly /// what `purgeDemoRecords()` already looks for — so anything that does leak /// into a store is removed at the next launch, by code that already ships. /// 3. **It has one switch.** [kMealMockEnabled] is the whole feature. When the /// meal endpoints land, that constant goes false and the provider falls /// through to the real call; nothing else in the app knows this file exists. /// ───────────────────────────────────────────────────────────────────────── /// Whether the day below is served instead of calling the API. /// /// ── One switch for the whole app, not two ── /// /// This used to be its own constant, and it was turned off on 2026-08-12 for a /// real reason: while it was on, a meal rider tapped **Arrived** and **Picked /// up**, watched the card advance, and the hub console never changed. Every /// status write on this line was recorded in a `Map` on the phone and reported /// as a success. /// /// That failure mode has not gone away — it is simply what running without a /// backend *means*, and it now applies to the whole app rather than to this /// file alone. So the two switches became one: [kMockBackend] takes sign-in, /// the day and every status write off the network together, and this follows /// it. There is no state where the meal day is fictional but the statuses are /// real, which is the confusing half of what used to be possible. /// /// Turn the app back on to the real backend with: /// /// flutter run --dart-define=MOCK_BACKEND=false /// /// The meal line then falls through to the three booking routes it actually /// depends on, all of which exist and are proven by the parcel line: /// /// list `GET /miler/bookings` /// arrived `POST /miler/bookings/:id/reached` /// picked up `POST /miler/bookings/:id/pickup-complete` /// Follows the one mock switch at runtime rather than at compile time, so a /// test (or a bench session) that turns the canned backend off turns this off /// with it. See [MockBackend.enabled]. bool get kMealMockEnabled => MockBackend.enabled; /// Wall-clock helper so the mock day always looks like *today's* shift rather /// than a fixed date that reads as stale the moment anybody opens it. String _slot(int hour, int minute) { final now = DateTime.now(); final t = DateTime(now.year, now.month, now.day, hour, minute); return t.toIso8601String(); } /// A meal run held in memory: two kitchens, seven subscribers, one rider. /// /// Coordinates are real Coimbatore addresses in route order, because a route /// that doubles back looks like a bug in the sequencing rather than what it is. class MealRunMock { MealRunMock._(); /// True when the app should serve this instead of calling the API. static bool get active => kMealMockEnabled && ServiceProfile.active.sourceIsKitchen; /// Status overrides the rider has caused this session, keyed by order id. /// /// The mock's own rows are `assigned`; everything after that is something he /// did. Held separately rather than mutated into the rows so a reset is one /// `clear()` and the day itself stays declarative. static final Map _status = {}; /// Records what a status call *would* have sent. Always succeeds — there is /// nothing to fail. static bool setStatus(String orderId, String status) { if (orderId.isEmpty) return true; _status[orderId] = status; debugPrint('[MEAL_MOCK] $orderId → $status'); return true; } /// Same, keyed by the numeric booking id the controllers carry. static bool setStatusByPickupId(int pickupId, String status) { if (pickupId <= 0) return true; final row = _day.firstWhere( (r) => r['pickupid'] == pickupId, orElse: () => const {}, ); final id = (row['orderid'] ?? '').toString(); return setStatus(id, status); } /// Puts the day back to the top. Wired to nothing yet — it exists so a /// demo can be run twice without reinstalling the app. static void reset() => _status.clear(); /// The day as the cards read it, with whatever the rider has done applied. /// /// A fresh copy every call: the screens mutate stop maps in place (compliance /// stamps, proof blocks), and handing out the master rows would let one run's /// leftovers show up in the next. static List> stops() => [ for (final row in _day) {...row, 'orderstatus': _status[row['orderid']] ?? row['orderstatus']}, ]; // ── The day itself ────────────────────────────────────────────────────── // // Two kitchens is deliberate rather than decorative: a single-kitchen day // hides every question the grouping exists to answer — which crate is this, // which bags belong to it, what happens when the second load is still at a // counter he has not reached. // // Five orders on the first counter and four on the second, because the whole // pickup flow is a claim about a *set*: `5 orders · 5 bags`, five names on the // confirmation, five deliveries afterwards. A day of ones would let every one // of those read correctly while being wrong. static final List> _day = [ // ── Vidhya Kitchen · Gandhi Nagar, Peelamedu ── _drop( id: 1001, step: 1, name: 'Joe Mathew', phone: '9840012001', address: '12, SNS Colony, Peelamedu', lat: 11.0271, lng: 76.9962, kitchen: _vidhya, bag: 'DG-1001', meals: 1, ), _drop( id: 1002, step: 2, name: 'Arun Prakash', phone: '9840012002', address: '21, New Street, Peelamedu', lat: 11.0248, lng: 76.9941, kitchen: _vidhya, bag: 'DG-1002', meals: 1, ), _drop( id: 1003, step: 3, name: 'Priya Venkatesh', phone: '9840012003', address: '45, West Avenue, RS Puram', lat: 11.0043, lng: 76.9518, kitchen: _vidhya, bag: 'DG-1003', meals: 1, ), _drop( id: 1004, step: 4, name: 'Kumar Selvam', phone: '9840012004', address: 'Flat 3C, Lakshmi Towers, 100 Feet Road, Gandhipuram', lat: 11.0168, lng: 76.9558, kitchen: _vidhya, bag: 'DG-1004', meals: 1, ), _drop( id: 1005, step: 5, name: 'Ravi Shankar', phone: '9840012005', address: '8, Krishna Colony, 2nd Street, Singanallur', lat: 10.9925, lng: 77.0289, kitchen: _vidhya, bag: 'DG-1005', meals: 1, ), // ── Annapoorna Mess · Thadagam Road ── _drop( id: 2001, step: 6, name: 'Meena Krishnan', phone: '9840012006', address: '31, Sarojini Street, Gandhipuram', lat: 11.0181, lng: 76.9603, kitchen: _annapoorna, bag: 'AM-2001', meals: 1, ), _drop( id: 2002, step: 7, name: 'Sai Ganesh', phone: '9840012007', address: '14, Trichy Road, above the pharmacy, Ramanathapuram', lat: 10.9871, lng: 76.9931, kitchen: _annapoorna, bag: 'AM-2002', meals: 1, ), _drop( id: 2003, step: 8, name: 'Devi Lakshmi', phone: '9840012008', address: '22, Kamarajar Road, near the school gate, Uppilipalayam', lat: 10.9903, lng: 76.9977, kitchen: _annapoorna, bag: 'AM-2003', meals: 1, ), _drop( id: 2004, step: 9, name: 'Karthik Raja', phone: '9840012009', address: '5B, Thadagam Road, opposite the water tank', lat: 11.0221, lng: 76.9391, kitchen: _annapoorna, bag: 'AM-2004', meals: 1, ), ]; /// A counter the rider collects from: what it is called, where it is, and the /// id everything groups on. static const _vidhya = ( id: 'K1', name: 'Vidhya Kitchen', address: 'Gandhi Nagar, Peelamedu, Coimbatore - 641004', lat: 11.0261, lng: 76.9931, locationId: 901, ); static const _annapoorna = ( id: 'K2', name: 'Annapoorna Mess', address: '19, Thadagam Road, RS Puram, Coimbatore - 641002', lat: 11.0195, lng: 76.9412, locationId: 902, ); /// One subscriber drop, in the legacy stop shape every card in the app reads. /// /// ── Both ends, in the right fields ── /// /// The `pickup*` fields are the **kitchen** and the `drop*` fields are the /// **customer**, because that is what the app navigates on: before collection /// `navigationTarget` reads `pickuplat/pickuplon`, and after it reads /// `droplat/droplon` — see [MilkRun]. This fixture used to put the customer in /// both, which sent a rider to a subscriber's flat to collect a lunch that was /// still at a counter three kilometres away. /// /// `type: delivery` is stated rather than inferred: the app would reach the /// same answer from the profile, but a fixture that relies on a fallback is /// testing the fallback rather than the screen. static Map _drop({ required int id, required int step, required String name, required String phone, required String address, required double lat, required double lng, required ({ String id, String name, String address, double lat, double lng, int locationId, }) kitchen, required String bag, required int meals, }) => { // `MOCK-` so `purgeDemoRecords()` recognises anything that leaks into a // store. See the note at the top of this file. 'orderid': 'MOCK-M-$id', 'pickupid': id, 'orderheaderid': id, 'pickuplocationid': kitchen.locationId, 'orderstatus': 'assigned', 'step': step, // Who is being handed food. 'pickupcustomer': name, 'pickupcontactno': phone, // Where he collects: the counter. 'pickupaddress': kitchen.address, 'pickuplat': kitchen.lat, 'pickuplon': kitchen.lng, 'pickuplong': kitchen.lng, // Where it goes: the door. 'dropaddress': '$address, Coimbatore, Tamil Nadu 641004', 'droplat': lat, 'droplon': lng, // The counter itself. The grouping, the headings, the manifest and the // bag identity all key off these. 'kitchenid': kitchen.id, 'kitchenname': kitchen.name, 'baglabel': bag, // A drop, and how many boxes are in it. One order is one bag — the count // here is the *meal* count inside that bag, and nothing reads it as a bag // count. See [BagManifest]. 'type': 'delivery', 'deliveryqty': meals, 'quantity': meals, // Nothing is owed at any door — a subscriber paid the client by the month. // Stated as zero rather than omitted so a payload reader cannot mistake a // missing field for an unknown amount. 'collectionamt': 0, 'pickupamt': 0, // The lunch slot the whole run belongs to. 'starttime': _slot(11, 30), 'endtime': _slot(13, 30), 'eta': '8', 'kms': '2.4', }; }