Files
MABEA/frontend/src/pages/DashboardPage.tsx
T
patrickandClaude Sonnet 5 15fb8dbb16
CI / backend-tests (push) Failing after 1m40s
CI / frontend-build (push) Successful in 17s
feat(fahrzeug): Fahrzeug-Spezialfelder + 5-stufiger Status (Roadmap Phase 3)
Neue Tabelle fahrzeugdetails (1:1 zu Objekt, additiv/optional): Kennzeichen,
Funkrufname, Baujahr, Kilometerstand, Betriebsstunden, nächste HU/UVV.
objekt_status um 'in_wartung' erweitert (Nutzer-Vorgabe: einsatzbereit/
eingeschränkt/nicht einsatzbereit/in Wartung/außer Dienst - die ersten drei
bleiben weiterhin rein rechnerisch über dashboard.einsatzbereitschaft(),
in_wartung/ausser_dienst sind die beiden manuell gesetzten Zustände).

dashboard.einsatzbereitschaft(): ausser_dienst-Objekte zählen nicht mehr mit,
in_wartung und überfällige HU/UVV sind neue Gründe für "nicht einsatzbereit".

dashboard.bevorstehende_prueftermine() (Kachel 2) zeigt jetzt Geräte- UND
Fahrzeugprüfungen (HU/UVV) in einer Liste (Nutzer-Vorgabe: ein Konzept für
"welche Prüfungen bald fällig sind") - Schema dafür verallgemeinert
(typ/bezeichnung/faelligkeitsdatum statt geräte-spezifischer Felder).

Frontend: Status-Dropdown + Fahrzeugdetails-Formular in ObjektSection
(nur für Objekttypen mit ist_zugfahrzeug=True), Dashboard-Kachel 2 zeigt
gemischte Liste.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
2026-09-05 13:36:01 +02:00

162 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 {
typ: string;
objekt_id: number;
bezeichnung: string;
faelligkeitsdatum: 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)",
mangel_kritisch_offen: "Kritischer Mangel offen",
in_wartung: "In Wartung",
hu_uvv_ueberfaellig: "HU/UVV überfällig",
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, i) => (
<li key={i} className="row-between" style={{ padding: "0.3rem 0" }}>
<span>{p.bezeichnung}</span>
<span style={{ color: p.status === "abgelaufen" ? "#b00020" : undefined }}>
{p.faelligkeitsdatum} ({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>
);
}