Commit graph

6 commits

Author SHA1 Message Date
42746d7321 fix(cms): KDS card text legible on dark mode
The age-escalation highlights (bg-amber-1 / bg-orange-1 /
bg-red-1) are very pale Quasar shades. On LNbits dark mode the
q-card inherits a near-white text color from the theme — paired
with the pale background that's white-on-cream, which is what
the user reported: the '1x Coffee' on a ready-card was barely
visible.

Pin an explicit text-grey-9 alongside each pale bg so dark text
on light background renders in both themes. The 'no highlight'
branch returns '' unchanged, so non-aged orders still use the
q-card's theme-aware default text color.
2026-05-11 19:18:33 +02:00
4827f5e10f feat(cms): q-tree menu builder
Replace the flat sidebar + dead-subcategory-modal with a real
arbitrary-depth tree builder using Quasar's q-tree.

templates/restaurant/menu.html:
  Three-pane layout (sidebar / tree / items).
  q-tree binds to the hydrated tree returned by
  GET /api/v1/restaurants/{id}/menu. Custom default-header slot
  renders the node name + an item-count badge + a child-count
  hint, with inline buttons:
    add (disabled at depth 3),
    edit, drive_file_move, delete (with cascade prompt).
  Top-level button above the tree adds root nodes.
  Items pane filters to the selected node, with a 'New item' that
  opens the item dialog with node_id pre-selected. The item
  dialog's node_id picker is a flat-indented q-select of every
  node in the restaurant (em-space indentation per depth level).

  A dedicated Move dialog uses the same flat-indented picker, but
  filters out the moved node + its descendants and any depth-3
  candidate (cycle / depth pre-checks; server enforces both too).

static/js/menu.js:
  Vue 3 + Quasar 2 UMD. Loads {tree, items} once, builds a
  flatNodes index for the option lists, and refetches after every
  mutation (≤50 nodes per restaurant — trivial; SSE/Nostr push is
  v2). Helpers:
    _findNode    — recursive lookup by id
    _flatten     — depth-first walk producing the option list
    selectedNode / filteredItems / allNodeOptions /
    moveTargetOptions / adminkey computeds.
  Delete prompts surface child-count + item-count and pass
  cascade=true when needed.

CMS now lets the operator build menus like
    Drinks
      ├─ Hot Beverages
      │   ├─ Coffee-based
      │   └─ Cacao-based
      └─ Cold (with its own items)
including items at any non-leaf level, satisfying the design
constraint.
2026-05-09 07:11:06 +02:00
b7fa1aec4a refactor(http): drop categories/subcategories shim
Remove the transitional layer added in commits 1+2:

models.py
  - Drop Category, Subcategory, CreateCategory, CreateSubcategory.

crud.py
  - Drop create_category / update_category / get_category /
    get_categories / delete_category and the subcategory variants
    along with the _node_row_to_category / _node_row_to_subcategory
    helpers. Tree state is owned exclusively by menu_node CRUD now.

views_api.py
  - Remove old endpoints:
      GET    /api/v1/restaurants/{id}/categories
      POST   /api/v1/categories
      DELETE /api/v1/categories/{id}
      GET    /api/v1/categories/{id}/subcategories
      POST   /api/v1/subcategories
      DELETE /api/v1/subcategories/{id}
    Hits return 404 now.
  - GET /api/v1/restaurants/{id}/menu loses the synthetic
    'categories' projection. Response is {restaurant, tree, items}.

static/js/api.js
  - Drop listCategories / createCategory / deleteCategory and the
    subcategory wrappers.

The CMS menu builder is broken between this commit and commit 4.
The plan acknowledged this trade-off: keeping commits revertible
beats the cost of an unshipped UI page rendering a stale empty
sidebar for one commit's lifetime.
2026-05-09 07:11:06 +02:00
ab87ddb2da feat(http): /menu_nodes endpoints + tree-shaped /menu response
views_api.py:
  - New endpoints (admin-key-gated, ownership-checked):
    * GET    /api/v1/restaurants/{id}/menu_nodes  flat list of nodes
    * GET    /api/v1/menu_nodes/{id}              single node
    * POST   /api/v1/menu_nodes                   create
    * PUT    /api/v1/menu_nodes/{id}              edit name / desc /
                                                  sort_order /
                                                  image_url
    * PUT    /api/v1/menu_nodes/{id}/move         body
                                                  {new_parent_id}
    * DELETE /api/v1/menu_nodes/{id}?cascade=true|false
  - ValueError from CRUD (depth, cycle, has-children-without-cascade)
    surfaces as 400 (creates / moves) or 409 (delete blocked).
  - GET /api/v1/restaurants/{id}/menu now returns three views in
    one round trip:
      tree:       hydrated tree (root nodes -> children + items)
      items:      flat enriched list (modifiers + availability)
      categories: transitional projection of depth-0 nodes with
                  their immediate items, in the legacy shape — kept
                  for one commit's lifetime so the existing CMS
                  keeps rendering. Drops in commit 3.

static/js/api.js:
  - listMenuNodes / getMenuNode / createMenuNode / updateMenuNode /
    moveMenuNode / deleteMenuNode added.
  - Old category/subcategory methods marked transitional in
    comments (drop in commit 3).

No JS / template churn — the CMS still reads from menu.categories
which is now produced from menu_nodes via the synthetic projection.
Commit 4 replaces the CMS with q-tree.
2026-05-09 07:11:06 +02:00
6272df1288 feat(db,models,crud): m002 menu tree + node CRUD with shim
Replace the fixed two-level (categories + subcategories) menu shape
with an arbitrary-depth tree, capped at 4 levels. The legacy
Atitlan.io project this carries forward already used a self-FK tree;
real menus need the depth (Drinks -> Hot Beverages -> Coffee-based
-> Espressos). As a side benefit, the current CMS has no UI for
subcategories at all, so this refactor incidentally fixes that gap.

Pattern: adjacency list (parent_id self-FK) + denormalized
materialized path (TEXT, '/'-separated) + denormalized depth.
Rejected closure table (overkill at n=5..50) and Postgres ltree
(not portable to SQLite). Subtree queries become
'WHERE path LIKE :p || '%''; subtree moves are a single
SUBSTR + concat UPDATE; max-depth and cycle checks are O(1).

migrations.py
  m002_menu_tree:
    - CREATE TABLE menu_nodes (id, restaurant_id, parent_id,
      name, description, sort_order, image_url, depth, path, time)
      with indexes on (restaurant_id), (parent_id), (path).
    - Backfill depth-0 from categories; depth-1 from subcategories
      with path = parent.id || '/' || own.id.
    - ALTER menu_items ADD COLUMN node_id; backfill via
      COALESCE(subcategory_id, category_id). Index on node_id.
    - DROP subcategory_id, category_id; DROP TABLE subcategories,
      categories.

models.py
  - New MAX_MENU_DEPTH = 3 (zero-indexed; 4 levels total).
  - New MenuNodeRow (DB I/O shape) + MenuNode (extends with
    children + items for hydrated tree responses; never persisted).
  - New CreateMenuNode.
  - MenuItem.node_id is Optional (orphans allowed when a parent
    is deleted with cascade=False); CreateMenuItem.node_id is
    required (newly created items must land somewhere).
  - Category / Subcategory / Create* kept temporarily as
    transitional shim shapes for the old endpoints; dropped in
    commit 3.

crud.py
  - New: create/update/get/get_all/move/delete_menu_node and
    get_menu_tree. move_menu_node uses single-statement subtree
    rewrite (path = new_prefix || SUBSTR(path, old_len + 1)).
    Cycle check: new_parent's path must not contain node_id.
    Depth check: max descendant depth + delta_depth <= MAX_MENU_DEPTH.
    delete_menu_node default cascade=False (block on children/items);
    cascade=True detaches items (sets node_id NULL) rather than
    hard-deletes, since items carry nostr_event_ids and are
    revenue-bearing.
  - get_menu_tree fetches nodes + items in two queries and assembles
    the tree in O(n+m) Python — no recursive CTEs, identical on
    SQLite + Postgres.
  - Old create_category / get_categories / create_subcategory etc.
    rewritten as thin shims that translate to/from menu_nodes.
    Old endpoints keep working.
  - delete_restaurant cascade now deletes from menu_nodes
    (single statement) instead of categories + subcategories.

views_api.py
  - GET /restaurants/{id}/menu temporarily sources from menu_nodes
    via the shims; surfaces items only at depth-0 nodes for now
    (commit 2 replaces the whole block with a real tree response).

static/js/menu.js + templates/restaurant/menu.html
  - Rename category_id -> node_id in the item dialog payload so
    POST /menu_items satisfies the new CreateMenuItem schema.
    The CMS still renders against the depth-0 'categories'
    projection; full q-tree rewrite lands in commit 4.
2026-05-09 07:11:06 +02:00
3382462af4 feat(cms): Vue 3 + Quasar 2 UMD CMS templates
LNbits convention: extends base.html, declares window.app =
Vue.createApp({mixins: [windowMixin], data, methods, created}); the
LNbits init-app.js loads after extension scripts and finishes the
mount with app.use(Quasar) + app.mount('#vue').

Pages
- index.html      restaurant list / dashboard with create dialog;
                  scoped to the logged-in user's wallets.
- menu.html       category sidebar + items grid; full item dialog
                  with price/currency/images/dietary/allergens/
                  ingredients/calories/stock/availability/featured.
                  Modifier groups managed in a separate dialog
                  with required|optional + one|many semantics.
- orders.html     filterable q-table with status colors and inline
                  state-machine actions (accept/ready/complete/
                  cancel). Polls every 8s.
- kds.html        kitchen display: card-per-order, items + selected
                  modifiers + notes, age-based color escalation
                  (>5min orange, >15min red), polls every 5s. The
                  poll loop is a stand-in until SSE/Nostr push
                  lands.
- settings.html   restaurant profile editor + delete + per-instance
                  ext settings panel (Nostr publish toggle, auto-
                  accept, invoice expiry).

Static
- js/api.js       single REST client (LNbits.api.request wrapper)
                  used by all pages.
- js/index.js     dashboard logic.
- js/menu.js      menu CRUD.
- js/orders.js    order monitor.
- js/kds.js       kitchen display.
- js/settings.js  settings persistence.

Customer kiosk UI lives in ~/dev/webapp; this extension only ships
the operator console.
2026-05-09 07:11:06 +02:00