From c1f6eaa14c52fbf43b9f87ef15b43c4bc376385c Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 4 Sep 2026 20:22:32 +0200 Subject: [PATCH] Kontrollverantwortung im Frontend anbinden (Karte 01) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: DELETE /kontrollverantwortung/{id} ergänzt (fehlte bisher). Frontend: neuer Admin-Tab "Kontrollverantwortung" - Objekt + Benutzer/Gruppe zuordnen, Liste, Entfernen. Reine Sichtbarkeit/Priorisierung, keine Pflichtkontrolle. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV --- DEVLOG.md | 48 +++++ backend/DEVLOG.md | 16 ++ .../app/api/v1/endpoints/zustaendigkeit.py | 10 + backend/tests/test_zustaendigkeit.py | 45 +++++ frontend/src/api/types.ts | 7 + frontend/src/pages/AdminPage.tsx | 8 +- .../admin/KontrollverantwortungSection.tsx | 185 ++++++++++++++++++ 7 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/admin/KontrollverantwortungSection.tsx diff --git a/DEVLOG.md b/DEVLOG.md index 13d8feb..63eb2f3 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -3058,3 +3058,51 @@ Keine Commits in dieser Session. - frontend/src/pages/admin/EskalationSection.tsx | 127 +++++++++++++++++++++++++ --- +## 2026-09-04 20:13 – 20:13 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** backend + +### Commits +- fbae3fc Kontroll-Erfassung: Positionen nach Fach gruppieren + +### Geänderte Dateien +- DEVLOG.md | 30 +++++++++++ +- backend/DEVLOG.md | 15 ++++++ +- frontend/e2e/mockApi.ts | 10 ++++ +- frontend/src/pages/KontrollPage.tsx | 78 ++++++++++++++++++++-------- +- frontend/src/pages/kontrolle/types.ts | 4 ++ +- frontend/src/pages/kontrolle/useKontrolle.ts | 13 +++-- + +--- +## 2026-09-04 20:13 – 20:14 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 30 +++++++++++ +- backend/DEVLOG.md | 15 ++++++ +- frontend/e2e/mockApi.ts | 10 ++++ +- frontend/src/pages/KontrollPage.tsx | 78 ++++++++++++++++++++-------- +- frontend/src/pages/kontrolle/types.ts | 4 ++ +- frontend/src/pages/kontrolle/useKontrolle.ts | 13 +++-- + +--- +## 2026-09-04 20:16 – 20:17 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 30 +++++++++++ +- backend/DEVLOG.md | 15 ++++++ +- frontend/e2e/mockApi.ts | 10 ++++ +- frontend/src/pages/KontrollPage.tsx | 78 ++++++++++++++++++++-------- +- frontend/src/pages/kontrolle/types.ts | 4 ++ +- frontend/src/pages/kontrolle/useKontrolle.ts | 13 +++-- + +--- diff --git a/backend/DEVLOG.md b/backend/DEVLOG.md index 8910ac3..b6cd7a5 100644 --- a/backend/DEVLOG.md +++ b/backend/DEVLOG.md @@ -131,3 +131,19 @@ Keine Commits in dieser Session. - frontend/src/pages/admin/EskalationSection.tsx | 127 +++++++++++++++++++++++++ --- +## 2026-09-04 20:21 – 20:22 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 30 +++++++++++ +- backend/DEVLOG.md | 15 ++++++ +- frontend/e2e/mockApi.ts | 10 ++++ +- frontend/src/pages/KontrollPage.tsx | 78 ++++++++++++++++++++-------- +- frontend/src/pages/kontrolle/types.ts | 4 ++ +- frontend/src/pages/kontrolle/useKontrolle.ts | 13 +++-- + +--- diff --git a/backend/app/api/v1/endpoints/zustaendigkeit.py b/backend/app/api/v1/endpoints/zustaendigkeit.py index 80cb3a6..03b58ce 100644 --- a/backend/app/api/v1/endpoints/zustaendigkeit.py +++ b/backend/app/api/v1/endpoints/zustaendigkeit.py @@ -87,3 +87,13 @@ async def erstelle_kontrollverantwortung( db.add(kontrollverantwortung) await db.flush() return kontrollverantwortung + + +@router.delete("/kontrollverantwortung/{kontrollverantwortung_id}", status_code=status.HTTP_204_NO_CONTENT) +async def loesche_kontrollverantwortung( + kontrollverantwortung_id: uuid.UUID, db: AsyncSession = Depends(get_db), _=Depends(_verantwortliche) +) -> None: + kontrollverantwortung = await db.get(Kontrollverantwortung, kontrollverantwortung_id) + if kontrollverantwortung is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Kontrollverantwortung nicht gefunden") + await db.delete(kontrollverantwortung) diff --git a/backend/tests/test_zustaendigkeit.py b/backend/tests/test_zustaendigkeit.py index 50c8de4..613d489 100644 --- a/backend/tests/test_zustaendigkeit.py +++ b/backend/tests/test_zustaendigkeit.py @@ -78,3 +78,48 @@ async def test_zustaendigkeit_braucht_standort_oder_objekt(client, admin_user, m headers=auth_header(token), ) assert response.status_code == 422 + + +@pytest.mark.asyncio +async def test_kontrollverantwortung_anlegen_listen_loeschen( + client, standort_factory, objekt_factory, admin_user, mitarbeiter_user +): + standort = await standort_factory("Wache K") + objekt = await objekt_factory(name="Rucksack K", code="K1", standort=standort) + token = await login(client, "admin1") + + erstellt = await client.post( + "/api/v1/kontrollverantwortung", + json={"objekt_id": objekt.id, "benutzer_id": mitarbeiter_user.id}, + headers=auth_header(token), + ) + assert erstellt.status_code == 201 + eintrag_id = erstellt.json()["id"] + + liste = await client.get("/api/v1/kontrollverantwortung", headers=auth_header(token)) + assert liste.status_code == 200 + assert any(e["id"] == eintrag_id for e in liste.json()) + + geloescht = await client.delete( + f"/api/v1/kontrollverantwortung/{eintrag_id}", headers=auth_header(token) + ) + assert geloescht.status_code == 204 + + liste_danach = await client.get("/api/v1/kontrollverantwortung", headers=auth_header(token)) + assert all(e["id"] != eintrag_id for e in liste_danach.json()) + + +@pytest.mark.asyncio +async def test_kontrollverantwortung_braucht_benutzer_oder_gruppe( + client, standort_factory, objekt_factory, admin_user +): + standort = await standort_factory("Wache L") + objekt = await objekt_factory(name="Rucksack L", code="L1", standort=standort) + token = await login(client, "admin1") + + response = await client.post( + "/api/v1/kontrollverantwortung", + json={"objekt_id": objekt.id}, + headers=auth_header(token), + ) + assert response.status_code == 422 diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 6caba9e..bcf3175 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -105,6 +105,13 @@ export interface Fach { sortierung: number; } +export interface Kontrollverantwortung { + id: string; + objekt_id: number; + benutzer_id: number | null; + gruppe: string | null; +} + export interface EskalationKonfiguration { erinnerung_tage: number; leitung_tage: number; diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx index b5ef2d3..36a6f37 100644 --- a/frontend/src/pages/AdminPage.tsx +++ b/frontend/src/pages/AdminPage.tsx @@ -8,6 +8,7 @@ import { EskalationSection } from "./admin/EskalationSection"; import { FachSection } from "./admin/FachSection"; import { HistorieSection } from "./admin/HistorieSection"; import { KategorieSection } from "./admin/KategorieSection"; +import { KontrollverantwortungSection } from "./admin/KontrollverantwortungSection"; import { MaterialSection } from "./admin/MaterialSection"; import { ObjektSection } from "./admin/ObjektSection"; import { ObjekttypSection } from "./admin/ObjekttypSection"; @@ -22,7 +23,8 @@ type Tab = | "standorte" | "benutzer" | "historie" - | "eskalation"; + | "eskalation" + | "kontrollverantwortung"; const TABS: { key: Tab; label: string }[] = [ { key: "objekte", label: "Objekte" }, @@ -31,6 +33,7 @@ const TABS: { key: Tab; label: string }[] = [ { key: "struktur", label: "Struktur" }, { key: "standorte", label: "Standorte" }, { key: "benutzer", label: "Benutzer" }, + { key: "kontrollverantwortung", label: "Kontrollverantwortung" }, { key: "historie", label: "Änderungslog" }, { key: "eskalation", label: "Eskalation" }, ]; @@ -165,6 +168,9 @@ export function AdminPage() { {tab === "benutzer" && ( )} + {tab === "kontrollverantwortung" && ( + + )} {tab === "historie" && } {tab === "eskalation" && } diff --git a/frontend/src/pages/admin/KontrollverantwortungSection.tsx b/frontend/src/pages/admin/KontrollverantwortungSection.tsx new file mode 100644 index 0000000..55f32e4 --- /dev/null +++ b/frontend/src/pages/admin/KontrollverantwortungSection.tsx @@ -0,0 +1,185 @@ +import { useEffect, useState } from "react"; + +import { apiRequest } from "../../api/client"; +import type { Benutzer, Kontrollverantwortung, Objekt } from "../../api/types"; + +interface Props { + objekte: Objekt[]; + benutzer: Benutzer[]; + onFehler: (text: string) => void; +} + +/** Karte 01 / Sprintplan E3: reine Sichtbarkeit/Priorisierung ("wer soll dieses + * Objekt kontrollieren"), erzwingt keine Pflichtkontrolle - entweder ein + * konkreter Benutzer oder eine Gruppe (Freitext, z.B. "Frühschicht"). */ +export function KontrollverantwortungSection({ objekte, benutzer, onFehler }: Props) { + const [eintraege, setEintraege] = useState([]); + const [laedt, setLaedt] = useState(true); + const [objektId, setObjektId] = useState(""); + const [zuordnungsart, setZuordnungsart] = useState<"benutzer" | "gruppe">("benutzer"); + const [benutzerId, setBenutzerId] = useState(""); + const [gruppe, setGruppe] = useState(""); + const [wirdAngelegt, setWirdAngelegt] = useState(false); + + async function laden() { + setLaedt(true); + try { + const daten = await apiRequest("/kontrollverantwortung"); + setEintraege(daten); + } catch { + onFehler("Kontrollverantwortung konnte nicht geladen werden."); + } finally { + setLaedt(false); + } + } + + useEffect(() => { + laden(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + async function anlegen() { + if (objektId === "") { + onFehler("Objekt auswählen."); + return; + } + setWirdAngelegt(true); + try { + await apiRequest("/kontrollverantwortung", { + method: "POST", + body: { + objekt_id: objektId, + benutzer_id: zuordnungsart === "benutzer" ? benutzerId || null : null, + gruppe: zuordnungsart === "gruppe" ? gruppe : null, + }, + }); + setObjektId(""); + setBenutzerId(""); + setGruppe(""); + await laden(); + } catch { + onFehler("Kontrollverantwortung konnte nicht angelegt werden."); + } finally { + setWirdAngelegt(false); + } + } + + async function loeschen(id: string) { + try { + await apiRequest(`/kontrollverantwortung/${id}`, { method: "DELETE" }); + await laden(); + } catch { + onFehler("Kontrollverantwortung konnte nicht gelöscht werden."); + } + } + + function objektLabel(id: number): string { + const o = objekte.find((x) => x.id === id); + return o ? `${o.name} (${o.code})` : `Objekt ${id}`; + } + + function benutzerLabel(id: number | null): string | null { + if (id === null) return null; + return benutzer.find((b) => b.id === id)?.name ?? `Benutzer ${id}`; + } + + const kannAnlegen = + objektId !== "" && ((zuordnungsart === "benutzer" && benutzerId !== "") || (zuordnungsart === "gruppe" && gruppe)); + + return ( +
+

Kontrollverantwortung

+
+

+ Wer soll dieses Objekt kontrollieren - reine Sichtbarkeit/Priorisierung, keine + Pflichtkontrolle. +

+
+
+ + +
+
+ + +
+ {zuordnungsart === "benutzer" ? ( +
+ + +
+ ) : ( +
+ + setGruppe(e.target.value)} + placeholder="z. B. Frühschicht" + /> +
+ )} + +
+ + {laedt &&

Lade…

} + {!laedt && eintraege.length === 0 && ( +

Keine Zuordnungen.

+ )} + {!laedt && eintraege.length > 0 && ( +
    + {eintraege.map((e) => ( +
  • + + {objektLabel(e.objekt_id)} {" "} + {benutzerLabel(e.benutzer_id) ?? e.gruppe} + + +
  • + ))} +
+ )} +
+
+ ); +}