from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user, require_roles, require_roles_or_permission from app.db.session import get_db from app.models.auth import RolleTyp from app.models.objekt import Objekt from app.models.stammdaten import Bereich, Fach, Kategorie, Material, Objekttyp, Standort from app.models.vorlage import Beladungsvorlage, Vorlagenposition from app.schemas.stammdaten import ( BereichCreate, BereichRead, BereichUpdate, FachCreate, FachErsetzen, FachRead, FachUpdate, FachVerwendungObjekt, KategorieCreate, KategorieRead, KategorieUpdate, MaterialCreate, MaterialRead, MaterialUpdate, ObjekttypCreate, ObjekttypRead, ObjekttypUpdate, StandortCreate, StandortRead, StandortUpdate, ) router = APIRouter() # -- Bereich ------------------------------------------------------------------ @router.get("/bereiche", response_model=list[BereichRead]) async def liste_bereiche( db: AsyncSession = Depends(get_db), _=Depends(get_current_user) ) -> list[Bereich]: result = await db.execute(select(Bereich)) return list(result.scalars().all()) @router.post("/bereiche", response_model=BereichRead, status_code=status.HTTP_201_CREATED) async def erstelle_bereich( payload: BereichCreate, db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)), ) -> Bereich: bereich = Bereich(**payload.model_dump()) db.add(bereich) await db.flush() return bereich @router.patch("/bereiche/{bereich_id}", response_model=BereichRead) async def aendere_bereich( bereich_id: int, payload: BereichUpdate, db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)), ) -> Bereich: bereich = await db.get(Bereich, bereich_id) if bereich is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bereich nicht gefunden") for feld, wert in payload.model_dump(exclude_unset=True).items(): setattr(bereich, feld, wert) await db.flush() return bereich # -- Kategorie ------------------------------------------------------------------ @router.get("/kategorien", response_model=list[KategorieRead]) async def liste_kategorien( db: AsyncSession = Depends(get_db), _=Depends(get_current_user) ) -> list[Kategorie]: result = await db.execute(select(Kategorie)) return list(result.scalars().all()) @router.post("/kategorien", response_model=KategorieRead, status_code=status.HTTP_201_CREATED) async def erstelle_kategorie( payload: KategorieCreate, db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)), ) -> Kategorie: kategorie = Kategorie(**payload.model_dump()) db.add(kategorie) await db.flush() return kategorie @router.patch("/kategorien/{kategorie_id}", response_model=KategorieRead) async def aendere_kategorie( kategorie_id: int, payload: KategorieUpdate, db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)), ) -> Kategorie: kategorie = await db.get(Kategorie, kategorie_id) if kategorie is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Kategorie nicht gefunden") for feld, wert in payload.model_dump(exclude_unset=True).items(): setattr(kategorie, feld, wert) await db.flush() return kategorie # -- Standort ------------------------------------------------------------------ @router.get("/standorte", response_model=list[StandortRead]) async def liste_standorte( db: AsyncSession = Depends(get_db), _=Depends(get_current_user) ) -> list[Standort]: result = await db.execute(select(Standort)) return list(result.scalars().all()) @router.post("/standorte", response_model=StandortRead, status_code=status.HTTP_201_CREATED) async def erstelle_standort( payload: StandortCreate, db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)), ) -> Standort: standort = Standort(**payload.model_dump()) db.add(standort) await db.flush() return standort @router.patch("/standorte/{standort_id}", response_model=StandortRead) async def aendere_standort( standort_id: int, payload: StandortUpdate, db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)), ) -> Standort: standort = await db.get(Standort, standort_id) if standort is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Standort nicht gefunden") for feld, wert in payload.model_dump(exclude_unset=True).items(): setattr(standort, feld, wert) await db.flush() return standort # -- Objekttyp ------------------------------------------------------------------ @router.get("/objekttypen", response_model=list[ObjekttypRead]) async def liste_objekttypen( db: AsyncSession = Depends(get_db), _=Depends(get_current_user) ) -> list[Objekttyp]: result = await db.execute(select(Objekttyp)) return list(result.scalars().all()) @router.post("/objekttypen", response_model=ObjekttypRead, status_code=status.HTTP_201_CREATED) async def erstelle_objekttyp( payload: ObjekttypCreate, db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)), ) -> Objekttyp: objekttyp = Objekttyp(**payload.model_dump()) db.add(objekttyp) await db.flush() return objekttyp @router.patch("/objekttypen/{objekttyp_id}", response_model=ObjekttypRead) async def aendere_objekttyp( objekttyp_id: int, payload: ObjekttypUpdate, db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)), ) -> Objekttyp: objekttyp = await db.get(Objekttyp, objekttyp_id) if objekttyp is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekttyp nicht gefunden") for feld, wert in payload.model_dump(exclude_unset=True).items(): setattr(objekttyp, feld, wert) await db.flush() return objekttyp # -- Fach (feste Fächer-Liste je Objekttyp) ---------------------------------- @router.get("/faecher", response_model=list[FachRead]) async def liste_faecher( db: AsyncSession = Depends(get_db), _=Depends(get_current_user) ) -> list[Fach]: result = await db.execute(select(Fach).order_by(Fach.objekttyp_id, Fach.sortierung, Fach.name)) return list(result.scalars().all()) @router.post("/faecher", response_model=FachRead, status_code=status.HTTP_201_CREATED) async def erstelle_fach( payload: FachCreate, db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)), ) -> Fach: vorhanden = await db.execute( select(Fach).where( Fach.objekttyp_id == payload.objekttyp_id, func.lower(Fach.name) == payload.name.strip().lower() ) ) if vorhanden.scalar_one_or_none() is not None: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Fach existiert für diesen Objekttyp bereits (Groß-/Kleinschreibung ignoriert)", ) fach = Fach(**payload.model_dump()) db.add(fach) await db.flush() return fach @router.get("/faecher/{fach_id}/objekte", response_model=list[FachVerwendungObjekt]) async def hole_fach_verwendung( fach_id: int, db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)) ) -> list[Objekt]: """Nutzer-Vorgabe: vor dem Umbenennen/Zusammenführen sehen, welche Objekte ein Fach tatsächlich verwenden (über die Vorlage, aus der sie entstanden sind).""" fach = await db.get(Fach, fach_id) if fach is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fach nicht gefunden") result = await db.execute( select(Objekt) .join(Beladungsvorlage, Objekt.vorlage_id == Beladungsvorlage.id) .join(Vorlagenposition, Vorlagenposition.vorlage_id == Beladungsvorlage.id) .where( Beladungsvorlage.objekttyp_id == fach.objekttyp_id, func.lower(Vorlagenposition.fach) == fach.name.strip().lower(), ) .distinct() ) return list(result.scalars().all()) @router.post("/faecher/{fach_id}/ersetzen", response_model=FachRead) async def ersetze_fach( fach_id: int, payload: FachErsetzen, db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)), ) -> Fach: """Benennt ein Fach um und zieht alle Vorlagenpositionen mit - landet der neue Name auf einem bereits bestehenden Fach desselben Objekttyps, werden beide zusammengeführt (dieses Fach verschwindet, das Ziel bleibt).""" fach = await db.get(Fach, fach_id) if fach is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fach nicht gefunden") neuer_name = payload.neuer_name.strip() if not neuer_name: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Name darf nicht leer sein") positionen = await db.execute( select(Vorlagenposition) .join(Beladungsvorlage, Vorlagenposition.vorlage_id == Beladungsvorlage.id) .where( Beladungsvorlage.objekttyp_id == fach.objekttyp_id, func.lower(Vorlagenposition.fach) == fach.name.strip().lower(), ) ) for position in positionen.scalars().all(): position.fach = neuer_name ziel_result = await db.execute( select(Fach).where( Fach.objekttyp_id == fach.objekttyp_id, func.lower(Fach.name) == neuer_name.lower(), Fach.id != fach.id, ) ) ziel = ziel_result.scalar_one_or_none() if ziel is not None: await db.delete(fach) await db.flush() return ziel fach.name = neuer_name await db.flush() return fach @router.patch("/faecher/{fach_id}", response_model=FachRead) async def aendere_fach( fach_id: int, payload: FachUpdate, db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)), ) -> Fach: fach = await db.get(Fach, fach_id) if fach is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fach nicht gefunden") for feld, wert in payload.model_dump(exclude_unset=True).items(): setattr(fach, feld, wert) await db.flush() return fach @router.delete("/faecher/{fach_id}", status_code=status.HTTP_204_NO_CONTENT) async def loesche_fach( fach_id: int, db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)), ) -> None: fach = await db.get(Fach, fach_id) if fach is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fach nicht gefunden") await db.delete(fach) await db.flush() # -- Material (Prompt 07) -------------------------------------------------- @router.get("/materialien", response_model=list[MaterialRead]) async def liste_materialien( db: AsyncSession = Depends(get_db), _=Depends(get_current_user) ) -> list[Material]: result = await db.execute(select(Material)) return list(result.scalars().all()) @router.get("/materialien/{material_id}", response_model=MaterialRead) async def hole_material( material_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user), ) -> Material: material = await db.get(Material, material_id) if material is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Material nicht gefunden") return material @router.post("/materialien", response_model=MaterialRead, status_code=status.HTTP_201_CREATED) async def erstelle_material( payload: MaterialCreate, db: AsyncSession = Depends(get_db), _=Depends(require_roles_or_permission(RolleTyp.administration, berechtigung="material.erstellen")), ) -> Material: material = Material(**payload.model_dump()) db.add(material) await db.flush() return material @router.patch("/materialien/{material_id}", response_model=MaterialRead) async def aendere_material( material_id: int, payload: MaterialUpdate, db: AsyncSession = Depends(get_db), _=Depends(require_roles_or_permission(RolleTyp.administration, berechtigung="material.bearbeiten")), ) -> Material: material = await db.get(Material, material_id) if material is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Material nicht gefunden") for feld, wert in payload.model_dump(exclude_unset=True).items(): setattr(material, feld, wert) await db.flush() return material