Admin-Portal: Eskalations-Konfiguration-UI (Karte 12)
CI / backend-tests (push) Successful in 1m12s
CI / frontend-build (push) Successful in 23s

Neuer Tab "Eskalation": Zeitschwellen (Erinnerung/Leitung, Tage) lesen/ändern
über GET/PUT /eskalation/konfiguration, "Jetzt prüfen"-Button stößt denselben
Lauf wie der tägliche systemd-Timer manuell an.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-04 20:04:08 +02:00
co-authored by Claude Sonnet 5
parent e949b5e9ac
commit 5805366306
5 changed files with 204 additions and 1 deletions
@@ -0,0 +1,127 @@
import { useEffect, useState } from "react";
import { apiRequest } from "../../api/client";
import type { EskalationKonfiguration } from "../../api/types";
interface Props {
onFehler: (text: string) => void;
}
/** Karte 12: Zeitschwellen für die Fehlbestand-Eskalation, zur Laufzeit
* änderbar (bewusst nicht fix im Code, siehe backend/app/services/eskalation.py).
* "Jetzt prüfen" stößt denselben Lauf an, den sonst der tägliche systemd-Timer
* macht - nützlich um eine Konfigurationsänderung sofort zu testen. */
export function EskalationSection({ onFehler }: Props) {
const [konfiguration, setKonfiguration] = useState<EskalationKonfiguration | null>(null);
const [erinnerungTage, setErinnerungTage] = useState("");
const [leitungTage, setLeitungTage] = useState("");
const [wirdGespeichert, setWirdGespeichert] = useState(false);
const [pruefErgebnis, setPruefErgebnis] = useState<string | null>(null);
const [wirdGeprueft, setWirdGeprueft] = useState(false);
async function laden() {
try {
const daten = await apiRequest<EskalationKonfiguration>("/eskalation/konfiguration");
setKonfiguration(daten);
setErinnerungTage(String(daten.erinnerung_tage));
setLeitungTage(String(daten.leitung_tage));
} catch {
onFehler("Eskalations-Konfiguration konnte nicht geladen werden.");
}
}
useEffect(() => {
laden();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function speichern() {
setWirdGespeichert(true);
try {
const daten = await apiRequest<EskalationKonfiguration>("/eskalation/konfiguration", {
method: "PUT",
body: { erinnerung_tage: Number(erinnerungTage), leitung_tage: Number(leitungTage) },
});
setKonfiguration(daten);
} catch {
onFehler("Eskalations-Konfiguration konnte nicht gespeichert werden.");
} finally {
setWirdGespeichert(false);
}
}
async function jetztPruefen() {
setWirdGeprueft(true);
setPruefErgebnis(null);
try {
const ergebnis = await apiRequest<{ erinnerungen: number; eskalationen: number }>(
"/eskalation/pruefen",
{ method: "POST" }
);
setPruefErgebnis(
`${ergebnis.erinnerungen} Erinnerung(en), ${ergebnis.eskalationen} Eskalation(en) verschickt.`
);
} catch {
onFehler("Prüflauf konnte nicht gestartet werden.");
} finally {
setWirdGeprueft(false);
}
}
if (!konfiguration) {
return (
<section className="section">
<h2>Eskalation</h2>
<p className="text-muted">Lade</p>
</section>
);
}
return (
<section className="section">
<h2>Eskalation offener Fehlbestände</h2>
<div className="card">
<p className="text-muted" style={{ marginBottom: "1rem" }}>
Läuft täglich automatisch (systemd-Timer). Zeitschwellen ab Entstehung des
Fehlbestands, in Tagen.
</p>
<div className="row" style={{ flexWrap: "wrap" }}>
<div className="field">
<label>Erinnerung an Materialverantwortliche nach (Tagen)</label>
<input
type="number"
min="1"
className="input"
value={erinnerungTage}
onChange={(e) => setErinnerungTage(e.target.value)}
/>
</div>
<div className="field">
<label>Eskalation an Leitung nach (Tagen)</label>
<input
type="number"
min="1"
className="input"
value={leitungTage}
onChange={(e) => setLeitungTage(e.target.value)}
/>
</div>
</div>
<button
className="btn btn-primary"
onClick={speichern}
disabled={!erinnerungTage || !leitungTage || wirdGespeichert}
>
Speichern
</button>
<div style={{ marginTop: "1.5rem" }}>
<button className="btn btn-secondary" onClick={jetztPruefen} disabled={wirdGeprueft}>
{wirdGeprueft ? "Prüft…" : "Jetzt prüfen"}
</button>
{pruefErgebnis && <p className="text-muted" style={{ marginTop: "0.5rem" }}>{pruefErgebnis}</p>}
</div>
</div>
</section>
);
}