Menu System
Each restaurant has its own menu with items organized into categories. Items support customization groups (e.g., size, toppings) and allergen tags.
Data Model
Menu Item
Core fields: name, price_cents, currency (HNL/USD), image_url, category_id, description, is_active.
Items store customizations two ways:
- Inline:
customizationsJSONB field — item-specific options - Linked:
linked_customization_group_ids— references to reusable groups shared across items
Customization Groups
Each group has: name, min_selections, max_selections, and an array of options (label, price_cents, allergen_tags).
Examples:
- “Size” — min: 1, max: 1 → must choose exactly one
- “Toppings” — min: 0, max: 3 → optional, up to 3
- “Sauces” — min: 1, max: null → must choose at least 1, no limit
Categories
Simple grouping: name, display_order, restaurant_id. Items reference a category via category_id. Deleting a category unlinks all its items (sets category_id: null).
Pricing & Tax
- All prices stored in cents (
price_cents) - Currency per item (usually HNL, some items in USD)
- USD items converted to HNL at display time using exchange rate from config
- Tax applied per category via
restaurant_category_tax_mappingstable - Fallback: direct
tax_idon item, then 0%
Customization option prices are added to the base item price. Tax applies to the total (base + customizations).
Image Uploads
POST /api/uploads (multipart form-data):
- Validates file is an image, under 20MB
- Optimizes: auto-rotate EXIF, resize to max 800×800, JPEG quality 80
- Uploads to Cloudflare R2 at
menu-items/{restaurantId}/{timestamp}-{name}.jpg - Returns public URL at
images.ricoya.net
Manager Console — Menu Tab
Four sub-tabs:
Items
- List all items grouped by category
- Quick add form: name, price, category, description, allergen tags
- Per-item: inline edit name/price/description, toggle active, upload image
- Full editor modal: all fields including customizations
Customizations
- Create/edit reusable customization groups
- Groups can be linked to multiple items
- Each group: name, min/max selections, options with prices
Categories
- Create, rename, reorder, delete categories
- Assign tax rates per category
- View item count per category
Import
- Bulk menu import functionality
Customer-Facing Menu
/menu?restaurantId=X renders via MenuPageContent.tsx:
- Items grouped by category, sorted by display_order
- Prices shown tax-inclusive in HNL
- Customization groups merged (inline + linked)
- Allergen tags displayed per item
- Only active items shown
API Routes
| Route | Method | Auth | Purpose |
|---|---|---|---|
/api/menu-items |
GET | Public | List active items for restaurant |
/api/categories |
GET | Public | List categories for restaurant |
/api/customization-groups |
GET | Public | List reusable groups |
/api/customization-groups |
POST | Auth | Create group |
/api/customization-groups |
PATCH | Auth | Update group |
/api/customization-groups |
DELETE | Auth | Delete group |
/api/admin/categories |
GET | Manager+ | List categories |
/api/admin/categories |
POST | Manager+ | Create category |
/api/admin/categories/[id] |
PATCH | Manager+ | Update category |
/api/admin/categories/[id] |
DELETE | Manager+ | Delete category |
/api/uploads |
POST | Auth | Upload menu item image |
Key Files
| File | Purpose |
|---|---|
src/data/models.ts |
MenuItem, RestaurantCategory, customization interfaces |
src/repositories/menuItemsRepo.ts |
Menu item CRUD |
src/repositories/categoriesRepo.ts |
Category queries |
src/repositories/menuCustomizationGroupsRepo.ts |
Customization group CRUD |
src/hooks/useMenuItems.ts |
SWR hook with optimistic updates |
src/app/menu/MenuPageContent.tsx |
Customer menu display |
src/components/manager/ManagerConsole.tsx |
Manager menu management UI |
src/app/api/uploads/route.ts |
Image upload + R2 storage |