from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import func from sqlalchemy.orm import Session from ..database import get_db from ..deps import get_current_user, require_admin from ..models import Location, User from ..schemas import LocationCreate, LocationOut, LocationUpdate router = APIRouter(prefix="/locations", tags=["locations"]) def _descendant_ids(db: Session, location_id: str) -> set[str]: """Alle Unterorte (rekursiv) – als Ziel beim Umhängen ausgeschlossen, sonst entstünde ein Ring.""" result: set[str] = set() stack = [location_id] while stack: cur = stack.pop() for kid in db.query(Location).filter(Location.parent_id == cur).all(): if kid.id not in result: result.add(kid.id) stack.append(kid.id) return result @router.get("", response_model=list[LocationOut]) def list_locations( db: Session = Depends(get_db), _: User = Depends(get_current_user) ) -> list[Location]: return db.query(Location).order_by(Location.name).all() @router.post("", response_model=LocationOut, status_code=status.HTTP_201_CREATED) def create_location( payload: LocationCreate, db: Session = Depends(get_db), _: User = Depends(require_admin), ) -> Location: loc = Location(name=payload.name, parent_id=payload.parent_id) db.add(loc) db.commit() db.refresh(loc) return loc @router.patch("/{location_id}", response_model=LocationOut) def update_location( location_id: str, payload: LocationUpdate, db: Session = Depends(get_db), _: User = Depends(require_admin), ) -> Location: """Umbenennen und/oder umhängen. Chargen haengen an der ID, behalten ihren Lagerort also.""" loc = db.get(Location, location_id) if loc is None: raise HTTPException(status.HTTP_404_NOT_FOUND, "Lagerort nicht gefunden") data = payload.model_dump(exclude_unset=True) if "name" in data and data["name"]: name = data["name"].strip() doppelt = ( db.query(Location) .filter(func.lower(Location.name) == name.lower(), Location.id != location_id) .first() ) if doppelt is not None: raise HTTPException(status.HTTP_409_CONFLICT, "Diesen Lagerort gibt es bereits") loc.name = name # parent_id nur anfassen, wenn ausdrücklich mitgeschickt (None = oberste Ebene). if "parent_id" in data: neu = data["parent_id"] if neu is not None: if db.get(Location, neu) is None: raise HTTPException( status.HTTP_404_NOT_FOUND, "Übergeordneter Lagerort nicht gefunden" ) if neu == location_id or neu in _descendant_ids(db, location_id): raise HTTPException( status.HTTP_400_BAD_REQUEST, "Ein Lagerort kann nicht sich selbst oder einem seiner Unterorte " "untergeordnet werden.", ) loc.parent_id = neu db.commit() db.refresh(loc) return loc @router.delete("/{location_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_location( location_id: str, db: Session = Depends(get_db), _: User = Depends(require_admin), ) -> None: loc = db.get(Location, location_id) if loc is None: raise HTTPException(status.HTTP_404_NOT_FOUND, "Lagerort nicht gefunden") db.delete(loc) db.commit()