feat(objekt): Zugfahrzeug-Flag am Objekttyp + Leitungs-Dashboard-Kacheln
Objekttyp.ist_zugfahrzeug (Migration 0011) grenzt die Fahrzeug-Zuordnung serverseitig auf tatsächliche Zugfahrzeuge ein (422 bei Verstoß), Frontend filtert die Fahrzeug-Dropdowns entsprechend. Neue Dashboard-Endpunkte /dashboard/einsatzbereitschaft und /dashboard/prueftermine plus DashboardPage mit drei Kacheln (Einsatzbereitschaft, Prüftermine, Ablaufdaten). Aktive Mindermengen-Genehmigung zählt als "eingeschränkt einsatzbereit", nicht als voll einsatzbereit (U2/Karte 08). Neue FehlbestandListePage: Fehlbestände eigenständig nachfüllen/Mindermenge genehmigen, ohne komplette Kontrolle des Objekts zu starten (Backend existierte bereits, nur das Frontend fehlte). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
This commit is contained in:
@@ -4,8 +4,18 @@ 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 RolleTyp
|
||||
from app.schemas.dashboard import AblaufdatumRead, KennzahlenRead
|
||||
from app.services.dashboard import bevorstehende_ablaufdaten, kennzahlen
|
||||
from app.schemas.dashboard import (
|
||||
AblaufdatumRead,
|
||||
EinsatzbereitschaftRead,
|
||||
KennzahlenRead,
|
||||
PrueftermineintragRead,
|
||||
)
|
||||
from app.services.dashboard import (
|
||||
bevorstehende_ablaufdaten,
|
||||
bevorstehende_prueftermine,
|
||||
einsatzbereitschaft,
|
||||
kennzahlen,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -25,3 +35,22 @@ async def hole_kennzahlen(db: AsyncSession = Depends(get_db), _=Depends(_verantw
|
||||
@router.get("/dashboard/ablaufdaten", response_model=list[AblaufdatumRead])
|
||||
async def hole_ablaufdaten(db: AsyncSession = Depends(get_db), _=Depends(_verantwortliche)) -> list[AblaufdatumRead]:
|
||||
return [AblaufdatumRead(**eintrag) for eintrag in await bevorstehende_ablaufdaten(db)]
|
||||
|
||||
|
||||
@router.get("/dashboard/einsatzbereitschaft", response_model=EinsatzbereitschaftRead)
|
||||
async def hole_einsatzbereitschaft(
|
||||
db: AsyncSession = Depends(get_db), _=Depends(_verantwortliche)
|
||||
) -> EinsatzbereitschaftRead:
|
||||
"""Kachel 1 Leitungs-Dashboard: einsatzbereit/eingeschränkt/nicht einsatzbereit
|
||||
je Objekt, siehe app/services/dashboard.py:einsatzbereitschaft für die
|
||||
Kriterien und die Mindermenge-Sonderrolle (U2, Karte 08)."""
|
||||
return EinsatzbereitschaftRead(**await einsatzbereitschaft(db))
|
||||
|
||||
|
||||
@router.get("/dashboard/prueftermine", response_model=list[PrueftermineintragRead])
|
||||
async def hole_prueftermine(
|
||||
db: AsyncSession = Depends(get_db), _=Depends(_verantwortliche)
|
||||
) -> list[PrueftermineintragRead]:
|
||||
"""Kachel 2 Leitungs-Dashboard: Geräteprüfungen (Karte 14), bewusst getrennt
|
||||
von /dashboard/ablaufdaten (Nutzer-Entscheidung: Prüfung ≠ Ablauf)."""
|
||||
return [PrueftermineintragRead(**eintrag) for eintrag in await bevorstehende_prueftermine(db)]
|
||||
|
||||
@@ -10,7 +10,7 @@ from app.db.session import get_db
|
||||
from app.models.auth import KnotenTyp, RolleTyp, Systemknoten
|
||||
from app.models.objekt import Objekt
|
||||
from app.models.objektposition import Objektposition
|
||||
from app.models.stammdaten import Material
|
||||
from app.models.stammdaten import Material, Objekttyp
|
||||
from app.models.vorlage import Beladungsvorlage
|
||||
from app.services.label import generiere_code128_label_pdf
|
||||
from app.schemas.objekt import (
|
||||
@@ -36,6 +36,22 @@ _admin_only = require_roles(RolleTyp.administration)
|
||||
_materialverantwortliche = require_roles(RolleTyp.administration, RolleTyp.materialverantwortlicher)
|
||||
|
||||
|
||||
async def _pruefe_zugfahrzeug(db: AsyncSession, fahrzeug_id: int) -> Objekt:
|
||||
"""Karte 11 Nachtrag (Option A+C): Fahrzeug-Zuordnung nur auf Objekte, deren
|
||||
Objekttyp ist_zugfahrzeug=True ist - grenzt Rucksäcke/Geräte serverseitig aus,
|
||||
unabhängig vom (evtl. umgangenen) Frontend-Dropdown."""
|
||||
ziel = await db.get(Objekt, fahrzeug_id)
|
||||
if ziel is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fahrzeug nicht gefunden")
|
||||
objekttyp = await db.get(Objekttyp, ziel.objekttyp_id)
|
||||
if objekttyp is None or not objekttyp.ist_zugfahrzeug:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="Zielobjekt ist kein Zugfahrzeug (Objekttyp.ist_zugfahrzeug=False)",
|
||||
)
|
||||
return ziel
|
||||
|
||||
|
||||
@router.get("/objekte", response_model=list[ObjektRead])
|
||||
async def liste_objekte(
|
||||
standort_id: int | None = None,
|
||||
@@ -196,8 +212,7 @@ async def erstelle_objekt(
|
||||
if vorlage is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vorlage nicht gefunden")
|
||||
if payload.fahrzeug_id is not None:
|
||||
if await db.get(Objekt, payload.fahrzeug_id) is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fahrzeug nicht gefunden")
|
||||
await _pruefe_zugfahrzeug(db, payload.fahrzeug_id)
|
||||
|
||||
hauptserver = await db.execute(select(Systemknoten.id).where(Systemknoten.typ == KnotenTyp.haupt))
|
||||
zustaendiger_server_id = hauptserver.scalar_one()
|
||||
@@ -231,9 +246,7 @@ async def aendere_objekt(
|
||||
daten = payload.model_dump(exclude_unset=True)
|
||||
ziel_id = daten.get("fahrzeug_id")
|
||||
if ziel_id is not None:
|
||||
ziel = await db.get(Objekt, ziel_id)
|
||||
if ziel is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fahrzeug nicht gefunden")
|
||||
ziel = await _pruefe_zugfahrzeug(db, ziel_id)
|
||||
if ziel.fahrzeug_id == objekt_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
|
||||
Reference in New Issue
Block a user