683 lines
33 KiB
Markdown
683 lines
33 KiB
Markdown
# 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.
|