feat(personal): Personal/Qualifikationen-Modul (Roadmap Phase 1)
CI / backend-tests (push) Failing after 1m32s
CI / frontend-build (push) Successful in 17s

Neues Modul für die BOS-Ressourcenplattform-Erweiterung: Einheit (Zug/Gruppe,
self-referenzierend), Qualifikationstyp (Führerschein/Lehrgang/Berechtigung),
BenutzerQualifikation (mit Gültigkeit) und ObjekttypQualifikationsanforderung
(M:N) - beantwortet "wer darf dieses Fahrzeug fahren?" über
GET /objekte/{id}/berechtigung/{benutzer_id}.

Benutzer und Objekt bekommen optionale einheit_id (additiv, analog
fahrzeug_id-Muster). Nebenbei Bugfix: PATCH /benutzer konnte einheit_id nicht
auf null setzen (Feld-vorhanden-Check via model_fields_set statt is not None).

Frontend: neuer Admin-Tab "Personal" (Einheiten, Qualifikationstypen,
Benutzer-Qualifikationszuordnung), Einheit-Auswahl im Benutzer-Formular.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
This commit is contained in:
2026-09-05 11:13:37 +02:00
co-authored by Claude Sonnet 5
parent c3f141f7b0
commit d71d95e17f
17 changed files with 1167 additions and 8 deletions
+25 -4
View File
@@ -2,9 +2,10 @@ import { useEffect, useState } from "react";
import { apiRequest } from "../api/client";
import { useAuth } from "../auth/AuthContext";
import type { Benutzer, Bereich, Beladungsvorlage, Fach, Kategorie, Material, Objekt, Objekttyp, Standort } from "../api/types";
import type { Benutzer, Bereich, Beladungsvorlage, Einheit, Fach, Kategorie, Material, Objekt, Objekttyp, Qualifikationstyp, Standort } from "../api/types";
import { BenutzerSection } from "./admin/BenutzerSection";
import { BereichSection } from "./admin/BereichSection";
import { EinheitSection } from "./admin/EinheitSection";
import { EskalationSection } from "./admin/EskalationSection";
import { FachSection } from "./admin/FachSection";
import { HistorieSection } from "./admin/HistorieSection";
@@ -13,6 +14,7 @@ import { KontrollverantwortungSection } from "./admin/KontrollverantwortungSecti
import { MaterialSection } from "./admin/MaterialSection";
import { ObjektSection } from "./admin/ObjektSection";
import { ObjekttypSection } from "./admin/ObjekttypSection";
import { QualifikationSection } from "./admin/QualifikationSection";
import { StandortSection } from "./admin/StandortSection";
import { VorlageSection } from "./admin/VorlageSection";
import { ZustaendigkeitSection } from "./admin/ZustaendigkeitSection";
@@ -27,7 +29,8 @@ type Tab =
| "historie"
| "eskalation"
| "kontrollverantwortung"
| "zustaendigkeit";
| "zustaendigkeit"
| "personal";
// Deckt sich mit den require_roles()-Gates der jeweiligen Backend-Endpunkte
// (Rollen-Vorgabe: Material-/Leitungsverantwortliche sahen bisher exakt
@@ -52,6 +55,7 @@ const TABS: { key: Tab; label: string; rollen: ("administration" | "materialvera
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
},
{ key: "eskalation", label: "Eskalation", rollen: ["administration"] },
{ key: "personal", label: "Personal", rollen: ["administration"] },
];
/**
@@ -75,6 +79,8 @@ export function AdminPage() {
const [materialien, setMaterialien] = useState<Material[]>([]);
const [benutzer, setBenutzer] = useState<Benutzer[]>([]);
const [faecher, setFaecher] = useState<Fach[]>([]);
const [einheiten, setEinheiten] = useState<Einheit[]>([]);
const [qualifikationstypen, setQualifikationstypen] = useState<Qualifikationstyp[]>([]);
const [fehler, setFehler] = useState<string | null>(null);
const [laedt, setLaedt] = useState(true);
const [tab, setTab] = useState<Tab | null>(null);
@@ -86,7 +92,7 @@ export function AdminPage() {
// Seite als "nicht geladen" zeigen, obwohl deren Tabs (z.B. Kontroll-
// verantwortung) trotzdem funktionieren sollen. allSettled + leerer
// Fallback statt hartem Fehler.
const [be, ka, ot, s, v, o, m, b, f] = await Promise.allSettled([
const [be, ka, ot, s, v, o, m, b, f, ei, qt] = await Promise.allSettled([
apiRequest<Bereich[]>("/bereiche"),
apiRequest<Kategorie[]>("/kategorien"),
apiRequest<Objekttyp[]>("/objekttypen"),
@@ -96,6 +102,8 @@ export function AdminPage() {
apiRequest<Material[]>("/materialien"),
apiRequest<Benutzer[]>("/benutzer"),
apiRequest<Fach[]>("/faecher"),
apiRequest<Einheit[]>("/einheiten"),
apiRequest<Qualifikationstyp[]>("/qualifikationstypen"),
]);
const wert = <T,>(r: PromiseSettledResult<T[]>): T[] => (r.status === "fulfilled" ? r.value : []);
setBereiche(wert(be));
@@ -107,6 +115,8 @@ export function AdminPage() {
setMaterialien(wert(m));
setBenutzer(wert(b));
setFaecher(wert(f));
setEinheiten(wert(ei));
setQualifikationstypen(wert(qt));
}
useEffect(() => {
@@ -193,7 +203,7 @@ export function AdminPage() {
<StandortSection standorte={standorte} onGeaendert={ladeAlles} onFehler={setFehler} />
)}
{aktiverTab === "benutzer" && (
<BenutzerSection benutzer={benutzer} onGeaendert={ladeAlles} onFehler={setFehler} />
<BenutzerSection benutzer={benutzer} einheiten={einheiten} onGeaendert={ladeAlles} onFehler={setFehler} />
)}
{aktiverTab === "zustaendigkeit" && (
<ZustaendigkeitSection standorte={standorte} objekte={objekte} benutzer={benutzer} onFehler={setFehler} />
@@ -203,6 +213,17 @@ export function AdminPage() {
)}
{aktiverTab === "historie" && <HistorieSection onFehler={setFehler} />}
{aktiverTab === "eskalation" && <EskalationSection onFehler={setFehler} />}
{aktiverTab === "personal" && (
<>
<EinheitSection einheiten={einheiten} standorte={standorte} onGeaendert={ladeAlles} onFehler={setFehler} />
<QualifikationSection
qualifikationstypen={qualifikationstypen}
benutzer={benutzer}
onGeaendert={ladeAlles}
onFehler={setFehler}
/>
</>
)}
{aktiverTab === null && <p className="text-muted">Keine Bereiche für deine Rolle verfügbar.</p>}
</main>
);