feat(objekt): Zugfahrzeug-Flag am Objekttyp + Leitungs-Dashboard-Kacheln
CI / backend-tests (push) Failing after 1m26s
CI / frontend-build (push) Successful in 24s

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:
2026-09-05 10:33:00 +02:00
co-authored by Claude Sonnet 5
parent 38b8ce1795
commit 54fc29690a
20 changed files with 894 additions and 19 deletions
+161
View File
@@ -0,0 +1,161 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { apiRequest } from "../api/client";
interface ObjektMangel {
objekt_id: number;
code: string;
name: string;
gruende: string[];
}
interface Einsatzbereitschaft {
einsatzbereit: number;
eingeschraenkt_einsatzbereit: number;
nicht_einsatzbereit: number;
gesamt: number;
details: ObjektMangel[];
}
interface Prueftermineintrag {
geraet_instanz_id: string;
objekt_id: number;
material_name: string;
seriennummer: string;
naechste_pruefung: string;
verbleibende_tage: number;
status: string;
}
interface Ablaufdatumeintrag {
objekt_id: number;
material_id: number;
ablaufdatum: string;
verbleibende_tage: number;
status: string;
}
const GRUND_TEXT: Record<string, string> = {
fehlbestand_offen: "Offener Fehlbestand",
ablaufdatum_abgelaufen: "Ablaufdatum überschritten",
pruefung_ueberfaellig: "Geräteprüfung überfällig",
kontrolle_laeuft: "Kontrolle läuft (gesperrt)",
mindermenge_genehmigt: "Mindermenge genehmigt",
};
// Leitungs-Dashboard (Karte-Nachtrag): zwei neue Kacheln (Einsatzbereitschaft,
// Prüftermine) plus die bestehende Ablaufdaten-Kachel, alle drei eigenständig -
// widersprechen sich nicht, da überfällige Prüfung/Ablauf beide zusätzlich als
// Grund in die Einsatzbereitschaft einfließen.
export function DashboardPage() {
const [bereitschaft, setBereitschaft] = useState<Einsatzbereitschaft | null>(null);
const [prueftermine, setPrueftermine] = useState<Prueftermineintrag[]>([]);
const [ablaufdaten, setAblaufdaten] = useState<Ablaufdatumeintrag[]>([]);
const [laedt, setLaedt] = useState(true);
const [fehler, setFehler] = useState<string | null>(null);
const [detailsOffen, setDetailsOffen] = useState(false);
useEffect(() => {
Promise.all([
apiRequest<Einsatzbereitschaft>("/dashboard/einsatzbereitschaft"),
apiRequest<Prueftermineintrag[]>("/dashboard/prueftermine"),
apiRequest<Ablaufdatumeintrag[]>("/dashboard/ablaufdaten"),
])
.then(([b, p, a]) => {
setBereitschaft(b);
setPrueftermine(p);
setAblaufdaten(a);
})
.catch(() => setFehler("Dashboard-Daten konnten nicht geladen werden."))
.finally(() => setLaedt(false));
}, []);
return (
<main className="page">
<h1>Dashboard</h1>
{fehler && <div className="alert alert-danger">{fehler}</div>}
{laedt && <p className="text-muted">Lade</p>}
{!laedt && bereitschaft && (
<div style={{ display: "flex", gap: "1.25rem", flexWrap: "wrap" }}>
<div className="card" style={{ flex: "1 1 18rem" }}>
<h3 style={{ marginTop: 0 }}>Einsatzbereites Material</h3>
<p style={{ fontSize: "1.5rem", margin: "0.25rem 0" }}>
{bereitschaft.einsatzbereit} von {bereitschaft.gesamt} Objekten einsatzbereit
</p>
{bereitschaft.eingeschraenkt_einsatzbereit > 0 && (
<p className="text-muted" style={{ color: "#b8860b" }}>
{bereitschaft.eingeschraenkt_einsatzbereit} eingeschränkt einsatzbereit (aktive
Mindermengen-Genehmigung)
</p>
)}
{bereitschaft.nicht_einsatzbereit > 0 && (
<p className="text-muted" style={{ color: "#b00020" }}>
{bereitschaft.nicht_einsatzbereit} nicht einsatzbereit
</p>
)}
{bereitschaft.details.length > 0 && (
<>
<button className="btn btn-secondary" onClick={() => setDetailsOffen(!detailsOffen)}>
{detailsOffen ? "Details ausblenden" : "Details anzeigen"}
</button>
{detailsOffen && (
<ul className="card-list" style={{ marginTop: "0.5rem" }}>
{bereitschaft.details.map((d) => (
<li key={d.objekt_id} className="row-between" style={{ padding: "0.3rem 0" }}>
<Link to={`/objekte/${d.objekt_id}/kontrolle`}>
{d.name} ({d.code})
</Link>
<span className="text-muted">
{d.gruende.map((g) => GRUND_TEXT[g] ?? g).join(", ")}
</span>
</li>
))}
</ul>
)}
</>
)}
</div>
<div className="card" style={{ flex: "1 1 18rem" }}>
<h3 style={{ marginTop: 0 }}>Nächste Prüftermine</h3>
{prueftermine.length === 0 ? (
<p className="text-muted">Keine anstehenden oder überfälligen Geräteprüfungen.</p>
) : (
<ul className="card-list">
{prueftermine.map((p) => (
<li key={p.geraet_instanz_id} className="row-between" style={{ padding: "0.3rem 0" }}>
<span>
{p.material_name} <span className="text-muted">(SN {p.seriennummer})</span>
</span>
<span className={p.status === "abgelaufen" ? "text-muted" : "text-muted"} style={{ color: p.status === "abgelaufen" ? "#b00020" : undefined }}>
{p.naechste_pruefung} ({p.verbleibende_tage} Tage)
</span>
</li>
))}
</ul>
)}
</div>
<div className="card" style={{ flex: "1 1 18rem" }}>
<h3 style={{ marginTop: 0 }}>Bevorstehende Ablaufdaten</h3>
{ablaufdaten.length === 0 ? (
<p className="text-muted">Keine bald ablaufenden Chargen.</p>
) : (
<ul className="card-list">
{ablaufdaten.map((a) => (
<li key={`${a.objekt_id}-${a.material_id}`} className="row-between" style={{ padding: "0.3rem 0" }}>
<span>Objekt #{a.objekt_id} Material #{a.material_id}</span>
<span className="text-muted" style={{ color: a.status === "abgelaufen" ? "#b00020" : undefined }}>
{a.ablaufdatum} ({a.verbleibende_tage} Tage)
</span>
</li>
))}
</ul>
)}
</div>
</div>
)}
</main>
);
}