feat(objekt): Zugfahrzeug-Flag am Objekttyp + Leitungs-Dashboard-Kacheln
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user