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
This commit is contained in:
@@ -1,3 +1,14 @@
|
||||
export interface Fahrzeugdetails {
|
||||
objekt_id: number;
|
||||
kennzeichen: string | null;
|
||||
funkrufname: string | null;
|
||||
baujahr: number | null;
|
||||
kilometerstand: number | null;
|
||||
betriebsstunden: string | null;
|
||||
naechste_hu: string | null;
|
||||
naechste_uvv: string | null;
|
||||
}
|
||||
|
||||
export interface Objekt {
|
||||
id: number;
|
||||
code: string;
|
||||
|
||||
@@ -19,11 +19,10 @@ interface Einsatzbereitschaft {
|
||||
}
|
||||
|
||||
interface Prueftermineintrag {
|
||||
geraet_instanz_id: string;
|
||||
typ: string;
|
||||
objekt_id: number;
|
||||
material_name: string;
|
||||
seriennummer: string;
|
||||
naechste_pruefung: string;
|
||||
bezeichnung: string;
|
||||
faelligkeitsdatum: string;
|
||||
verbleibende_tage: number;
|
||||
status: string;
|
||||
}
|
||||
@@ -42,6 +41,8 @@ const GRUND_TEXT: Record<string, string> = {
|
||||
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",
|
||||
};
|
||||
|
||||
@@ -124,13 +125,11 @@ export function DashboardPage() {
|
||||
<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)
|
||||
{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>
|
||||
))}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { apiRequest } from "../../api/client";
|
||||
import type { Beladungsvorlage, Material, Objekt, Objekttyp, Standort } from "../../api/types";
|
||||
import type { Beladungsvorlage, Fahrzeugdetails, Material, Objekt, Objekttyp, Standort } from "../../api/types";
|
||||
import { ObjektPositionenPanel } from "./ObjektPositionenPanel";
|
||||
|
||||
const STATUS_OPTIONEN = [
|
||||
{ wert: "aktiv", label: "Aktiv" },
|
||||
{ wert: "in_wartung", label: "In Wartung" },
|
||||
{ wert: "ausser_dienst", label: "Außer Dienst" },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
objekte: Objekt[];
|
||||
objekttypen: Objekttyp[];
|
||||
@@ -46,6 +52,52 @@ export function ObjektSection({
|
||||
const [fahrzeugBearbeitenId, setFahrzeugBearbeitenId] = useState<number | null>(null);
|
||||
const [fahrzeugAuswahl, setFahrzeugAuswahl] = useState<number | "">("");
|
||||
const [suche, setSuche] = useState("");
|
||||
const [detailsOffenId, setDetailsOffenId] = useState<number | null>(null);
|
||||
const [details, setDetails] = useState<Partial<Fahrzeugdetails>>({});
|
||||
|
||||
async function statusAendern(o: Objekt, status: string) {
|
||||
try {
|
||||
await apiRequest(`/objekte/${o.id}`, { method: "PATCH", body: { status } });
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Status konnte nicht geändert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
async function fahrzeugdetailsOeffnen(o: Objekt) {
|
||||
if (detailsOffenId === o.id) {
|
||||
setDetailsOffenId(null);
|
||||
return;
|
||||
}
|
||||
setDetailsOffenId(o.id);
|
||||
try {
|
||||
const daten = await apiRequest<Fahrzeugdetails>(`/objekte/${o.id}/fahrzeugdetails`);
|
||||
setDetails(daten);
|
||||
} catch {
|
||||
setDetails({});
|
||||
}
|
||||
}
|
||||
|
||||
async function fahrzeugdetailsSpeichern(objektId: number) {
|
||||
try {
|
||||
await apiRequest(`/objekte/${objektId}/fahrzeugdetails`, {
|
||||
method: "PUT",
|
||||
body: {
|
||||
kennzeichen: details.kennzeichen || null,
|
||||
funkrufname: details.funkrufname || null,
|
||||
baujahr: details.baujahr || null,
|
||||
kilometerstand: details.kilometerstand || null,
|
||||
betriebsstunden: details.betriebsstunden || null,
|
||||
naechste_hu: details.naechste_hu || null,
|
||||
naechste_uvv: details.naechste_uvv || null,
|
||||
},
|
||||
});
|
||||
setDetailsOffenId(null);
|
||||
onFehler("");
|
||||
} catch {
|
||||
onFehler("Fahrzeugdetails konnten nicht gespeichert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
// Option A+C (Karte 11 Nachtrag): Dropdown zeigt nur Zugfahrzeuge (Objekttyp-
|
||||
// Flag ist_zugfahrzeug) ohne eigene Kopplung - serverseitig zusätzlich per
|
||||
@@ -286,6 +338,24 @@ export function ObjektSection({
|
||||
</span>
|
||||
</span>
|
||||
<div className="row">
|
||||
{!nurPflegen && (
|
||||
<select
|
||||
className="input"
|
||||
value={o.status}
|
||||
onChange={(e) => statusAendern(o, e.target.value)}
|
||||
>
|
||||
{STATUS_OPTIONEN.map((s) => (
|
||||
<option key={s.wert} value={s.wert}>
|
||||
{s.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{objekttypen.find((t) => t.id === o.objekttyp_id)?.ist_zugfahrzeug && (
|
||||
<button className="btn btn-secondary" onClick={() => fahrzeugdetailsOeffnen(o)}>
|
||||
{detailsOffenId === o.id ? "Schließen" : "Fahrzeugdetails"}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setAusgeklapptId(ausgeklapptId === o.id ? null : o.id)}
|
||||
@@ -304,6 +374,63 @@ export function ObjektSection({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{detailsOffenId === o.id && (
|
||||
<div className="stack" style={{ marginTop: "0.5rem", maxWidth: "24rem" }}>
|
||||
<input
|
||||
className="input"
|
||||
placeholder="Kennzeichen"
|
||||
value={details.kennzeichen ?? ""}
|
||||
onChange={(e) => setDetails((d) => ({ ...d, kennzeichen: e.target.value }))}
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
placeholder="Funkrufname"
|
||||
value={details.funkrufname ?? ""}
|
||||
onChange={(e) => setDetails((d) => ({ ...d, funkrufname: e.target.value }))}
|
||||
/>
|
||||
<div className="row">
|
||||
<input
|
||||
className="input"
|
||||
type="number"
|
||||
placeholder="Baujahr"
|
||||
value={details.baujahr ?? ""}
|
||||
onChange={(e) => setDetails((d) => ({ ...d, baujahr: Number(e.target.value) || undefined }))}
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
type="number"
|
||||
placeholder="Kilometerstand"
|
||||
value={details.kilometerstand ?? ""}
|
||||
onChange={(e) =>
|
||||
setDetails((d) => ({ ...d, kilometerstand: Number(e.target.value) || undefined }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field">
|
||||
<label>Nächste HU</label>
|
||||
<input
|
||||
className="input"
|
||||
type="date"
|
||||
value={details.naechste_hu ?? ""}
|
||||
onChange={(e) => setDetails((d) => ({ ...d, naechste_hu: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Nächste UVV</label>
|
||||
<input
|
||||
className="input"
|
||||
type="date"
|
||||
value={details.naechste_uvv ?? ""}
|
||||
onChange={(e) => setDetails((d) => ({ ...d, naechste_uvv: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={() => fahrzeugdetailsSpeichern(o.id)}>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{fahrzeugBearbeitenId === o.id && (
|
||||
<div className="row" style={{ marginTop: "0.5rem", flexWrap: "wrap" }}>
|
||||
<select
|
||||
|
||||
Reference in New Issue
Block a user