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
76 lines
2.6 KiB
Python
76 lines
2.6 KiB
Python
import os
|
|
from contextlib import asynccontextmanager
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from database import engine, Base
|
|
from middleware.license_check import LicenseCheckMiddleware
|
|
from services.cloud_sync import start_cloud_sync
|
|
|
|
# Import all models so SQLAlchemy can create their tables
|
|
import models.user # noqa: F401
|
|
import models.table # noqa: F401
|
|
import models.printer # noqa: F401
|
|
import models.product # noqa: F401
|
|
import models.order # noqa: F401
|
|
|
|
from routers import auth, tables, products, orders, waiters, reports, system
|
|
|
|
|
|
def _run_migrations():
|
|
"""Apply additive schema changes that create_all won't handle."""
|
|
from sqlalchemy import text
|
|
with engine.connect() as conn:
|
|
# Add extra_cost to product_ingredients if missing
|
|
try:
|
|
conn.execute(text("ALTER TABLE product_ingredients ADD COLUMN extra_cost REAL NOT NULL DEFAULT 0.0"))
|
|
conn.commit()
|
|
except Exception:
|
|
pass
|
|
# Add image_url to products if missing
|
|
try:
|
|
conn.execute(text("ALTER TABLE products ADD COLUMN image_url VARCHAR"))
|
|
conn.commit()
|
|
except Exception:
|
|
pass
|
|
# Add group_id to tables if missing (added in Phase 3 table groups)
|
|
try:
|
|
conn.execute(text("ALTER TABLE tables ADD COLUMN group_id INTEGER REFERENCES table_groups(id)"))
|
|
conn.commit()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
Base.metadata.create_all(bind=engine)
|
|
_run_migrations()
|
|
sync_task = await start_cloud_sync()
|
|
yield
|
|
sync_task.cancel()
|
|
|
|
|
|
app = FastAPI(title="POS Local Backend", lifespan=lifespan)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
app.add_middleware(LicenseCheckMiddleware)
|
|
|
|
# Serve product images as static files
|
|
IMAGE_DIR = "/app/data/product_images"
|
|
os.makedirs(IMAGE_DIR, exist_ok=True)
|
|
app.mount("/static/product_images", StaticFiles(directory=IMAGE_DIR), name="product_images")
|
|
|
|
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
|
app.include_router(tables.router, prefix="/api/tables", tags=["tables"])
|
|
app.include_router(products.router, prefix="/api/products", tags=["products"])
|
|
app.include_router(orders.router, prefix="/api/orders", tags=["orders"])
|
|
app.include_router(waiters.router, prefix="/api/waiters", tags=["waiters"])
|
|
app.include_router(reports.router, prefix="/api/reports", tags=["reports"])
|
|
app.include_router(system.router, prefix="/api/system", tags=["system"])
|