from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user, require_roles from app.db.session import get_db from app.models.auth import KnotenTyp, RolleTyp, Systemknoten from app.models.personal import ( BenutzerQualifikation, Einheit, ObjekttypQualifikationsanforderung, Qualifikationstyp, ) from app.schemas.personal import ( BenutzerQualifikationCreate, BenutzerQualifikationRead, BerechtigungspruefungRead, EinheitCreate, EinheitRead, EinheitUpdate, ObjekttypQualifikationsanforderungCreate, ObjekttypQualifikationsanforderungRead, QualifikationstypCreate, QualifikationstypRead, QualifikationstypUpdate, ) from app.services import historie as historie_service from app.services.personal import pruefe_berechtigung router = APIRouter() async def _hauptserver_id(db: AsyncSession) -> int: result = await db.execute(select(Systemknoten.id).where(Systemknoten.typ == KnotenTyp.haupt)) return result.scalar_one() _admin_only = require_roles(RolleTyp.administration) # -- Einheit ------------------------------------------------------------------ @router.get("/einheiten", response_model=list[EinheitRead]) async def liste_einheiten(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)) -> list[Einheit]: result = await db.execute(select(Einheit)) return list(result.scalars().all()) @router.post("/einheiten", response_model=EinheitRead, status_code=status.HTTP_201_CREATED) async def erstelle_einheit( payload: EinheitCreate, db: AsyncSession = Depends(get_db), _=Depends(_admin_only) ) -> Einheit: einheit = Einheit(**payload.model_dump()) db.add(einheit) try: await db.flush() except IntegrityError as exc: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Ungültige Zuordnung") from exc return einheit @router.patch("/einheiten/{einheit_id}", response_model=EinheitRead) async def aendere_einheit( einheit_id: int, payload: EinheitUpdate, db: AsyncSession = Depends(get_db), _=Depends(_admin_only) ) -> Einheit: einheit = await db.get(Einheit, einheit_id) if einheit is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Einheit nicht gefunden") for feld, wert in payload.model_dump(exclude_unset=True).items(): setattr(einheit, feld, wert) try: await db.flush() except IntegrityError as exc: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Ungültige Zuordnung") from exc return einheit # -- Qualifikationstyp --------------------------------------------------------- @router.get("/qualifikationstypen", response_model=list[QualifikationstypRead]) async def liste_qualifikationstypen( db: AsyncSession = Depends(get_db), _=Depends(get_current_user) ) -> list[Qualifikationstyp]: result = await db.execute(select(Qualifikationstyp)) return list(result.scalars().all()) @router.post("/qualifikationstypen", response_model=QualifikationstypRead, status_code=status.HTTP_201_CREATED) async def erstelle_qualifikationstyp( payload: QualifikationstypCreate, db: AsyncSession = Depends(get_db), _=Depends(_admin_only) ) -> Qualifikationstyp: qualifikationstyp = Qualifikationstyp(**payload.model_dump()) db.add(qualifikationstyp) await db.flush() return qualifikationstyp @router.patch("/qualifikationstypen/{qualifikationstyp_id}", response_model=QualifikationstypRead) async def aendere_qualifikationstyp( qualifikationstyp_id: int, payload: QualifikationstypUpdate, db: AsyncSession = Depends(get_db), _=Depends(_admin_only), ) -> Qualifikationstyp: qualifikationstyp = await db.get(Qualifikationstyp, qualifikationstyp_id) if qualifikationstyp is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Qualifikationstyp nicht gefunden") for feld, wert in payload.model_dump(exclude_unset=True).items(): setattr(qualifikationstyp, feld, wert) await db.flush() return qualifikationstyp # -- Benutzer-Qualifikation ----------------------------------------------------- @router.get("/benutzer/{benutzer_id}/qualifikationen", response_model=list[BenutzerQualifikationRead]) async def liste_benutzer_qualifikationen( benutzer_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user) ) -> list[BenutzerQualifikation]: result = await db.execute( select(BenutzerQualifikation).where(BenutzerQualifikation.benutzer_id == benutzer_id) ) return list(result.scalars().all()) @router.post( "/benutzer/{benutzer_id}/qualifikationen", response_model=BenutzerQualifikationRead, status_code=status.HTTP_201_CREATED, ) async def erfasse_benutzer_qualifikation( benutzer_id: int, payload: BenutzerQualifikationCreate, db: AsyncSession = Depends(get_db), current_user=Depends(_admin_only), ) -> BenutzerQualifikation: if payload.benutzer_id != benutzer_id: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="benutzer_id im Pfad und Body weichen ab") qualifikation = BenutzerQualifikation(**payload.model_dump()) db.add(qualifikation) try: await db.flush() except IntegrityError as exc: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Qualifikation mit diesem Erwerbsdatum existiert bereits" ) from exc # Historie-Lücke (Roadmap-Review): wer hat wem wann eine Qualifikation # bestätigt, ist sicherheitsrelevant ("wer darf fahren" hängt fachlich # direkt daran) und war bisher nirgends protokolliert. await historie_service.log( db, zustaendiger_server_id=await _hauptserver_id(db), benutzer_id=current_user.id, ereignistyp="qualifikation_erfasst", entitaet_typ="benutzer_qualifikation", entitaet_id=qualifikation.id, neuer_wert={ "benutzer_id": benutzer_id, "qualifikationstyp_id": payload.qualifikationstyp_id, "gueltig_bis": payload.gueltig_bis.isoformat() if payload.gueltig_bis else None, }, ) return qualifikation # -- Objekttyp-Qualifikationsanforderung ----------------------------------------- @router.get( "/objekttypen/{objekttyp_id}/qualifikationsanforderungen", response_model=list[ObjekttypQualifikationsanforderungRead], ) async def liste_qualifikationsanforderungen( objekttyp_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user) ) -> list[ObjekttypQualifikationsanforderung]: result = await db.execute( select(ObjekttypQualifikationsanforderung).where( ObjekttypQualifikationsanforderung.objekttyp_id == objekttyp_id ) ) return list(result.scalars().all()) @router.post( "/objekttypen/{objekttyp_id}/qualifikationsanforderungen", response_model=ObjekttypQualifikationsanforderungRead, status_code=status.HTTP_201_CREATED, ) async def erstelle_qualifikationsanforderung( objekttyp_id: int, payload: ObjekttypQualifikationsanforderungCreate, db: AsyncSession = Depends(get_db), _=Depends(_admin_only), ) -> ObjekttypQualifikationsanforderung: if payload.objekttyp_id != objekttyp_id: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="objekttyp_id im Pfad und Body weichen ab") anforderung = ObjekttypQualifikationsanforderung(**payload.model_dump()) db.add(anforderung) try: await db.flush() except IntegrityError as exc: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Anforderung existiert bereits") from exc return anforderung @router.delete( "/objekttypen/{objekttyp_id}/qualifikationsanforderungen/{qualifikationstyp_id}", status_code=status.HTTP_204_NO_CONTENT, ) async def loesche_qualifikationsanforderung( objekttyp_id: int, qualifikationstyp_id: int, db: AsyncSession = Depends(get_db), _=Depends(_admin_only), ) -> None: anforderung = await db.get( ObjekttypQualifikationsanforderung, {"objekttyp_id": objekttyp_id, "qualifikationstyp_id": qualifikationstyp_id} ) if anforderung is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Anforderung nicht gefunden") await db.delete(anforderung) await db.flush() # -- Berechtigungsprüfung ------------------------------------------------------- @router.get("/objekte/{objekt_id}/berechtigung/{benutzer_id}", response_model=BerechtigungspruefungRead) async def pruefe_objekt_berechtigung( objekt_id: int, benutzer_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user) ) -> BerechtigungspruefungRead: """Nutzer-Beispiel Personal-Modul: "wer darf dieses Fahrzeug fahren?".""" fehlende = await pruefe_berechtigung(db, benutzer_id=benutzer_id, objekt_id=objekt_id) return BerechtigungspruefungRead( berechtigt=len(fehlende) == 0, fehlende_qualifikationstypen=list(fehlende), )