Manager Dashboard: product reorder/bulk actions, preference sub-choices UI, expanded reports with DateInput component, waiter management updates, order detail improvements, Docker config and backend dockerignore added. Backend: table groups, auto-numbering, has_active_order flag, expanded reporting endpoints, waiter zone management, user schema updates, system router additions, table router fixes. Waiter PWA: TableDetailPage order/payment event improvements. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
124 lines
5.2 KiB
Python
124 lines
5.2 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 — also registers WaiterZone
|
|
import models.table # noqa: F401
|
|
import models.printer # noqa: F401
|
|
import models.product # noqa: F401
|
|
import models.order # noqa: F401 — also registers OrderAuditLog, OrderDiscount
|
|
|
|
from routers import auth, tables, products, orders, waiters, reports, system
|
|
|
|
|
|
def _run_migrations():
|
|
"""Apply additive schema changes that create_all won't handle.
|
|
Each migration gets its own connection so a no-op (column already exists)
|
|
doesn't leave a dirty transaction that blocks subsequent migrations."""
|
|
from sqlalchemy import text
|
|
|
|
migrations = [
|
|
"ALTER TABLE product_ingredients ADD COLUMN extra_cost REAL NOT NULL DEFAULT 0.0",
|
|
"ALTER TABLE products ADD COLUMN image_url VARCHAR",
|
|
"ALTER TABLE tables ADD COLUMN group_id INTEGER REFERENCES table_groups(id)",
|
|
"ALTER TABLE table_groups ADD COLUMN prefix VARCHAR",
|
|
"ALTER TABLE table_groups ADD COLUMN color VARCHAR",
|
|
"ALTER TABLE products ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0",
|
|
"ALTER TABLE product_preference_sets ADD COLUMN default_choice_id INTEGER",
|
|
"ALTER TABLE product_preference_choices ADD COLUMN sub_choices TEXT",
|
|
"ALTER TABLE product_preference_choices ADD COLUMN disables_subset INTEGER NOT NULL DEFAULT 0",
|
|
"ALTER TABLE product_preference_sets ADD COLUMN shared_subset TEXT",
|
|
"ALTER TABLE product_options ADD COLUMN sub_choices TEXT",
|
|
# Zone-based access control
|
|
"""CREATE TABLE IF NOT EXISTS waiter_zones (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
waiter_id INTEGER NOT NULL REFERENCES users(id),
|
|
group_id INTEGER REFERENCES table_groups(id),
|
|
assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)""",
|
|
# Payment tracking on items
|
|
"ALTER TABLE order_items ADD COLUMN paid_by INTEGER REFERENCES users(id)",
|
|
"ALTER TABLE order_items ADD COLUMN paid_at DATETIME",
|
|
"ALTER TABLE order_items ADD COLUMN payment_method VARCHAR",
|
|
# Full audit log
|
|
"""CREATE TABLE IF NOT EXISTS order_audit_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
order_id INTEGER NOT NULL REFERENCES orders(id),
|
|
event_type VARCHAR NOT NULL,
|
|
waiter_id INTEGER REFERENCES users(id),
|
|
item_ids TEXT,
|
|
amount REAL,
|
|
payment_method VARCHAR,
|
|
note TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)""",
|
|
# Waiter profile fields
|
|
"ALTER TABLE users ADD COLUMN full_name VARCHAR",
|
|
"ALTER TABLE users ADD COLUMN nickname VARCHAR",
|
|
"ALTER TABLE users ADD COLUMN mobile_phone VARCHAR",
|
|
"ALTER TABLE users ADD COLUMN avatar_url VARCHAR",
|
|
# Discounts table (future-proofed, schema ready now)
|
|
"""CREATE TABLE IF NOT EXISTS order_discounts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
order_id INTEGER NOT NULL REFERENCES orders(id),
|
|
item_id INTEGER REFERENCES order_items(id),
|
|
discount_type VARCHAR NOT NULL,
|
|
discount_value REAL NOT NULL,
|
|
applied_by INTEGER NOT NULL REFERENCES users(id),
|
|
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
reason TEXT
|
|
)""",
|
|
]
|
|
for sql in migrations:
|
|
try:
|
|
with engine.connect() as conn:
|
|
conn.execute(text(sql))
|
|
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")
|
|
|
|
# Serve waiter avatars as static files
|
|
AVATAR_DIR = "/app/data/avatars"
|
|
os.makedirs(AVATAR_DIR, exist_ok=True)
|
|
app.mount("/static/avatars", StaticFiles(directory=AVATAR_DIR), name="avatars")
|
|
|
|
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"])
|