From 85574a85aaf3b94c36d9fedfd9ccb11cf2520908 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Mon, 9 Mar 2026 21:32:11 +0100 Subject: [PATCH] feat: add user management UI (create, edit, activate/deactivate) - New UserFormModal component for creating and editing users - AdminUsersPage: add "Neuer Benutzer" button, actions column - German role labels, toggle activate/deactivate from table - Uses React Query mutations with query invalidation Co-Authored-By: Claude Opus 4.6 --- .../frontend/src/admin/AdminUsersPage.tsx | 195 +++++++--- packages/frontend/src/admin/UserFormModal.tsx | 341 ++++++++++++++++++ 2 files changed, 490 insertions(+), 46 deletions(-) create mode 100644 packages/frontend/src/admin/UserFormModal.tsx diff --git a/packages/frontend/src/admin/AdminUsersPage.tsx b/packages/frontend/src/admin/AdminUsersPage.tsx index 2f510b3..da4129a 100644 --- a/packages/frontend/src/admin/AdminUsersPage.tsx +++ b/packages/frontend/src/admin/AdminUsersPage.tsx @@ -1,5 +1,7 @@ -import { useQuery } from '@tanstack/react-query'; +import { useState } from 'react'; +import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'; import api from '../api/client'; +import { UserFormModal } from './UserFormModal'; interface User { id: string; @@ -22,7 +24,31 @@ interface UsersResponse { }; } +const ROLE_LABELS: Record = { + USER: 'Benutzer', + TENANT_ADMIN: 'Mandanten-Admin', + PLATFORM_ADMIN: 'Plattform-Admin', +}; + +const ROLE_COLORS: Record = { + PLATFORM_ADMIN: { bg: '#dbeafe', color: '#1e40af' }, + TENANT_ADMIN: { bg: '#fef3c7', color: '#92400e' }, + USER: { bg: '#f3f4f6', color: '#374151' }, +}; + +const thStyle: React.CSSProperties = { + padding: '0.75rem 1rem', + textAlign: 'left', + fontSize: '0.75rem', + textTransform: 'uppercase', + color: 'var(--color-text-muted)', +}; + export function AdminUsersPage() { + const queryClient = useQueryClient(); + const [isCreateModalOpen, setCreateModalOpen] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const { data, isLoading, error } = useQuery({ queryKey: ['admin', 'users'], queryFn: async () => { @@ -31,6 +57,14 @@ export function AdminUsersPage() { }, }); + const toggleActiveMutation = useMutation({ + mutationFn: ({ id, isActive }: { id: string; isActive: boolean }) => + api.patch(`/users/${id}`, { isActive }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); + }, + }); + if (isLoading) return

Laden...

; if (error) return

Fehler beim Laden der Benutzer

; @@ -38,9 +72,26 @@ export function AdminUsersPage() {

Benutzerverwaltung

- - {data?.meta.total ?? 0} Benutzer gesamt - +
+ + {data?.meta.total ?? 0} Benutzer gesamt + + +
- Name - E-Mail - Rolle - Status - Letzter Login + Name + E-Mail + Rolle + Status + Letzter Login + Aktionen - {data?.data.map((user) => ( - - - {user.firstName} {user.lastName} - - - {user.email} - - - - {user.role} - - - - - {user.isActive ? 'Aktiv' : 'Inaktiv'} - - - {user.lastLogin ? new Date(user.lastLogin).toLocaleDateString('de-DE') : 'Nie'} - - - ))} + {data?.data.map((user) => { + const roleStyle = ROLE_COLORS[user.role] ?? ROLE_COLORS.USER; + return ( + + + {user.firstName} {user.lastName} + + + {user.email} + + + + {ROLE_LABELS[user.role] ?? user.role} + + + + + {user.isActive ? 'Aktiv' : 'Inaktiv'} + + + {user.lastLogin ? new Date(user.lastLogin).toLocaleDateString('de-DE') : 'Nie'} + + +
+ + +
+ + + ); + })}
+ + {/* Modal: Neuen Benutzer anlegen */} + setCreateModalOpen(false)} + onSuccess={() => setCreateModalOpen(false)} + /> + + {/* Modal: Benutzer bearbeiten */} + setEditingUser(null)} + user={editingUser} + onSuccess={() => setEditingUser(null)} + />
); } diff --git a/packages/frontend/src/admin/UserFormModal.tsx b/packages/frontend/src/admin/UserFormModal.tsx new file mode 100644 index 0000000..b3acd1d --- /dev/null +++ b/packages/frontend/src/admin/UserFormModal.tsx @@ -0,0 +1,341 @@ +import { useState, useEffect, type FormEvent } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { Modal } from '../components/Modal'; +import api from '../api/client'; + +interface User { + id: string; + email: string; + firstName: string; + lastName: string; + role: string; + isActive: boolean; + lastLogin: string | null; + createdAt: string; +} + +interface UserFormModalProps { + isOpen: boolean; + onClose: () => void; + user?: User | null; + onSuccess: () => void; +} + +const ROLE_OPTIONS = [ + { value: 'USER', label: 'Benutzer' }, + { value: 'TENANT_ADMIN', label: 'Mandanten-Admin' }, + { value: 'PLATFORM_ADMIN', label: 'Plattform-Admin' }, +]; + +const ROLE_LABELS: Record = { + USER: 'Benutzer', + TENANT_ADMIN: 'Mandanten-Admin', + PLATFORM_ADMIN: 'Plattform-Admin', +}; + +const inputStyle: React.CSSProperties = { + width: '100%', + padding: '0.625rem 0.75rem', + border: '1px solid var(--color-border)', + borderRadius: 'var(--radius-sm)', + fontSize: '0.9375rem', + outline: 'none', + boxSizing: 'border-box', +}; + +const inputDisabledStyle: React.CSSProperties = { + ...inputStyle, + background: '#f3f4f6', + color: 'var(--color-text-muted)', + cursor: 'not-allowed', +}; + +const labelStyle: React.CSSProperties = { + display: 'block', + fontSize: '0.875rem', + fontWeight: 500, + color: 'var(--color-text)', + marginBottom: '0.25rem', +}; + +export function UserFormModal({ isOpen, onClose, user, onSuccess }: UserFormModalProps) { + const isEditMode = !!user; + const queryClient = useQueryClient(); + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [role, setRole] = useState('USER'); + const [isActive, setIsActive] = useState(true); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + useEffect(() => { + if (isOpen) { + setError(''); + setSuccess(''); + if (user) { + setEmail(user.email); + setFirstName(user.firstName); + setLastName(user.lastName); + setRole(user.role); + setIsActive(user.isActive); + setPassword(''); + } else { + setEmail(''); + setPassword(''); + setFirstName(''); + setLastName(''); + setRole('USER'); + setIsActive(true); + } + } + }, [isOpen, user]); + + const createMutation = useMutation({ + mutationFn: (data: { email: string; password: string; firstName: string; lastName: string; role: string }) => + api.post('/users', data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); + setSuccess('Benutzer wurde erfolgreich angelegt.'); + setTimeout(() => onSuccess(), 1000); + }, + onError: (err: unknown) => { + const message = (err as { response?: { data?: { message?: string } } })?.response?.data?.message; + setError(message ?? 'Fehler beim Anlegen des Benutzers.'); + }, + }); + + const updateMutation = useMutation({ + mutationFn: (data: { firstName: string; lastName: string; isActive: boolean }) => + api.patch(`/users/${user!.id}`, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); + setSuccess('Änderungen wurden gespeichert.'); + setTimeout(() => onSuccess(), 1000); + }, + onError: (err: unknown) => { + const message = (err as { response?: { data?: { message?: string } } })?.response?.data?.message; + setError(message ?? 'Fehler beim Speichern der Änderungen.'); + }, + }); + + const isLoading = createMutation.isPending || updateMutation.isPending; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + setError(''); + setSuccess(''); + + if (!firstName.trim() || !lastName.trim()) { + setError('Vor- und Nachname sind Pflichtfelder.'); + return; + } + + if (isEditMode) { + updateMutation.mutate({ firstName: firstName.trim(), lastName: lastName.trim(), isActive }); + } else { + if (!email.trim()) { + setError('E-Mail-Adresse ist ein Pflichtfeld.'); + return; + } + if (password.length < 8) { + setError('Passwort muss mindestens 8 Zeichen lang sein.'); + return; + } + createMutation.mutate({ + email: email.trim(), + password, + firstName: firstName.trim(), + lastName: lastName.trim(), + role, + }); + } + }; + + return ( + +
+ {error && ( +
+ {error} +
+ )} + + {success && ( +
+ {success} +
+ )} + +
+ {/* E-Mail */} +
+ + setEmail(e.target.value)} + disabled={isEditMode} + style={isEditMode ? inputDisabledStyle : inputStyle} + placeholder="max.mustermann@beispiel.de" + required={!isEditMode} + /> +
+ + {/* Passwort (nur Anlegen) */} + {!isEditMode && ( +
+ + setPassword(e.target.value)} + style={inputStyle} + placeholder="Mindestens 8 Zeichen" + minLength={8} + maxLength={128} + required + /> +
+ )} + + {/* Vorname + Nachname nebeneinander */} +
+
+ + setFirstName(e.target.value)} + style={inputStyle} + placeholder="Vorname" + maxLength={100} + required + /> +
+
+ + setLastName(e.target.value)} + style={inputStyle} + placeholder="Nachname" + maxLength={100} + required + /> +
+
+ + {/* Rolle */} +
+ + {isEditMode ? ( + + ) : ( + + )} +
+ + {/* Aktiv-Status (nur Bearbeiten) */} + {isEditMode && ( +
+ setIsActive(e.target.checked)} + style={{ width: 16, height: 16, cursor: 'pointer' }} + /> + +
+ )} +
+ + {/* Buttons */} +
+ + +
+
+
+ ); +}