359 lines
13 KiB
Dart
359 lines
13 KiB
Dart
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<String, String> _status = <String, String>{};
|
|
|
|
/// 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 <String, dynamic>{},
|
|
);
|
|
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<Map<String, dynamic>> 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<Map<String, dynamic>> _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,
|
|
label: '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,
|
|
label: '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,
|
|
label: '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,
|
|
label: '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,
|
|
label: '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,
|
|
label: '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,
|
|
label: '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,
|
|
label: '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,
|
|
label: '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<String, dynamic> _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 label,
|
|
required int meals,
|
|
}) => <String, dynamic>{
|
|
// `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': label,
|
|
|
|
// 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 [OrderManifest].
|
|
'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',
|
|
};
|
|
}
|