feat(mangel): Mangel-Ticket-System (Roadmap Phase 2)
Eigener Workflow für Schäden/Defekte, getrennt von Fehlbestand (Mengen- Abweichung Soll/Ist). Status: neu → in_bearbeitung → ersatzteil_bestellt/ reparatur_geplant → erledigt. entitaet_typ/entitaet_id optional (Objektposition oder Geräteinstanz), analog zum bestehenden historie.entitaet_typ-Muster, objekt_id immer gesetzt für Dashboard-Abfragen. Ein offener Mangel mit prioritaet=hoch macht das Objekt "nicht einsatzbereit" (neuer Grund mangel_kritisch_offen in dashboard.einsatzbereitschaft) - niedrige/normale Mängel blockieren nicht. Melden/Lesen: alle Mitarbeiter+. Status ändern: Materialverantwortliche+. Neue Seite /maengel für alle eingeloggten Nutzer (Nav-Link "Mängel"). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
This commit is contained in:
@@ -8,6 +8,7 @@ import { DashboardPage } from "./pages/DashboardPage";
|
||||
import { FehlbestandListePage } from "./pages/FehlbestandListePage";
|
||||
import { KontrollPage } from "./pages/KontrollPage";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
import { MangelListePage } from "./pages/MangelListePage";
|
||||
import { ObjektListPage } from "./pages/ObjektListPage";
|
||||
|
||||
function GeschuetzteRoute({ children }: { children: ReactNode }) {
|
||||
@@ -61,6 +62,14 @@ export default function App() {
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/maengel"
|
||||
element={
|
||||
<GeschuetzteRoute>
|
||||
<MangelListePage />
|
||||
</GeschuetzteRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/fehlbestaende"
|
||||
element={
|
||||
|
||||
@@ -45,6 +45,23 @@ export interface Kontrolle {
|
||||
abbruch_grund: string | null;
|
||||
}
|
||||
|
||||
export type MangelPrioritaet = "niedrig" | "normal" | "hoch";
|
||||
export type MangelStatus = "neu" | "in_bearbeitung" | "ersatzteil_bestellt" | "reparatur_geplant" | "erledigt";
|
||||
|
||||
export interface Mangel {
|
||||
id: string;
|
||||
objekt_id: number;
|
||||
entitaet_typ: string | null;
|
||||
entitaet_id: string | null;
|
||||
beschreibung: string;
|
||||
prioritaet: MangelPrioritaet;
|
||||
status: MangelStatus;
|
||||
gemeldet_von: number;
|
||||
gemeldet_am: string;
|
||||
erledigt_von: number | null;
|
||||
erledigt_am: string | null;
|
||||
}
|
||||
|
||||
export interface Fehlbestand {
|
||||
id: string;
|
||||
objekt_id: number;
|
||||
|
||||
@@ -23,6 +23,9 @@ export function AppShell({ children }: { children: ReactNode }) {
|
||||
<NavLink to="/objekte" className={({ isActive }) => (isActive ? "active" : "")}>
|
||||
Objekte
|
||||
</NavLink>
|
||||
<NavLink to="/maengel" className={({ isActive }) => (isActive ? "active" : "")}>
|
||||
Mängel
|
||||
</NavLink>
|
||||
{istVerantwortlich && (
|
||||
<NavLink to="/dashboard" className={({ isActive }) => (isActive ? "active" : "")}>
|
||||
Dashboard
|
||||
|
||||
@@ -41,6 +41,7 @@ const GRUND_TEXT: Record<string, string> = {
|
||||
ablaufdatum_abgelaufen: "Ablaufdatum überschritten",
|
||||
pruefung_ueberfaellig: "Geräteprüfung überfällig",
|
||||
kontrolle_laeuft: "Kontrolle läuft (gesperrt)",
|
||||
mangel_kritisch_offen: "Kritischer Mangel offen",
|
||||
mindermenge_genehmigt: "Mindermenge genehmigt",
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { apiRequest } from "../api/client";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import type { Mangel, MangelPrioritaet, MangelStatus, Objekt } from "../api/types";
|
||||
|
||||
const STATUS_LABEL: Record<MangelStatus, string> = {
|
||||
neu: "Neu",
|
||||
in_bearbeitung: "In Bearbeitung",
|
||||
ersatzteil_bestellt: "Ersatzteil bestellt",
|
||||
reparatur_geplant: "Reparatur geplant",
|
||||
erledigt: "Erledigt",
|
||||
};
|
||||
|
||||
const STATUS_OPTIONEN: MangelStatus[] = [
|
||||
"neu",
|
||||
"in_bearbeitung",
|
||||
"ersatzteil_bestellt",
|
||||
"reparatur_geplant",
|
||||
"erledigt",
|
||||
];
|
||||
|
||||
// Mangel-Ticket-System (Roadmap Phase 2): Schäden/Defekte melden und bis zur
|
||||
// Erledigung verfolgen - jeder Mitarbeiter darf melden/lesen, Status ändern
|
||||
// bleibt Materialverantwortlichen+ vorbehalten (Backend erzwingt das).
|
||||
export function MangelListePage() {
|
||||
const { istMaterialverantwortlich, istAdmin } = useAuth();
|
||||
const darfStatusAendern = istMaterialverantwortlich || istAdmin;
|
||||
|
||||
const [maengel, setMaengel] = useState<Mangel[]>([]);
|
||||
const [objekte, setObjekte] = useState<Objekt[]>([]);
|
||||
const [laedt, setLaedt] = useState(true);
|
||||
const [fehler, setFehler] = useState<string | null>(null);
|
||||
const [nurOffene, setNurOffene] = useState(true);
|
||||
|
||||
const [objektId, setObjektId] = useState<number | "">("");
|
||||
const [beschreibung, setBeschreibung] = useState("");
|
||||
const [prioritaet, setPrioritaet] = useState<MangelPrioritaet>("normal");
|
||||
const [wirdGemeldet, setWirdGemeldet] = useState(false);
|
||||
|
||||
async function laden() {
|
||||
setLaedt(true);
|
||||
try {
|
||||
const [m, o] = await Promise.all([
|
||||
apiRequest<Mangel[]>("/maengel"),
|
||||
apiRequest<Objekt[]>("/objekte"),
|
||||
]);
|
||||
setMaengel(m);
|
||||
setObjekte(o);
|
||||
} catch {
|
||||
setFehler("Mängel konnten nicht geladen werden.");
|
||||
} finally {
|
||||
setLaedt(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
laden();
|
||||
}, []);
|
||||
|
||||
function objektName(id: number): string {
|
||||
const o = objekte.find((x) => x.id === id);
|
||||
return o ? `${o.name} (${o.code})` : `#${id}`;
|
||||
}
|
||||
|
||||
async function melden() {
|
||||
if (objektId === "" || !beschreibung) {
|
||||
setFehler("Objekt und Beschreibung angeben.");
|
||||
return;
|
||||
}
|
||||
setWirdGemeldet(true);
|
||||
try {
|
||||
await apiRequest("/maengel", {
|
||||
method: "POST",
|
||||
body: { objekt_id: objektId, beschreibung, prioritaet },
|
||||
});
|
||||
setBeschreibung("");
|
||||
setPrioritaet("normal");
|
||||
await laden();
|
||||
} catch {
|
||||
setFehler("Mangel konnte nicht gemeldet werden.");
|
||||
} finally {
|
||||
setWirdGemeldet(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function statusAendern(mangel: Mangel, status: MangelStatus) {
|
||||
try {
|
||||
await apiRequest(`/maengel/${mangel.id}`, { method: "PATCH", body: { status } });
|
||||
await laden();
|
||||
} catch {
|
||||
setFehler("Status konnte nicht geändert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
const angezeigt = nurOffene ? maengel.filter((m) => m.status !== "erledigt") : maengel;
|
||||
|
||||
return (
|
||||
<main className="page">
|
||||
<h1>Mängel</h1>
|
||||
{fehler && <div className="alert alert-danger">{fehler}</div>}
|
||||
|
||||
<div className="card" style={{ marginBottom: "1.25rem" }}>
|
||||
<h3 style={{ marginTop: 0 }}>Mangel melden</h3>
|
||||
<div className="stack">
|
||||
<select className="input" value={objektId} onChange={(e) => setObjektId(Number(e.target.value) || "")}>
|
||||
<option value="">Objekt wählen…</option>
|
||||
{objekte.map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{o.name} ({o.code})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
className="input"
|
||||
value={beschreibung}
|
||||
onChange={(e) => setBeschreibung(e.target.value)}
|
||||
placeholder="Beschreibung (z.B. Blaulicht vorne rechts defekt)"
|
||||
/>
|
||||
<select className="input" value={prioritaet} onChange={(e) => setPrioritaet(e.target.value as MangelPrioritaet)}>
|
||||
<option value="niedrig">Niedrig</option>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="hoch">Hoch (blockiert Einsatzbereitschaft)</option>
|
||||
</select>
|
||||
<button className="btn btn-primary" onClick={melden} disabled={wirdGemeldet}>
|
||||
Melden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="row" style={{ alignItems: "center", gap: "0.4rem", marginBottom: "1rem" }}>
|
||||
<input type="checkbox" checked={nurOffene} onChange={(e) => setNurOffene(e.target.checked)} />
|
||||
Nur offene Mängel anzeigen
|
||||
</label>
|
||||
|
||||
{laedt && <p className="text-muted">Lade…</p>}
|
||||
{!laedt && angezeigt.length === 0 && <p className="text-muted">Keine Mängel.</p>}
|
||||
<ul className="card-list">
|
||||
{angezeigt.map((m) => (
|
||||
<li key={m.id} className="card" style={{ marginBottom: "0.75rem" }}>
|
||||
<div className="row-between">
|
||||
<span>
|
||||
<strong>{objektName(m.objekt_id)}</strong>
|
||||
{m.prioritaet === "hoch" && <span style={{ color: "#b00020" }}> ⚠ hoch</span>}
|
||||
</span>
|
||||
<span className="badge badge-neutral">{STATUS_LABEL[m.status]}</span>
|
||||
</div>
|
||||
<div style={{ margin: "0.4rem 0" }}>{m.beschreibung}</div>
|
||||
{darfStatusAendern && m.status !== "erledigt" && (
|
||||
<select
|
||||
className="input"
|
||||
style={{ maxWidth: "16rem" }}
|
||||
value={m.status}
|
||||
onChange={(e) => statusAendern(m, e.target.value as MangelStatus)}
|
||||
>
|
||||
{STATUS_OPTIONEN.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{STATUS_LABEL[s]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user