feat(dashboard): drei fehlende Kacheln (Kennzahlen, Mängel, Qualifikationsablauf)
CI / backend-tests (push) Failing after 1m40s
CI / frontend-build (push) Successful in 17s

Kennzahlen: bestehender Endpoint /dashboard/kennzahlen war seit Prompt 12.1
nie im Frontend verdrahtet - jetzt eigene Kachel (offene Fehlbestände,
genehmigte Mindermengen, kürzlich erledigt, problematische Objekte, Objekte
gesamt).

Mängel: eigene Übersicht (offen/kritisch-Zahl + aufklappbare Liste) zusätzlich
zur bisherigen Erwähnung als Grund in Kachel 1 - nutzt bestehendes GET
/maengel, kein neuer Backend-Code.

Qualifikationsablauf: neuer Endpoint /dashboard/qualifikationsablaeufe
(bevorstehende_qualifikationsablaeufe(), 60 Tage Vorlauf) - analog
bevorstehende_prueftermine, aber für BenutzerQualifikation.gueltig_bis
(Nutzer-Vorgabe: "wer verliert bald Führerschein/Lehrgang-Gültigkeit").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
This commit is contained in:
2026-09-05 13:38:42 +02:00
co-authored by Claude Sonnet 5
parent 15fb8dbb16
commit e9904d5bea
6 changed files with 248 additions and 1 deletions
+13
View File
@@ -9,10 +9,12 @@ from app.schemas.dashboard import (
EinsatzbereitschaftRead,
KennzahlenRead,
PrueftermineintragRead,
QualifikationsablaufRead,
)
from app.services.dashboard import (
bevorstehende_ablaufdaten,
bevorstehende_prueftermine,
bevorstehende_qualifikationsablaeufe,
einsatzbereitschaft,
kennzahlen,
)
@@ -54,3 +56,14 @@ async def hole_prueftermine(
"""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)]
@router.get("/dashboard/qualifikationsablaeufe", response_model=list[QualifikationsablaufRead])
async def hole_qualifikationsablaeufe(
db: AsyncSession = Depends(get_db), _=Depends(_verantwortliche)
) -> list[QualifikationsablaufRead]:
"""Kachel Personal-Modul: bald ablaufende/abgelaufene Führerscheine/
Lehrgänge/Berechtigungen."""
return [
QualifikationsablaufRead(**eintrag) for eintrag in await bevorstehende_qualifikationsablaeufe(db)
]
+14
View File
@@ -52,3 +52,17 @@ class PrueftermineintragRead(BaseModel):
objektposition_id: uuid.UUID | None = None
material_id: int | None = None
seriennummer: str | None = None
class QualifikationsablaufRead(BaseModel):
"""Personal-Modul-Kachel (Nutzer-Vorgabe: "wer verliert bald Führerschein/
Lehrgang-Gültigkeit")."""
benutzer_qualifikation_id: uuid.UUID
benutzer_id: int
benutzer_name: str
qualifikationstyp_id: int
qualifikationstyp_name: str
gueltig_bis: date
verbleibende_tage: int
status: str
+35
View File
@@ -8,8 +8,10 @@ from app.models.fehlbestand import Fehlbestand, FehlbestandStatus
from app.models.geraet_instanz import GeraetInstanz
from app.models.kontrolle import Kontrolle, KontrollStatus
from app.models.mindermenge import MindermengeStatus, MindermengenGenehmigung
from app.models.auth import Benutzer
from app.models.objekt import Objekt, ObjektStatus
from app.models.objektposition import Objektposition
from app.models.personal import BenutzerQualifikation, Qualifikationstyp
from app.models.stammdaten import Material
from app.services.mangel import objekt_ids_mit_kritischem_offenem_mangel
@@ -298,3 +300,36 @@ async def bevorstehende_prueftermine(db: AsyncSession, warnzeitraum_tage: int =
eintraege.sort(key=lambda e: e["verbleibende_tage"])
return eintraege
async def bevorstehende_qualifikationsablaeufe(db: AsyncSession, warnzeitraum_tage: int = 60) -> list[dict]:
"""Personal-Modul-Kachel (Nutzer-Vorgabe: "wer verliert bald Führerschein/
Lehrgang-Gültigkeit"). Analog bevorstehende_prueftermine, aber 60 statt 30
Tage Standard-Vorlauf (Nachweise wie Führerscheine brauchen typischerweise
mehr Vorlaufzeit zur Erneuerung als eine Geräteprüfung)."""
heute = datetime.now(timezone.utc).date()
result = await db.execute(
select(BenutzerQualifikation, Benutzer, Qualifikationstyp)
.join(Benutzer, BenutzerQualifikation.benutzer_id == Benutzer.id)
.join(Qualifikationstyp, BenutzerQualifikation.qualifikationstyp_id == Qualifikationstyp.id)
.where(BenutzerQualifikation.gueltig_bis.is_not(None))
)
eintraege = []
for qualifikation, benutzer, qualifikationstyp in result.all():
status_wert, verbleibende_tage = _ablauf_status(qualifikation.gueltig_bis, warnzeitraum_tage, heute)
if status_wert == "gueltig":
continue
eintraege.append(
{
"benutzer_qualifikation_id": qualifikation.id,
"benutzer_id": benutzer.id,
"benutzer_name": benutzer.name,
"qualifikationstyp_id": qualifikationstyp.id,
"qualifikationstyp_name": qualifikationstyp.name,
"gueltig_bis": qualifikation.gueltig_bis,
"verbleibende_tage": verbleibende_tage,
"status": status_wert,
}
)
eintraege.sort(key=lambda e: e["verbleibende_tage"])
return eintraege