from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from typing import List from database import get_db from models.table import Table, TableGroup from models.order import Order from models.user import User, WaiterZone from schemas.table import ( TableCreate, TableUpdate, TableFloorplanUpdate, TableOut, TableGroupCreate, TableGroupUpdate, TableGroupOut, TableBatchCreate, ) from routers.deps import get_current_user, require_manager from services.sse_bus import broadcast_sync router = APIRouter() # ── Table Groups ────────────────────────────────────────────────────────────── @router.get("/groups", response_model=List[TableGroupOut]) def list_groups(db: Session = Depends(get_db), user: User = Depends(get_current_user)): return db.query(TableGroup).order_by(TableGroup.sort_order).all() @router.post("/groups", response_model=TableGroupOut, status_code=status.HTTP_201_CREATED) def create_group(body: TableGroupCreate, db: Session = Depends(get_db), user: User = Depends(require_manager)): if db.query(TableGroup).filter(TableGroup.name == body.name).first(): raise HTTPException(status_code=400, detail="Group name already exists") sort_order = db.query(TableGroup).count() group = TableGroup(name=body.name, prefix=body.prefix, color=body.color, sort_order=sort_order) db.add(group) db.commit() db.refresh(group) return group @router.put("/groups/{group_id}", response_model=TableGroupOut) def update_group(group_id: int, body: TableGroupUpdate, db: Session = Depends(get_db), user: User = Depends(require_manager)): group = db.query(TableGroup).filter(TableGroup.id == group_id).first() if not group: raise HTTPException(status_code=404, detail="Group not found") for field, value in body.model_dump(exclude_none=True).items(): setattr(group, field, value) db.commit() db.refresh(group) return group @router.delete("/groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_group(group_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)): group = db.query(TableGroup).filter(TableGroup.id == group_id).first() if not group: raise HTTPException(status_code=404, detail="Group not found") db.query(Table).filter(Table.group_id == group_id).update({"group_id": None}) db.delete(group) db.commit() # ── Tables ──────────────────────────────────────────────────────────────────── def _next_global_number(db: Session) -> int: last = db.query(Table).order_by(Table.number.desc()).first() return (last.number + 1) if last else 1 @router.get("/", response_model=List[TableOut]) def list_tables(include_inactive: bool = False, db: Session = Depends(get_db), user: User = Depends(get_current_user)): q = db.query(Table) if not include_inactive: q = q.filter(Table.is_active == True) # Zone-based filtering for waiters if user.role not in ("manager", "sysadmin"): zones = db.query(WaiterZone).filter(WaiterZone.waiter_id == user.id).all() # No zone rows → sees nothing if not zones: return [] # Any row with group_id=None → sees all tables (all-zones sentinel) has_all_zones = any(z.group_id is None for z in zones) if not has_all_zones: allowed_group_ids = [z.group_id for z in zones] q = q.filter(Table.group_id.in_(allowed_group_ids)) tables = q.order_by(Table.group_id, Table.number).all() active_table_ids = { row[0] for row in db.query(Order.table_id).filter( Order.status.in_(["open", "partially_paid", "paid"]) ).all() } result = [] for t in tables: out = TableOut.model_validate(t) out.has_active_order = t.id in active_table_ids result.append(out) return result @router.post("/", response_model=TableOut, status_code=status.HTTP_201_CREATED) def create_table(body: TableCreate, db: Session = Depends(get_db), user: User = Depends(require_manager)): number = _next_global_number(db) table = Table(number=number, label=body.label, group_id=body.group_id, is_active=True) db.add(table) db.commit() db.refresh(table) broadcast_sync("table_list_changed", {"action": "created", "table_id": table.id}) return table @router.post("/batch", response_model=List[TableOut], status_code=status.HTTP_201_CREATED) def batch_create_tables(body: TableBatchCreate, db: Session = Depends(get_db), user: User = Depends(require_manager)): if body.count < 1 or body.count > 200: raise HTTPException(status_code=400, detail="Count must be between 1 and 200") # Group-local label numbering: find highest suffix already used in this group existing_in_group = ( db.query(Table) .filter(Table.group_id == body.group_id) .all() ) if body.group_id else [] # Extract trailing integers from existing labels that start with this prefix used = [] for t in existing_in_group: if t.label and t.label.startswith(body.name_prefix): suffix = t.label[len(body.name_prefix):] if suffix.isdigit(): used.append(int(suffix)) start_label_n = (max(used) + 1) if used else 1 created = [] for i in range(body.count): label_n = start_label_n + i global_number = _next_global_number(db) table = Table( number=global_number, label=f"{body.name_prefix}{label_n}", group_id=body.group_id, is_active=True, ) db.add(table) db.flush() created.append(table) db.commit() for t in created: db.refresh(t) return created @router.put("/{table_id}", response_model=TableOut) def update_table(table_id: int, body: TableUpdate, db: Session = Depends(get_db), user: User = Depends(require_manager)): table = db.query(Table).filter(Table.id == table_id).first() if not table: raise HTTPException(status_code=404, detail="Table not found") for field, value in body.model_dump(exclude_none=True).items(): setattr(table, field, value) db.commit() db.refresh(table) return table @router.delete("/{table_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_table(table_id: int, hard: bool = False, db: Session = Depends(get_db), user: User = Depends(require_manager)): table = db.query(Table).filter(Table.id == table_id).first() if not table: raise HTTPException(status_code=404, detail="Table not found") active_order = db.query(Order).filter( Order.table_id == table_id, Order.status.in_(["open", "partially_paid", "paid"]) ).first() if active_order: raise HTTPException( status_code=400, detail="Cannot delete or deactivate a table with an active order" ) if hard: # Delete all past (non-active) orders for this table so FK constraint doesn't block deletion. # Active orders are already blocked above. Items/waiters/print_logs cascade via ORM. past_orders = db.query(Order).filter(Order.table_id == table_id).all() for order in past_orders: db.delete(order) db.flush() db.delete(table) else: table.is_active = False db.commit() @router.get("/{table_id}/status") def table_status(table_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)): table = db.query(Table).filter(Table.id == table_id).first() if not table: raise HTTPException(status_code=404, detail="Table not found") active_order = ( db.query(Order) .filter(Order.table_id == table_id, Order.status.in_(["open", "partially_paid", "paid"])) .first() ) return { "table": TableOut.model_validate(table), "active_order_id": active_order.id if active_order else None, "order_status": active_order.status if active_order else None, } @router.put("/{table_id}/floorplan", response_model=TableOut) def update_floorplan(table_id: int, body: TableFloorplanUpdate, db: Session = Depends(get_db), user: User = Depends(require_manager)): table = db.query(Table).filter(Table.id == table_id).first() if not table: raise HTTPException(status_code=404, detail="Table not found") table.floor_x = body.floor_x table.floor_y = body.floor_y db.commit() db.refresh(table) return table