Skip to content

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)

AreaPages / capabilityStatus
Menuslist, create, edit (MenusListPage, CreateMenuPage, EditMenuPage)🟢
Categorieslist, create, edit, nested-under-menu, item counts, status toggles, bulk actions🟢
Itemslist, 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/)🟢
Analyticsdashboard stats page (AnalyticsPage)🟢 (scope TBD)

Scope built (public menu)

/:userSlug/digital-menuPublicMenuPage 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/* under DigitalMenuLayoutmenus, menus/create, menus/:menuId, categories, items, qr-code, cash-calculator, analytics, menu-settings, plus nested menus/:menuId/categories|items/....
  • Public: /:userSlug/digital-menu (lazy, error-boundaried).

Data model

digital_menusdigital_menu_categoriesdigital_menu_itemsdigital_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)

  1. Introduce constants/edgeFunctions.ts with an EDGE_FN map; move writes to an Edge Function.
  2. Add get_* RPCs for the dashboard reads; convert hooks to TanStack Query useQuery.
  3. Repoint the public menu + status banner to supabase.functions.invoke('get-public-menu') (or a public RPC) and delete the hardcoded-URL fallback chain.
  4. Convert services/*.js + hooks/*.js to .ts.
  5. 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.