import os import uuid import bcrypt from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File from sqlalchemy.orm import Session from typing import List from database import get_db from models.user import User, AssistantAssignment, WaiterZone from schemas.user import UserCreate, UserUpdate, UserOut, AssistantAssignmentOut, SetZonesRequest from routers.deps import require_manager router = APIRouter() AVATAR_DIR = "/app/data/avatars" # ── Helpers ─────────────────────────────────────────────────────────────────── def _waiter_or_404(waiter_id: int, db: Session) -> User: w = db.query(User).filter(User.id == waiter_id).first() if not w: raise HTTPException(status_code=404, detail="Waiter not found") return w # ── CRUD ────────────────────────────────────────────────────────────────────── @router.get("/", response_model=List[UserOut]) def list_waiters(db: Session = Depends(get_db), user: User = Depends(require_manager)): return db.query(User).filter(User.role == "waiter").all() @router.post("/", response_model=UserOut, status_code=status.HTTP_201_CREATED) def create_waiter(body: UserCreate, db: Session = Depends(get_db), user: User = Depends(require_manager)): if db.query(User).filter(User.username == body.username).first(): raise HTTPException(status_code=400, detail="Username already exists") pin_hash = bcrypt.hashpw(body.pin.encode(), bcrypt.gensalt()).decode() new_user = User(username=body.username, pin_hash=pin_hash, role=body.role, is_active=body.is_active) db.add(new_user) db.commit() db.refresh(new_user) return new_user @router.put("/{waiter_id}", response_model=UserOut) def update_waiter(waiter_id: int, body: UserUpdate, db: Session = Depends(get_db), user: User = Depends(require_manager)): waiter = _waiter_or_404(waiter_id, db) for field, value in body.model_dump(exclude_none=True).items(): setattr(waiter, field, value) db.commit() db.refresh(waiter) return waiter @router.put("/{waiter_id}/reset-pin") def reset_pin(waiter_id: int, pin: str, db: Session = Depends(get_db), user: User = Depends(require_manager)): waiter = _waiter_or_404(waiter_id, db) waiter.pin_hash = bcrypt.hashpw(pin.encode(), bcrypt.gensalt()).decode() db.commit() return {"status": "pin reset"} @router.put("/{waiter_id}/block") def toggle_block(waiter_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)): waiter = _waiter_or_404(waiter_id, db) waiter.is_active = not waiter.is_active db.commit() return {"is_active": waiter.is_active} @router.delete("/{waiter_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_waiter(waiter_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)): waiter = _waiter_or_404(waiter_id, db) db.delete(waiter) db.commit() # ── Avatar upload / delete ─────────────────────────────────────────────────── @router.post("/{waiter_id}/avatar", response_model=UserOut) async def upload_avatar(waiter_id: int, file: UploadFile = File(...), db: Session = Depends(get_db), user: User = Depends(require_manager)): waiter = _waiter_or_404(waiter_id, db) if not file.content_type.startswith("image/"): raise HTTPException(status_code=400, detail="File must be an image") # Delete old avatar file if present if waiter.avatar_url: old_path = os.path.join(AVATAR_DIR, os.path.basename(waiter.avatar_url)) if os.path.exists(old_path): os.remove(old_path) ext = os.path.splitext(file.filename or "")[1] or ".jpg" filename = f"waiter_{waiter_id}_{uuid.uuid4().hex[:8]}{ext}" dest = os.path.join(AVATAR_DIR, filename) os.makedirs(AVATAR_DIR, exist_ok=True) content = await file.read() with open(dest, "wb") as f: f.write(content) waiter.avatar_url = f"/static/avatars/{filename}" db.commit() db.refresh(waiter) return waiter @router.delete("/{waiter_id}/avatar", response_model=UserOut) def delete_avatar(waiter_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)): waiter = _waiter_or_404(waiter_id, db) if waiter.avatar_url: old_path = os.path.join(AVATAR_DIR, os.path.basename(waiter.avatar_url)) if os.path.exists(old_path): os.remove(old_path) waiter.avatar_url = None db.commit() db.refresh(waiter) return waiter # ── Zone assignments ────────────────────────────────────────────────────────── @router.put("/{waiter_id}/zones") def set_zones(waiter_id: int, body: SetZonesRequest, db: Session = Depends(get_db), user: User = Depends(require_manager)): """Replace all zone assignments for a waiter atomically. - all_zones=True → single NULL group_id row (sees everything) - group_ids=[1,2] → rows for groups 1 and 2 only - group_ids=[] → no rows at all (sees nothing) """ _waiter_or_404(waiter_id, db) # Wipe existing assignments db.query(WaiterZone).filter(WaiterZone.waiter_id == waiter_id).delete() if body.all_zones: db.add(WaiterZone(waiter_id=waiter_id, group_id=None)) elif body.group_ids: for gid in body.group_ids: db.add(WaiterZone(waiter_id=waiter_id, group_id=gid)) db.commit() zones = db.query(WaiterZone).filter(WaiterZone.waiter_id == waiter_id).all() return {"waiter_id": waiter_id, "zones": [{"id": z.id, "group_id": z.group_id} for z in zones]} # ── Assistant assignments (kept for backwards compat) ───────────────────────── @router.post("/{waiter_id}/assign-assistant", response_model=AssistantAssignmentOut) def assign_assistant(waiter_id: int, assistant_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)): existing = db.query(AssistantAssignment).filter( AssistantAssignment.primary_waiter_id == waiter_id, AssistantAssignment.assistant_waiter_id == assistant_id, ).first() if existing: raise HTTPException(status_code=400, detail="Assignment already exists") assignment = AssistantAssignment(primary_waiter_id=waiter_id, assistant_waiter_id=assistant_id) db.add(assignment) db.commit() db.refresh(assignment) return assignment @router.delete("/{waiter_id}/assistant", status_code=status.HTTP_204_NO_CONTENT) def remove_assistant(waiter_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)): assignment = db.query(AssistantAssignment).filter( AssistantAssignment.primary_waiter_id == waiter_id ).first() if not assignment: raise HTTPException(status_code=404, detail="Assignment not found") db.delete(assignment) db.commit()