first commit
This commit is contained in:
682
ABOUT_MILER.md
Normal file
682
ABOUT_MILER.md
Normal file
@@ -0,0 +1,682 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user