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
+105 -1
View File
@@ -2,6 +2,27 @@ import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { apiRequest } from "../api/client";
import type { Mangel } from "../api/types";
interface Kennzahlen {
anzahl_offener_fehlbestaende: number;
anzahl_genehmigter_mindermengen: number;
anzahl_kuerzlich_erledigter_fehlbestaende: number;
anzahl_bevorstehender_ablaufdaten: number;
anzahl_problematischer_objekte: number;
anzahl_objekte_gesamt: number;
}
interface Qualifikationsablauf {
benutzer_qualifikation_id: string;
benutzer_id: number;
benutzer_name: string;
qualifikationstyp_id: number;
qualifikationstyp_name: string;
gueltig_bis: string;
verbleibende_tage: number;
status: string;
}
interface ObjektMangel {
objekt_id: number;
@@ -54,25 +75,39 @@ export function DashboardPage() {
const [bereitschaft, setBereitschaft] = useState<Einsatzbereitschaft | null>(null);
const [prueftermine, setPrueftermine] = useState<Prueftermineintrag[]>([]);
const [ablaufdaten, setAblaufdaten] = useState<Ablaufdatumeintrag[]>([]);
const [kennzahlen, setKennzahlen] = useState<Kennzahlen | null>(null);
const [maengel, setMaengel] = useState<Mangel[]>([]);
const [qualifikationsablaeufe, setQualifikationsablaeufe] = useState<Qualifikationsablauf[]>([]);
const [laedt, setLaedt] = useState(true);
const [fehler, setFehler] = useState<string | null>(null);
const [detailsOffen, setDetailsOffen] = useState(false);
const [maengelOffen, setMaengelOffen] = useState(false);
useEffect(() => {
Promise.all([
apiRequest<Einsatzbereitschaft>("/dashboard/einsatzbereitschaft"),
apiRequest<Prueftermineintrag[]>("/dashboard/prueftermine"),
apiRequest<Ablaufdatumeintrag[]>("/dashboard/ablaufdaten"),
apiRequest<Kennzahlen>("/dashboard/kennzahlen"),
apiRequest<Mangel[]>("/maengel"),
apiRequest<Qualifikationsablauf[]>("/dashboard/qualifikationsablaeufe"),
])
.then(([b, p, a]) => {
.then(([b, p, a, k, m, q]) => {
setBereitschaft(b);
setPrueftermine(p);
setAblaufdaten(a);
setKennzahlen(k);
setMaengel(m);
setQualifikationsablaeufe(q);
})
.catch(() => setFehler("Dashboard-Daten konnten nicht geladen werden."))
.finally(() => setLaedt(false));
}, []);
const maengelOffenAnzahl = maengel.filter((m) => m.status !== "erledigt").length;
const maengelKritischAnzahl = maengel.filter((m) => m.status !== "erledigt" && m.prioritaet === "hoch").length;
const maengelOffenListe = maengel.filter((m) => m.status !== "erledigt");
return (
<main className="page">
<h1>Dashboard</h1>
@@ -154,6 +189,75 @@ export function DashboardPage() {
</ul>
)}
</div>
<div className="card" style={{ flex: "1 1 18rem" }}>
<h3 style={{ marginTop: 0 }}>Kennzahlen</h3>
{kennzahlen && (
<ul className="card-list">
<li style={{ padding: "0.3rem 0" }}>Offene Fehlbestände: {kennzahlen.anzahl_offener_fehlbestaende}</li>
<li style={{ padding: "0.3rem 0" }}>
Genehmigte Mindermengen: {kennzahlen.anzahl_genehmigter_mindermengen}
</li>
<li style={{ padding: "0.3rem 0" }}>
Kürzlich erledigt (7 Tage): {kennzahlen.anzahl_kuerzlich_erledigter_fehlbestaende}
</li>
<li style={{ padding: "0.3rem 0" }}>
Problematische Objekte: {kennzahlen.anzahl_problematischer_objekte}
</li>
<li style={{ padding: "0.3rem 0" }}>Objekte gesamt: {kennzahlen.anzahl_objekte_gesamt}</li>
</ul>
)}
</div>
<div className="card" style={{ flex: "1 1 18rem" }}>
<h3 style={{ marginTop: 0 }}>Mängel</h3>
<p style={{ fontSize: "1.5rem", margin: "0.25rem 0" }}>{maengelOffenAnzahl} offen</p>
{maengelKritischAnzahl > 0 && (
<p className="text-muted" style={{ color: "#b00020" }}>
{maengelKritischAnzahl} kritisch (blockiert Einsatzbereitschaft)
</p>
)}
{maengelOffenListe.length > 0 && (
<>
<button className="btn btn-secondary" onClick={() => setMaengelOffen(!maengelOffen)}>
{maengelOffen ? "Details ausblenden" : "Details anzeigen"}
</button>
{maengelOffen && (
<ul className="card-list" style={{ marginTop: "0.5rem" }}>
{maengelOffenListe.map((m) => (
<li key={m.id} className="row-between" style={{ padding: "0.3rem 0" }}>
<Link to="/maengel">{m.beschreibung}</Link>
<span className="text-muted" style={{ color: m.prioritaet === "hoch" ? "#b00020" : undefined }}>
{m.prioritaet}
</span>
</li>
))}
</ul>
)}
</>
)}
{maengelOffenListe.length === 0 && <p className="text-muted">Keine offenen Mängel.</p>}
</div>
<div className="card" style={{ flex: "1 1 18rem" }}>
<h3 style={{ marginTop: 0 }}>Qualifikationsablauf</h3>
{qualifikationsablaeufe.length === 0 ? (
<p className="text-muted">Keine bald ablaufenden Qualifikationen.</p>
) : (
<ul className="card-list">
{qualifikationsablaeufe.map((q) => (
<li key={q.benutzer_qualifikation_id} className="row-between" style={{ padding: "0.3rem 0" }}>
<span>
{q.benutzer_name} <span className="text-muted">({q.qualifikationstyp_name})</span>
</span>
<span style={{ color: q.status === "abgelaufen" ? "#b00020" : undefined }}>
{q.gueltig_bis} ({q.verbleibende_tage} Tage)
</span>
</li>
))}
</ul>
)}
</div>
</div>
)}
</main>