feat(maintenance): MAINT-Epic (Wartungspläne/-aufträge, Werkstatt) + Akte-Redesign
Neues Wartungskonzept: Wartungsplan+Position+Intervall (zeit/km/ Betriebsstunden, kombinierbar - fällig sobald ein Kriterium erreicht ist), Wartungsauftrag mit intern/externer Werkstatt, Status offen/ in_arbeit/erledigt, Kosten, Historie, Dokument-Anhang. Ad-hoc-Aufträge ohne Plan möglich (z.B. kleinere Reparatur). Admin-Tab "Wartungspläne". Objektakte-Redesign (Epic 21, Nutzer-Feedback "wirkt nicht modern"): CSS-Grid statt Karten-Stapel, einspaltig mobil, zweispaltig ab 900px; Fahrzeug-Objekte zeigen Wartung/Fahrzeugdaten prominent, Geräte/ Rucksäcke die Beladung. Dabei gefundene Lücke: Beladung (Soll/Ist je Position) fehlte in der Akte komplett, nur über eine laufende Kontrolle sichtbar - jetzt eigene Karte. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -47,6 +47,57 @@ export interface GeraetInstanz {
|
||||
bemerkung: string | null;
|
||||
}
|
||||
|
||||
export interface Wartungsintervall {
|
||||
id: number;
|
||||
wartungsplan_position_id: number;
|
||||
typ: "zeit" | "km" | "betriebsstunden";
|
||||
wert: number;
|
||||
}
|
||||
|
||||
export interface WartungsplanPosition {
|
||||
id: number;
|
||||
wartungsplan_id: number;
|
||||
wartungsart: string;
|
||||
beschreibung: string | null;
|
||||
intervalle: Wartungsintervall[];
|
||||
}
|
||||
|
||||
export interface Wartungsplan {
|
||||
id: number;
|
||||
objekttyp_id: number;
|
||||
name: string;
|
||||
positionen: WartungsplanPosition[];
|
||||
}
|
||||
|
||||
export interface FaelligeWartung {
|
||||
wartungsplan_position_id: number;
|
||||
wartungsart: string;
|
||||
objekt_id: number;
|
||||
faellig: boolean;
|
||||
faellig_am: string | null;
|
||||
faellig_bei_km: number | null;
|
||||
faellig_bei_betriebsstunden: string | null;
|
||||
}
|
||||
|
||||
export interface Wartungsauftrag {
|
||||
id: string;
|
||||
objekt_id: number;
|
||||
geraet_instanz_id: string | null;
|
||||
wartungsplan_position_id: number | null;
|
||||
wartungsart: string;
|
||||
beschreibung: string | null;
|
||||
status: "offen" | "in_arbeit" | "erledigt";
|
||||
werkstatt_intern: boolean;
|
||||
werkstatt_name: string | null;
|
||||
faelligkeit_am: string | null;
|
||||
erstellt_am: string;
|
||||
erledigt_am: string | null;
|
||||
durchgefuehrt_von: number | null;
|
||||
kosten: string | null;
|
||||
kilometerstand_bei_erledigung: number | null;
|
||||
betriebsstunden_bei_erledigung: string | null;
|
||||
}
|
||||
|
||||
export interface Kontrolle {
|
||||
id: string;
|
||||
objekt_id: number;
|
||||
@@ -260,6 +311,16 @@ export interface ZustaendigerBenutzer {
|
||||
quelle: "standort" | "objekt";
|
||||
}
|
||||
|
||||
export interface BeladungsPosition {
|
||||
objektposition_id: string;
|
||||
material_id: number;
|
||||
material_name: string;
|
||||
einheit: string;
|
||||
fach: string | null;
|
||||
sollmenge: string;
|
||||
istmenge: string;
|
||||
}
|
||||
|
||||
export interface Akte {
|
||||
objekt: Objekt;
|
||||
einsatzbereitschaft_status: "ready" | "limited" | "not-ready" | "unknown";
|
||||
@@ -274,6 +335,7 @@ export interface Akte {
|
||||
dokumente: Dokument[];
|
||||
geraete: GeraetInstanz[];
|
||||
maengel: Mangel[];
|
||||
beladung: BeladungsPosition[];
|
||||
}
|
||||
|
||||
export interface HistorieEintrag {
|
||||
|
||||
@@ -70,6 +70,7 @@ export function CommandPalette() {
|
||||
},
|
||||
{ id: "nav-lager", label: "Lager", gehe: () => navigate("/admin?tab=lager") },
|
||||
{ id: "nav-ausgabe", label: "Ausgabe/Rückgabe", gehe: () => navigate("/admin?tab=ausgabe") },
|
||||
{ id: "nav-wartungsplaene", label: "Wartungspläne", gehe: () => navigate("/admin?tab=wartungsplaene") },
|
||||
{ id: "nav-personal", label: "Personal", gehe: () => navigate("/admin?tab=personal") },
|
||||
{
|
||||
id: "nav-material",
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { apiRequest } from "../api/client";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import type { FaelligeWartung, Wartungsauftrag } from "../api/types";
|
||||
import { DokumentePanel } from "./DokumentePanel";
|
||||
|
||||
interface Props {
|
||||
objektId: number;
|
||||
onFehler: (text: string) => void;
|
||||
}
|
||||
|
||||
const STATUS_LABEL: Record<Wartungsauftrag["status"], string> = {
|
||||
offen: "Offen",
|
||||
in_arbeit: "In Arbeit",
|
||||
erledigt: "Erledigt",
|
||||
};
|
||||
|
||||
const STATUS_BADGE: Record<Wartungsauftrag["status"], string> = {
|
||||
offen: "badge-warning",
|
||||
in_arbeit: "badge-warning",
|
||||
erledigt: "badge-success",
|
||||
};
|
||||
|
||||
export function WartungSection({ objektId, onFehler }: Props) {
|
||||
const { istVerantwortlich } = useAuth();
|
||||
const [faellig, setFaellig] = useState<FaelligeWartung[]>([]);
|
||||
const [auftraege, setAuftraege] = useState<Wartungsauftrag[]>([]);
|
||||
const [dokumenteOffenId, setDokumenteOffenId] = useState<string | null>(null);
|
||||
|
||||
const [neuArt, setNeuArt] = useState("");
|
||||
const [neuExtern, setNeuExtern] = useState(false);
|
||||
const [neuWerkstattName, setNeuWerkstattName] = useState("");
|
||||
|
||||
async function laden() {
|
||||
try {
|
||||
const [f, a] = await Promise.all([
|
||||
apiRequest<FaelligeWartung[]>(`/objekte/${objektId}/faellige-wartungen`),
|
||||
apiRequest<Wartungsauftrag[]>(`/wartungsauftraege?objekt_id=${objektId}`),
|
||||
]);
|
||||
setFaellig(f);
|
||||
setAuftraege(a);
|
||||
} catch {
|
||||
onFehler("Wartungsdaten konnten nicht geladen werden.");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
laden();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [objektId]);
|
||||
|
||||
async function auftragAnlegen() {
|
||||
if (!neuArt) {
|
||||
onFehler("Wartungsart angeben.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiRequest("/wartungsauftraege", {
|
||||
method: "POST",
|
||||
body: {
|
||||
objekt_id: objektId,
|
||||
wartungsart: neuArt,
|
||||
werkstatt_intern: !neuExtern,
|
||||
werkstatt_name: neuExtern ? neuWerkstattName || null : null,
|
||||
},
|
||||
});
|
||||
setNeuArt("");
|
||||
setNeuExtern(false);
|
||||
setNeuWerkstattName("");
|
||||
await laden();
|
||||
} catch {
|
||||
onFehler("Wartungsauftrag konnte nicht angelegt werden.");
|
||||
}
|
||||
}
|
||||
|
||||
async function statusAendern(auftrag: Wartungsauftrag, status: Wartungsauftrag["status"]) {
|
||||
try {
|
||||
await apiRequest(`/wartungsauftraege/${auftrag.id}/status`, { method: "PATCH", body: { status } });
|
||||
await laden();
|
||||
} catch {
|
||||
onFehler("Status konnte nicht geändert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
async function erledigen(auftrag: Wartungsauftrag) {
|
||||
const kosten = window.prompt("Kosten (optional, in €):") ?? undefined;
|
||||
try {
|
||||
await apiRequest(`/wartungsauftraege/${auftrag.id}/erledigen`, {
|
||||
method: "POST",
|
||||
body: { kosten: kosten || null },
|
||||
});
|
||||
await laden();
|
||||
} catch {
|
||||
onFehler("Wartungsauftrag konnte nicht als erledigt markiert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
const offeneAuftraege = auftraege.filter((a) => a.status !== "erledigt");
|
||||
const erledigteAuftraege = auftraege.filter((a) => a.status === "erledigt");
|
||||
const faelligeOhneOffenenAuftrag = faellig.filter((f) => f.faellig);
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<h3 style={{ marginTop: 0 }}>Wartung</h3>
|
||||
|
||||
{faelligeOhneOffenenAuftrag.length > 0 && (
|
||||
<div className="alert alert-warning">
|
||||
Fällig: {faelligeOhneOffenenAuftrag.map((f) => f.wartungsart).join(", ")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{istVerantwortlich && (
|
||||
<div className="row" style={{ flexWrap: "wrap", marginBottom: "0.75rem" }}>
|
||||
<input
|
||||
className="input"
|
||||
placeholder="Wartungsart (z.B. Ölwechsel, Reifen, Reparatur)"
|
||||
value={neuArt}
|
||||
onChange={(e) => setNeuArt(e.target.value)}
|
||||
/>
|
||||
<label className="row" style={{ gap: "0.3rem" }}>
|
||||
<input type="checkbox" checked={neuExtern} onChange={(e) => setNeuExtern(e.target.checked)} />
|
||||
externe Werkstatt
|
||||
</label>
|
||||
{neuExtern && (
|
||||
<input
|
||||
className="input"
|
||||
placeholder="Werkstatt-Name"
|
||||
value={neuWerkstattName}
|
||||
onChange={(e) => setNeuWerkstattName(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
<button className="btn btn-primary" onClick={auftragAnlegen}>
|
||||
Auftrag anlegen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{offeneAuftraege.length === 0 && erledigteAuftraege.length === 0 && (
|
||||
<p className="text-muted">Keine Wartungsaufträge.</p>
|
||||
)}
|
||||
|
||||
{[...offeneAuftraege, ...erledigteAuftraege].length > 0 && (
|
||||
<ul className="card-list">
|
||||
{[...offeneAuftraege, ...erledigteAuftraege].map((a) => (
|
||||
<li key={a.id} style={{ padding: "0.4rem 0" }}>
|
||||
<div className="row-between">
|
||||
<span>
|
||||
{a.wartungsart}{" "}
|
||||
<span className="text-muted">
|
||||
({a.werkstatt_intern ? "intern" : a.werkstatt_name || "extern"})
|
||||
</span>
|
||||
</span>
|
||||
<span className={`badge ${STATUS_BADGE[a.status]}`}>{STATUS_LABEL[a.status]}</span>
|
||||
</div>
|
||||
{a.kosten && <div className="text-muted" style={{ fontSize: "0.85rem" }}>Kosten: {a.kosten} €</div>}
|
||||
{a.erledigt_am && (
|
||||
<div className="text-muted" style={{ fontSize: "0.85rem" }}>
|
||||
Erledigt am {new Date(a.erledigt_am).toLocaleDateString("de-DE")}
|
||||
</div>
|
||||
)}
|
||||
<div className="row" style={{ gap: "0.4rem", marginTop: "0.3rem", flexWrap: "wrap" }}>
|
||||
{istVerantwortlich && a.status === "offen" && (
|
||||
<button className="btn btn-secondary" onClick={() => statusAendern(a, "in_arbeit")}>
|
||||
In Arbeit
|
||||
</button>
|
||||
)}
|
||||
{istVerantwortlich && a.status !== "erledigt" && (
|
||||
<button className="btn btn-secondary" onClick={() => erledigen(a)}>
|
||||
Erledigt melden
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setDokumenteOffenId(dokumenteOffenId === a.id ? null : a.id)}
|
||||
>
|
||||
{dokumenteOffenId === a.id ? "Dokumente schließen" : "Dokumente/Rechnung"}
|
||||
</button>
|
||||
</div>
|
||||
{dokumenteOffenId === a.id && (
|
||||
<div style={{ marginTop: "0.5rem" }}>
|
||||
<DokumentePanel entitaetTyp="wartungsauftrag" entitaetId={a.id} onFehler={onFehler} />
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { ObjekttypSection } from "./admin/ObjekttypSection";
|
||||
import { QualifikationSection } from "./admin/QualifikationSection";
|
||||
import { StandortSection } from "./admin/StandortSection";
|
||||
import { VorlageSection } from "./admin/VorlageSection";
|
||||
import { WartungsplanSection } from "./admin/WartungsplanSection";
|
||||
import { ZustaendigkeitSection } from "./admin/ZustaendigkeitSection";
|
||||
|
||||
type Tab =
|
||||
@@ -39,6 +40,7 @@ type Tab =
|
||||
| "lagerbewegungen"
|
||||
| "lager"
|
||||
| "ausgabe"
|
||||
| "wartungsplaene"
|
||||
| "rollen";
|
||||
|
||||
// Deckt sich mit den require_roles()-Gates der jeweiligen Backend-Endpunkte
|
||||
@@ -76,6 +78,11 @@ const TABS: { key: Tab; label: string; rollen: ("administration" | "materialvera
|
||||
label: "Ausgabe/Rückgabe",
|
||||
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
|
||||
},
|
||||
{
|
||||
key: "wartungsplaene",
|
||||
label: "Wartungspläne",
|
||||
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
|
||||
},
|
||||
{ key: "rollen", label: "Rollen & Rechte", rollen: ["administration"] },
|
||||
];
|
||||
|
||||
@@ -247,6 +254,9 @@ export function AdminPage() {
|
||||
{aktiverTab === "ausgabe" && (
|
||||
<AusgabeSection materialien={materialien} benutzer={benutzer} onFehler={setFehler} />
|
||||
)}
|
||||
{aktiverTab === "wartungsplaene" && (
|
||||
<WartungsplanSection objekttypen={objekttypen} onFehler={setFehler} />
|
||||
)}
|
||||
{aktiverTab === "rollen" && <RolleSection benutzer={benutzer} onFehler={setFehler} />}
|
||||
{aktiverTab === "eskalation" && <EskalationSection onFehler={setFehler} />}
|
||||
{aktiverTab === "personal" && (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { apiRequest } from "../api/client";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import type { Akte } from "../api/types";
|
||||
import { DokumentePanel } from "../components/DokumentePanel";
|
||||
import { WartungSection } from "../components/WartungSection";
|
||||
import { ReadinessBadge } from "../components/status/ReadinessBadge";
|
||||
import { GRUND_TEXT } from "../components/status/gruendeText";
|
||||
import { EREIGNIS_LABEL } from "../components/status/ereignisText";
|
||||
@@ -15,8 +16,6 @@ const MANGEL_PRIORITAET_LABEL: Record<string, string> = {
|
||||
hoch: "Hoch",
|
||||
};
|
||||
|
||||
// FILE-006: EIN Screen für alle Teilbereiche einer Ressource statt mehrerer
|
||||
// Einzelseiten - konsumiert den FILE-001-Aggregations-Endpunkt.
|
||||
export function AktePage() {
|
||||
const { objektId } = useParams<{ objektId: string }>();
|
||||
const { istVerantwortlich } = useAuth();
|
||||
@@ -52,10 +51,11 @@ export function AktePage() {
|
||||
if (!akte) return null;
|
||||
|
||||
const { objekt } = akte;
|
||||
const istFahrzeug = akte.fahrzeugdetails !== null;
|
||||
|
||||
return (
|
||||
<main className="page">
|
||||
<div className="row-between" style={{ alignItems: "flex-start" }}>
|
||||
<main className={`page-wide akte-grid${istFahrzeug ? " ist-fahrzeug" : ""}`}>
|
||||
<div className="akte-kopf row-between" style={{ alignItems: "flex-start" }}>
|
||||
<div>
|
||||
<h1 style={{ marginBottom: "0.15rem" }}>{objekt.name}</h1>
|
||||
<div className="text-muted">
|
||||
@@ -65,7 +65,7 @@ export function AktePage() {
|
||||
<ReadinessBadge status={akte.einsatzbereitschaft_status} />
|
||||
</div>
|
||||
{akte.einsatzbereitschaft_gruende.length > 0 && (
|
||||
<ul className="card-list" style={{ marginTop: "0.5rem" }}>
|
||||
<ul className="akte-kopf card-list" style={{ marginTop: "0.5rem" }}>
|
||||
{akte.einsatzbereitschaft_gruende.map((g) => (
|
||||
<li key={g} className="text-muted" style={{ fontSize: "0.9rem" }}>
|
||||
• {GRUND_TEXT[g] ?? g}
|
||||
@@ -74,9 +74,7 @@ export function AktePage() {
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* nach dem QR-Scan (MOBILE-002) landet man hier - die
|
||||
Kernaktionen sollen direkt sichtbar sein, nicht erst am Seitenende. */}
|
||||
<div className="row" style={{ flexWrap: "wrap", marginTop: "1rem" }}>
|
||||
<div className="akte-aktionen row" style={{ flexWrap: "wrap" }}>
|
||||
<Link to={`/objekte/${objekt.id}/kontrolle`} className="btn btn-primary">
|
||||
Kontrolle starten
|
||||
</Link>
|
||||
@@ -85,7 +83,7 @@ export function AktePage() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginTop: "1rem" }}>
|
||||
<div className="akte-stamm card">
|
||||
<h3 style={{ marginTop: 0 }}>Stammdaten</h3>
|
||||
<div className="stack">
|
||||
<div>Standort: {akte.standort_name}</div>
|
||||
@@ -108,7 +106,40 @@ export function AktePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginTop: "1rem" }}>
|
||||
<div className="akte-beladung card">
|
||||
<h3 style={{ marginTop: 0 }}>Beladung</h3>
|
||||
{akte.beladung.length === 0 && <p className="text-muted">Keine Positionen zugeordnet.</p>}
|
||||
{akte.beladung.length > 0 && (
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Material</th>
|
||||
<th>Fach</th>
|
||||
<th>Soll</th>
|
||||
<th>Ist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{akte.beladung.map((b) => (
|
||||
<tr key={b.objektposition_id}>
|
||||
<td>{b.material_name}</td>
|
||||
<td>{b.fach ?? "–"}</td>
|
||||
<td>
|
||||
{b.sollmenge} {b.einheit}
|
||||
</td>
|
||||
<td className={Number(b.istmenge) < Number(b.sollmenge) ? "text-danger" : undefined}>
|
||||
{b.istmenge} {b.einheit}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="akte-verantwortlich card">
|
||||
<h3 style={{ marginTop: 0 }}>Verantwortlichkeit</h3>
|
||||
{akte.zustaendige_benutzer.length === 0 && akte.kontrollverantwortung.length === 0 && (
|
||||
<p className="text-muted">Keine Zuordnung hinterlegt.</p>
|
||||
@@ -124,7 +155,7 @@ export function AktePage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginTop: "1rem" }}>
|
||||
<div className="akte-pruefungen card">
|
||||
<h3 style={{ marginTop: 0 }}>Prüfungen (Geräte)</h3>
|
||||
{akte.geraete.length === 0 && <p className="text-muted">Keine Geräte-Instanzen erfasst.</p>}
|
||||
{akte.geraete.length > 0 && (
|
||||
@@ -149,7 +180,11 @@ export function AktePage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginTop: "1rem" }}>
|
||||
<div className="akte-wartung">
|
||||
<WartungSection objektId={objekt.id} onFehler={setFehler} />
|
||||
</div>
|
||||
|
||||
<div className="akte-maengel card">
|
||||
<h3 style={{ marginTop: 0 }}>Mängel</h3>
|
||||
{akte.maengel.length === 0 && <p className="text-muted">Keine Mängel gemeldet.</p>}
|
||||
{akte.maengel.length > 0 && (
|
||||
@@ -163,12 +198,12 @@ export function AktePage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginTop: "1rem" }}>
|
||||
<div className="akte-dokumente card">
|
||||
<h3 style={{ marginTop: 0 }}>Dokumente</h3>
|
||||
<DokumentePanel entitaetTyp="objekt" entitaetId={String(objekt.id)} onFehler={setFehler} />
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginTop: "1rem" }}>
|
||||
<div className="akte-historie card">
|
||||
<h3 style={{ marginTop: 0 }}>Historie</h3>
|
||||
{akte.historie.length === 0 && <p className="text-muted">Noch keine Änderungen protokolliert.</p>}
|
||||
{akte.historie.length > 0 && (
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { apiRequest } from "../../api/client";
|
||||
import type { Objekttyp, Wartungsplan } from "../../api/types";
|
||||
|
||||
interface Props {
|
||||
objekttypen: Objekttyp[];
|
||||
onFehler: (text: string) => void;
|
||||
}
|
||||
|
||||
interface NeueIntervallZeile {
|
||||
typ: "zeit" | "km" | "betriebsstunden";
|
||||
wert: string;
|
||||
}
|
||||
|
||||
export function WartungsplanSection({ objekttypen, onFehler }: Props) {
|
||||
const [plaene, setPlaene] = useState<Wartungsplan[]>([]);
|
||||
const [objekttypId, setObjekttypId] = useState<number | "">("");
|
||||
const [name, setName] = useState("");
|
||||
const [wartungsart, setWartungsart] = useState("");
|
||||
const [intervalle, setIntervalle] = useState<NeueIntervallZeile[]>([{ typ: "zeit", wert: "" }]);
|
||||
|
||||
async function laden() {
|
||||
try {
|
||||
setPlaene(await apiRequest<Wartungsplan[]>("/wartungsplaene"));
|
||||
} catch {
|
||||
onFehler("Wartungspläne konnten nicht geladen werden.");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
laden();
|
||||
}, []);
|
||||
|
||||
function objekttypName(id: number): string {
|
||||
return objekttypen.find((o) => o.id === id)?.name ?? `#${id}`;
|
||||
}
|
||||
|
||||
function intervallZeileAendern(index: number, feld: keyof NeueIntervallZeile, wert: string) {
|
||||
setIntervalle((zeilen) => zeilen.map((z, i) => (i === index ? { ...z, [feld]: wert } : z)));
|
||||
}
|
||||
|
||||
async function planAnlegen() {
|
||||
if (objekttypId === "" || !name || !wartungsart) {
|
||||
onFehler("Objekttyp, Name und mindestens eine Wartungsart angeben.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiRequest("/wartungsplaene", {
|
||||
method: "POST",
|
||||
body: {
|
||||
objekttyp_id: objekttypId,
|
||||
name,
|
||||
positionen: [
|
||||
{
|
||||
wartungsart,
|
||||
intervalle: intervalle
|
||||
.filter((z) => z.wert)
|
||||
.map((z) => ({ typ: z.typ, wert: Number(z.wert) })),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
setName("");
|
||||
setWartungsart("");
|
||||
setIntervalle([{ typ: "zeit", wert: "" }]);
|
||||
await laden();
|
||||
} catch {
|
||||
onFehler("Wartungsplan konnte nicht angelegt werden.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="section">
|
||||
<h2>Wartungspläne</h2>
|
||||
|
||||
<div className="card">
|
||||
<h3 style={{ marginTop: 0 }}>Neuer Plan (eine Wartungsart)</h3>
|
||||
<div className="stack">
|
||||
<select className="input" value={objekttypId} onChange={(e) => setObjekttypId(Number(e.target.value) || "")}>
|
||||
<option value="">Objekttyp wählen…</option>
|
||||
{objekttypen.map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{o.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input className="input" placeholder="Plan-Name (z.B. RTW Standard)" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
<input
|
||||
className="input"
|
||||
placeholder="Wartungsart (z.B. Ölwechsel)"
|
||||
value={wartungsart}
|
||||
onChange={(e) => setWartungsart(e.target.value)}
|
||||
/>
|
||||
{intervalle.map((zeile, i) => (
|
||||
<div className="row" key={i}>
|
||||
<select
|
||||
className="input"
|
||||
value={zeile.typ}
|
||||
onChange={(e) => intervallZeileAendern(i, "typ", e.target.value)}
|
||||
>
|
||||
<option value="zeit">Zeit (Monate)</option>
|
||||
<option value="km">Kilometer</option>
|
||||
<option value="betriebsstunden">Betriebsstunden</option>
|
||||
</select>
|
||||
<input
|
||||
className="input"
|
||||
type="number"
|
||||
placeholder="Wert"
|
||||
value={zeile.wert}
|
||||
onChange={(e) => intervallZeileAendern(i, "wert", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setIntervalle((z) => [...z, { typ: "zeit", wert: "" }])}
|
||||
>
|
||||
+ weiteres Intervall
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={planAnlegen}>
|
||||
Plan anlegen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginTop: "1rem" }}>
|
||||
<h3 style={{ marginTop: 0 }}>Bestehende Pläne</h3>
|
||||
{plaene.length === 0 && <p className="text-muted">Keine Wartungspläne angelegt.</p>}
|
||||
<ul className="card-list">
|
||||
{plaene.map((p) => (
|
||||
<li key={p.id}>
|
||||
<strong>{p.name}</strong> <span className="text-muted">({objekttypName(p.objekttyp_id)})</span>
|
||||
<ul>
|
||||
{p.positionen.map((pos) => (
|
||||
<li key={pos.id}>
|
||||
{pos.wartungsart}
|
||||
{pos.intervalle.length > 0 && (
|
||||
<span className="text-muted">
|
||||
{" "}
|
||||
– {pos.intervalle.map((i) => `${i.wert} ${i.typ}`).join(", ")}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -677,6 +677,59 @@ select.input:focus {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.akte-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"kopf"
|
||||
"aktionen"
|
||||
"stamm"
|
||||
"beladung"
|
||||
"wartung"
|
||||
"pruefungen"
|
||||
"verantwortlich"
|
||||
"maengel"
|
||||
"dokumente"
|
||||
"historie";
|
||||
}
|
||||
|
||||
.akte-kopf { grid-area: kopf; }
|
||||
.akte-aktionen { grid-area: aktionen; }
|
||||
.akte-stamm { grid-area: stamm; }
|
||||
.akte-beladung { grid-area: beladung; }
|
||||
.akte-wartung { grid-area: wartung; }
|
||||
.akte-pruefungen { grid-area: pruefungen; }
|
||||
.akte-verantwortlich { grid-area: verantwortlich; }
|
||||
.akte-maengel { grid-area: maengel; }
|
||||
.akte-dokumente { grid-area: dokumente; }
|
||||
.akte-historie { grid-area: historie; }
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.akte-grid {
|
||||
grid-template-columns: 2fr 1fr;
|
||||
grid-template-areas:
|
||||
"kopf kopf"
|
||||
"aktionen aktionen"
|
||||
"stamm wartung"
|
||||
"beladung pruefungen"
|
||||
"verantwortlich maengel"
|
||||
"dokumente dokumente"
|
||||
"historie historie";
|
||||
}
|
||||
|
||||
.akte-grid.ist-fahrzeug {
|
||||
grid-template-areas:
|
||||
"kopf kopf"
|
||||
"aktionen aktionen"
|
||||
"wartung stamm"
|
||||
"pruefungen beladung"
|
||||
"maengel verantwortlich"
|
||||
"dokumente dokumente"
|
||||
"historie historie";
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.split-layout {
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user