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
+20
View File
@@ -3008,3 +3008,23 @@ Keine Commits in dieser Session.
- ...07_indizes_und_unique_constraints.py => 0007_indizes_und_locks.py} | 4 ++--
---
## 2026-09-04 19:53 19:54 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** backend
### Commits
- e949b5e Materialänderungslog: Historie-Endpunkt + Admin-Tab
### Geänderte Dateien
- DEVLOG.md | 57 ++++++++++++++++
- backend/DEVLOG.md | 11 ++++
- backend/app/api/v1/api.py | 2 +
- backend/app/api/v1/endpoints/historie.py | 63 ++++++++++++++++++
- backend/app/schemas/historie.py | 18 +++++
- backend/tests/test_historie.py | 37 +++++++++++
- frontend/src/api/types.ts | 13 ++++
- frontend/src/pages/AdminPage.tsx | 5 +-
- frontend/src/pages/admin/HistorieSection.tsx | 98 ++++++++++++++++++++++++++++
- frontend/src/styles/global.css | 19 ++++++
---
+40
View File
@@ -53,3 +53,43 @@ Keine Commits in dieser Session.
- backend/tests/test_eskalation.py | 73 ++++++++++++++++
---
## 2026-09-04 19:54 19:54 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 57 ++++++++++++++++
- backend/DEVLOG.md | 11 ++++
- backend/app/api/v1/api.py | 2 +
- backend/app/api/v1/endpoints/historie.py | 63 ++++++++++++++++++
- backend/app/schemas/historie.py | 18 +++++
- backend/tests/test_historie.py | 37 +++++++++++
- frontend/src/api/types.ts | 13 ++++
- frontend/src/pages/AdminPage.tsx | 5 +-
- frontend/src/pages/admin/HistorieSection.tsx | 98 ++++++++++++++++++++++++++++
- frontend/src/styles/global.css | 19 ++++++
---
## 2026-09-04 19:55 19:55 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** frontend
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 57 ++++++++++++++++
- backend/DEVLOG.md | 11 ++++
- backend/app/api/v1/api.py | 2 +
- backend/app/api/v1/endpoints/historie.py | 63 ++++++++++++++++++
- backend/app/schemas/historie.py | 18 +++++
- backend/tests/test_historie.py | 37 +++++++++++
- frontend/src/api/types.ts | 13 ++++
- frontend/src/pages/AdminPage.tsx | 5 +-
- frontend/src/pages/admin/HistorieSection.tsx | 98 ++++++++++++++++++++++++++++
- frontend/src/styles/global.css | 19 ++++++
---
+5
View File
@@ -105,6 +105,11 @@ export interface Fach {
sortierung: number;
}
export interface EskalationKonfiguration {
erinnerung_tage: number;
leitung_tage: number;
}
export interface HistorieEintrag {
id: string;
zeitpunkt: string;
+12 -1
View File
@@ -4,6 +4,7 @@ import { apiRequest } from "../api/client";
import type { Benutzer, Bereich, Beladungsvorlage, Fach, Kategorie, Material, Objekt, Objekttyp, Standort } from "../api/types";
import { BenutzerSection } from "./admin/BenutzerSection";
import { BereichSection } from "./admin/BereichSection";
import { EskalationSection } from "./admin/EskalationSection";
import { FachSection } from "./admin/FachSection";
import { HistorieSection } from "./admin/HistorieSection";
import { KategorieSection } from "./admin/KategorieSection";
@@ -13,7 +14,15 @@ import { ObjekttypSection } from "./admin/ObjekttypSection";
import { StandortSection } from "./admin/StandortSection";
import { VorlageSection } from "./admin/VorlageSection";
type Tab = "objekte" | "vorlagen" | "material" | "struktur" | "standorte" | "benutzer" | "historie";
type Tab =
| "objekte"
| "vorlagen"
| "material"
| "struktur"
| "standorte"
| "benutzer"
| "historie"
| "eskalation";
const TABS: { key: Tab; label: string }[] = [
{ key: "objekte", label: "Objekte" },
@@ -23,6 +32,7 @@ const TABS: { key: Tab; label: string }[] = [
{ key: "standorte", label: "Standorte" },
{ key: "benutzer", label: "Benutzer" },
{ key: "historie", label: "Änderungslog" },
{ key: "eskalation", label: "Eskalation" },
];
/**
@@ -156,6 +166,7 @@ export function AdminPage() {
<BenutzerSection benutzer={benutzer} onGeaendert={ladeAlles} onFehler={setFehler} />
)}
{tab === "historie" && <HistorieSection onFehler={setFehler} />}
{tab === "eskalation" && <EskalationSection onFehler={setFehler} />}
</main>
);
}
@@ -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>
);
}