refactor(admin): Menüstruktur in thematische Gruppen umgebaut
CI / backend-tests (push) Successful in 3m18s
CI / frontend-build (push) Successful in 21s

Admin-Tabs (25 Stück) waren eine flache Liste. Jetzt gruppiert nach
Stammdaten, Personal & Rechte, Betrieb, Wartung & Prüfung, Einsätze &
Meldungen, System. Mobile-Sonderfall (Sheet/istDesktop) entfernt,
bestehende split-layout-CSS übernimmt das responsive Verhalten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
patrickandClaude Sonnet 5 committed 2026-09-10 23:02:16 +02:00
1 parent e39d704de8
commit 6e9c131cbc
1 file changed
+118 -88
+118 -88
View File
@@ -4,8 +4,6 @@ import { useSearchParams } from "react-router-dom";
import { apiRequest } from "../api/client";
import { AusgabeSection } from "./admin/AusgabeSection";
import { useAuth } from "../auth/AuthContext";
import { useIsDesktop } from "../hooks/useIsDesktop";
import { Sheet } from "../components/ui/sheet";
import type { Benutzer, Bereich, Beladungsvorlage, Einheit, Einsatzfunktion, Fach, Kategorie, Material, Nummernschema, Objekt, Objekttyp, Person, Qualifikationstyp, Standort } from "../api/types";
import { BenutzerSection } from "./admin/BenutzerSection";
import { BereichSection } from "./admin/BereichSection";
@@ -62,63 +60,103 @@ type Tab =
| "fehlerTracking"
| "pruefarten";
type Gruppe = "stammdaten" | "personalUndRechte" | "betrieb" | "wartungUndPruefung" | "einsaetzeUndMeldungen" | "system";
// Nutzer-Vorgabe 2026-09-10: 25 flache Tabs nebeneinander waren unübersichtlich
// geworden - Gruppierung in thematische Bereiche, Reihenfolge/Label der
// Gruppen siehe GRUPPEN_LABEL unten.
const GRUPPEN_REIHENFOLGE: Gruppe[] = [
"stammdaten",
"personalUndRechte",
"betrieb",
"wartungUndPruefung",
"einsaetzeUndMeldungen",
"system",
];
const GRUPPEN_LABEL: Record<Gruppe, string> = {
stammdaten: "Stammdaten",
personalUndRechte: "Personal & Rechte",
betrieb: "Betrieb",
wartungUndPruefung: "Wartung & Prüfung",
einsaetzeUndMeldungen: "Einsätze & Meldungen",
system: "System",
};
// 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: "ausgabe",
label: "Ausgabe/Rückgabe",
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
},
{
key: "wartungsplaene",
label: "Wartungspläne",
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
},
{ key: "rollen", label: "Rollen & Rechte", rollen: ["administration"] },
{ key: "readinessRegeln", label: "Bereitschaftsregeln", rollen: ["administration"] },
{ key: "einsaetze", label: "Einsätze", rollen: ["administration", "leitungsverantwortlicher"] },
{ key: "materialSets", label: "Material-Sets", rollen: ["administration", "materialverantwortlicher"] },
{ key: "verleih", label: "Verleih", rollen: ["administration", "materialverantwortlicher"] },
{
key: "sollmengenGruppen",
label: "Sollmengen-Gruppen",
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
},
{ key: "inventur", label: "Inventur", rollen: ["administration", "materialverantwortlicher"] },
{ key: "fehlerTracking", label: "Fehler-Tracking", rollen: ["administration"] },
const TABS: {
key: Tab;
label: string;
gruppe: Gruppe;
rollen: ("administration" | "materialverantwortlicher" | "leitungsverantwortlicher")[];
}[] = [
{ key: "objekte", label: "Objekte", gruppe: "stammdaten", rollen: ["administration", "materialverantwortlicher"] },
{ key: "vorlagen", label: "Vorlagen", gruppe: "stammdaten", rollen: ["administration", "materialverantwortlicher"] },
{ key: "material", label: "Material", gruppe: "stammdaten", rollen: ["administration"] },
{ key: "struktur", label: "Struktur", gruppe: "stammdaten", rollen: ["administration"] },
{ key: "standorte", label: "Standorte", gruppe: "stammdaten", rollen: ["administration"] },
{ key: "materialSets", label: "Material-Sets", gruppe: "stammdaten", rollen: ["administration", "materialverantwortlicher"] },
{
key: "pruefarten",
label: "Prüfarten",
gruppe: "stammdaten",
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
},
{ key: "benutzer", label: "Benutzer", gruppe: "personalUndRechte", rollen: ["administration"] },
{ key: "personal", label: "Personal", gruppe: "personalUndRechte", rollen: ["administration"] },
{ key: "zustaendigkeit", label: "Zuständigkeit", gruppe: "personalUndRechte", rollen: ["administration"] },
{
key: "kontrollverantwortung",
label: "Kontrollverantwortung",
gruppe: "personalUndRechte",
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
},
{ key: "rollen", label: "Rollen & Rechte", gruppe: "personalUndRechte", rollen: ["administration"] },
{
key: "lagerbewegungen",
label: "Lagerbewegungen",
gruppe: "betrieb",
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
},
{ key: "lager", label: "Lager", gruppe: "betrieb", rollen: ["administration", "materialverantwortlicher"] },
{
key: "ausgabe",
label: "Ausgabe/Rückgabe",
gruppe: "betrieb",
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
},
{ key: "inventur", label: "Inventur", gruppe: "betrieb", rollen: ["administration", "materialverantwortlicher"] },
{ key: "verleih", label: "Verleih", gruppe: "betrieb", rollen: ["administration", "materialverantwortlicher"] },
{
key: "sollmengenGruppen",
label: "Sollmengen-Gruppen",
gruppe: "betrieb",
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
},
{
key: "wartungsplaene",
label: "Wartungspläne",
gruppe: "wartungUndPruefung",
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
},
{ key: "readinessRegeln", label: "Bereitschaftsregeln", gruppe: "wartungUndPruefung", rollen: ["administration"] },
{ key: "einsaetze", label: "Einsätze", gruppe: "einsaetzeUndMeldungen", rollen: ["administration", "leitungsverantwortlicher"] },
{ key: "eskalation", label: "Eskalation", gruppe: "einsaetzeUndMeldungen", rollen: ["administration"] },
{
key: "historie",
label: "Änderungslog",
gruppe: "system",
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
},
{ key: "fehlerTracking", label: "Fehler-Tracking", gruppe: "system", rollen: ["administration"] },
];
/**
@@ -132,8 +170,6 @@ const TABS: { key: Tab; label: string; rollen: ("administration" | "materialvera
*/
export function AdminPage() {
const { rollen, istAdmin } = useAuth();
const istDesktop = useIsDesktop();
const [menuOffen, setMenuOffen] = useState(false);
const sichtbareTabs = TABS.filter((t) => t.rollen.some((r) => rollen.includes(r)));
const [bereiche, setBereiche] = useState<Bereich[]>([]);
const [kategorien, setKategorien] = useState<Kategorie[]>([]);
@@ -219,42 +255,34 @@ export function AdminPage() {
<h1>{istAdmin ? "Administration" : "Verwaltung"}</h1>
{fehler && <div className="alert alert-danger">{fehler}</div>}
{istDesktop ? (
<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>
) : (
<div style={{ marginBottom: "1.25rem" }}>
<button className="btn btn-secondary" onClick={() => setMenuOffen(true)}>
Menü: {sichtbareTabs.find((t) => t.key === aktiverTab)?.label ?? "Wählen…"}
</button>
<Sheet offen={menuOffen} titel="Bereich wählen" onSchliessen={() => setMenuOffen(false)}>
<div className="stack">
{sichtbareTabs.map((t) => (
<button
key={t.key}
className={aktiverTab === t.key ? "btn btn-primary" : "btn btn-secondary"}
onClick={() => {
setTab(t.key);
setMenuOffen(false);
}}
>
{t.label}
</button>
))}
</div>
</Sheet>
</div>
)}
<div className="split-layout">
<nav className="card split-layout-sidebar-sm">
{GRUPPEN_REIHENFOLGE.map((gruppe) => {
const tabsInGruppe = sichtbareTabs.filter((t) => t.gruppe === gruppe);
if (tabsInGruppe.length === 0) return null;
return (
<div key={gruppe} style={{ marginBottom: "1rem" }}>
<div
className="text-muted"
style={{ fontSize: "0.75rem", fontWeight: 600, textTransform: "uppercase", marginBottom: "0.4rem" }}
>
{GRUPPEN_LABEL[gruppe]}
</div>
{tabsInGruppe.map((t) => (
<button
key={t.key}
className={aktiverTab === t.key ? "btn btn-primary btn-block" : "btn btn-secondary btn-block"}
style={{ marginBottom: "0.3rem" }}
onClick={() => setTab(t.key)}
>
{t.label}
</button>
))}
</div>
);
})}
</nav>
<div className="split-layout-content">
{aktiverTab === "objekte" && (
<ObjektSection
objekte={objekte}
@@ -361,6 +389,8 @@ export function AdminPage() {
)}
{aktiverTab === null && <p className="text-muted">Keine Bereiche für deine Rolle verfügbar.</p>}
{istAdmin && <ScanAppDownloadHinweis />}
</div>
</div>
</main>
);
}