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

4.6 KiB

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 UniversalHits — 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.