diff --git a/frontend/src/pages/UserManagementPage.tsx b/frontend/src/pages/UserManagementPage.tsx index 6ec0641..450db21 100644 --- a/frontend/src/pages/UserManagementPage.tsx +++ b/frontend/src/pages/UserManagementPage.tsx @@ -1,470 +1,178 @@ -import { FormEvent, useEffect, useMemo, useState } from "react"; -import { apiDelete, apiGet, apiPost } from "../lib/api"; +import { useEffect, useState } from "react"; +import { apiGet, apiPost } from "../lib/api"; import { useSession } from "../lib/session"; -import type { UserOption, UserRole, UserRow } from "../lib/types"; +import type { UserRow } from "../lib/types"; -type UserDraft = UserRow & { - password: string; - passwordRepeat: string; -}; - -function toDraft(user: UserRow): UserDraft { - return { - ...user, - companyName: user.companyName ?? "", - address: user.address ?? "", - street: user.street ?? "", - houseNumber: user.houseNumber ?? "", - postalCode: user.postalCode ?? "", - city: user.city ?? "", - email: user.email ?? "", - phoneNumber: user.phoneNumber ?? "", - password: "", - passwordRepeat: "", - }; -} - -function emptyUser(): UserDraft { - return { - id: "", - primaryUser: false, - displayName: "", - companyName: "", - address: "", - street: "", - houseNumber: "", - postalCode: "", - city: "", - email: "", - phoneNumber: "", - password: "", - passwordRepeat: "", - active: true, - role: "CUSTOMER", - updatedAt: new Date().toISOString(), - }; -} - -function toDraftFromSession(user: UserOption): UserDraft { - return { - id: user.id, - primaryUser: user.primaryUser, - displayName: user.displayName, - companyName: user.companyName ?? "", - address: user.address ?? "", - street: user.street ?? "", - houseNumber: user.houseNumber ?? "", - postalCode: user.postalCode ?? "", - city: user.city ?? "", - email: user.email ?? "", - phoneNumber: user.phoneNumber ?? "", - password: "", - passwordRepeat: "", - active: true, - role: user.role, - updatedAt: new Date().toISOString(), - }; -} - -function isAccessDenied(error: unknown): boolean { - return error instanceof Error && error.message.trim().toLowerCase() === "access denied"; -} - -function toMutation(user: UserDraft) { - return { - id: user.id || null, - displayName: user.displayName, - companyName: user.companyName || null, - address: user.address || null, - street: user.street || null, - houseNumber: user.houseNumber || null, - postalCode: user.postalCode || null, - city: user.city || null, - email: user.email || null, - phoneNumber: user.phoneNumber || null, - password: user.password || null, - active: user.active, - role: user.role, - }; +interface PrimaryUserRow { + id: string; + displayName: string; + companyName: string | null; + email: string | null; + active: boolean; + updatedAt: string; } export default function UserManagementPage() { const { user } = useSession(); - const [users, setUsers] = useState([]); - const [newUser, setNewUser] = useState(emptyUser()); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); const [message, setMessage] = useState(null); - const [showValidation, setShowValidation] = useState(false); const isAdmin = user?.role === "ADMIN"; - async function loadUsers() { - try { - const response = await apiGet("/portal/users"); - setUsers(response.map(toDraft)); - setMessage(null); - } catch (error) { - if (!isAdmin && user?.primaryUser && isAccessDenied(error)) { - setUsers([toDraftFromSession(user)]); - setMessage(null); - return; - } - throw error; - } - } - useEffect(() => { - void loadUsers().catch((error) => setMessage((error as Error).message)); + async function loadUsers() { + try { + const response = await apiGet("/portal/users"); + // Nur Hauptnutzer (primaryUser=true) anzeigen + const primaryUsers = response + .filter((u) => u.primaryUser) + .map((u) => ({ + id: u.id, + displayName: u.displayName, + companyName: u.companyName, + email: u.email, + active: u.active, + updatedAt: u.updatedAt, + })); + setUsers(primaryUsers); + } catch (error) { + setMessage((error as Error).message); + } finally { + setLoading(false); + } + } + + void loadUsers(); }, []); - const primaryUser = useMemo( - () => users.find((entry) => entry.primaryUser) ?? null, - [users], - ); - const secondaryUsers = useMemo( - () => users.filter((entry) => !entry.primaryUser), - [users], - ); - - function updateExistingUser(userId: string, patch: Partial) { - setUsers((current) => - current.map((entry) => (entry.id === userId ? { ...entry, ...patch } : entry)), - ); - } - - async function saveUser(draft: UserDraft) { - setShowValidation(true); - if (!draft.displayName.trim()) { - setMessage("Bitte alle Pflichtfelder ausfuellen."); - return; - } - + async function toggleUserStatus(userId: string, newStatus: boolean) { try { - const saved = await apiPost("/portal/users", toMutation(draft)); + const userToUpdate = users.find((u) => u.id === userId); + if (!userToUpdate) return; + + await apiPost("/portal/users", { + id: userId, + active: newStatus, + }); + setUsers((current) => - current.map((entry) => (entry.id === draft.id ? toDraft(saved) : entry)), + current.map((u) => (u.id === userId ? { ...u, active: newStatus } : u)) ); - setMessage(draft.primaryUser ? "Stammdaten gespeichert." : "Benutzer gespeichert."); + + setMessage( + `Benutzer "${userToUpdate.displayName}" wurde ${ + newStatus ? "freigegeben" : "gesperrt" + }.` + ); + + // Nachricht nach 3 Sekunden ausblenden + setTimeout(() => setMessage(null), 3000); } catch (error) { setMessage((error as Error).message); } } - async function createUser(event: FormEvent) { - event.preventDefault(); - setShowValidation(true); - if (!newUser.displayName.trim() || !(newUser.email ?? "").trim() || !newUser.password.trim()) { - setMessage("Bitte alle Pflichtfelder ausfuellen."); - return; - } - if (newUser.password !== newUser.passwordRepeat) { - setMessage("Die Passwoerter stimmen nicht ueberein."); - return; - } - - try { - await apiPost("/portal/users", toMutation(newUser)); - setNewUser(emptyUser()); - setMessage("Benutzer angelegt."); - await loadUsers(); - } catch (error) { - setMessage((error as Error).message); - } + // Formatierungsfunktion für das Datum + function formatDate(value: string) { + return new Intl.DateTimeFormat("de-DE", { + dateStyle: "medium", + timeStyle: "short", + }).format(new Date(value)); } - async function removeUser(userId: string) { - try { - await apiDelete(`/portal/users/${userId}`); - setUsers((current) => current.filter((entry) => entry.id !== userId)); - setMessage("Benutzer geloescht."); - void loadUsers().catch(() => undefined); - } catch (error) { - setMessage((error as Error).message); - } + // Nicht-Admin Ansicht (sollte nicht passieren, da Route geschützt ist) + if (!isAdmin) { + return ( +
+
+
+ Zugriff verweigert. Diese Seite ist nur für Administratoren. +
+
+
+ ); } return (
-
+ {/* Header */} +
-

Verwaltung

-

Benutzer und Stammdaten

+

Benutzerverwaltung

+

Hauptnutzer freigeben oder sperren

- Hier pflegen Sie den Hauptbenutzer Ihres Kontos und legen weitere Benutzer an. + Verwalten Sie den Zugriff von Hauptnutzern auf das System. + Gesperrte Benutzer können sich nicht mehr anmelden.

- - {message ? ( -
- {message} -
- ) : null}
- {primaryUser ? ( -
-
-
-

Hauptbenutzer

-

Stammdaten bearbeiten

-
-
{primaryUser.email ?? primaryUser.displayName}
-
- -
- - - - - - - - - -
- -
- -
-
+ {/* Status-Meldung */} + {message ? ( +
+ {message} +
) : null} + {/* Tabelle mit Hauptnutzern */}
-

Benutzer

-

Benutzer anlegen

+

Hauptnutzer

+

Registrierte Hauptnutzer

-
- - - - - {isAdmin ? ( - - ) : null} -
- -
-
- -
- -
-
-
-

Benutzerliste

-

Bereits angelegte Benutzer

-
-
- - {secondaryUsers.length === 0 ? ( -
Noch keine weiteren Benutzer vorhanden.
+ {loading ? ( +
Benutzer werden geladen...
+ ) : users.length === 0 ? ( +
Keine Hauptnutzer vorhanden.
) : (
- + + - - - + + - {secondaryUsers.map((entry) => ( - + {users.map((entry) => ( + + + - - - + @@ -474,6 +182,18 @@ export default function UserManagementPage() { )} + + {/* Info-Box */} +
+
+ Hinweis +

+ Hauptnutzer sind die primären Kontoinhaber. Wenn Sie einen Hauptnutzer sperren, + können sich dieser und alle zugehörigen Nebennutzer nicht mehr anmelden. + Die Daten bleiben erhalten und können durch Freigabe wieder aktiviert werden. +

+
+
); } diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 8ab43dc..d7ad76a 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -1244,6 +1244,61 @@ a { font-size: 1.25rem; } +/* User Management Table Styles */ +.table-row--inactive { + background-color: rgba(157, 60, 48, 0.05); +} + +.status-pill--active { + background-color: rgba(74, 124, 89, 0.15); + color: #4a7c59; + padding: 4px 12px; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 500; +} + +.status-pill--inactive { + background-color: rgba(157, 60, 48, 0.15); + color: #9d3c30; + padding: 4px 12px; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 500; +} + +.action-button { + padding: 8px 16px; + border-radius: 8px; + border: none; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.action-button--success { + background-color: rgba(74, 124, 89, 0.15); + color: #4a7c59; +} + +.action-button--success:hover { + background-color: rgba(74, 124, 89, 0.25); +} + +.action-button--danger { + background-color: rgba(157, 60, 48, 0.15); + color: #9d3c30; +} + +.action-button--danger:hover { + background-color: rgba(157, 60, 48, 0.25); +} + +.text-muted { + color: var(--muted); +} + .dialog-backdrop { position: fixed; inset: 0;
NameNameFirma E-MailPasswortAktiv + StatusLetzte ÄnderungAktion
- - updateExistingUser(entry.id, { displayName: event.target.value }) - } - /> + {entry.displayName} {entry.companyName ?? "-"}{entry.email ?? "-"} - updateExistingUser(entry.id, { email: event.target.value })} - /> - - - updateExistingUser(entry.id, { password: event.target.value }) - } - placeholder="Neues Passwort" - /> - - + {entry.active ? "Freigegeben" : "Gesperrt"} + + {formatDate(entry.updatedAt)} -