Admin-Portal: Benutzerverwaltung (anlegen, Rollen, aktivieren/deaktivieren)
Backend-API existierte bereits (/benutzer, admin-only), war im Frontend bisher nicht angebunden. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
@@ -95,3 +95,14 @@ export interface Me {
|
|||||||
login: string;
|
login: string;
|
||||||
rollen: string[];
|
rollen: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Rolle = "mitarbeiter" | "materialverantwortlicher" | "leitungsverantwortlicher" | "administration";
|
||||||
|
|
||||||
|
export interface Benutzer {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
login: string;
|
||||||
|
email: string | null;
|
||||||
|
aktiv: boolean;
|
||||||
|
rollen: string[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { apiRequest } from "../api/client";
|
import { apiRequest } from "../api/client";
|
||||||
import type { Bereich, Beladungsvorlage, Kategorie, Material, Objekt, Objekttyp, Standort } from "../api/types";
|
import type { Benutzer, Bereich, Beladungsvorlage, Kategorie, Material, Objekt, Objekttyp, Standort } from "../api/types";
|
||||||
|
import { BenutzerSection } from "./admin/BenutzerSection";
|
||||||
import { BereichSection } from "./admin/BereichSection";
|
import { BereichSection } from "./admin/BereichSection";
|
||||||
import { KategorieSection } from "./admin/KategorieSection";
|
import { KategorieSection } from "./admin/KategorieSection";
|
||||||
import { MaterialSection } from "./admin/MaterialSection";
|
import { MaterialSection } from "./admin/MaterialSection";
|
||||||
@@ -10,7 +11,7 @@ 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";
|
type Tab = "objekte" | "vorlagen" | "material" | "struktur" | "standorte" | "benutzer";
|
||||||
|
|
||||||
const TABS: { key: Tab; label: string }[] = [
|
const TABS: { key: Tab; label: string }[] = [
|
||||||
{ key: "objekte", label: "Objekte" },
|
{ key: "objekte", label: "Objekte" },
|
||||||
@@ -18,6 +19,7 @@ const TABS: { key: Tab; label: string }[] = [
|
|||||||
{ key: "material", label: "Material" },
|
{ key: "material", label: "Material" },
|
||||||
{ key: "struktur", label: "Struktur" },
|
{ key: "struktur", label: "Struktur" },
|
||||||
{ key: "standorte", label: "Standorte" },
|
{ key: "standorte", label: "Standorte" },
|
||||||
|
{ key: "benutzer", label: "Benutzer" },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,12 +39,13 @@ export function AdminPage() {
|
|||||||
const [vorlagen, setVorlagen] = useState<Beladungsvorlage[]>([]);
|
const [vorlagen, setVorlagen] = useState<Beladungsvorlage[]>([]);
|
||||||
const [objekte, setObjekte] = useState<Objekt[]>([]);
|
const [objekte, setObjekte] = useState<Objekt[]>([]);
|
||||||
const [materialien, setMaterialien] = useState<Material[]>([]);
|
const [materialien, setMaterialien] = useState<Material[]>([]);
|
||||||
|
const [benutzer, setBenutzer] = useState<Benutzer[]>([]);
|
||||||
const [fehler, setFehler] = useState<string | null>(null);
|
const [fehler, setFehler] = useState<string | null>(null);
|
||||||
const [laedt, setLaedt] = useState(true);
|
const [laedt, setLaedt] = useState(true);
|
||||||
const [tab, setTab] = useState<Tab>("objekte");
|
const [tab, setTab] = useState<Tab>("objekte");
|
||||||
|
|
||||||
async function ladeAlles() {
|
async function ladeAlles() {
|
||||||
const [be, ka, ot, s, v, o, m] = await Promise.all([
|
const [be, ka, ot, s, v, o, m, b] = await Promise.all([
|
||||||
apiRequest<Bereich[]>("/bereiche"),
|
apiRequest<Bereich[]>("/bereiche"),
|
||||||
apiRequest<Kategorie[]>("/kategorien"),
|
apiRequest<Kategorie[]>("/kategorien"),
|
||||||
apiRequest<Objekttyp[]>("/objekttypen"),
|
apiRequest<Objekttyp[]>("/objekttypen"),
|
||||||
@@ -50,6 +53,7 @@ export function AdminPage() {
|
|||||||
apiRequest<Beladungsvorlage[]>("/vorlagen"),
|
apiRequest<Beladungsvorlage[]>("/vorlagen"),
|
||||||
apiRequest<Objekt[]>("/objekte"),
|
apiRequest<Objekt[]>("/objekte"),
|
||||||
apiRequest<Material[]>("/materialien"),
|
apiRequest<Material[]>("/materialien"),
|
||||||
|
apiRequest<Benutzer[]>("/benutzer"),
|
||||||
]);
|
]);
|
||||||
setBereiche(be);
|
setBereiche(be);
|
||||||
setKategorien(ka);
|
setKategorien(ka);
|
||||||
@@ -58,6 +62,7 @@ export function AdminPage() {
|
|||||||
setVorlagen(v);
|
setVorlagen(v);
|
||||||
setObjekte(o);
|
setObjekte(o);
|
||||||
setMaterialien(m);
|
setMaterialien(m);
|
||||||
|
setBenutzer(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -134,6 +139,9 @@ export function AdminPage() {
|
|||||||
{tab === "standorte" && (
|
{tab === "standorte" && (
|
||||||
<StandortSection standorte={standorte} onGeaendert={ladeAlles} onFehler={setFehler} />
|
<StandortSection standorte={standorte} onGeaendert={ladeAlles} onFehler={setFehler} />
|
||||||
)}
|
)}
|
||||||
|
{tab === "benutzer" && (
|
||||||
|
<BenutzerSection benutzer={benutzer} onGeaendert={ladeAlles} onFehler={setFehler} />
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { apiRequest } from "../../api/client";
|
||||||
|
import type { Benutzer, Rolle } from "../../api/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
benutzer: Benutzer[];
|
||||||
|
onGeaendert: () => Promise<void>;
|
||||||
|
onFehler: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALLE_ROLLEN: Rolle[] = [
|
||||||
|
"mitarbeiter",
|
||||||
|
"materialverantwortlicher",
|
||||||
|
"leitungsverantwortlicher",
|
||||||
|
"administration",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function BenutzerSection({ benutzer, onGeaendert, onFehler }: Props) {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [login, setLogin] = useState("");
|
||||||
|
const [passwort, setPasswort] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [rollen, setRollen] = useState<Rolle[]>(["mitarbeiter"]);
|
||||||
|
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||||
|
|
||||||
|
function rolleUmschalten(rolle: Rolle) {
|
||||||
|
setRollen((bisherige) =>
|
||||||
|
bisherige.includes(rolle) ? bisherige.filter((r) => r !== rolle) : [...bisherige, rolle]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function anlegen() {
|
||||||
|
setWirdAngelegt(true);
|
||||||
|
try {
|
||||||
|
await apiRequest("/benutzer", {
|
||||||
|
method: "POST",
|
||||||
|
body: { name, login, passwort, email: email || null, rollen },
|
||||||
|
});
|
||||||
|
setName("");
|
||||||
|
setLogin("");
|
||||||
|
setPasswort("");
|
||||||
|
setEmail("");
|
||||||
|
setRollen(["mitarbeiter"]);
|
||||||
|
await onGeaendert();
|
||||||
|
} catch {
|
||||||
|
onFehler("Benutzer konnte nicht angelegt werden (Login evtl. schon vergeben).");
|
||||||
|
} finally {
|
||||||
|
setWirdAngelegt(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function aktivSchalten(b: Benutzer) {
|
||||||
|
try {
|
||||||
|
await apiRequest(`/benutzer/${b.id}`, { method: "PATCH", body: { aktiv: !b.aktiv } });
|
||||||
|
await onGeaendert();
|
||||||
|
} catch {
|
||||||
|
onFehler("Status konnte nicht geändert werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="section">
|
||||||
|
<h2>Benutzer anlegen</h2>
|
||||||
|
<div className="card">
|
||||||
|
<div className="row" style={{ flexWrap: "wrap", gap: "0.5rem", marginBottom: "0.75rem" }}>
|
||||||
|
<input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
|
||||||
|
<input className="input" value={login} onChange={(e) => setLogin(e.target.value)} placeholder="Login" />
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="password"
|
||||||
|
value={passwort}
|
||||||
|
onChange={(e) => setPasswort(e.target.value)}
|
||||||
|
placeholder="Passwort"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="E-Mail (optional)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="row" style={{ flexWrap: "wrap", gap: "0.5rem", marginBottom: "0.75rem" }}>
|
||||||
|
{ALLE_ROLLEN.map((r) => (
|
||||||
|
<label key={r} className="row" style={{ gap: "0.3rem" }}>
|
||||||
|
<input type="checkbox" checked={rollen.includes(r)} onChange={() => rolleUmschalten(r)} />
|
||||||
|
{r}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={anlegen}
|
||||||
|
disabled={!name || !login || !passwort || wirdAngelegt}
|
||||||
|
>
|
||||||
|
Anlegen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{benutzer.length > 0 && (
|
||||||
|
<ul className="card-list" style={{ marginTop: "1rem" }}>
|
||||||
|
{benutzer.map((b) => (
|
||||||
|
<li key={b.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||||
|
<span>
|
||||||
|
<strong>{b.name}</strong> ({b.login}) – {b.rollen.join(", ")}
|
||||||
|
{!b.aktiv && " – deaktiviert"}
|
||||||
|
</span>
|
||||||
|
<button className="btn btn-secondary" onClick={() => aktivSchalten(b)}>
|
||||||
|
{b.aktiv ? "Deaktivieren" : "Aktivieren"}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user