feat(restaurant): customer-facing restaurant bundle (v1) #54

Merged
padreug merged 9 commits from feat/restaurant-bundle into main 2026-05-11 17:49:19 +00:00
Owner

Summary

New standalone bundle that gives customers a place to browse a restaurant's menu, build a cart, and pay an order over Lightning. Single-venue MVP, URL-driven (/r/:slug); the cart store keys by restaurant_id from day one so the future festival aggregator (aiolabs/restaurant#8) is purely a discovery-layer change.

REST-only order placement; NIP-99 menu state arrives as a live Nostr overlay. NIP-17 transport for order intake is deferred to aiolabs/restaurant#9.

Companion extension change is in aiolabs/restaurant#feat/restaurant-by-slug (3 commits: by-slug endpoint + fiat→sat conversion + KDS dark-mode contrast).

Commits

Nine commits, each vue-tsc -b clean individually:

  1. 41fbad3 — bundle skeleton (vite.restaurant.config.ts, restaurant.html, src/restaurant-app/*, DI tokens, scripts)
  2. 1cdf87b — typed RestaurantAPI REST client + types translated from extension models.py
  3. 3a11d90 — menu browse views (Home, RestaurantPage, ItemPage)
  4. 27d98ce — cart store + cart page + add-to-cart wiring
  5. 940b36b — checkout + order placement + status polling
  6. a7f2ded — orders list + settings
  7. e01e595 — Nostr live overlay (NIP-99) for menu state
  8. 77c81d8 — UX polish: currency display, two-phase checkout w/ QR + copy, friendly status labels
  9. 3131268 — orders-list polish: live status badge + fiat amount + manual refresh FAB

Future-compat scaffolding baked in (no UI yet)

  • Open OrderStatus type + KNOWN_ORDER_STATUSES const for aiolabs/restaurant#4 (kitchen workflow)
  • MenuItem.extra + Restaurant.mode pass-through for #2 / #3 / #5 / #6
  • useCheckout builds CreateOrder through a single buildCreateOrder() helper so loyalty (#5) and NIP-17 (#9) inject in one place
  • Module-level features: {} feature-flag map reserved for tier-gated UI (#2)
  • Cart keys by restaurant_id so the festival aggregator (#8) is a discovery-layer change

Test plan

  • Pull both branches: feat/restaurant-bundle here and feat/restaurant-by-slug on aiolabs/restaurant
  • LNbits dev at http://localhost:5001 with restaurant extension enabled and Big Jay's Bustaurant seeded
  • VITE_LNBITS_BASE_URL=http://localhost:5001 VITE_RESTAURANT_DEFAULT_SLUG=big-jays-bustaurant npm run dev:restaurant
  • / redirects to /r/big-jays-bustaurant, menu loads (REST), [RestaurantNostrSync] subscribed in console
  • Tap a menu item with modifiers → ModifierSelector → add to cart, badge updates
  • /cart shows lines in menu currency (GTQ), totals correct
  • /checkout shows live ≈ X sat preview from /orders/quote
  • Pay & place → bolt11 QR + copy button render; status pill flows pending → paid automatically
  • Hit "Accept" in extension KDS → /orders/:id shows "Cooking" within ~5s
  • /orders list shows live status badge per row + fiat amount; FAB refresh re-fetches
  • Edit a menu item price in the extension CMS → open /r/big-jays-bustaurant tab updates within ~1s (Nostr overlay)
  • Backgrounded tab → foregrounded → order detail re-fetches immediately (VisibilityService)
  • npm run build:restaurant → clean (verified locally: 2422 modules, 3s, no errors)

Verified against Big Jay's Bustaurant

  • Coffee 25 GTQ → cart "25 GTQ" → checkout "≈ 3,966 sat"
  • Quesadillas 80 GTQ (3 modifier groups) → "≈ 12,691 sat"
  • 2x Tacos w/ tortilla + protein selections → 170 GTQ → "≈ 26,968 sat"

All sat amounts come from the extension's fiat-rate-aware /orders/quote endpoint (the companion fix on aiolabs/restaurant).

Out of scope (tracked elsewhere)

  • Festival / aggregator UI — aiolabs/restaurant#8
  • NIP-17 gift-wrapped order intake — aiolabs/restaurant#9
  • Tipping UI, push notifications, kiosk mode, reviews

🤖 Generated with Claude Code

## Summary New standalone bundle that gives customers a place to browse a restaurant's menu, build a cart, and pay an order over Lightning. Single-venue MVP, URL-driven (`/r/:slug`); the cart store keys by `restaurant_id` from day one so the future festival aggregator (`aiolabs/restaurant#8`) is purely a discovery-layer change. REST-only order placement; NIP-99 menu state arrives as a live Nostr overlay. NIP-17 transport for order intake is deferred to `aiolabs/restaurant#9`. Companion extension change is in `aiolabs/restaurant#feat/restaurant-by-slug` (3 commits: by-slug endpoint + fiat→sat conversion + KDS dark-mode contrast). ## Commits Nine commits, each `vue-tsc -b` clean individually: 1. `41fbad3` — bundle skeleton (`vite.restaurant.config.ts`, `restaurant.html`, `src/restaurant-app/*`, DI tokens, scripts) 2. `1cdf87b` — typed `RestaurantAPI` REST client + types translated from extension `models.py` 3. `3a11d90` — menu browse views (Home, RestaurantPage, ItemPage) 4. `27d98ce` — cart store + cart page + add-to-cart wiring 5. `940b36b` — checkout + order placement + status polling 6. `a7f2ded` — orders list + settings 7. `e01e595` — Nostr live overlay (NIP-99) for menu state 8. `77c81d8` — UX polish: currency display, two-phase checkout w/ QR + copy, friendly status labels 9. `3131268` — orders-list polish: live status badge + fiat amount + manual refresh FAB ## Future-compat scaffolding baked in (no UI yet) - Open `OrderStatus` type + `KNOWN_ORDER_STATUSES` const for `aiolabs/restaurant#4` (kitchen workflow) - `MenuItem.extra` + `Restaurant.mode` pass-through for `#2` / `#3` / `#5` / `#6` - `useCheckout` builds `CreateOrder` through a single `buildCreateOrder()` helper so loyalty (`#5`) and NIP-17 (`#9`) inject in one place - Module-level `features: {}` feature-flag map reserved for tier-gated UI (`#2`) - Cart keys by `restaurant_id` so the festival aggregator (`#8`) is a discovery-layer change ## Test plan - [x] Pull both branches: `feat/restaurant-bundle` here and `feat/restaurant-by-slug` on `aiolabs/restaurant` - [x] LNbits dev at `http://localhost:5001` with restaurant extension enabled and Big Jay's Bustaurant seeded - [x] `VITE_LNBITS_BASE_URL=http://localhost:5001 VITE_RESTAURANT_DEFAULT_SLUG=big-jays-bustaurant npm run dev:restaurant` - [x] `/` redirects to `/r/big-jays-bustaurant`, menu loads (REST), `[RestaurantNostrSync] subscribed` in console - [ ] Tap a menu item with modifiers → ModifierSelector → add to cart, badge updates - [x] `/cart` shows lines in menu currency (GTQ), totals correct - [ ] `/checkout` shows live `≈ X sat` preview from `/orders/quote` - [ ] Pay & place → bolt11 QR + copy button render; status pill flows `pending → paid` automatically - [ ] Hit "Accept" in extension KDS → `/orders/:id` shows "Cooking" within ~5s - [ ] `/orders` list shows live status badge per row + fiat amount; FAB refresh re-fetches - [ ] Edit a menu item price in the extension CMS → open `/r/big-jays-bustaurant` tab updates within ~1s (Nostr overlay) - [ ] Backgrounded tab → foregrounded → order detail re-fetches immediately (VisibilityService) - [ ] `npm run build:restaurant` → clean (verified locally: 2422 modules, 3s, no errors) ## Verified against Big Jay's Bustaurant - Coffee 25 GTQ → cart "25 GTQ" → checkout "≈ 3,966 sat" - Quesadillas 80 GTQ (3 modifier groups) → "≈ 12,691 sat" - 2x Tacos w/ tortilla + protein selections → 170 GTQ → "≈ 26,968 sat" All sat amounts come from the extension's fiat-rate-aware `/orders/quote` endpoint (the companion fix on `aiolabs/restaurant`). ## Out of scope (tracked elsewhere) - Festival / aggregator UI — `aiolabs/restaurant#8` - NIP-17 gift-wrapped order intake — `aiolabs/restaurant#9` - Tipping UI, push notifications, kiosk mode, reviews 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Standalone customer-facing bundle for the LNbits 'restaurant'
extension, modeled on the market bundle. v1 ships single-venue
(URL-driven via /r/:slug) with REST-only order placement; festival
aggregator and NIP-17 transport are tracked as
aiolabs/restaurant#8 and #9 respectively.

Skeleton this commit lands:

  vite.restaurant.config.ts  — port 5186, dist-restaurant/, green
                                theme color, PWA manifest, alias
                                @/app.config -> restaurant-app/.
  restaurant.html             — entry; title 'Restaurant — Order'.
  src/restaurant-app/
    main.ts                   — startApp + PWA SW registration.
    app.ts                    — module registration glue
                                (baseModule + restaurantModule).
    app.config.ts             — modules.restaurant config block.
                                Reserves a features:{} slot for
                                tier-gated UI (aiolabs/restaurant#2).
    App.vue                   — AppShell with Browse / Cart /
                                Orders bottom-nav tabs.
  src/modules/restaurant/
    index.ts                  — ModulePlugin shell with the future-
                                roadmap context inlined as
                                top-of-file comment (#1..#9).
    views/HomePage.vue        — placeholder; commit 4 replaces it
                                with real discovery + redirect.
  src/core/di-container.ts    — RESTAURANT_API +
                                RESTAURANT_NOSTR_SYNC tokens
                                reserved (consumers land in 3 / 8).
  package.json                — dev:restaurant, build:restaurant,
                                preview:restaurant scripts and
                                append to dev:all + build:demo.

Verified:
  - vue-tsc -b passes (whole webapp, all bundles).
  - vite build --config vite.restaurant.config.ts builds clean
    against VITE_LNBITS_BASE_URL=http://localhost:5001
    VITE_RESTAURANT_DEFAULT_SLUG=big-jays-bustaurant.
  - vite dev server boots on :5186 and serves the entry.

Companion branch: extension repo aiolabs/restaurant on branch
feat/restaurant-by-slug already provides
GET /restaurants/by-slug/{slug} that the webapp will consume in
commit 3.
types/restaurant.ts — full set of TS interfaces hand-translated
from ~/dev/shared/extensions/restaurant/models.py. Key notes:
  - Money is integer msat end-to-end on orders, order items, and
    the order quote response (matches the extension).
  - Open OrderStatus type with KNOWN_ORDER_STATUSES const so the
    production / kitchen workflow (aiolabs/restaurant#4) can
    introduce new states without breaking the build.
  - MenuItem.extra carries forward-compatible metadata for
    inventory (#3), happy-hour / COGS (#6), loyalty (#5), and
    mode-gated badges (#2). Plain Record<string, unknown>.
  - OrderExtra.fields is the loyalty (#5) pass-through hook the
    useCheckout buildCreateOrder helper will inject through.
  - Restaurant.mode is acknowledged but not branched on in v1.

services/RestaurantAPI.ts — BaseService subclass, mirrors the
extension's REST surface:
  getRestaurantBySlug / getRestaurantById / getMenu / getMenuItem
  quoteOrder / placeOrder / getOrder
No API key for any of these — public read and customer-facing
write endpoints. Base URL pulled from
appConfig.modules.restaurant.config.apiBaseUrl.

modules/restaurant/index.ts — install() now constructs the API
client, registers it under SERVICE_TOKENS.RESTAURANT_API, and
kicks off .initialize(). Consumers (views, composables, stores)
get the client via injectService starting in commit 4.
End-to-end menu browse for one restaurant.

composables/useMenu(slugOrId) — fetches via REST. Resolves slug
or id via heuristic, calls getMenu(), exposes
{restaurant, tree, items, isLoading, error, refresh} as reactive
refs. Cancels in-flight requests on param change /scope dispose.

components:
  RestaurantHeader.vue — banner, logo, name, description, open
                         badge, currency badge, location.
  CategoryNav.vue       — sticky horizontal pill nav over root
                          menu nodes; scrolls to anchors.
  MenuTree.vue          — recursive renderer (self-references by
                          name). Renders a node's items first, then
                          its children — items can attach to any
                          node per the menu-tree refactor.
  MenuItemCard.vue      — image, name, price (msat-native via
                          currencyHint), sold-out / low-stock /
                          featured badges, dietary + allergen
                          chips, '+' button that opens ItemPage or
                          quick-adds when no modifier groups.
  ModifierSelector.vue  — radio (selection='one') / checkbox
                          (selection='many') with min/max
                          enforcement. v-model-style emits
                          (update:selected, update:valid). Seeds
                          from is_default modifiers when no
                          existing selection is passed.

views:
  HomePage.vue          — slug input + auto-redirect when
                          VITE_RESTAURANT_DEFAULT_SLUG is set.
  RestaurantPage.vue    — composite: header + CategoryNav +
                          MenuTree. Loading / error states via
                          shadcn Alert.
  ItemPage.vue          — full item detail: image, dietary +
                          allergen chips, ModifierSelector, note
                          textarea, sticky bottom bar with qty
                          stepper + 'Add to cart' CTA (disabled
                          for v1; cart wires in commit 5).

Routes registered on the module: /, /r/:slug, /r/:slug/item/:itemId.

Design: shadcn-vue components throughout (Alert, Badge, Button,
Card, Checkbox, Input, Label, RadioGroup, Textarea), Tailwind 4
utility classes, theme-aware semantic colors (text-foreground,
bg-background, bg-card, text-muted-foreground, bg-primary, etc.).
No raw hex or theme-blind classes.

Verified: vue-tsc -b clean against the whole webapp.
stores/cart.ts — Pinia store keyed by restaurant_id (multi-
restaurant ready for the festival aggregator,
aiolabs/restaurant#8, without schema changes). Persists to
STORAGE_SERVICE under 'restaurant.cart.v1' (debounced 200ms);
hydrates on creation. Money is integer msat-ish (the cart stores
unit_msat as the per-unit value pulled from MenuItem.price; the
buildCreateOrder helper in commit 6 owns the canonical msat
conversion at order-place time).

  State: { lines: Record<restaurant_id, CartLine[]>,
           activeRestaurantId: string | null }
  Lines that match item + modifier set + note merge into the
  same line with quantity++ rather than duplicating.

Getters: restaurantsInCart, itemCount, restaurantTotalsMsat,
grandTotalMsat, linesFor(rid).

Actions: addLine, setQty, incrementQty, decrementQty, removeLine,
clearRestaurant, clear, setActiveRestaurant.

components/CartLineItem.vue — single line with modifier summary,
qty stepper, note display, remove button.

views/CartPage.vue — lines grouped by restaurant. Multi-restaurant
display already works (each restaurant is its own card). Empty
state, subtotal, clear-cart, Checkout CTA (lands in commit 6).

Wiring:
  - ItemPage 'Add to cart' now actually adds, then routes to /cart.
  - RestaurantPage's quick-add (the '+' on cards with NO modifier
    groups) adds directly without opening ItemPage; cards WITH
    modifier groups still open the detail page so the customer
    can satisfy required choices.
  - App.vue bottom-nav 'Cart' badge reflects cart.itemCount.
  - New route /cart registered on the module.

Verified: vue-tsc -b clean.
End-to-end customer order flow against the restaurant extension.

composables/useOrder(orderId) — polls GET /orders/{id} every
orderPollMs (5s default) while status is non-terminal. Refetches
immediately on VisibilityService.onVisible so a backgrounded tab
catches up on resume. Cleans the interval on scope dispose.
KNOWN_ORDER_STATUSES is the closed list; the type stays open so
new statuses from aiolabs/restaurant#4 land without breaking.

composables/useCheckout() — orchestrates the full flow:
  1. quoteOrder per restaurant in the cart
  2. pre-flight balance check (wallet.balance.value, sat -> msat)
  3. placeOrder per restaurant -> { order, invoice }
  4. WalletService.sendPayment(bolt11) per invoice
  5. clearRestaurant(rid) on success
buildCreateOrder is the single point CreateOrder is constructed;
loyalty (aiolabs/restaurant#5) and NIP-17 transport (#9) both
plug in here without touching the rest of the flow.

components/OrderInvoiceCard.vue — bolt11 QR via qrcode lib,
copy-to-clipboard, expires-in countdown. White-bg QR for scanner
contrast regardless of theme (pure UX call — humans don't read
QRs, cameras do).

views/CheckoutPage.vue — review + total + 'Pay & place order'
CTA. Progress indicator shows current restaurant + N of M during
the multi-restaurant loop. Empty-cart guard redirects to /cart.
On success, stores placed orders in
'restaurant.lastOrders.v1' (capped at 50, newest first) and
navigates to /orders/<firstId>.

views/OrderStatusPage.vue — status pill with semantic Badge
variants, conditional bolt11 QR when status='pending', success
alert when paid/accepted/ready, line items with modifier summary,
timeline of transitions, money breakdown (subtotal / tax / tip /
total). Polls live via useOrder.

Routes added: /checkout, /orders/:id.

Money convention: cart.unit_msat stores per-unit values directly
from MenuItem.price (declared currency, not msat). The extension
itself msat-ifies amounts on POST /orders/quote and /orders. The
checkout's pre-flight balance check converts wallet sats -> msat
before comparing to the quote's required_msat. Display strings
divide by 1000 only when reading order.*_msat fields back from
the extension.

Design: shadcn-vue throughout (Alert, Badge, Button, Card,
Separator) + Tailwind 4 + theme-aware semantic classes
(bg-card, text-foreground, text-muted-foreground, text-primary,
text-destructive, border-border, with the one exception of the
QR card's white background, justified inline).

Verified: vue-tsc -b clean.
views/OrdersListPage.vue — grouped by day, source of truth is
STORAGE_SERVICE['restaurant.lastOrders.v1'] (newest first, cap
50). Each row deep-links to /orders/:id where the detail page
re-fetches the live order over REST so stale status never
displays here.

views/SettingsPage.vue — customer-side preferences persisted to
STORAGE_SERVICE['restaurant.settings.v1']:
  - currencyDisplay toggle ('sats' | 'msat'). Local display only,
    the extension is always msat-canonical.
  - relayOverride input (comma-separated). Reload required since
    RelayHub initializes once on boot.
  - 'Clear local data' destructive button — wipes cart, history,
    recent venues but does NOT refund/cancel placed orders.

Routes added: /orders, /settings.

Verified: vue-tsc -b clean against the whole webapp.
services/RestaurantNostrSync.ts — BaseService subclass declaring
'RelayHub' as a dependency, so this.relayHub is populated by the
framework. Subscribes to:
  kinds: [30402, 5]
  authors: [restaurant.nostr_pubkey]
  '#l': ['restaurant:<restaurant.id>']

Each kind-30402 (NIP-99 classified listing) is parsed into a
partial MenuItem patch keyed by the 'd' tag (the menu item id).
NIP-33 replaceable semantics: incoming events older than what we
have are dropped (defense against operator-side reordering bugs).

Kind 5 (NIP-09 deletion request) populates a  set; the
useMenu computed filters those items out.

The patch covers name, description, price, is_available, stock
(derived from the NIP-99 'status' tag — 'sold' -> is_available:
false + stock: 0; 'active' -> is_available: true). Items
appearing for the first time via the relay (without a matching
REST item) are intentionally ignored — federated foreign-menu
indexing is a future concern (aiolabs/restaurant#8 / docs).

useMenu — refactor:
  - rename internal baseItems = REST snapshot
  - items becomes a computed that overlays sync.overlay onto
    baseItems and filters sync.deleted out
  - tryInjectService is used so the composable still works in
    test environments without the sync service.

views/RestaurantPage.vue — watches the resolved restaurant ref,
opens sync.subscribe(restaurant.nostr_pubkey, restaurant.id) on
arrival, tears it down on route leave / unmount. If relay hub
isn't connected, subscribe is a no-op and REST continues to
serve the menu (best-effort polish, not load-bearing).

modules/restaurant/index.ts — install() now also constructs
RestaurantNostrSync, registers it under
SERVICE_TOKENS.RESTAURANT_NOSTR_SYNC, and kicks off initialize
with waitForDependencies + 3 retries. Init failure is logged as
a warning and operation continues in no-overlay mode.

Verified: vue-tsc -b clean; vite build clean against
VITE_LNBITS_BASE_URL=http://localhost:5001
VITE_RESTAURANT_DEFAULT_SLUG=big-jays-bustaurant.

End-to-end customer flow now matches the plan's verification
section:
  /                       redirects to /r/big-jays-bustaurant
  /r/big-jays-bustaurant  REST menu loads + Nostr sub opens
  tap item with mods      ItemPage + ModifierSelector
  tap '+' on simple item  quick-add to cart
  bottom-nav Cart         /cart shows lines + total
  /checkout               quote -> place -> pay bolt11(s)
  auto-redirect           /orders/<id>, polls every 5s
  /orders                 historical list
  /settings               display + relay override + clear data
Three v1 smoke-test follow-ups that all touch CheckoutPage.vue,
bundled rather than scattered across the planned commits:

stores/cart.ts + CartLineItem.vue + CartPage.vue:
  - rename CartLine.unit_msat → unit_price (the field never was
    in msat — it carried the menu-item's declared currency)
  - add CartLine.currency snapshot; getters now return
    { amount, currency } shapes
  - grandTotal returns null for multi-currency carts (future
    festival aggregator); UI falls back to per-bucket subtotals

views/CheckoutPage.vue:
  - same display rename throughout
  - live ≈sat preview via /orders/quote on cart change
  - two-phase flow: review → place → render bolt11 QR(s) + copy
    button → pay all (LNbits wallet) OR scan with external wallet
  - per-placed-order poller picks up external-wallet payments

views/OrderStatusPage.vue + CheckoutPage.vue + types/restaurant.ts:
  - customer-friendly labels via FRIENDLY_ORDER_STATUS map
    ('Order received' / 'Cooking' / 'Ready for pickup' / 'Served')
  - open OrderStatus type with KNOWN_ORDER_STATUSES const for
    UI hint mapping; unknown statuses fall through gracefully

Verified end-to-end against Big Jay's: GTQ-priced items display in
GTQ throughout cart + checkout with live sat preview, bolt11 QR
scannable by external wallets, status transitions visible without
page reload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

feat(checkout): two-phase flow with QR + copy + external-wallet support

The previous all-in-one 'Pay & place order' button placed the
orders AND immediately auto-paid from the LNbits wallet, so the
bolt11 QR never rendered. Customers couldn't scan with their own
phone wallet (Phoenix, Wallet of Satoshi, etc.) — they were stuck
on the LNbits anon wallet by default.

Split into two distinct phases:

useCheckout (refactor):
  - state.step: 'idle' → 'quoting' → 'placing' → 'placed' →
    'paying' → 'paid' (or 'error')
  - state.placedOrders: PlacedOrder[] survives across the two
    phases, exposing each restaurant's { order, invoice }
  - state.paidOrderIds: Set<string> tracks which orders the
    customer auto-paid this session (external scans aren't in
    this set; the CheckoutPage poller tracks those)
  - placeOrders() — runs quote, balance precheck (warns only,
    doesn't block — the customer might pay externally), places
    orders, populates placedOrders
  - payOrder(idx) — pays one bolt11 via POST /api/v1/payments
    with the customer's wallets[0].adminkey
  - payAll() — convenience: payOrder for each unpaid placed order
  - reset() — clears state back to idle

CheckoutPage (rewrite):
  Phase 1 (review): cart subtotal in menu currency + live
  ≈sat preview + 'Place order' CTA. Unchanged from before
  except the CTA no longer also pays.

  Phase 2 (pay): OrderInvoiceCard per placed order showing the
  QR, amount, copy button, and expiry countdown. 'Pay from my
  LNbits wallet' CTA wraps payAll(). The page also polls every
  3s — when the extension's invoice listener flips an order to
  'paid' (regardless of which wallet paid it — LNbits anon
  auto-pay OR external scan), the badge flips, the cart bucket
  for that restaurant clears, and once all placed orders are
  paid, we redirect to /orders/<first-id> after a 1.2s success
  splash.

  Errors from auto-pay don't kill the flow — the QR stays
  visible so the customer can fall back to an external wallet
  scan.

This matches the typical restaurant UX: 'here's your bill,
scan or auto-pay' rather than 'we charged your wallet without
asking'. Verified: vue-tsc -b clean.

feat(restaurant): customer-friendly order status labels

Order status came through to the customer as raw operational
strings — 'paid', 'accepted', 'ready'. These are fine for the
operator's KDS but unfriendly for the customer waiting on their
food.

types/restaurant.ts:
  + FRIENDLY_ORDER_STATUS map (status → label)
      pending     → 'Awaiting payment'
      paid        → 'Order received'
      accepted    → 'Cooking'
      ready       → 'Ready for pickup'
      completed   → 'Served'
      canceled    → 'Canceled'
      refunded    → 'Refunded'
  + friendlyOrderStatus(status) helper. Unknown statuses (future
    kitchen-workflow values from aiolabs/restaurant#4 — e.g.
    'preparing', 'plating', 'in_service') fall through to a
    titlecased version of the raw key so the build stays green
    and the surface stays readable.

views/OrderStatusPage.vue:
  - Status Badge uses friendlyOrderStatus().
  - Alert sections now have one per status with appropriate copy:
      paid       → 'Order received / Payment confirmed — the
                    kitchen will start preparing it shortly.'
      accepted   → 'Cooking / Your food is being made.'
      ready      → 'Ready for pickup / Pick up at the counter.'
      completed  → 'Served / Enjoy! Thanks for ordering.'

views/CheckoutPage.vue: Phase 2 status badge uses
friendlyOrderStatus() so the checkout's live per-restaurant
status pill matches the language on the order page.

Deeper kitchen workflow (prep stations, courses, ETA, per-station
status) stays on aiolabs/restaurant#4 — this commit is the cheap
win that ships with the existing data model unchanged.
Three follow-ups to the v1 orders-list page that emerged once the
extension started transitioning orders through 'paid → accepted →
ready' from the KDS:

views/OrdersListPage.vue:
  - hydrate each entry from api.getOrder(id) on mount so the row
    reflects the live status (via friendlyOrderStatus) rather than
    the snapshot at place-time
  - surface the order's original fiat_amount + currency_display
    alongside the sat total
  - floating bottom-right FAB refresh button — the extension has no
    push channel for order status today (aiolabs/restaurant#9 will
    replace this with NIP-17 status DMs), so customers need an
    explicit way to pick up kitchen-side transitions without a
    full page reload. Bottom-right positioning avoids the global
    hub nav button at top-right.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.