# Miler — the Doormile rider app > *Miler Rider — Smarter Pickups, Powered by AI* > Flutter · Android + iOS · `com.doormile.partner` · v1.2.28+144 This is the single reference for what this app is, who it serves, how it is built, and how it was designed. If you are new to the codebase, read sections 1–3. If you are about to change UI, read sections 5–7 before you write a line. --- ## Contents 1. [What Miler is](#1-what-miler-is) 2. [The people and the workflow](#2-the-people-and-the-workflow) 3. [The routing problem underneath](#3-the-routing-problem-underneath) 4. [Architecture](#4-architecture) 5. [Design principles](#5-design-principles) 6. [The design system](#6-the-design-system) 7. [Screen by screen — what was designed and why](#7-screen-by-screen--what-was-designed-and-why) 8. [Backend integration and known gaps](#8-backend-integration-and-known-gaps) 9. [What we are doing with this app now](#9-what-we-are-doing-with-this-app-now) 10. [Conventions for anyone touching this code](#10-conventions-for-anyone-touching-this-code) --- ## 1. What Miler is Miler is the **rider app** built by **Doormile**, a logistics company that moves parcels between customers and its hubs. It is the tool carried by the field agent we call a **"miler"** — the person on a bike who actually collects and delivers packages on the ground. The system works like this. Customers use the separate **Doormile customer app** to book a **time slot** — a window in which they want a parcel picked up or delivered. Behind the scenes, for each slot, a **hub manager (admin)** gathers all the customer bookings falling in that window and **assigns them to one miler as a single ordered route** — Stop 1, Stop 2, Stop 3, through to the last stop. That route appears inside this app in the exact sequence the admin set. The miler goes **On Duty**, works the stops one by one in order, and at each stop does either a pickup or a delivery or both : - **Pickup** — collects the parcel from the customer or store, photographs it, takes payment if money is owed (cash or UPI/QR), confirms it as picked up. - **Delivery** — hands over a parcel carried from the hub, verifies with an **OTP and a photo**, confirms it as delivered. Deliveries are prepaid, so no money is collected. He can **skip** a stop when a store is closed or a customer isn't available, and come back to it later. He **cannot reorder** the route — the sequence is fixed by the admin. After the last stop he **returns to the same hub he started from**, bringing back everything he collected plus anything he couldn't deliver. In short: a **first-mile + last-mile round trip**, `Hub → stop 1 → stop 2 → … → stop N → back to the same Hub`, much like an Amazon or Flipkart delivery associate runs a route. This app is the miler's companion for the whole journey. ### Doormile's two apps | App | Who uses it | What they do | |---|---|---| | **Doormile customer app** | End customers | Book a time slot for a pickup or delivery | | **Miler app** (this one) | The rider | Carry out the assigned route | ### Who this app is for One person, on a bike, working through an assigned list of stops for a shift. That is the entire user base. Every design decision in this document traces back to that single user and one fact about him: **the daily loop repeats 30–50 times a day.** --- ## 2. The people and the workflow | Role | What they do | Which app | |---|---|---| | **Customer (CX)** | Books a time slot for a pickup or delivery | Doormile customer app | | **Hub manager / Admin** | Bundles a slot's bookings into an ordered route and assigns it to a miler | Admin / hub system | | **Miler (rider)** | Runs the route — pickups and deliveries — and returns to the hub | **This app** | ### The loop, step by step 1. **Slot booking** — customers book time slots in the customer app. 2. **Route assignment** — for a slot, the hub admin groups the bookings and assigns them to one miler as a **fixed-order route** (Stop 1 … Stop N). 3. **Route appears** — the ordered stops show up in this app. 4. **On Duty** — the miler goes online to start working. 5. **Work each stop in order** - *Pickup stop* → navigate → collect parcel → photo proof (+ weight) → take payment if owed (Cash / UPI-QR) → confirm **Picked up**. - *Delivery stop* → navigate → hand over parcel → verify **OTP + photo** → confirm **Delivered** (prepaid, no payment). 6. **Skip if needed** — a stop can be skipped and resumed later. The order of the remaining stops never changes. 7. **Move to next stop** — after each success the app prompts the next stop. 8. **Return to hub** — after the last stop, back to the **same hub** with all collected parcels and any undelivered (RTO) items. ### Hard rules - **One route per slot**, assigned by the admin. - **Stops are strictly ordered** — the miler follows the sequence and cannot reorder it. - **Skip is allowed and normal** — not a failure; the stop can be resumed. - **Round trip** — always starts and ends at the **same hub**. - **Pickup vs Delivery** — each stop is one or the other; the app shows the type clearly and runs the right flow. ### Vocabulary Rider-facing language is deliberately narrow. **Route** (the whole assignment) → **Stops** (ordered, each tagged Pickup or Delivery) → **Hub** (start and return point). Avoid "booking", "task", and "order" in UI copy — they were used interchangeably in the old build and meant nothing specific to a rider. --- ## 3. The routing problem underneath A miler's day is one solved **Vehicle Routing Problem** instance. The hub admin solves it; the rider executes it. He is explicitly *not* allowed to re-optimise — he can't reorder stops — but he absolutely needs to see the **shape of the solution he has been handed**, because every classic VRP constraint has a physical consequence on the bike. This is the intellectual backbone of the Home screen redesign. Each constraint maps to something concrete: | VRP concept | What it means to a miler | Where it appears | |---|---|---| | **Depot / round trip** | The route starts and ends at the same hub, so the return leg is real distance he has to ride | `hub → progress → hub` rail + `≈ km round trip` | | **Capacity (CVRP)** | Total parcels and weight must fit in the box. Discovering an overload at stop 9 is too late | Parcels · weight kg | | **Time windows (VRPTW)** | Each booking has a customer slot. Arriving after it closes is a failed stop | "*N* stops past their booked slot" / "close within 30 min" | | **Route duration limit** | The shift is the hard cap on the route. Time left ÷ stops left is the pace he is actually racing | Shift countdown + "only *X* per stop left" | | **Fixed sequence** | The admin's order is the solution; re-sorting it would break it | The list never reorders; skip-not-reorder | | **Service time** | Each stop takes real minutes (photo, weight, payment, OTP) | Folded into the per-stop budget | | **Cash on route** | Not a textbook VRP term, but he is carrying it and is accountable for it | ₹ to collect | **All of this is derived from data the app already has** — stop coordinates, quantities, `expected_pickup_time`, the shift window in prefs. No new endpoint was required. The computation lives in `lib/views/Dashboard/home/route_brief.dart` as a pure, unit-tested `RouteBrief` value object. Two honesty constraints in that code: - Distance is **straight-line haversine**, so it under-reads real road distance. It is always rendered with a `≈` and never presented as a promise. - Accepted stops have left the Home list for the Bookings tab, but they are still on the route — their parcels still need collecting, their kilometres still need riding. `RouteBrief.from(extraStops: …)` pulls them back into the totals so the brief never understates the day. Reference reading: [FarEye — Vehicle Routing Problem](https://fareye.com/resources/blogs/vehicle-routing-problem-how-to-solve-it), [ArcGIS Pro — Solve Vehicle Routing Problem](https://pro.arcgis.com/en/pro-app/latest/tool-reference/ready-to-use/itemdesc-solvevehicleroutingproblem.htm). --- ## 4. Architecture ### Stack | Concern | Choice | |---|---| | Framework | Flutter (Dart SDK ^3.9.2) | | State / DI / routing | **GetX** (`GetMaterialApp`, `Get.put`, `Get.offAll`) + a little `provider` | | Responsive sizing | `flutter_screenutil`, design size **390 × 844** — `.w` `.h` `.sp` `.r` everywhere | | Persistence | `shared_preferences` (session, duty state, shift window, last GPS fix, accepted stops) | | Maps & nav | `google_maps_flutter`, `flutter_polyline_points`, `geolocator`, `geocoding` | | Push | Firebase Core + Messaging + `flutter_local_notifications` | | Live tracking | MQTT (`mqtt_client`) + `flutter_foreground_task` | | Media | `image_picker` (proof photos), `minio` (uploads), `qr_flutter` (UPI QR) | | Motion | `lottie`, `shimmer`, custom primitives in `app_widgets.dart` | ### Directory map ``` lib/ ├── main.dart App bootstrap, Firebase, GetMaterialApp, lifecycle ├── helpers/ │ ├── app_bootstrap.dart Decides the opening screen (replaced the splash) │ ├── shift_end_alarm.dart Android AlarmManager via MethodChannel doormile/shift_end │ └── http_overrides.dart ├── data/ │ ├── api_config.dart THE backend switch + legacy/v1 adapter │ ├── accepted_store.dart Locally persisted accepted stops │ ├── assignment_lookup.dart bookingid → bookingassignmentid resolver │ └── mock_bookings.dart Demo data (gated behind !useNewApi) ├── Models/ │ └── stop_status.dart Canonical StopStatus enum + raw-string normaliser ├── controllers/ GetX controllers (auth, duty, pickups, summary, …) ├── providers/ HTTP layer, one per domain ├── background/ Foreground service, live tracking, background logs ├── utils/ Kalman filter, MQTT service, device info └── views/ ├── introscreens/ First-run intro ├── onboardscreens/ Sign in, OTP, MPIN ├── Dashboard/ │ ├── home/ Home + route_brief.dart │ ├── pickups/ The whole per-stop flow (see §7) │ ├── summary/ Earnings │ └── profile/ Account, support, rewards, FAQ ├── helpers/constants/ Colors, fonts, spacing, theme ├── helpers/widgets/ Shared kit: app_widgets.dart, miler_app_bar.dart └── offline/ Full-screen offline state ``` ### App entry `main.dart` initialises Firebase, registers permanent controllers (`ProfileController`, `RiderLogController`, `PickupController`, `LogController`), starts logging, checks whether the shift already ended while the app was closed, and runs `GetMaterialApp` with `AppBootstrap` as `home`. **`AppBootstrap`** (`lib/helpers/app_bootstrap.dart`) decides the opening screen and nothing else: - app version changed since last run → force re-verification (MPIN if the rider was signed in, otherwise Sign In) - signed in + on duty → dashboard - signed in + off duty → the go-on-duty banner - signed out, intro already seen → Sign In - first ever launch → Introscreen This replaced a four-second animated splash screen. That splash held two jobs: a brand animation and these routing rules. Only the rules were load-bearing. The animation was pure cost for someone opening this app 30–50 times a shift, layered on top of the native launch image that already covers cold start. The routing moved across intact; the four seconds are gone. ### Shell `BottomPage` (`lib/widget/Bottom_page.dart`) is a floating pill nav over an `IndexedStack` — four tabs, state preserved, no page ever mounted twice: | Tab | Screen | Purpose | |---|---|---| | **Home** | `Homepage` | Greeting, duty toggle, route brief, new stops | | **Bookings** | `MyPickups` | The active route — ordered stops, hub → progress → hub | | **Earnings** | `Summary` | Pickup counts, success rate, reward points, distance | | **Account** | `ProfilePage` | Rider details, support, settings | Tab switches run a single 280 ms fade + rise controller. Pushed pages use `Transition.cupertino` at 300 ms globally. ### Background and location Location is the app's most safety-critical data path, so it gets real treatment: - **`MilerKalmanFilter`** (`lib/utils/kalman_filter.dart`) — a 4-D Kalman filter (`[lat, lng, v_lat, v_lng]`) smoothing raw GPS. Phone GPS on a moving bike is noisy; unsmoothed fixes produce jumping breadcrumbs and inflated distances, which matters because distance feeds rider payout. - **`LiveTrackingService`** — position stream → Kalman → MQTT publish, with battery level attached. - **`foreground_service.dart`** — keeps logging alive with the screen off via `flutter_foreground_task` in its own isolate. - **`ShiftEndAlarm`** — Android `AlarmManager` through a `doormile/shift_end` `MethodChannel`, so the shift-end break log fires **even if the app is killed**. This is why "Shift not set" is treated as a genuine fault, not a cosmetic gap: with no end time there is no alarm. ### Duty state `DutyController` is the single source of truth for "is the rider online?". Duty state used to be re-derived from raw prefs in a dozen places, and the server `onduty` int is clobbered by a flaky rider-log sync — which auto-kicked riders offline after a pickup. The controller fixes the priority in one place: 1. `onduty == 1` → a fresh server shift is authoritative 2. explicit `online` flag → the rider's own toggle, which survives the flaky sync 3. `onduty == 0` → offline 4. otherwise → default online Similarly, `StopStatus` (`lib/Models/stop_status.dart`) normalises the backend's free-form `orderstatus` string. It used to be compared as raw literals in ~90 places with case-sensitivity landmines (`'active'` vs `'ACTIVE'`) and variant spellings (`'picked'` / `'picked up'` / `'pickuped'`). One enum, one parser, and every filter in the app now agrees. --- ## 5. Design principles These are not aspirational. Each one was derived from a specific failure in the old build and is enforced in the code today. ### 1. The 30–50× rule The daily loop repeats 30–50 times a day. Any confusion or extra tap becomes real frustration multiplied many times over. This is the lens for every trade-off: a one-second delay is not one second, it is a minute a day; a redundant tap is not one tap, it is fifty. *Consequence:* the splash animation was deleted. Slide gestures were replaced with taps everywhere except money. ### 2. One screen, one primary action The rider is standing at a gate with a parcel under one arm. There is exactly one thing he wants to do next. That thing gets the biggest, highest-contrast control; everything else recedes. *Consequence:* the confirm sheet's hidden "swipe to update status" became three visible tap buttons — one primary (Picked up / Delivered), two secondary (Skip, Cancel). The Home stop card was cut down to one identity and one primary action. ### 3. Gestures are earned, not default A slide is a deliberate friction device. It belongs only where an accidental tap would be expensive. Everything else is a tap. *Consequence:* slides remain only on **payment confirmation** and **going Off Duty**. Start-pickup, arrived, and status confirm are all taps now. ### 4. Alert only when actionable; stay silent when healthy If a coloured banner appears when nothing is wrong, riders learn to ignore all coloured banners. Warnings must be rare enough to still mean something. *Consequence:* the route brief's risk lines render **only** when there is risk. A healthy route is a short, calm card. "Shift set" is a quiet grey strip; "Shift not set" is a full warning surface with a pulsing icon, the consequence spelled out, and a Retry — because an alert with no way out is just noise. ### 5. Never move things under the finger The Bookings list polls every few seconds. Re-sorting a list while a thumb is descending on it is how riders tap the wrong stop. *Consequence:* polls don't reorder the visible list mid-interaction. `Reveal` animations are keyed per logical item and play **once**, in `initState`, so a 3-second poll never replays them. ### 6. Readable at arm's length, in sunlight, with gloves Minimum font sizes were raised (the old build went down to 7sp). Touch targets have a 44 px floor. Contrast is checked against a white surface in daylight, not against a designer's dark monitor. ### 7. Colour carries meaning, never decoration See §6. Green means go. Maroon means brand or pickup. Blue means delivery. Amber means warning or skip. Red means danger. A colour never appears for variety. ### 8. Motion is feedback, not garnish Every animation answers a question: *did my tap register* (`PressScale`), *is this new* (`Reveal`), *is something loading* (shimmer skeletons), *did I change tabs* (the nav fade + rise). Nothing animates just to look expensive. ### 9. Degrade gracefully, log loudly The backend has real gaps (§8). The app never blocks on a missing endpoint — it falls back, logs `[API_GAP]`, and keeps the rider moving. A rider stuck at a gate because a summary endpoint is missing is an unacceptable failure mode. ### 10. Honest numbers If a number is an estimate, it is rendered as one. Straight-line distance shows `≈`. Missing data renders `—`, never `0`. A metric that can't include accepted stops doesn't get called "Stops left" until it does. --- ## 6. The design system Reuse the tokens. Do not hardcode new shades or sizes. ### Colour — `lib/views/helpers/constants/Colorconstants.dart` **Semantic roles** (agreed; use these names, not raw hex): | Role | Token | Value | |---|---|---| | Brand / pickup accent | `ColorConstants.primary` | `#960019` maroon | | Go / success / positive completion | `ColorConstants.acceptGreen` | `#12B76A` | | Delivery / info accent | (in `stop_type.dart`) | `#2563EB` blue | | Danger / cancel | `ColorConstants.errorRed` | `#DC2626` | | Warning / skip | `ColorConstants.warning` | `#B45309` amber | The green was consolidated from four competing values (`#16A34A`, `#34C759`, `#2E7D32`, `#4CAF50`) that were scattered across Accept, Picked up, Delivered, Confirm, payment dialogs, verify success, and the done screen. They are now one token. A few `#007AFF` and `#BA1A1A` strays remain in nav and summary. The full palette is a Material-3 role set — `surface*`, `onSurface*`, `outline*`, `primary`/`onPrimary`/`primaryContainer`, `error*`, `warning*`, `success*` — so new surfaces have a correct token without inventing one. ### Typography — `Font_constant.dart` **Manrope**, bundled as the OFL variable font (`assets/fonts/Manrope/Manrope-VF.ttf`). It is the free, legal stand-in for **Uber Move**, which is proprietary and cannot be shipped. Fallback is Plus Jakarta Sans. Every text style in the app routes through `FontConstants.fontFamily` — there are no hardcoded family strings in `lib/`, so a single constant swaps the entire app's typeface. Scale runs `displayLg 32` → `headlineMd 24` → `headlineSm 20` → `bodyLg 18` → `bodyMd 16` → `labelBold 14` → `labelSm 12`. ### Spacing, radius, shadow — `design_constants.dart` 4 px base scale (`spacingXs 2` … `spacing5xl 40`), radius scale (`radiusXs 2` … `radius2xl 20`, `radiusFull 999`), and five shadow levels (`shadowXs` … `shadowXl`). ### Shared components — `views/helpers/widgets/` | Component | File | Use | |---|---|---| | `AppCard` | `app_widgets.dart` | White rounded card with system shadow | | `PrimaryButton` | `app_widgets.dart` | Solid maroon CTA | | `Reveal` | `app_widgets.dart` | One-shot fade + rise; **keyed**, plays once | | `staggerDelay(index)` | `app_widgets.dart` | Staggered list entrance | | `PressScale` | `app_widgets.dart` | Press feedback for custom tappables | | `SkeletonBone` / `MilerShimmer` / `skeletonCard()` / `SkeletonList` | `app_widgets.dart` | Shimmer loading, replaces bare spinners | | `MilerAppBar` | `miler_app_bar.dart` | The one top bar — logo, title, subtitle, trailing, 60 px, hairline | | `RouteBrief` / `RouteBriefCard` / `ShiftBanner` | `home/route_brief.dart` | VRP route summary + shift alert | | `stopKindOf` / `StopKindUi` | `pickups/stop_type.dart` | Pickup vs delivery branch — accent, icon, labels, verbs | **Do not hand-roll an AppBar.** Use `MilerAppBar`. Home is the one exception — it carries a richer dashboard header — and its logo and typography are kept aligned to the shared bar by hand. ### Auth screen pattern All onboarding screens follow one shape: **top-anchored** content (heading and input in the upper third, never vertically centred — this fixed the "input box too low" complaint), a large left-aligned heading (26sp, w700, `-0.6` tracking), a one-line helper (14sp, height 1.45), and a **pinned footer** with a single high-contrast CTA — solid maroon fill, white text, w700, radius 14, 56.h, disabled at alpha .35. Structure: top Column → `Expanded(SingleChildScrollView)` → pinned footer. Mirror this for any new auth screen. --- ## 7. Screen by screen — what was designed and why ### Home — `views/Dashboard/home/homepage.dart` The rider's staging area: who he is, whether he's on duty, what the day looks like, and what's new. **Header.** The brand mark sits at 56 px on a bordered, brand-tinted tile. At its old 46 px, flat on a white header, it read as a smudge rather than a logo — the tile gives it an edge. Next to it, **"Hi, {name}"** is the heaviest type in the header (17sp / w800, near-black), with `Miler · {city}` beneath at 11.5sp / w600 secondary. The greeting is the anchor of the screen, so it gets the weight; the location is context, so it recedes. On the right, the duty toggle. **Shift banner.** Two deliberately different states. *Set* is a quiet grey strip with the window and time remaining — confirmation, not news. *Not set* is a warning surface with a left accent bar, a pulsing icon, the consequence stated plainly ("duty hours and the shift-end reminder are off"), and a **Retry**. Without a shift window the app cannot close duty and cannot fire the shift-end alarm, so this is an operational fault. Retry re-reads prefs and says so honestly when the fix is off-app — there is no standalone endpoint to re-pull the shift, only the login / MPIN profile sync. **Route brief.** The VRP card described in §3 — a `hub → progress → hub` rail showing *X of Y stops done*, then stops left · parcels + weight · `≈ km round trip` · ₹ to collect, then risk lines only when there is risk. **Stop cards.** Redesigned Uber-style: PICKUP/DELIVERY badge + order id + circular select in the header, a hero name (16.5sp w700, `-0.3` tracking), pin + address, a grey items chip, and labelled Call / Details buttons. Removed along the way: a misleading two-dot route timeline (a single stop is not a route — now a compact location badge), and a redundant second identity row. ### Bookings / My Pickups — `views/Dashboard/pickups/pickups.dart` The active route: the ordered list of stops with a hub → progress → hub overview. Staggered `Reveal` on entry, keyed per stop so the poll never replays it. ### The per-stop flow — `views/Dashboard/pickups/` | File | Role | |---|---| | `card.dart` | The stop card in the route list | | `map.dart` | Map preview, navigation launch, **and the owner of the success-screen transition** | | `multi_map.dart` | Whole-route map with real step numbers | | `map_btn.dart` | The map-view entry row on the stop card | | `pip.dart` | Picture-in-picture info card while navigating | | `pickup_verify.dart` | Proof capture — photo, weight, OTP, signature plumbing | | `sheet.dart` | The confirm bottom sheet | | `skip_sheet.dart` | Skip with reason | | `payment_screen.dart` | Cash / UPI-QR collection | | `done.dart` | Success screen + "move to next stop" | **Critical navigation architecture — do not regress this.** The confirm sheet (`_PickupBottomSheet._handleConfirm`) must **not** navigate to `PickupsDone` itself. Pushing a full page from inside a modal bottom sheet is unreliable — the success screen silently never showed, and an `isCurrent` race then popped everything. Instead the sheet **pops with an outcome map**: ```dart {'outcome': 'completed'|'cancelled', 'isDelivery': bool, 'bonusPoints': int, 'paymentMethod': String} // skip pops `true`; dismiss pops `null` ``` The **caller** — `map.dart`'s `_startPickupNavigation`, which is a normal page route — inspects the result and does `Navigator.pushReplacement(context, PickupsDone(...))` from its own route context (`map.dart:788`). It also removes the finished stop from the list first, so it doesn't linger as "active" blocking the next stop. This is what makes "Move to next stop" appear after picked up / delivered / cancelled. Other flow decisions: no-payment pickups skip the payment-method list entirely (the reason list only shows for cancel or pickup-with-collection). The signature step was dropped for pickups — photo plus weight is enough proof. Test pre-fills in `pickup_verify.dart` were removed. ### Earnings — `views/Dashboard/summary/summary.dart` Pickup counts, success rate, reward points, distance ridden. The hero count is wrapped in an `AnimatedSwitcher` (fade + scale) so switching period animates rather than cutting. ### Account — `views/Dashboard/profile/` Rider details, support tickets, FAQ, notifications, alert sound, saved addresses, rewards, terms. Page-entrance `Reveal`; `PressScale` on the tile rows. ### Offline — `views/offline/offline_page.dart` A full-screen state with a breathing halo, stacked over the app whenever connectivity drops. Retry routes back through `AppBootstrap`, which re-decides login vs. home from scratch. --- ## 8. Backend integration and known gaps The app targets the **new v1 backend** — `https://api.doormile.com/api/v1`, Bearer-token auth, `{success, data, message}` envelopes, camelCase — alongside the legacy jupiter/workolik backend. **Strategy: adapter + env flag.** `lib/data/api_config.dart` is the one switch and mapper: - `ApiConfig.useNewApi` — defaults **true**; override with `--dart-define=USE_NEW_API=false` to go back to the old backend and mock data. - `ApiConfig.toLegacyEnvelope()` re-wraps v1 responses into the legacy `{status, details}` shape with legacy field names. - Providers branch on the flag and call the new endpoints; **the ~90 UI files are untouched.** Two mock layers had to be disabled for live data: `getMockQueues()` / `getMockBookingPageItems()` fallbacks (now gated behind `!useNewApi`), and the auth controller, which used to fake a Demo Rider (userid 9999) and bypass the server entirely. In live mode `verifyPinWithServer` now does a real `POST /miler/verify-pin` and succeeds only on a server OK plus a stored token. **The pasted API doc was inaccurate** (verified live 2026-07-18). `verify-pin` returns the login under **`user` / `user.profile`**, not `data`. The mapper reads the real shape. Booking field names are still based on the unreliable doc — **re-verify the `pickupFromBooking` mapping once a real booking is assigned.** `AssignmentLookup` (`lib/data/assignment_lookup.dart`) exists because of one of these mismatches: `POST /miler/assignments/{id}/accept` keys on `bookingassignmentid`, but everything the app renders comes from `GET /miler/bookings`, whose rows only carry `bookingid`. Different sequences, different values. Sending a booking id made the backend answer 404 while the rider saw success and the booking was never actually accepted. `GET /miler/assignments` is the only place the pairing is exposed, so the app fetches it and keeps a short-lived map. ### Known backend gaps The app degrades gracefully around all of these and logs `[API_GAP]`: - no skip / resume endpoint - no booking-cancel endpoint - **no per-stop `type` field** (pickup vs delivery) and no `step` ordering - booking carries no `assignmentid` / `consignmentid` — accept, reject and deliver assume they equal the booking id - no COD amount on the booking - no update-PIN or rider-count endpoints - no bonus-summary endpoint (rewards are derived from `/miler/earnings`) ### Release safety Demo and test toggles are tied to `kDebugMode` so they auto-disable in release builds: `kBypassGeofenceForTesting` (`pickups_controller.dart`), `kDemoMixedRoute` (`stop_type.dart`), and the summary weekly-kms mock fallback. --- ## 9. What we are doing with this app now The programme is one thing: **make the app simple enough that a rider running it 30–50 times a day never has to think about it.** The app was already well-built — branded app bars, a coherent maroon palette, a premium Earnings hero, clean cards. It did not need a layout rewrite. It needed clarity, motion, and honest data. ### Done - **Terminology and IA** — one noun set (Route / Stops / Hub). Home = new stops and route start; Bookings = the active route. - **Typeface** — Plus Jakarta Sans → **Manrope**, verified on device. - **Semantic colour consolidation** — four greens → one `acceptGreen`; roles documented and applied. - **Slide fatigue killed** — start-pickup and arrived became taps; the confirm sheet's hidden swipe became three visible buttons. Slides remain only on payment and Off Duty. - **Motion layer** — `Reveal`, `PressScale`, shimmer skeletons; staggered lists on Bookings, Home and notifications; `Transition.cupertino` globally; animated tab switches over a state-preserving `IndexedStack`. - **Auth screens** — one top-anchored pattern with a pinned solid-maroon CTA; logo shrunk 180 → 104.h so content sits higher. - **Mixed pickup/delivery plumbing** — `stop_type.dart` gives one branch point; the delivery flow is fully wired end to end (card badge, OTP + photo verify, no-payment "Delivered", done screen) behind `kDemoMixedRoute`. - **Duty and status normalisation** — `DutyController` and `StopStatus` ended the "rider randomly goes offline" and "status string mismatch" classes of bug. - **Splash screen deleted** — routing preserved in `AppBootstrap`, four seconds per launch removed. - **Home redesign** — bigger, framed logo; bold greeting; shift-not-set promoted to a real alert with a Retry; and the **VRP route brief** (§3), covered by 15 unit tests in `test/route_brief_test.dart`. ### Next 1. **Ship mixed routes for real.** Everything on the app side is built. It is blocked on the backend sending a per-stop `type` (`pickup` | `delivery`) and a `step` order. The moment it does, `stopKindOf` prefers it automatically and `kDemoMixedRoute` becomes irrelevant. 2. **Re-verify `pickupFromBooking`** against a real assigned booking (§8). 3. **Retire the accept/reject metaphor on Home.** This is not an on-demand marketplace — the route is pre-assigned, so multi-select checkboxes plus "Select All" plus single-tap accept is three interaction models for a decision the rider doesn't actually get to make. Reframe as *start assigned route*. 4. **Make skip/resume first-class** in the route view, and get a real backend endpoint behind it. 5. **Close the remaining colour strays** (`#007AFF`, `#BA1A1A`) in nav and summary. 6. **Real road distance** in the route brief once a routing/matrix API is available — the `≈` haversine is a deliberate placeholder. --- ## 10. Conventions for anyone touching this code 1. **Read §7's navigation warning before touching the pickup flow.** The sheet-pops-an-outcome architecture is load-bearing and was arrived at after a silent-failure bug. 2. **Use the tokens.** `ColorConstants`, `DesignConstants`, `FontConstants`. No new hex values, no hardcoded font families, no magic spacing. 3. **Use the shared components.** `MilerAppBar` for headers, `AppCard` for cards, `PrimaryButton` for CTAs, `Reveal`/`PressScale`/skeletons for motion and loading. Hand-rolling one of these is how the app got four different greens. 4. **Size with ScreenUtil.** `.w` `.h` `.sp` `.r` against the 390 × 844 design size. Use `.w` (not `.sp`) for anything that must not resize with the user's font setting — logos, icon frames. 5. **New gestures are taps.** A slide needs a justification: it is only for actions that would be expensive to trigger by accident. 6. **Keep `Reveal` keyed.** An unkeyed `Reveal` in a polled list replays every few seconds and looks broken. 7. **Never block the rider on a missing endpoint.** Fall back, log `[API_GAP]`, keep him moving. 8. **Render honesty.** `≈` for estimates, `—` for missing, never a fake `0`. 9. **Run `flutter analyze lib/ test/` before you call it done.** The target is zero errors; the existing infos are legacy noise, don't add to them. 10. **Test the pure logic.** `RouteBrief` is a value object with no Flutter dependency precisely so it can be unit-tested. Follow that pattern for new derived data. --- ## The core idea, simply put **A miler is handed a fixed, ordered route for a time slot, does every pickup and delivery on it in sequence, and brings everything back to the hub he started from — and this app guides him through every stop of that journey.** Everything else in this document is in service of making that loop cost him as little attention as possible, fifty times a day.