Admin-Portal: Eskalations-Konfiguration-UI (Karte 12)
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user