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:
@@ -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 ++++++
|
||||
|
||||
---
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user