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.
This commit is contained in:
Padreug 2026-05-02 09:05:39 +02:00
commit ab87ddb2da
2 changed files with 202 additions and 27 deletions

View file

@ -23,17 +23,29 @@
deleteRestaurant: (key, id) =>
call(key, 'DELETE', `/restaurants/${id}`),
// Categories
// Categories (transitional shim, drop in commit 3)
listCategories: (id) => call(null, 'GET', `/restaurants/${id}/categories`),
createCategory: (key, data) => call(key, 'POST', '/categories', data),
deleteCategory: (key, id) => call(key, 'DELETE', `/categories/${id}`),
// Subcategories
// Subcategories (transitional shim, drop in commit 3)
listSubcategories: (catId) =>
call(null, 'GET', `/categories/${catId}/subcategories`),
createSubcategory: (key, data) => call(key, 'POST', '/subcategories', data),
deleteSubcategory: (key, id) => call(key, 'DELETE', `/subcategories/${id}`),
// Menu nodes (the tree)
listMenuNodes: (restaurantId) =>
call(null, 'GET', `/restaurants/${restaurantId}/menu_nodes`),
getMenuNode: (id) => call(null, 'GET', `/menu_nodes/${id}`),
createMenuNode: (key, data) => call(key, 'POST', '/menu_nodes', data),
updateMenuNode: (key, id, data) =>
call(key, 'PUT', `/menu_nodes/${id}`, data),
moveMenuNode: (key, id, newParentId) =>
call(key, 'PUT', `/menu_nodes/${id}/move`, {new_parent_id: newParentId}),
deleteMenuNode: (key, id, cascade = false) =>
call(key, 'DELETE', `/menu_nodes/${id}?cascade=${cascade ? 'true' : 'false'}`),
// Menu items
getMenu: (id) => call(null, 'GET', `/restaurants/${id}/menu`),
getMenuItem: (id) => call(null, 'GET', `/menu_items/${id}`),