views.py (Jinja CMS pages, /restaurant/...):
- / restaurant list / dashboard
- /{slug} menu builder
- /{slug}/orders order monitor
- /{slug}/kds kitchen display
- /{slug}/settings restaurant + Nostr settings
views_api.py (REST under /restaurant/api/v1/):
Owner write-side (require_admin_key, ownership-checked):
- restaurants CRUD (publishes kind 0 metadata to Nostr on
create/update; signs with restaurant.nostr_pubkey override
or LNbits Account fallback)
- categories + subcategories CRUD
- menu_items CRUD (publishes/replaces kind 30402 NIP-99
listings on create/update; sends kind 5 NIP-09 deletion on
delete)
- modifier_groups + modifiers CRUD
- availability_windows CRUD
- orders status transitions (PUT /api/v1/orders/{id}/status/{new})
- print_jobs/{id}/ack
- settings (admin-only)
Customer-facing (no auth, customer pubkey optional):
- GET /api/v1/restaurants/{id} profile
- GET /api/v1/restaurants/{id}/menu full menu tree
(categories +
subcategories +
items + modifiers +
availability) in
one round trip
- POST /api/v1/orders/quote pre-flight balance
check; webapp calls
this *before* opening
any per-restaurant
invoice
- POST /api/v1/orders place an order on
one restaurant,
returns bolt11
KDS / order monitor (require_invoice_key, ownership-checked):
- GET /api/v1/restaurants/{id}/orders
- GET /api/v1/restaurants/{id}/print_jobs
crud.py: added get_print_job(job_id) helper used by the ack endpoint.
135 lines
4.2 KiB
Python
135 lines
4.2 KiB
Python
"""
|
|
Server-rendered CMS routes for restaurant owners.
|
|
|
|
Mounted at `/restaurant/...`. Customer-facing pages live in the AIO
|
|
webapp (~/dev/webapp); this extension only renders the CMS.
|
|
|
|
Pages
|
|
-----
|
|
/restaurant/ dashboard (restaurant list)
|
|
/restaurant/{slug} menu builder
|
|
/restaurant/{slug}/orders order monitor
|
|
/restaurant/{slug}/kds kitchen display
|
|
/restaurant/{slug}/settings restaurant + Nostr settings
|
|
|
|
All pages require a logged-in LNbits user (check_user_exists).
|
|
"""
|
|
|
|
import json
|
|
from http import HTTPStatus
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
from starlette.exceptions import HTTPException
|
|
from starlette.responses import HTMLResponse
|
|
|
|
from lnbits.core.models import User
|
|
from lnbits.decorators import check_user_exists
|
|
from lnbits.helpers import template_renderer
|
|
|
|
from .crud import get_restaurant_by_slug
|
|
from .models import Restaurant
|
|
|
|
restaurant_generic_router = APIRouter()
|
|
|
|
|
|
def restaurant_renderer():
|
|
return template_renderer(["restaurant/templates"])
|
|
|
|
|
|
def _restaurant_jsonable(restaurant: Restaurant) -> dict:
|
|
"""
|
|
Convert a Restaurant pydantic model to a plain JSON-serializable
|
|
dict for Jinja's `tojson` filter.
|
|
|
|
`restaurant.dict()` returns a dict with a `datetime` on `time`,
|
|
which Python's stdlib `JSONEncoder` (used by Jinja `tojson`) can't
|
|
serialize — it errors out as
|
|
TypeError: JSONEncoder.default() missing 1 required positional argument: 'o'
|
|
Pydantic v1's `.json()` knows how to serialize datetime as
|
|
ISO-8601, so we round-trip via JSON to get a clean dict.
|
|
"""
|
|
return json.loads(restaurant.json())
|
|
|
|
|
|
@restaurant_generic_router.get("/", response_class=HTMLResponse)
|
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
|
return restaurant_renderer().TemplateResponse(
|
|
"restaurant/index.html",
|
|
{"request": request, "user": user.json()},
|
|
)
|
|
|
|
|
|
@restaurant_generic_router.get("/{slug}", response_class=HTMLResponse)
|
|
async def menu_builder(
|
|
request: Request, slug: str, user: User = Depends(check_user_exists)
|
|
):
|
|
restaurant = await get_restaurant_by_slug(slug)
|
|
if not restaurant:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
|
|
)
|
|
return restaurant_renderer().TemplateResponse(
|
|
"restaurant/menu.html",
|
|
{
|
|
"request": request,
|
|
"user": user.json(),
|
|
"restaurant": _restaurant_jsonable(restaurant),
|
|
},
|
|
)
|
|
|
|
|
|
@restaurant_generic_router.get("/{slug}/orders", response_class=HTMLResponse)
|
|
async def orders(
|
|
request: Request, slug: str, user: User = Depends(check_user_exists)
|
|
):
|
|
restaurant = await get_restaurant_by_slug(slug)
|
|
if not restaurant:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
|
|
)
|
|
return restaurant_renderer().TemplateResponse(
|
|
"restaurant/orders.html",
|
|
{
|
|
"request": request,
|
|
"user": user.json(),
|
|
"restaurant": _restaurant_jsonable(restaurant),
|
|
},
|
|
)
|
|
|
|
|
|
@restaurant_generic_router.get("/{slug}/kds", response_class=HTMLResponse)
|
|
async def kds(
|
|
request: Request, slug: str, user: User = Depends(check_user_exists)
|
|
):
|
|
restaurant = await get_restaurant_by_slug(slug)
|
|
if not restaurant:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
|
|
)
|
|
return restaurant_renderer().TemplateResponse(
|
|
"restaurant/kds.html",
|
|
{
|
|
"request": request,
|
|
"user": user.json(),
|
|
"restaurant": _restaurant_jsonable(restaurant),
|
|
},
|
|
)
|
|
|
|
|
|
@restaurant_generic_router.get("/{slug}/settings", response_class=HTMLResponse)
|
|
async def settings_page(
|
|
request: Request, slug: str, user: User = Depends(check_user_exists)
|
|
):
|
|
restaurant = await get_restaurant_by_slug(slug)
|
|
if not restaurant:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
|
|
)
|
|
return restaurant_renderer().TemplateResponse(
|
|
"restaurant/settings.html",
|
|
{
|
|
"request": request,
|
|
"user": user.json(),
|
|
"restaurant": _restaurant_jsonable(restaurant),
|
|
},
|
|
)
|