WFDASH-WEBAPP/docs/manager-orders.md
Dave b212d0fcd8 feat(manager): a mobile orders board for the managers at /manager
A hidden, installable PWA: one link, opened on a phone, showing where
every order is. New / Processing / Completed, with a tap-to-preview
popup and the invoice/line detail Gary needs. Read-only, and NOT in
the sidebar — the route lives outside `(app)`, so `<Sidebar />` never
renders there.

The preview popup is reused, not rebuilt: the service emits
`UniversalHit`s and the page hands them to the existing
`SearchPreviewDialog`, so the manager gets the desk's quick-overview
verbatim.

Also fixes the /orders Completed tab, which was matching ZERO orders.
`delivered_at` is stamped on only 2 of 12,957 delivered lines and the
business stopped writing `delivered` on 2026-08-14 (the live pipeline
now ends at `invoiced` / `checked_in`), so the old filter never
matched. "Settled" is now defined once — `SETTLED_STATUSES` +
`SETTLED_DATE_FIELDS` — and dated per status: `invoiced_date` for
invoiced (the only properly stamped field, 2,199 lines), `delivered_at`
for delivered, `updated` only where no dedicated column exists.

`delivery_note` and `checked_in` move into `completed` as instructed,
and the standalone `post_processing` tab is retired (a DN is not a
stage of its own). `invoiced` stays in `collected` as well, so
invoiced orders don't vanish from the workflow tabs.

Completed tab: 0 -> 341 orders over 30 days, 112 over 7.
2026-10-26 23:09:01 +02:00

101 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.