Fix order saving, isMyOrder, blocked waiters, options pricing; add preferences, table groups, product images
Backend:
- OrderItemInput accepts option objects {id,name,price_delta} instead of int IDs
- extra_cost from selected options added to unit_price snapshot
- GET /api/products/?all=true for manager (includes unavailable)
- PUT /api/products/{id} now replaces options, ingredients, preference_sets
- POST /api/products/{id}/image — persistent image upload to /app/data/product_images
- New models: ProductPreferenceSet, ProductPreferenceChoice, TableGroup
- tables: group_id FK, hard delete (?hard=true), batch create POST /api/tables/batch
- GET /api/tables/groups + POST/PUT/DELETE groups endpoints
- POST /api/auth/me endpoint for token rehydration
- Auto-migration on startup for new columns
PWA:
- AuthRehydrator: fetches /auth/me on load so isMyOrder works after page reload
- 401 response force-logs out (covers blocked waiters)
- ItemOptionsModal: uses extra_cost correctly, shows preferences as radio buttons
Manager:
- ProductsPage: shows unavailable products greyed out, category color picker + reorder,
full option/ingredient/preference editing, image upload
- TablesPage: table groups, auto-increment, deactivate vs hard delete, batch add
This commit is contained in:
@@ -3,11 +3,18 @@ from datetime import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class SelectedOptionInput(BaseModel):
|
||||
id: Optional[int] = None
|
||||
name: Optional[str] = None
|
||||
price_delta: Optional[float] = None
|
||||
extra_cost: Optional[float] = None
|
||||
|
||||
|
||||
class OrderItemInput(BaseModel):
|
||||
product_id: int
|
||||
quantity: int
|
||||
selected_options: Optional[List[int]] = None
|
||||
removed_ingredients: Optional[List[int]] = None
|
||||
selected_options: Optional[List[SelectedOptionInput]] = None
|
||||
removed_ingredients: Optional[List[str]] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
|
||||
@@ -2,28 +2,34 @@ from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class CategoryBase(BaseModel):
|
||||
class CategoryCreate(BaseModel):
|
||||
name: str
|
||||
color: Optional[str] = None
|
||||
sort_order: int = 0
|
||||
|
||||
|
||||
class CategoryCreate(CategoryBase):
|
||||
pass
|
||||
|
||||
|
||||
class CategoryUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
sort_order: Optional[int] = None
|
||||
|
||||
|
||||
class CategoryOut(CategoryBase):
|
||||
class CategoryOut(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
color: Optional[str] = None
|
||||
sort_order: int = 0
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class CategoryReorderItem(BaseModel):
|
||||
id: int
|
||||
sort_order: int
|
||||
|
||||
|
||||
# ── Options ──────────────────────────────────────────────────────────────────
|
||||
|
||||
class ProductOptionBase(BaseModel):
|
||||
name: str
|
||||
extra_cost: float = 0.0
|
||||
@@ -40,8 +46,11 @@ class ProductOptionOut(ProductOptionBase):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ── Ingredients ───────────────────────────────────────────────────────────────
|
||||
|
||||
class ProductIngredientBase(BaseModel):
|
||||
name: str
|
||||
extra_cost: float = 0.0
|
||||
|
||||
|
||||
class ProductIngredientCreate(ProductIngredientBase):
|
||||
@@ -55,6 +64,42 @@ class ProductIngredientOut(ProductIngredientBase):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ── Preferences ───────────────────────────────────────────────────────────────
|
||||
|
||||
class PreferenceChoiceBase(BaseModel):
|
||||
name: str
|
||||
extra_cost: float = 0.0
|
||||
|
||||
|
||||
class PreferenceChoiceCreate(PreferenceChoiceBase):
|
||||
pass
|
||||
|
||||
|
||||
class PreferenceChoiceOut(PreferenceChoiceBase):
|
||||
id: int
|
||||
set_id: int
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class PreferenceSetBase(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class PreferenceSetCreate(PreferenceSetBase):
|
||||
choices: List[PreferenceChoiceCreate] = []
|
||||
|
||||
|
||||
class PreferenceSetOut(PreferenceSetBase):
|
||||
id: int
|
||||
product_id: int
|
||||
choices: List[PreferenceChoiceOut] = []
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ── Products ──────────────────────────────────────────────────────────────────
|
||||
|
||||
class ProductBase(BaseModel):
|
||||
name: str
|
||||
category_id: Optional[int] = None
|
||||
@@ -66,6 +111,7 @@ class ProductBase(BaseModel):
|
||||
class ProductCreate(ProductBase):
|
||||
options: List[ProductOptionCreate] = []
|
||||
ingredients: List[ProductIngredientCreate] = []
|
||||
preference_sets: List[PreferenceSetCreate] = []
|
||||
|
||||
|
||||
class ProductUpdate(BaseModel):
|
||||
@@ -74,11 +120,16 @@ class ProductUpdate(BaseModel):
|
||||
base_price: Optional[float] = None
|
||||
is_available: Optional[bool] = None
|
||||
printer_zone_id: Optional[int] = None
|
||||
options: Optional[List[ProductOptionCreate]] = None
|
||||
ingredients: Optional[List[ProductIngredientCreate]] = None
|
||||
preference_sets: Optional[List[PreferenceSetCreate]] = None
|
||||
|
||||
|
||||
class ProductOut(ProductBase):
|
||||
id: int
|
||||
options: List[ProductOptionOut] = []
|
||||
ingredients: List[ProductIngredientOut] = []
|
||||
preference_sets: List[PreferenceSetOut] = []
|
||||
image_url: Optional[str] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class TableGroupCreate(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class TableGroupUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
class TableGroupOut(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
sort_order: int = 0
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class TableBase(BaseModel):
|
||||
number: int
|
||||
label: Optional[str] = None
|
||||
group_id: Optional[int] = None
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
@@ -12,9 +29,17 @@ class TableCreate(TableBase):
|
||||
pass
|
||||
|
||||
|
||||
class TableBatchCreate(BaseModel):
|
||||
group_id: Optional[int] = None
|
||||
count: int
|
||||
name_prefix: str # e.g. "Out-" → Out-1, Out-2 ...
|
||||
start_number: int = 1
|
||||
|
||||
|
||||
class TableUpdate(BaseModel):
|
||||
number: Optional[int] = None
|
||||
label: Optional[str] = None
|
||||
group_id: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
@@ -27,5 +52,6 @@ class TableOut(TableBase):
|
||||
id: int
|
||||
floor_x: Optional[float] = None
|
||||
floor_y: Optional[float] = None
|
||||
group: Optional[TableGroupOut] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
Reference in New Issue
Block a user