feat(dashboard): drei fehlende Kacheln (Kennzahlen, Mängel, Qualifikationsablauf)
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:
@@ -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)
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -137,3 +137,62 @@ async def test_benutzer_einheit_zuordnung(client, admin_user):
|
||||
f"/api/v1/benutzer/{benutzer_id}", json={"einheit_id": None}, headers=auth_header(token)
|
||||
)
|
||||
assert geloest.json()["einheit_id"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bald_ablaufende_qualifikation_erscheint_im_dashboard(
|
||||
client, admin_user, mitarbeiter_user
|
||||
):
|
||||
from datetime import date, timedelta
|
||||
|
||||
token = await login(client, "admin1")
|
||||
quali = await client.post(
|
||||
"/api/v1/qualifikationstypen",
|
||||
json={"name": "Führerschein C1", "kategorie": "fuehrerschein"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
quali_id = quali.json()["id"]
|
||||
|
||||
await client.post(
|
||||
f"/api/v1/benutzer/{mitarbeiter_user.id}/qualifikationen",
|
||||
json={
|
||||
"benutzer_id": mitarbeiter_user.id,
|
||||
"qualifikationstyp_id": quali_id,
|
||||
"erworben_am": (date.today() - timedelta(days=365)).isoformat(),
|
||||
"gueltig_bis": (date.today() + timedelta(days=10)).isoformat(),
|
||||
},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
|
||||
response = await client.get("/api/v1/dashboard/qualifikationsablaeufe", headers=auth_header(token))
|
||||
body = response.json()
|
||||
assert len(body) == 1
|
||||
assert body[0]["status"] == "bald_ablaufend"
|
||||
assert body[0]["benutzer_id"] == mitarbeiter_user.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unbefristete_qualifikation_erscheint_nicht_im_dashboard(client, admin_user, mitarbeiter_user):
|
||||
from datetime import date, timedelta
|
||||
|
||||
token = await login(client, "admin1")
|
||||
quali = await client.post(
|
||||
"/api/v1/qualifikationstypen",
|
||||
json={"name": "Sanitätshelfer", "kategorie": "lehrgang"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
quali_id = quali.json()["id"]
|
||||
|
||||
await client.post(
|
||||
f"/api/v1/benutzer/{mitarbeiter_user.id}/qualifikationen",
|
||||
json={
|
||||
"benutzer_id": mitarbeiter_user.id,
|
||||
"qualifikationstyp_id": quali_id,
|
||||
"erworben_am": (date.today() - timedelta(days=365)).isoformat(),
|
||||
"gueltig_bis": None,
|
||||
},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
|
||||
response = await client.get("/api/v1/dashboard/qualifikationsablaeufe", headers=auth_header(token))
|
||||
assert response.json() == []
|
||||
|
||||
Reference in New Issue
Block a user