Files
MABEA/backend/app/api/v1/endpoints/mangel.py
T
patrickandClaude Sonnet 5 11ad747136
CI / backend-tests (push) Failing after 1m47s
CI / frontend-build (push) Successful in 17s
fix(historie): Audit-Lücke bei Mangel/Personal/Objekt-Änderungen geschlossen
Bisher waren nur Kontrolle/Fehlbestand/Nachfüllung/Mindermenge/Geräteinstanz
im append-only Audit-Log (historie.log() war seit Prompt 13 nie flächendeckend
verdrahtet, docstring sagte das bereits so). Drei neue Ereignisse ergänzt:

- mangel_gemeldet / mangel_status_geaendert (Mangel-Modul)
- qualifikation_erfasst (Personal-Modul, sicherheitsrelevant: "wer hat wem
  wann eine Qualifikation bestätigt" hängt fachlich direkt an "wer darf
  fahren")
- objekt_geaendert (Status-/Fahrzeug-Zuordnungsänderungen via PATCH /objekte)

Lagerbewegung bewusst NICHT zusätzlich in historie dupliziert - hat bereits
eigenes vollständiges Audit-Trail (Wer/Wann/Von/Nach/Grund in eigener
Tabelle).

Frontend: Änderungslog-Filter um geraet_instanz/mangel/benutzer_qualifikation/
objekt ergänzt (geraet_instanz fehlte dort zuvor ebenfalls).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
2026-09-05 13:50:48 +02:00

105 lines
3.6 KiB
Python

import uuid
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import require_roles
from app.db.session import get_db
from app.models.auth import KnotenTyp, RolleTyp, Systemknoten
from app.models.mangel import Mangel, MangelStatus
from app.models.objekt import Objekt
from app.schemas.mangel import MangelCreate, MangelRead, MangelStatusUpdate
from app.services import historie as historie_service
from app.services.mangel import aendere_status
router = APIRouter()
_mitarbeiter_plus = require_roles(
RolleTyp.mitarbeiter,
RolleTyp.materialverantwortlicher,
RolleTyp.leitungsverantwortlicher,
RolleTyp.administration,
)
_materialverantwortliche = require_roles(
RolleTyp.administration, RolleTyp.materialverantwortlicher, RolleTyp.leitungsverantwortlicher
)
async def _hauptserver_id(db: AsyncSession) -> int:
result = await db.execute(select(Systemknoten.id).where(Systemknoten.typ == KnotenTyp.haupt))
return result.scalar_one()
@router.get("/maengel", response_model=list[MangelRead])
async def liste_maengel(
objekt_id: int | None = None,
status_filter: MangelStatus | None = None,
db: AsyncSession = Depends(get_db),
_=Depends(_mitarbeiter_plus),
) -> list[Mangel]:
"""Prompt-Erweiterung (Mangel-Modul): jeder Mitarbeiter+ darf lesen (Mängel
sind für alle im Einsatzalltag relevant), Status ändern bleibt
Materialverantwortlichen+ vorbehalten (Melden vs. Bearbeiten getrennt)."""
stmt = select(Mangel)
if objekt_id is not None:
stmt = stmt.where(Mangel.objekt_id == objekt_id)
if status_filter is not None:
stmt = stmt.where(Mangel.status == status_filter)
stmt = stmt.order_by(Mangel.gemeldet_am.desc())
result = await db.execute(stmt)
return list(result.scalars().all())
@router.post("/maengel", response_model=MangelRead, status_code=status.HTTP_201_CREATED)
async def melde_mangel(
payload: MangelCreate, db: AsyncSession = Depends(get_db), current_user=Depends(_mitarbeiter_plus)
) -> Mangel:
objekt = await db.get(Objekt, payload.objekt_id)
if objekt is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekt nicht gefunden")
mangel = Mangel(
objekt_id=payload.objekt_id,
entitaet_typ=payload.entitaet_typ,
entitaet_id=payload.entitaet_id,
beschreibung=payload.beschreibung,
prioritaet=payload.prioritaet,
gemeldet_von=current_user.id,
gemeldet_am=datetime.now(timezone.utc),
)
db.add(mangel)
await db.flush()
await historie_service.log(
db,
zustaendiger_server_id=await _hauptserver_id(db),
benutzer_id=current_user.id,
ereignistyp="mangel_gemeldet",
entitaet_typ="mangel",
entitaet_id=mangel.id,
neuer_wert={"beschreibung": mangel.beschreibung, "prioritaet": mangel.prioritaet.value},
)
return mangel
@router.patch("/maengel/{mangel_id}", response_model=MangelRead)
async def aendere_mangel_status(
mangel_id: uuid.UUID,
payload: MangelStatusUpdate,
db: AsyncSession = Depends(get_db),
current_user=Depends(_materialverantwortliche),
) -> Mangel:
mangel = await db.get(Mangel, mangel_id)
if mangel is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mangel nicht gefunden")
return await aendere_status(
db,
mangel=mangel,
status=payload.status,
benutzer_id=current_user.id,
zustaendiger_server_id=await _hauptserver_id(db),
)