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
128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
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>
|
|
);
|
|
}
|