feat: initial commit — local services (backend + manager dashboard + waiter PWA)

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>
This commit is contained in:
2026-05-20 14:04:38 +03:00
commit 8ba8c95ecd
209 changed files with 48017 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from database import get_db
from models.flag import TableFlagDef, TableFlagAssignment
from schemas.flag import FlagDefCreate, FlagDefUpdate, FlagDefOut, FlagAssignmentOut, SetTableFlagsRequest
from routers.deps import get_current_user, require_manager
from models.user import User
from services.sse_bus import broadcast_sync
router = APIRouter()
# ─── Flag definitions (manager only) ─────────────────────────────────────────
@router.get("/defs", response_model=List[FlagDefOut])
def list_flag_defs(
include_inactive: bool = False,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
q = db.query(TableFlagDef)
if not include_inactive:
q = q.filter(TableFlagDef.is_active == True)
return q.order_by(TableFlagDef.sort_order, TableFlagDef.id).all()
@router.post("/defs", response_model=FlagDefOut, status_code=status.HTTP_201_CREATED)
def create_flag_def(
body: FlagDefCreate,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
):
flag = TableFlagDef(**body.model_dump())
db.add(flag)
db.commit()
db.refresh(flag)
return flag
@router.put("/defs/{flag_id}", response_model=FlagDefOut)
def update_flag_def(
flag_id: int,
body: FlagDefUpdate,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
):
flag = db.query(TableFlagDef).filter(TableFlagDef.id == flag_id).first()
if not flag:
raise HTTPException(status_code=404, detail="Flag not found")
for k, v in body.model_dump(exclude_unset=True).items():
setattr(flag, k, v)
db.commit()
db.refresh(flag)
return flag
@router.patch("/defs/{flag_id}/toggle-active", response_model=FlagDefOut)
def toggle_flag_active(
flag_id: int,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
):
flag = db.query(TableFlagDef).filter(TableFlagDef.id == flag_id).first()
if not flag:
raise HTTPException(status_code=404, detail="Flag not found")
flag.is_active = not flag.is_active
db.commit()
db.refresh(flag)
return flag
@router.delete("/defs/{flag_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_flag_def(
flag_id: int,
db: Session = Depends(get_db),
user: User = Depends(require_manager),
):
flag = db.query(TableFlagDef).filter(TableFlagDef.id == flag_id).first()
if not flag:
raise HTTPException(status_code=404, detail="Flag not found")
in_use = db.query(TableFlagAssignment).filter(TableFlagAssignment.flag_id == flag_id).count()
if in_use:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Η σήμανση χρησιμοποιείται σε {in_use} τραπέζι{'α' if in_use != 1 else ''}. Αφαιρέστε την πρώτα.",
)
db.delete(flag)
db.commit()
# ─── All assignments (bulk endpoint for manager views) ───────────────────────
@router.get("/assignments", response_model=List[FlagAssignmentOut])
def get_all_assignments(
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
"""All active flag assignments across all tables (for manager dashboard bulk load)."""
return db.query(TableFlagAssignment).all()
# ─── Table flag assignments ───────────────────────────────────────────────────
@router.get("/table/{table_id}", response_model=List[FlagAssignmentOut])
def get_table_flags(
table_id: int,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
return db.query(TableFlagAssignment).filter(
TableFlagAssignment.table_id == table_id
).all()
@router.put("/table/{table_id}", response_model=List[FlagAssignmentOut])
def set_table_flags(
table_id: int,
body: SetTableFlagsRequest,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
"""Replace all flags on a table with the given set of flag_ids."""
# Validate all flag_ids exist and are active
if body.flag_ids:
valid = db.query(TableFlagDef).filter(
TableFlagDef.id.in_(body.flag_ids),
TableFlagDef.is_active == True,
).count()
if valid != len(body.flag_ids):
raise HTTPException(status_code=400, detail="One or more flag IDs are invalid")
# Delete existing assignments for this table
db.query(TableFlagAssignment).filter(
TableFlagAssignment.table_id == table_id
).delete(synchronize_session=False)
# Insert new assignments
for flag_id in body.flag_ids:
db.add(TableFlagAssignment(
table_id=table_id,
flag_id=flag_id,
assigned_by=user.id,
))
db.commit()
result = db.query(TableFlagAssignment).filter(
TableFlagAssignment.table_id == table_id
).all()
broadcast_sync("table_flags_changed", {"table_id": table_id, "flag_ids": body.flag_ids})
return result
@router.delete("/table/{table_id}/all", status_code=status.HTTP_204_NO_CONTENT)
def clear_table_flags(
table_id: int,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
db.query(TableFlagAssignment).filter(
TableFlagAssignment.table_id == table_id
).delete(synchronize_session=False)
db.commit()
broadcast_sync("table_flags_changed", {"table_id": table_id, "flag_ids": []})