Lagerort-Hierarchie (organisation/standort/gebaeude/raum/lager, self-referenzierend), Lagerplätze mit eindeutigem Code, Bestand je Lagerplatz+Material, Ein-/Auslagerung mit lückenlosem Materialbewegungsprotokoll (append-only, analog Lagerbewegung auf Objekt-Ebene, hier aber Material-/Lagerplatz-fein). - Migration 0019: 4 neue Tabellen (lagerort, lagerplatz, bestand, materialbewegung) - Backend: CRUD Lagerort/Lagerplatz, Bestandsabfrage, Ein-/Auslagern-Endpunkte, Negativbestand vorab abgefangen (kein DB-Constraint-Exception-Pfad, gleiches Muster wie objekte.py-Fix vom 2026-09-05) - Frontend: neuer Admin-Tab "Lager" (Lagerort/-platz-Verwaltung, Bestand+Buchung je Lagerplatz) - 2 neue Tests, 139 Tests weiterhin grün WH-005 (Umlagerung) und WH-006 (Inventur) bewusst zurückgestellt - Kern zuerst, laut Nutzer-Entscheidung. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
249 lines
10 KiB
TypeScript
249 lines
10 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
|
|
import { apiRequest } from "../api/client";
|
|
import { useAuth } from "../auth/AuthContext";
|
|
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";
|
|
import { KategorieSection } from "./admin/KategorieSection";
|
|
import { LagerbewegungSection } from "./admin/LagerbewegungSection";
|
|
import { LagerSection } from "./admin/LagerSection";
|
|
import { RolleSection } from "./admin/RolleSection";
|
|
import { KontrollverantwortungSection } from "./admin/KontrollverantwortungSection";
|
|
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";
|
|
|
|
type Tab =
|
|
| "objekte"
|
|
| "vorlagen"
|
|
| "material"
|
|
| "struktur"
|
|
| "standorte"
|
|
| "benutzer"
|
|
| "historie"
|
|
| "eskalation"
|
|
| "kontrollverantwortung"
|
|
| "zustaendigkeit"
|
|
| "personal"
|
|
| "lagerbewegungen"
|
|
| "lager"
|
|
| "rollen";
|
|
|
|
// Deckt sich mit den require_roles()-Gates der jeweiligen Backend-Endpunkte
|
|
// (Rollen-Vorgabe: Material-/Leitungsverantwortliche sahen bisher exakt
|
|
// dasselbe wie Mitarbeiter - hier je Tab, wer laut Backend tatsächlich
|
|
// etwas damit anfangen kann).
|
|
const TABS: { key: Tab; label: string; rollen: ("administration" | "materialverantwortlicher" | "leitungsverantwortlicher")[] }[] = [
|
|
{ key: "objekte", label: "Objekte", rollen: ["administration", "materialverantwortlicher"] },
|
|
{ key: "vorlagen", label: "Vorlagen", rollen: ["administration", "materialverantwortlicher"] },
|
|
{ key: "material", label: "Material", rollen: ["administration"] },
|
|
{ key: "struktur", label: "Struktur", rollen: ["administration"] },
|
|
{ key: "standorte", label: "Standorte", rollen: ["administration"] },
|
|
{ key: "benutzer", label: "Benutzer", rollen: ["administration"] },
|
|
{ key: "zustaendigkeit", label: "Zuständigkeit", rollen: ["administration"] },
|
|
{
|
|
key: "kontrollverantwortung",
|
|
label: "Kontrollverantwortung",
|
|
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
|
|
},
|
|
{
|
|
key: "historie",
|
|
label: "Änderungslog",
|
|
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
|
|
},
|
|
{ key: "eskalation", label: "Eskalation", rollen: ["administration"] },
|
|
{ key: "personal", label: "Personal", rollen: ["administration"] },
|
|
{
|
|
key: "lagerbewegungen",
|
|
label: "Lagerbewegungen",
|
|
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
|
|
},
|
|
{ key: "lager", label: "Lager", rollen: ["administration", "materialverantwortlicher"] },
|
|
{ key: "rollen", label: "Rollen & Rechte", rollen: ["administration"] },
|
|
];
|
|
|
|
/**
|
|
* Admin-Seite (Stammdaten/Objekt-Anlage), nachgezogen weil das Frontend
|
|
* bisher nur den Mitarbeiter-Kontrollflow (Sprint 7) abdeckte und
|
|
* Stammdaten/Objekte nur per API/Skript angelegt werden konnten.
|
|
*
|
|
* Jeder Abschnitt ist eine eigene Komponente unter pages/admin/ - hält diese
|
|
* Datei als reinen Orchestrator klein (siehe Projekt-Vorgabe: keine großen
|
|
* Monolith-Dateien).
|
|
*/
|
|
export function AdminPage() {
|
|
const { rollen, istAdmin } = useAuth();
|
|
const sichtbareTabs = TABS.filter((t) => t.rollen.some((r) => rollen.includes(r)));
|
|
const [bereiche, setBereiche] = useState<Bereich[]>([]);
|
|
const [kategorien, setKategorien] = useState<Kategorie[]>([]);
|
|
const [objekttypen, setObjekttypen] = useState<Objekttyp[]>([]);
|
|
const [standorte, setStandorte] = useState<Standort[]>([]);
|
|
const [vorlagen, setVorlagen] = useState<Beladungsvorlage[]>([]);
|
|
const [objekte, setObjekte] = useState<Objekt[]>([]);
|
|
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);
|
|
const aktiverTab = tab && sichtbareTabs.some((t) => t.key === tab) ? tab : sichtbareTabs[0]?.key ?? null;
|
|
|
|
async function ladeAlles() {
|
|
// /benutzer ist admin-only (Backend require_roles) - Promise.all würde bei
|
|
// Material-/Leitungsverantwortlichen mit 403 durchfallen und die komplette
|
|
// 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, ei, qt] = await Promise.allSettled([
|
|
apiRequest<Bereich[]>("/bereiche"),
|
|
apiRequest<Kategorie[]>("/kategorien"),
|
|
apiRequest<Objekttyp[]>("/objekttypen"),
|
|
apiRequest<Standort[]>("/standorte"),
|
|
apiRequest<Beladungsvorlage[]>("/vorlagen"),
|
|
apiRequest<Objekt[]>("/objekte"),
|
|
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));
|
|
setKategorien(wert(ka));
|
|
setObjekttypen(wert(ot));
|
|
setStandorte(wert(s));
|
|
setVorlagen(wert(v));
|
|
setObjekte(wert(o));
|
|
setMaterialien(wert(m));
|
|
setBenutzer(wert(b));
|
|
setFaecher(wert(f));
|
|
setEinheiten(wert(ei));
|
|
setQualifikationstypen(wert(qt));
|
|
}
|
|
|
|
useEffect(() => {
|
|
ladeAlles()
|
|
.catch(() => setFehler("Stammdaten konnten nicht geladen werden."))
|
|
.finally(() => setLaedt(false));
|
|
}, []);
|
|
|
|
if (laedt) {
|
|
return (
|
|
<main className="page page-wide">
|
|
<p className="text-muted">Lade…</p>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<main className="page page-wide">
|
|
<h1>{istAdmin ? "Administration" : "Verwaltung"}</h1>
|
|
{fehler && <div className="alert alert-danger">{fehler}</div>}
|
|
|
|
<nav className="row" style={{ marginBottom: "1.25rem", flexWrap: "wrap", gap: "0.5rem" }}>
|
|
{sichtbareTabs.map((t) => (
|
|
<button
|
|
key={t.key}
|
|
className={aktiverTab === t.key ? "btn btn-primary" : "btn btn-secondary"}
|
|
onClick={() => setTab(t.key)}
|
|
>
|
|
{t.label}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
|
|
{aktiverTab === "objekte" && (
|
|
<ObjektSection
|
|
objekte={objekte}
|
|
objekttypen={objekttypen}
|
|
vorlagen={vorlagen.filter((v) => v.status === "aktiv")}
|
|
standorte={standorte}
|
|
materialien={materialien}
|
|
nurPflegen={!istAdmin}
|
|
onGeaendert={ladeAlles}
|
|
onFehler={setFehler}
|
|
/>
|
|
)}
|
|
{aktiverTab === "vorlagen" && (
|
|
<VorlageSection
|
|
vorlagen={vorlagen}
|
|
objekttypen={objekttypen}
|
|
materialien={materialien}
|
|
faecher={faecher}
|
|
onGeaendert={ladeAlles}
|
|
onFehler={setFehler}
|
|
/>
|
|
)}
|
|
{aktiverTab === "material" && (
|
|
<MaterialSection materialien={materialien} onGeaendert={ladeAlles} onFehler={setFehler} />
|
|
)}
|
|
{aktiverTab === "struktur" && (
|
|
<>
|
|
<BereichSection bereiche={bereiche} onGeaendert={ladeAlles} onFehler={setFehler} />
|
|
<KategorieSection
|
|
bereiche={bereiche}
|
|
kategorien={kategorien}
|
|
onGeaendert={ladeAlles}
|
|
onFehler={setFehler}
|
|
/>
|
|
<ObjekttypSection
|
|
bereiche={bereiche}
|
|
kategorien={kategorien}
|
|
objekttypen={objekttypen}
|
|
onGeaendert={ladeAlles}
|
|
onFehler={setFehler}
|
|
/>
|
|
<FachSection
|
|
objekttypen={objekttypen}
|
|
faecher={faecher}
|
|
onGeaendert={ladeAlles}
|
|
onFehler={setFehler}
|
|
/>
|
|
</>
|
|
)}
|
|
{aktiverTab === "standorte" && (
|
|
<StandortSection standorte={standorte} onGeaendert={ladeAlles} onFehler={setFehler} />
|
|
)}
|
|
{aktiverTab === "benutzer" && (
|
|
<BenutzerSection benutzer={benutzer} einheiten={einheiten} onGeaendert={ladeAlles} onFehler={setFehler} />
|
|
)}
|
|
{aktiverTab === "zustaendigkeit" && (
|
|
<ZustaendigkeitSection standorte={standorte} objekte={objekte} benutzer={benutzer} onFehler={setFehler} />
|
|
)}
|
|
{aktiverTab === "kontrollverantwortung" && (
|
|
<KontrollverantwortungSection objekte={objekte} benutzer={benutzer} onFehler={setFehler} />
|
|
)}
|
|
{aktiverTab === "historie" && <HistorieSection onFehler={setFehler} />}
|
|
{aktiverTab === "lagerbewegungen" && (
|
|
<LagerbewegungSection objekte={objekte} standorte={standorte} onFehler={setFehler} />
|
|
)}
|
|
{aktiverTab === "lager" && <LagerSection materialien={materialien} onFehler={setFehler} />}
|
|
{aktiverTab === "rollen" && <RolleSection benutzer={benutzer} 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>
|
|
);
|
|
}
|