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:
@@ -3008,3 +3008,23 @@ Keine Commits in dieser Session.
|
|||||||
- ...07_indizes_und_unique_constraints.py => 0007_indizes_und_locks.py} | 4 ++--
|
- ...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 ++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -53,3 +53,43 @@ Keine Commits in dieser Session.
|
|||||||
- backend/tests/test_eskalation.py | 73 ++++++++++++++++
|
- 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;
|
sortierung: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EskalationKonfiguration {
|
||||||
|
erinnerung_tage: number;
|
||||||
|
leitung_tage: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HistorieEintrag {
|
export interface HistorieEintrag {
|
||||||
id: string;
|
id: string;
|
||||||
zeitpunkt: 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 type { Benutzer, Bereich, Beladungsvorlage, Fach, Kategorie, Material, Objekt, Objekttyp, Standort } from "../api/types";
|
||||||
import { BenutzerSection } from "./admin/BenutzerSection";
|
import { BenutzerSection } from "./admin/BenutzerSection";
|
||||||
import { BereichSection } from "./admin/BereichSection";
|
import { BereichSection } from "./admin/BereichSection";
|
||||||
|
import { EskalationSection } from "./admin/EskalationSection";
|
||||||
import { FachSection } from "./admin/FachSection";
|
import { FachSection } from "./admin/FachSection";
|
||||||
import { HistorieSection } from "./admin/HistorieSection";
|
import { HistorieSection } from "./admin/HistorieSection";
|
||||||
import { KategorieSection } from "./admin/KategorieSection";
|
import { KategorieSection } from "./admin/KategorieSection";
|
||||||
@@ -13,7 +14,15 @@ import { ObjekttypSection } from "./admin/ObjekttypSection";
|
|||||||
import { StandortSection } from "./admin/StandortSection";
|
import { StandortSection } from "./admin/StandortSection";
|
||||||
import { VorlageSection } from "./admin/VorlageSection";
|
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 }[] = [
|
const TABS: { key: Tab; label: string }[] = [
|
||||||
{ key: "objekte", label: "Objekte" },
|
{ key: "objekte", label: "Objekte" },
|
||||||
@@ -23,6 +32,7 @@ const TABS: { key: Tab; label: string }[] = [
|
|||||||
{ key: "standorte", label: "Standorte" },
|
{ key: "standorte", label: "Standorte" },
|
||||||
{ key: "benutzer", label: "Benutzer" },
|
{ key: "benutzer", label: "Benutzer" },
|
||||||
{ key: "historie", label: "Änderungslog" },
|
{ key: "historie", label: "Änderungslog" },
|
||||||
|
{ key: "eskalation", label: "Eskalation" },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,6 +166,7 @@ export function AdminPage() {
|
|||||||
<BenutzerSection benutzer={benutzer} onGeaendert={ladeAlles} onFehler={setFehler} />
|
<BenutzerSection benutzer={benutzer} onGeaendert={ladeAlles} onFehler={setFehler} />
|
||||||
)}
|
)}
|
||||||
{tab === "historie" && <HistorieSection onFehler={setFehler} />}
|
{tab === "historie" && <HistorieSection onFehler={setFehler} />}
|
||||||
|
{tab === "eskalation" && <EskalationSection onFehler={setFehler} />}
|
||||||
</main>
|
</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