# Screens Inventory — Dine Module

> **Verified accurate:** 2026-05-02 — Stage 3 backend pre-verification still holds, screen-card schema and per-screen entries accurate. **One drift:** `front/dine/` is now 4 files (not 2) — added `dine-screens-batch6.js` and one Batch 6 screen split; net-new surfaces, original 9 top-level screens still owned by `dine.compiled.js` + `dine-modules.compiled.js` as documented. The `workflows/{kds,server,host,pos}` aggregate-routes warning in §6.3 still applies.
> **Status:** active source-of-truth doc; the canonical Dine screen catalog.

**Audience:** integration engineers wiring the restaurant surface to the real backend (`dine.*` namespace + `/orders`, `/tables`, `/kds`, `/menus`, `/recipes`, `/aggregator/*`, `/delivery/*`, `/qr/*`).
**Scope:** every consumer-visible screen, modal, and drawer under `front/dine/`. **2 source files, 9 top-level screens, 0 modals.**
**Companion docs:** `MODULE-MAP.md`, `DESIGN-SYSTEM.md`, `API-USAGE-MAP.md §4.4`, `INTEGRATION-NOTES.md`, `SCREENS-INVENTORY-Retail.md` (sale capture in dine-mode flows through the same `retail.sales` endpoint as the POS).

> **Backend pre-verification note** (per integration-team direction): Dine routes have been confirmed in `routes/api.php` per the Stage 3 backend tag (`ready-for-handoff-v6-stage3`) and the OpenAPI spec at `docs/openapi/openapi.yaml`. Specifically: `/orders` (POST/cancel/close/fire/items/serve), `/order-items/{id}` (PATCH + void), `/tables` (list + `/seat`, `/move`), `/menus`, `/menu-items/bulk-availability`, `/modifier-groups`, `/combos`, `/recipes` (list + patch), `/recipe-drift`, `/reservations`, `/waitlist`, `/aggregator/inbox`, `/aggregator/{id}/{accept,reject}`, `/delivery/{queue,assign,stack}`, `/delivery/{id}/{handoff,pod}`, `/couriers/{id}/close-shift`, `/kds/items/{id}/{action}`, `/production-orders`, `/qr/{token}/{menu,orders}`. **The `workflows/{kds,server,host,pos}` aggregate routes referenced in earlier discussions are NOT in the OpenAPI spec** — see §6.3 for what this means at cut-over.

---

## How to read this doc

Same shape as `SCREENS-INVENTORY-Retail.md` and `SCREENS-INVENTORY-Pay.md`:

> **<Route key> · <Screen name>** — one-line purpose
> **File(s):** source files
> **Mounts at:** route key in `DALSEEN_NAV`
> **Owns / Composes / States / Actions / Data / Permissions / Quirks**

When the FE today reads from a static demo array (`MENU_ITEMS = [...]`, `RECIPES = [...]`, `RESERVATIONS = [...]`, `DELIVERY = [...]` — all four are inline arrays inside `dine-modules.compiled.js`) **or** from `window.DALSEEN_DATA.{menu,tables,tickets}` (the legacy fixture), I list **both** the demo source and the canonical `window.API.dine.<resource>.list` it will resolve to after cut-over.

---

## 1 · Module overview

### 1.1 File census (2 files)

| File                         | Lines | Owns                                                                                  |
|------------------------------|------:|---------------------------------------------------------------------------------------|
| `dine.compiled.js`           |   477 | `DineOrder`, `DineTables`, `DineKDS`                                                  |
| `dine-modules.compiled.js`   |   ~570 | `DineMenu`, `DineRecipes`, `DineReservations`, `DineDelivery`, `DineWaiter`, `DineReports` (IIFE-scoped, all globals via `window.DineX = DineX`) |

Compared to Retail (50 files), Dine is **intentionally compact** — like Pay. The screens are demonstrative scaffolds for the restaurant surface; the heavy lifting (KDS routing, aggregator webhooks, recipe-cost layers, courier dispatch) sits **behind** the boundary in the real backend, not in the FE. Several screens here are status quo "this is what the data looks like" displays rather than fully wired flows — flagged per-screen below.

### 1.2 Top-level routes

The Dine module contributes 9 route keys to `DALSEEN_NAV` (`front/common/tokens.js` lines 154–163):

| Route key            | Sidebar label                  | Component             | File                       | Status     |
|----------------------|--------------------------------|-----------------------|----------------------------|------------|
| `dine.order`         | Order Screen                   | `DineOrder`           | `dine.compiled.js`         | live (fixture) — `DALSEEN_DATA.menu` |
| `dine.tables`        | Tables · Floor Plan            | `DineTables`          | `dine.compiled.js`         | live (fixture) — `DALSEEN_DATA.tables` |
| `dine.kds`           | KDS · Kitchen Display          | `DineKDS`             | `dine.compiled.js`         | live (fixture) — `DALSEEN_DATA.tickets` |
| `dine.menu`          | Menu & Modifiers               | `DineMenu`            | `dine-modules.compiled.js` | live (fixture) — inline `MENU_ITEMS` |
| `dine.recipes`       | Recipes & Ingredients          | `DineRecipes`         | `dine-modules.compiled.js` | live (fixture) — inline `RECIPES` |
| `dine.waiter`        | Waiter Handheld                | `DineWaiter`          | `dine-modules.compiled.js` | live (fixture) — inline `WAITER_*` |
| `dine.reservations`  | Reservations                   | `DineReservations`    | `dine-modules.compiled.js` | live (fixture) — inline `RESERVATIONS` |
| `dine.delivery`      | Delivery Dispatch              | `DineDelivery`        | `dine-modules.compiled.js` | live (fixture) — inline `DELIVERY` |
| `dine.reports`       | Reports                        | `DineReports`         | `dine-modules.compiled.js` | live (fixture) — synthesised from `DALSEEN_DATA` |

**No live wrapper exists** for Dine. Unlike `owner-live.jsx` and `platform-live.jsx`, there is no `dine-live.jsx`. Every screen reads its own data source directly. This is the **single biggest gap** in the Dine surface and the first cut-over task.

### 1.3 Data sources in play

Dine reads from **two different** fixture surfaces depending on which screen you're looking at, which is itself a bug worth flagging:

| Surface                            | Used by                                | Notes |
|------------------------------------|----------------------------------------|-------|
| `window.DALSEEN_DATA.{menu,tables,tickets}` | `DineOrder`, `DineTables`, `DineKDS` (`dine.compiled.js`) | The original cross-module fixture from before the Dine module was split out. `tickets` is what becomes `kdsTickets` over the wire. |
| Inline arrays inside `dine-modules.compiled.js` (`MENU_ITEMS`, `RECIPES`, `RESERVATIONS`, `DELIVERY`, `WAITER_TABLES`, `WAITER_DOCKETS`) | The 6 screens defined inside the IIFE  | These were written as one-off demo arrays and **were never lifted to the `DINE_*` window globals** that `api-seeds.js` expects. |
| `window.DINE_TABLES` / `DINE_KDS` / `DINE_MENU` / `DINE_RECIPES` / `DINE_RESERVATIONS` / `DINE_DELIVERIES` / `DINE_WAITERS` / `DINE_REPORTS` | **Nobody (yet)** | Registered via `D('dine.<x>', fromGlobal('DINE_<X>'))` in `api-seeds.js` lines 370–377 — the seed factories return `[]` because the globals are never assigned. The auto-CRUD endpoints exist, but the mock returns empty arrays. |

> **Cut-over implication:** wiring real APIs into Dine is a two-step move, not one. Step 1: write a `dine-live.jsx` wrapper and point the 9 screens at `useApi(API.dine.<x>.list)`. Step 2: delete the inline arrays and `DALSEEN_DATA` reads. Doing it in one shot will surface the empty-fixture bug above and confuse testers.

### 1.4 The two distinct surfaces

The 9 screens cluster into two operationally-distinct surfaces:

**Front-of-house (server, host, customer-facing):**
- `dine.order` — full-keyboard order entry (counter-style)
- `dine.tables` — host stand floor plan
- `dine.waiter` — handheld pad (different form factor; mobile-shaped)
- `dine.reservations` — phone/walk-in booking calendar

**Kitchen / back-of-house / configuration:**
- `dine.kds` — kitchen display
- `dine.menu` — menu builder
- `dine.recipes` — recipe + ingredient cost
- `dine.delivery` — courier dispatch
- `dine.reports` — sales mix / hourly heat / cost reports

This split mirrors the backend's `workflows/{server,host,kds,pos}` aggregates. **However those aggregate endpoints are not in the current OpenAPI** — see §6.3.

---

## 2 · Front-of-house screens

### 2.1 `dine.order` · Order Screen
> Counter-mode order entry — 2-pane layout (left: category tabs + item grid; right: cart with subtotal / 10% service / 15% VAT / total + "Send to Kitchen" CTA).

- **File:** `dine.compiled.js` (`DineOrder`)
- **Mounts at:** `dine.order`
- **Owns:** category filter, item-tap-to-add, cart qty +/−, totals strip
- **Composes:** `window.SectionHeader`, `window.fmtSAR`
- **States:** idle (cart pre-seeded with 3 items for demo); no loading/empty/error states
- **Actions:** `add(item)`, `inc(id, ±1)`, "Send to Kitchen" (no-op today)
- **Data today:** `window.DALSEEN_DATA.menu` (filtered by `cat in {Mains, Starters, Drinks, Desserts}`)
- **Data at cut-over:** `API.dine.menuItems.list({ available: true })` for the grid; `API.dine.tables.get(:tableId)` for the open ticket; `POST /orders` and `POST /orders/{id}/items` to send to kitchen
- **Permissions:** `dine.orders.create`, `dine.orders.modify`
- **Quirks:** "Send to Kitchen" button has **no handler** today — at cut-over it must:
  1. Either `POST /orders` (new) or `POST /orders/{id}/items` (existing) with `Idempotency-Key` to prevent double-fire
  2. Then `POST /orders/{id}/fire` to push to KDS
  3. Then refetch the table's open order

### 2.2 `dine.tables` · Floor Plan
> 4-column grid of table cards. State = `free | busy | reserved | dirty`. Busy tables show open total + minutes; reserved tables show party name.

- **File:** `dine.compiled.js` (`DineTables`)
- **Mounts at:** `dine.tables`
- **Owns:** table grid render, state-color mapping
- **Composes:** `window.SectionHeader`, `window.fmtSAR`
- **States:** static — no loading/empty/error states
- **Actions:** **none today** (cards are not clickable)
- **Data today:** `window.DALSEEN_DATA.tables` (12 tables)
- **Data at cut-over:** `API.dine.tables.list({ section, branchId })`
- **Permissions:** `dine.tables.view`
- **Missing actions at cut-over:** seat (`POST /tables/{id}/seat`), move (`POST /tables/{id}/move`), mark-dirty / mark-clean (PATCH on resource), open-ticket-for-table (jump to `dine.order` with `tableId`)
- **Quirks:** No drag-to-rearrange, no merge/split. Add at v1.1.

### 2.3 `dine.waiter` · Waiter Handheld
> Compact section-switcher pad (sections A/B/C). Shows tables in section, current dockets, and 4 quick actions.

- **File:** `dine-modules.compiled.js` (`DineWaiter`)
- **Mounts at:** `dine.waiter`
- **Owns:** section toggle, table list (compact), dockets list (compact)
- **Composes:** `Page`, `Header`, `Tabs`, `Pill`, `TableShell`, `Btn` (all from `dine-modules` shared helpers)
- **States:** static — no loading/empty/error
- **Actions:** "Add item", "Send to KDS", "Print bill", "Settle" — all **no-op** today
- **Data today:** inline `WAITER_TABLES`, `WAITER_DOCKETS`
- **Data at cut-over:** `API.dine.tables.list({ waiterId, section })`, `API.dine.{orders}.list({ waiterId, status: 'open' })` (note: `orders` is not yet seeded — needs `D('dine.orders', …)`)
- **Permissions:** `dine.orders.create`, `dine.orders.modify`, `dine.tables.view`
- **Quirks:** form factor is **counter-shaped**, not phone-shaped. The brief implies a phone (Samsung Sunmi handheld) — this needs a phone bezel + reflow at v1.1, mirroring `front/owner/ios-frame.jsx`.

### 2.4 `dine.reservations` · Reservations
> Filter chips (Today / Tomorrow / Week) + reservation list. Walk-ins, phone bookings, online (via QR / aggregator partner sites).

- **File:** `dine-modules.compiled.js` (`DineReservations`)
- **Mounts at:** `dine.reservations`
- **Owns:** date filter, table layout
- **Composes:** `Page`, `Header`, `Tabs`, `TableShell`, `Pill`
- **States:** static
- **Actions:** none — display only
- **Data today:** inline `RESERVATIONS` (8 entries)
- **Data at cut-over:** `API.dine.reservations.list({ from, to, branchId })`. Custom routes the OpenAPI confirms: `POST /reservations` (create), `POST /waitlist` (walk-in queue).
- **Permissions:** `dine.reservations.view` (read), `dine.reservations.manage` (create/seat/cancel)
- **Missing actions at cut-over:** create reservation, seat-now (push to `tables` + open order), no-show, cancel, link-to-customer (CRM hook)

---

## 3 · Kitchen / back-of-house screens

### 3.1 `dine.kds` · Kitchen Display
> Auto-fill grid of ticket cards. Header shows ticket id, table, elapsed time. Body shows items (qty × name). Late tickets (>12m elapsed) get a danger border. Status = `fired | cooking | plating`.

- **File:** `dine.compiled.js` (`DineKDS`)
- **Mounts at:** `dine.kds`
- **Owns:** ticket grid, late-detection (`elapsed > 12`)
- **Composes:** `window.SectionHeader`
- **States:** static — no loading/empty/error
- **Actions:** **none today** (no bump button, no recall)
- **Data today:** `window.DALSEEN_DATA.tickets`
- **Data at cut-over:** `API.dine.kdsTickets.list({ station, status: 'active' })`. Mutations: `POST /kds/items/{id}/{action}` where `action ∈ {bump, recall, fire, ready}` per OpenAPI line 5717.
- **Permissions:** `dine.kds.view`, `dine.kds.bump`
- **Quirks:** No station filter (Hot / Cold / Drinks). At cut-over add `?station=` filter chip and route to `/kds/items/{id}/bump`.
- **Real-time gap:** KDS needs **push** updates (new ticket arrives → screen refreshes). Today there's no polling, websocket, or SSE channel. This is a backend decision — pick one before Stage 3 ship.

### 3.2 `dine.menu` · Menu & Modifiers
> Category filter (All / Mains / Starters / Drinks / Desserts) + item table with availability toggle, modifier group count, and price.

- **File:** `dine-modules.compiled.js` (`DineMenu`)
- **Mounts at:** `dine.menu`
- **Owns:** category filter, list render
- **Composes:** `Page`, `Header`, `Tabs`, `TableShell`, `Pill`, `Btn`
- **States:** static
- **Actions:** "Toggle availability", "Edit" — no-op
- **Data today:** inline `MENU_ITEMS` (~10 entries)
- **Data at cut-over:** `API.dine.menuItems.list()`. Bulk availability flip = `POST /menu-items/bulk-availability` per OpenAPI line 6303.
- **Modifier groups:** `GET /modifier-groups` per OpenAPI line 6429 — **no FE screen consumes this yet** (gap).
- **Combos:** `GET /combos` per OpenAPI line 4007 — **no FE screen consumes this yet** (gap).
- **Permissions:** `dine.menu.view`, `dine.menu.edit`
- **Missing screens at cut-over:** Modifier-group editor, Combo builder, Menu sections per channel (dine-in / takeout / aggregator-X / aggregator-Y)

### 3.3 `dine.recipes` · Recipes & Ingredients
> Two-pane layout: left = dish list, right = recipe detail (yield, ingredient lines with qty + unit + cost, prep steps, theoretical food cost %).

- **File:** `dine-modules.compiled.js` (`DineRecipes`)
- **Mounts at:** `dine.recipes`
- **Owns:** dish selector, ingredient table, cost-percent calc
- **Composes:** `Page`, `Header`, `TableShell`, `Pill`
- **States:** static
- **Actions:** "Save recipe" (no-op)
- **Data today:** inline `RECIPES` (~6 dishes)
- **Data at cut-over:** `API.dine.recipes.list()` + `API.dine.recipes.get(id)`. PATCH at `/recipes/{id}` per OpenAPI line 10172.
- **Recipe drift:** `GET /recipe-drift` exists per OpenAPI line 10060 (variance between theoretical and actual ingredient consumption). **No FE screen for this yet** — needs a new screen at `dine.drift` or a tab in `dine.reports`. Mohammed flagged this explicitly.
- **Permissions:** `dine.recipes.view`, `dine.recipes.edit`
- **Quirks:** ingredient picker today is a static select; at cut-over it should drive `retail.products` (recipes consume ingredients from inventory).

### 3.4 `dine.delivery` · Delivery Dispatch
> 3-column layout: live stack (preparing / ready / in-transit), available couriers, today's stats. Each delivery card shows order id, address, courier, ETA.

- **File:** `dine-modules.compiled.js` (`DineDelivery`)
- **Mounts at:** `dine.delivery`
- **Owns:** status filter, courier list, dispatch column
- **Composes:** `Page`, `Header`, `StatCard`, `Pill`, `Btn`
- **States:** static
- **Actions:** "Assign courier", "Mark handed off", "Capture POD" — all no-op
- **Data today:** inline `DELIVERY` (~6 entries)
- **Data at cut-over:**
  - List: `GET /delivery/queue` per OpenAPI line 4402 (also `API.dine.deliveries.list`)
  - Assign: `POST /delivery/assign` (line 4370)
  - Stack-batch: `POST /delivery/stack` (line 4436) — multi-drop optimization
  - Handoff: `POST /delivery/{id}/handoff` (line 4468)
  - POD (proof of delivery): `POST /delivery/{id}/pod` (line 4506) — **photo + signature upload**, needs multipart/form-data not JSON
  - Courier shift close: `POST /couriers/{id}/close-shift` (line 4142)
- **Aggregator inbox** (HungerStation, Jahez, Mrsool, Marn, Talabat orders ingested via webhook):
  - List: `GET /aggregator/inbox` (line 3297)
  - Accept: `POST /aggregator/{id}/accept` (line 3336)
  - Reject: `POST /aggregator/{id}/reject` (line 3374)
  - Ingest webhook (server→server, not consumer-facing): `POST /webhooks/aggregator`
  - **No FE screen for the aggregator inbox yet** — needs a new sub-tab inside `dine.delivery` or a new route `dine.aggregators`.
- **Permissions:** `dine.delivery.dispatch`, `dine.delivery.view`, `dine.aggregators.manage`
- **Real-time gap:** dispatch needs polling or push (new aggregator order arrives → dispatcher sees it). Same architectural decision as KDS.

### 3.5 `dine.reports` · Reports
> Period filter (today / week / month / custom). KPIs: covers, avg ticket, food cost %, voids. Sales mix donut by category. Hourly heat-map. Top items table.

- **File:** `dine-modules.compiled.js` (`DineReports`)
- **Mounts at:** `dine.reports`
- **Owns:** period filter, all chart synthesis from `DALSEEN_DATA`
- **Composes:** `Page`, `Header`, `Tabs`, `StatCard`, `TableShell`
- **States:** static — synthesised from fixture
- **Actions:** "Export CSV" (no-op)
- **Data today:** synthesised — there is no `DALSEEN_DATA.dineReports` blob; the screen runs reductions over `DALSEEN_DATA.menu` + `DALSEEN_DATA.tickets`
- **Data at cut-over:** `API.dine.dineReports.list()` (auto-CRUD, currently empty) **OR** new aggregate routes `/reports/dine/{covers,sales-mix,food-cost,hourly-heat}` matching the `/reports/pay/*` and `/reports/finance/*` pattern. The latter is more honest — reports aren't a CRUD resource.
- **Permissions:** `dine.reports.view`
- **Recommendation:** delete `dine.dineReports` from `api-seeds.js` and wire to four named report endpoints. See §7.

---

## 4 · API namespace map

### 4.1 Auto-CRUD resources (`api-seeds.js` lines 370–377)

All resources below get the seven verbs (`list / get / create / update / patch / replace / remove`) per `API-USAGE-MAP §5`. Today every seed is empty (the `DINE_*` globals are never assigned — see §1.3).

| Resource             | Window global       | Backed by                    | Consumed by    |
|----------------------|---------------------|------------------------------|----------------|
| `dine.tables`        | `window.DINE_TABLES` (unset) | `GET /tables`           | none yet — `DineTables` reads `DALSEEN_DATA.tables` |
| `dine.kdsTickets`    | `window.DINE_KDS` (unset)    | `GET /kds/...` (no list endpoint in OpenAPI — only `/kds/items/{id}/{action}`) | none yet — `DineKDS` reads `DALSEEN_DATA.tickets` |
| `dine.menuItems`     | `window.DINE_MENU` (unset)   | `GET /menus`, `GET /menu-items` | none yet — `DineMenu` reads inline |
| `dine.recipes`       | `window.DINE_RECIPES` (unset) | `GET /recipes`              | none yet — `DineRecipes` reads inline |
| `dine.reservations`  | `window.DINE_RESERVATIONS` (unset) | `GET /reservations`     | none yet — `DineReservations` reads inline |
| `dine.deliveries`    | `window.DINE_DELIVERIES` (unset) | `GET /delivery/queue`    | none yet — `DineDelivery` reads inline |
| `dine.waiters`       | `window.DINE_WAITERS` (unset) | (no direct endpoint — derives from `users` filtered by role) | none yet |
| `dine.dineReports`   | `window.DINE_REPORTS` (unset) | (should be deleted — see §7) | `DineReports` synthesises from `DALSEEN_DATA` |

### 4.2 Custom routes (registered in OpenAPI, **not yet** in `api-seeds.js`)

These need `defineRoute(...)` entries in `api-seeds.js` so consumers can call them via `API.dine.*` instead of `API._call('POST', '...')`.

| Method | Path                                       | OpenAPI line | Purpose                            | Consumer (after cut-over)      |
|--------|--------------------------------------------|-------------:|------------------------------------|--------------------------------|
| POST   | `/orders`                                  | 6561         | Open new order                     | `DineOrder`, `DineWaiter`      |
| POST   | `/orders/{id}/items`                       | 6708         | Add line items                     | `DineOrder`, `DineWaiter`      |
| POST   | `/orders/{id}/fire`                        | 6670         | Send to KDS                        | `DineOrder`, `DineWaiter`      |
| POST   | `/orders/{id}/serve`                       | 6746         | Mark items served                  | `DineWaiter`                   |
| POST   | `/orders/{id}/cancel`                      | 6594         | Void unfired order                 | `DineOrder`, `DineWaiter`      |
| POST   | `/orders/{id}/close`                       | 6632         | Close order (settle)               | `DineWaiter`, POS settle flow  |
| PATCH  | `/order-items/{id}`                        | 6485         | Modify line (qty, mods)            | `DineOrder`, `DineWaiter`      |
| POST   | `/order-items/{id}/void`                   | 6523         | Void after fire (manager auth)     | `DineWaiter`                   |
| POST   | `/tables/{id}/seat`                        | 13060        | Seat party (links to `customers`)  | `DineTables`                   |
| POST   | `/tables/{id}/move`                        | 13022        | Move party to another table        | `DineTables`                   |
| POST   | `/menu-items/bulk-availability`            | 6303         | 86 multiple items at once          | `DineMenu`                     |
| POST   | `/kds/items/{id}/{action}`                 | 5717         | bump / recall / fire / ready       | `DineKDS`                      |
| POST   | `/waitlist`                                | 13732        | Add walk-in to queue               | `DineReservations`             |
| GET    | `/aggregator/inbox`                        | 3297         | Pending aggregator orders          | (new screen, `dine.aggregators`) |
| POST   | `/aggregator/{id}/accept`                  | 3336         | Accept aggregator order            | (new screen)                   |
| POST   | `/aggregator/{id}/reject`                  | 3374         | Reject (unable to fulfill)         | (new screen)                   |
| GET    | `/delivery/queue`                          | 4402         | Live dispatch queue                | `DineDelivery`                 |
| POST   | `/delivery/assign`                         | 4370         | Assign courier                     | `DineDelivery`                 |
| POST   | `/delivery/stack`                          | 4436         | Multi-drop batch                   | `DineDelivery`                 |
| POST   | `/delivery/{id}/handoff`                   | 4468         | Mark handed to courier             | `DineDelivery`                 |
| POST   | `/delivery/{id}/pod`                       | 4506         | Capture POD (photo + signature)    | `DineDelivery`                 |
| POST   | `/couriers/{id}/close-shift`               | 4142         | Courier shift close                | `DineDelivery`                 |
| POST   | `/production-orders`                       | 9626         | Prep ahead (e.g. dough)            | (new screen — gap)             |
| GET    | `/recipe-drift`                            | 10060        | Theoretical-vs-actual variance     | (new tab in `dine.reports`)    |
| GET    | `/qr/{token}/menu`                         | 9972         | Public QR menu (customer)          | (new public screen — gap)      |
| POST   | `/qr/{token}/orders`                       | 9996         | Customer self-order via QR         | (new public screen — gap)      |
| GET    | `/modifier-groups`                         | 6429         | Modifier-group list                | (new editor — gap)             |
| GET    | `/combos`                                  | 4007         | Combo definitions                  | (new editor — gap)             |

---

## 5 · State + permission matrix

| Screen               | Loading | Empty | Error | Permission                          |
|----------------------|:-------:|:-----:|:-----:|-------------------------------------|
| `dine.order`         | ✗       | ✗     | ✗     | `dine.orders.create`                |
| `dine.tables`        | ✗       | ✗     | ✗     | `dine.tables.view`                  |
| `dine.kds`           | ✗       | ✗     | ✗     | `dine.kds.view`                     |
| `dine.waiter`        | ✗       | ✗     | ✗     | `dine.orders.create`                |
| `dine.reservations`  | ✗       | ✗     | ✗     | `dine.reservations.view`            |
| `dine.menu`          | ✗       | ✗     | ✗     | `dine.menu.view`                    |
| `dine.recipes`       | ✗       | ✗     | ✗     | `dine.recipes.view`                 |
| `dine.delivery`      | ✗       | ✗     | ✗     | `dine.delivery.dispatch`            |
| `dine.reports`       | ✗       | ✗     | ✗     | `dine.reports.view`                 |

**Every Dine screen is missing all five canonical states** (idle / loading / empty / error / forbidden — the `useApi` contract from `API-USAGE-MAP §6.4`). This is consistent with §1.2's note that no `dine-live.jsx` exists yet. The wrapper file is the single biggest cut-over task and gets all five states for free per the `makeLive(...)` factory pattern in `owner-live.jsx`.

---

## 6 · Mock-router behaviours the real backend must replicate (Dine-specific)

Inherited from `API-USAGE-MAP §7`, plus three Dine-specific items:

### 6.1 Idempotency on order writes
**Every order mutation MUST be idempotency-keyed.** Specifically `POST /orders`, `POST /orders/{id}/items`, `POST /orders/{id}/fire`, and the cancel/close pair. Servers must store `(tenantId, idempotencyKey) → response` for ≥24h and replay on collision. Why this matters more for Dine than for Retail: a flaky tablet WiFi at the table is far more common than a flaky desktop browser at the till, so double-submits are statistically guaranteed.

### 6.2 KDS actions are state-machine transitions, not CRUD
`POST /kds/items/{id}/{action}` where `action ∈ {bump, recall, fire, ready}` is a guarded transition, not a generic PATCH. Backend must reject invalid transitions (e.g. cannot `bump` a `pending` item; must `fire` first) with `INVALID_TRANSITION` code. The FE today does not handle this error; at cut-over, surface it as a toast inline on the ticket card.

### 6.3 The `workflows/{kds,server,host,pos}` aggregates referenced upstream are NOT in the current OpenAPI

Mohammed's brief mentions `/workflows/kds`, `/workflows/server`, `/workflows/host`, `/workflows/pos`. **Searching `docs/openapi/openapi.yaml` for these returns zero hits** (only `workflows/ecom-orders`, `workflows/ecom-fulfillment`, `workflows/finance-period-close` exist). `routes/api.php` line 374–375 carries an explicit comment: *"workflows/pos, workflows/kds, workflows/server, workflows/host live under the ['branch', 'plan_module:dine'] subgroup elsewhere in the file"*, but the routes themselves were not exported into the OpenAPI generator.

**Decision needed (Mohammed):** are these aggregates real and just missing from the OpenAPI spec, or were they planned-but-not-shipped? The four screens that would naturally consume them (`DineKDS`, `DineWaiter`, `DineTables`, `DineOrder`) work fine with the granular endpoints listed in §4.2 — the aggregates would just save 2–3 round-trips per screen-load. **Recommendation:** ship without the aggregates first; add them in v1.1 as a network optimization.

### 6.4 Real-time channel
KDS, dispatch, and the aggregator inbox all need push updates. **The backend has not committed to a channel yet** (websocket / SSE / long-poll). Ship with 5-second polling (`useApi` with a `setInterval` refetch) at v1.0; revisit at v1.1.

---

## 7 · Highest-impact gaps (TL;DR)

1. **No `dine-live.jsx` wrapper.** Every screen reads its own fixture directly. Building this is the single biggest cut-over win. Mirror `owner-live.jsx` exactly: one `makeLive(...)` per top-level screen, `permission` + `fetch` props, automatic skeleton/error/forbidden.
2. **Two competing fixture surfaces** (`DALSEEN_DATA.{menu,tables,tickets}` vs. `dine-modules` inline arrays vs. unset `DINE_*` globals). Pick one (the auto-CRUD `DINE_*` route) and migrate the other two before cut-over, or three different sets of cut-over PRs collide.
3. **No screens for aggregator inbox, modifier groups, combos, production orders, recipe drift, or QR-code self-order.** The backend has all six endpoints (§4.2). Decide which ship in v1.0 vs. v1.1.
4. **No real-time channel.** KDS/dispatch/aggregator-inbox all need push. Ship polling for v1.0; commit to a channel at v1.1.
5. **No idempotency keys on order mutations.** "Send to Kitchen" today is a no-op; when wired, every write needs a key (§6.1).
6. **`dine.dineReports` resource is wrong shape.** Reports aren't CRUD; should be split into `/reports/dine/{covers,sales-mix,food-cost,hourly-heat,recipe-drift}` mirroring `/reports/pay/*` and `/reports/finance/*`.
7. **`workflows/{kds,server,host,pos}` aggregates need a decision** (§6.3) — are they real and missing from OpenAPI, or never shipped?
8. **Waiter handheld is counter-shaped, not phone-shaped.** Reflow with a phone bezel at v1.1.
9. **`Send to Kitchen`, `Toggle availability`, `Save recipe`, `Assign courier`, `Print bill`, `Settle`, `Export CSV` are all no-op buttons** today. Each maps to a specific endpoint in §4.2 — wire them as part of the `dine-live.jsx` rollout.

---

## Cross-references

- **API surface for everything above:** `docs/handoff/API-USAGE-MAP.md §4.4` (`dine` namespace) + this doc's §4.2 (custom routes not yet registered).
- **Endpoint registrations:** `front/common/api-seeds.js` lines 370–377 (8 auto-CRUD resources).
- **OpenAPI source of truth:** `docs/openapi/openapi.yaml` — see line numbers in §4.2.
- **Routes file summary:** `docs/handoff/routes-api.php` lines 398–401 (Dine route group under `['branch', 'plan_module:dine']`).
- **Live-wrapper template:** `front/owner/owner-live.jsx` and `front/platform/platform-live.jsx` — copy this pattern for `front/dine/dine-live.jsx`.
- **Sale capture:** when a Dine order is settled, payment goes through `retail.sales.create` (the FE treats payment as a step inside the sale, not its own endpoint). See `SCREENS-INVENTORY-Pay.md §1.1` and `SCREENS-INVENTORY-Retail.md §6` for the cross-module flow.
