Includes all work to date: - local_backend: FastAPI backend with products, orders, tables, shifts, cloud sync - manager_dashboard: React manager UI with product/category management, reports, settings - waiter_pwa: React PWA for waiter devices - Category reparent endpoint and UI - Waiter domain: local_ip sent on heartbeat, waiter_domain persisted from cloud response - QR code modal in AppInfoTab for waiter domain - Product form: number input spinners removed, category pre-selected on new product - Category row: count badge moved to far right Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
106 lines
4.7 KiB
Python
106 lines
4.7 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from datetime import datetime, timezone
|
|
|
|
from database import get_db
|
|
from models.settings import PosSettings
|
|
from schemas.settings import UpdateSettingRequest
|
|
from routers.deps import get_current_user, require_manager
|
|
from models.user import User
|
|
|
|
router = APIRouter()
|
|
|
|
VALID_SETTINGS = {
|
|
# Security / auth
|
|
"security.login_method": "How managers authenticate on first login: 'password' | 'pin' | 'none'",
|
|
"security.autofill_username": "Auto-fill username when only one manager exists: 'true' | 'false'",
|
|
"security.auto_lock": "Lock screen after inactivity: 'true' | 'false'",
|
|
"security.auto_lock_seconds": "Seconds of inactivity before locking (0 = disabled)",
|
|
"security.auto_logout": "Log out after inactivity: 'true' | 'false'",
|
|
"security.auto_logout_seconds":"Seconds of inactivity before logging out (0 = disabled)",
|
|
"shifts.waiter_self_start": "Allow waiters to start their own shifts without manager action",
|
|
"shifts.waiter_self_end": "Allow waiters to end their own shifts without manager action",
|
|
"business_day.force_close_allowed": "Allow force-closing business day with open tables",
|
|
"system.timezone": "IANA timezone name used by the backend container (e.g. Europe/Athens). Requires container restart to take effect.",
|
|
"ui.table_colours": "JSON blob of table card colour scheme (light + dark modes) for the Waiter PWA.",
|
|
"dev.spoof_printing": "When enabled, all print jobs are silently dropped. Devices behave as if printing succeeded.",
|
|
# Print layout
|
|
"print.ticket_mode": "Kitchen ticket layout mode: 'detailed' or 'compact'",
|
|
"print.divider_style": "Divider character used between sections: dash, equals, star, or empty",
|
|
# Print font settings — values are "SIZE:BOLD:CAPS" where SIZE is ESC ! base byte (0/16/32/48), BOLD 0|1, CAPS 0|1
|
|
"print.font_order_number": "Font for order number header: SIZE:BOLD:CAPS",
|
|
"print.font_meta": "Font for table/waiter/time header block: SIZE:BOLD:CAPS",
|
|
"print.font_item_name": "Font for item name lines: SIZE:BOLD:CAPS",
|
|
"print.font_quick": "Font for quick option lines (* marker): SIZE:BOLD:CAPS",
|
|
"print.font_pref": "Font for preference choice lines (> marker): SIZE:BOLD:CAPS",
|
|
"print.font_extra": "Font for extra/option lines (+ marker): SIZE:BOLD:CAPS",
|
|
"print.font_ingredient": "Font for removed ingredient lines (- marker): SIZE:BOLD:CAPS",
|
|
"print.font_item_note": "Font for per-item note lines: SIZE:BOLD:CAPS",
|
|
"print.font_order_note": "Font for order-level notes: SIZE:BOLD:CAPS",
|
|
}
|
|
|
|
DEFAULTS = {
|
|
"security.login_method": "password",
|
|
"security.autofill_username": "true",
|
|
"security.auto_lock": "false",
|
|
"security.auto_lock_seconds": "300",
|
|
"security.auto_logout": "false",
|
|
"security.auto_logout_seconds": "1800",
|
|
"shifts.waiter_self_start": "true",
|
|
"shifts.waiter_self_end": "true",
|
|
"business_day.force_close_allowed": "true",
|
|
"system.timezone": "Europe/Athens",
|
|
"ui.table_colours": "",
|
|
"dev.spoof_printing": "false",
|
|
"print.ticket_mode": "detailed",
|
|
"print.divider_style": "dash",
|
|
"print.font_order_number": "48:1:0",
|
|
"print.font_meta": "0:0:0",
|
|
"print.font_item_name": "16:1:0",
|
|
"print.font_quick": "0:0:0",
|
|
"print.font_pref": "0:0:0",
|
|
"print.font_extra": "0:0:0",
|
|
"print.font_ingredient": "0:0:0",
|
|
"print.font_item_note": "0:0:0",
|
|
"print.font_order_note": "0:1:0",
|
|
}
|
|
|
|
|
|
@router.get("/")
|
|
def get_all_settings(
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
stored = {s.key: s.value for s in db.query(PosSettings).all()}
|
|
result = {}
|
|
for key, description in VALID_SETTINGS.items():
|
|
result[key] = {
|
|
"value": stored.get(key, DEFAULTS.get(key, "true")),
|
|
"description": description,
|
|
}
|
|
return result
|
|
|
|
|
|
@router.put("/{key}")
|
|
def update_setting(
|
|
key: str,
|
|
body: UpdateSettingRequest,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
if key not in VALID_SETTINGS:
|
|
raise HTTPException(status_code=400, detail=f"Unknown setting key: {key}")
|
|
|
|
setting = db.query(PosSettings).filter(PosSettings.key == key).first()
|
|
if setting:
|
|
setting.value = body.value
|
|
setting.updated_at = datetime.now(timezone.utc)
|
|
setting.updated_by_id = user.id
|
|
else:
|
|
setting = PosSettings(key=key, value=body.value, updated_by_id=user.id)
|
|
db.add(setting)
|
|
|
|
db.commit()
|
|
db.refresh(setting)
|
|
return {"key": setting.key, "value": setting.value}
|