Appearance
Digital Menu — feature status
Status: functionally mature; standards retrofit pending. Digital Menu is the most complete feature in the platform — full dashboard CRUD + a public menu experience + three sub-modules (QR code, menu settings, cash calculator) + analytics, with 74 passing tests across 14 files. It is the structural reference feature (folder layout, TS-first types/schemas/constants, test depth). It is not the data-access reference: it predates the RPC/Edge-Function rule and is the single largest
supabase.from()user in the codebase. Verified against the codebase 2026-07-14.
What it delivers
The Interaction pillar's flagship: a restaurant/café owner builds menus in the dashboard, generates a QR, and customers scan it to a fast public menu.
Scope built (dashboard)
| Area | Pages / capability | Status |
|---|---|---|
| Menus | list, create, edit (MenusListPage, CreateMenuPage, EditMenuPage) | 🟢 |
| Categories | list, create, edit, nested-under-menu, item counts, status toggles, bulk actions | 🟢 |
| Items | list, create, edit, view/delete modals, variants, filters, dietary tags, availability scheduler, bulk status | 🟢 |
| Menu Settings (sub-module) | business hours, greeting/status banner, offers — tabbed (menu-settings/) | 🟢 |
| QR Code (sub-module) | generate, customize, download, print, share (qr-code/) | 🟢 |
| Cash Calculator (sub-module) | denomination counting, history, analytics, trend/distribution charts (cash-calculator/) | 🟢 |
| Analytics | dashboard stats page (AnalyticsPage) | 🟢 (scope TBD) |
Scope built (public menu)
/:userSlug/digital-menu → PublicMenuPage with tabs: Home, Menu, Today's Special, Favorites (favorites persisted client-side), plus a live Status Banner (open/closed by business hours). Reads via the deployed get-public-menu Edge Function.
Routes (all wired)
- Dashboard:
dashboard/digital-menu/*underDigitalMenuLayout—menus,menus/create,menus/:menuId,categories,items,qr-code,cash-calculator,analytics,menu-settings, plus nestedmenus/:menuId/categories|items/.... - Public:
/:userSlug/digital-menu(lazy, error-boundaried).
Data model
digital_menus → digital_menu_categories → digital_menu_items → digital_menu_item_variants (+ digital_menu_item_images, digital_menu_settings, digital_menu_qr_codes, digital_menu_qr_scan_analytics, digital_menu_analytics, digital_menu_cash_counts, digital_menu_item_favorites, digital_menu_item_revisions). See Database & RLS.
TypeScript-first status
Strong. types/, schemas/ (Zod), constants/, and most utils/ are .ts with co-located tests. Still .js: the data hooks (hooks/*.js) and the core services (services/*.js). Converting these is the natural next retrofit step. Note the resolved AvailabilityConfig collision (item-level per-day schedule in types/item.types.ts vs. menu-level operating hours in types/menu.types.ts) — keep them distinct.
Tests
74 passing / 14 files (npx vitest run src/tiers/user/features/digital-menu). Covers schemas, types, utils, index barrels, and the full cash-calculator sub-module (calculations, analytics, service, components incl. an integration test). Public-tier and dashboard component smoke coverage is lighter — an area to grow.
⚠️ Standards-compliance gaps (the honest part)
This feature was built before the data-access rule was enforced. It works, but do not copy its data layer into new features.
1. 🔴 supabase.from() everywhere — no RPC, no EDGE_FN map
~52 .from() calls in the dashboard half, zero rpc(), zero functions.invoke(). Tables hit directly: digital_menu_items (14), digital_menu_categories (11), digital_menus (7), digital_menu_cash_counts (5), digital_menu_settings (4), digital_menu_qr_codes (4), profiles (2). This is the bulk of the platform-wide from() debt. Reads should become get_* RPCs (via TanStack Query); writes should move to an Edge Function behind an EDGE_FN map.
2. 🔴 Public menu hardcodes production URLs
useCategories.js, usePublicMenuItems.js, and PublicMenuPage.jsx call get-public-menu via a raw fetch() fallback chain to hardcoded hosts (https://api.qrsetu.com/..., https://www.qrsetu.com/api/..., then ${supabaseUrl}/functions/v1/...). This violates the "no hardcoded production-fallback URL" client rule (an E2E/cold-stack run could hit production) and bypasses supabase.functions.invoke(). Route through the client + a service instead.
3. 🟠 Public status banner reads tables directly
useStatusBanner.js calls .from('profiles'), .from('digital_menus'), .from('digital_menu_settings') from the public (unauthenticated) tier — relying on anon + RLS. Should read through get-public-menu (or a dedicated public RPC), not base tables.
4. 🟠 Services/hooks still .js
Migrate services/*.js and hooks/*.js to .ts during the retrofit (types/schemas already exist to lean on).
Retrofit sequence (suggested)
- Introduce
constants/edgeFunctions.tswith anEDGE_FNmap; move writes to an Edge Function. - Add
get_*RPCs for the dashboard reads; convert hooks to TanStack QueryuseQuery. - Repoint the public menu + status banner to
supabase.functions.invoke('get-public-menu')(or a public RPC) and delete the hardcoded-URL fallback chain. - Convert
services/*.js+hooks/*.jsto.ts. - Add the "never calls
from()" compliance test once migrated.
Tracked as candidates on the Dev Tracker. Until migrated, treat digital-menu as the reference for structure and TS-first typing, and Data Access as the reference for the data layer new features must follow.