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
162 lines
6.1 KiB
TypeScript
162 lines
6.1 KiB
TypeScript
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>
|
||
);
|
||
}
|