102 lines
4.6 KiB
Markdown
102 lines
4.6 KiB
Markdown
|
|
# Manager orders view (`/manager`)
|
||
|
|
|
||
|
|
A hidden, installable PWA for the managers: one link, opened on a
|
||
|
|
phone, showing where every order is — **New / Processing / Completed**.
|
||
|
|
|
||
|
|
It is deliberately not in the sidebar. The route lives in the
|
||
|
|
`(mobile)` route group, outside `(app)`, so `<Sidebar />` never
|
||
|
|
renders on it and there is no nav entry anywhere.
|
||
|
|
|
||
|
|
## Where things live
|
||
|
|
|
||
|
|
| Piece | File |
|
||
|
|
|---|---|
|
||
|
|
| Page (route group) | `src/app/(mobile)/manager/` — `layout.tsx`, `auth-gate.tsx`, `page.tsx`, `manager-orders-view.tsx` |
|
||
|
|
| Read model | `src/lib/server/manager-orders-service.ts` |
|
||
|
|
| API route | `src/app/api/v1/manager-orders/route.ts` |
|
||
|
|
| PWA manifest | `public/manifest-manager.json` |
|
||
|
|
| Service worker | `public/sw-manager.js` |
|
||
|
|
| SW registration | `src/components/service-worker-registration.tsx` (adds `/manager` → `/sw-manager.js`) |
|
||
|
|
| Bucket + settled-status contract | `src/lib/item-status-ui.ts` |
|
||
|
|
| Shared window filter | `settledWindowFilter` in `src/lib/server/orders-service.ts` |
|
||
|
|
|
||
|
|
## The three buckets
|
||
|
|
|
||
|
|
`PIPELINE_GROUP_ITEM_STATUSES` in `src/lib/item-status-ui.ts` is a
|
||
|
|
coarse view of the desk's eight workflow tabs. Both surfaces read the
|
||
|
|
same status lists, so they can never disagree about what a status
|
||
|
|
means.
|
||
|
|
|
||
|
|
| Bucket | Statuses | Orders (live, 2026-10-26) |
|
||
|
|
|---|---|---|
|
||
|
|
| **New** | `pending`, `approval_needed`, `approval_rejected` | 8 |
|
||
|
|
| **Processing** | `processed`, `po_generated`, `collection_pool`, `assigned`, `ready_delivery`, `collected`, `stock_verified`, `picking`, `ready_invoice`, `stock_booked`, `with_driver` | 79 |
|
||
|
|
| **Completed** | the settled set, windowed to 7 / 14 / 30 days | 65 / 112 / 200 |
|
||
|
|
|
||
|
|
An order lands in a bucket when **at least one of its lines** is in
|
||
|
|
that bucket. That is why Processing holds 79 orders for only 206
|
||
|
|
open lines: 45 of those orders have only ONE line left, and 40 have
|
||
|
|
other lines already invoiced or collected. `cancelled` and `removed`
|
||
|
|
are deliberately outside the pipeline (they live on /orders/archive).
|
||
|
|
|
||
|
|
## The Completed window — read before changing it
|
||
|
|
|
||
|
|
There is no single "finished at" column. `SETTLED_STATUSES` +
|
||
|
|
`SETTLED_DATE_FIELDS` in `src/lib/item-status-ui.ts` encode which
|
||
|
|
field dates each settled status:
|
||
|
|
|
||
|
|
| Status | Dated on | Why |
|
||
|
|
|---|---|---|
|
||
|
|
| `delivered` | `delivered_at` | populated on only 2 of 12,957 lines; the rest are legacy rows whose `updated` is the MIGRATION timestamp, so an un-dated delivered row is ARCHIVED, not recent |
|
||
|
|
| `invoiced` | `invoiced_date` | the only properly stamped settlement date in the schema (2,199 lines) |
|
||
|
|
| `completed`, `checked_in`, `delivery_note` | `updated` | no dedicated column exists, so `updated` is the only signal |
|
||
|
|
|
||
|
|
`primary` and `fallback` are mutually exclusive. Giving a status both
|
||
|
|
would resurface archived rows every time an old row was touched.
|
||
|
|
|
||
|
|
Before this change the Completed tab matched **0** orders: the filter
|
||
|
|
only looked at `delivered` + `completed` dated on `delivered_at` /
|
||
|
|
`updated`, and the business stopped writing `delivered` on 2026-08-14
|
||
|
|
(the live pipeline now ends at `invoiced` / `checked_in`). It now
|
||
|
|
matches 341 orders over 30 days, 112 over 7.
|
||
|
|
|
||
|
|
`settledWindowFilter(days)` is shared with `/orders` (30 days) — only
|
||
|
|
the window length differs, so the two surfaces never disagree.
|
||
|
|
|
||
|
|
## The preview popup is reused, not rebuilt
|
||
|
|
|
||
|
|
The service emits `UniversalHit`s — the same shape the universal
|
||
|
|
search returns — and the page hands them to the existing
|
||
|
|
`SearchPreviewDialog` (`src/app/(app)/search/search-preview.tsx`). So
|
||
|
|
the manager gets the desk's quick-overview verbatim: status chips,
|
||
|
|
stats, invoice refs, delivery-note numbers, the line list and the
|
||
|
|
lifecycle timeline, plus a link out to the full order page.
|
||
|
|
|
||
|
|
**Read-only.** Nothing on the phone can change an order's state.
|
||
|
|
|
||
|
|
## API
|
||
|
|
|
||
|
|
```
|
||
|
|
GET /api/v1/manager-orders?group=new|processing|completed
|
||
|
|
&window=5|7|14|30
|
||
|
|
&search=<order no|customer|company|rep>
|
||
|
|
&page=1&perPage=20
|
||
|
|
```
|
||
|
|
|
||
|
|
Auth is the PB browser session in the `Authorization` header — the
|
||
|
|
same `assertBrowserSession` pattern as `/api/v1/boss-dashboard`. The
|
||
|
|
response carries `counts` (distinct orders per bucket), `total`,
|
||
|
|
`totalPages`, the page of `orders` and `viewerName`.
|
||
|
|
|
||
|
|
## Installing it on a phone
|
||
|
|
|
||
|
|
Open the link on the phone → browser menu → **Add to Home Screen**. It
|
||
|
|
installs as a standalone app ("WFDASH — Orders") with its own icon,
|
||
|
|
launched in portrait with no browser chrome. iOS needs the
|
||
|
|
`apple-mobile-web-app-capable` meta, which the layout sets (Next 16
|
||
|
|
only emits the standardised name, which iOS ignores — see the note in
|
||
|
|
`(mobile)/m/layout.tsx`).
|
||
|
|
|
||
|
|
The service worker precaches the shell and never caches `/api/`, so a
|
||
|
|
stale order count can never be shown.
|