Problem 1+5: Dynamic roles dropdown via useRoles(), delete button with ConfirmDialog, fixed create form

This commit is contained in:
2026-07-03 20:11:04 +00:00
parent 493d99aff2
commit 0d3550ec86
+120 -20
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useUsers, useCreateUser, useUpdateUser, useDeleteUser } from '@/api/hooks';
import { useUsers, useCreateUser, useUpdateUser, useDeleteUser, useRoles } from '@/api/hooks';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
@@ -13,52 +13,107 @@ import { Modal } from '@/components/ui/Modal';
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
import { useToast } from '@/components/ui/Toast';
const ROLES = [
const LEGACY_ROLES = [
{ value: 'admin', label: 'Administrator' },
{ value: 'manager', label: 'Manager' },
{ value: 'user', label: 'Mitarbeiter' },
{ value: 'guest', label: 'Gast' },
{ value: 'viewer', label: 'Viewer' },
];
export function SettingsUsersPage() {
const { t } = useTranslation();
const toast = useToast();
const { data, isLoading } = useUsers(1, 100);
const { data: rolesData } = useRoles();
const createUserMutation = useCreateUser();
const updateUserMutation = useUpdateUser();
const deleteUserMutation = useDeleteUser();
const [inviteOpen, setInviteOpen] = useState(false);
const [inviteEmail, setInviteEmail] = useState('');
const [inviteRole, setInviteRole] = useState('user');
const [inviteName, setInviteName] = useState('');
const [invitePassword, setInvitePassword] = useState('');
const [inviteRoleId, setInviteRoleId] = useState('');
const [confirmDeactivate, setConfirmDeactivate] = useState<any>(null);
const [confirmDelete, setConfirmDelete] = useState<any>(null);
const users = data?.items ?? [];
const customRoles = rolesData?.items ?? [];
const roleOptions = useMemo(() => {
const options = [...LEGACY_ROLES];
for (const role of customRoles) {
options.push({ value: `role_id:${role.id}`, label: role.name });
}
return options;
}, [customRoles]);
const roleLabelMap = useMemo(() => {
const map = new Map<string, string>();
for (const opt of roleOptions) {
map.set(opt.value, opt.label);
}
return map;
}, [roleOptions]);
const getRoleValueForUser = (user: any): string => {
if (user.role_id) {
const key = `role_id:${user.role_id}`;
if (roleLabelMap.has(key)) return key;
}
if (user.role) {
const legacy = LEGACY_ROLES.find((r) => r.value === user.role);
if (legacy) return legacy.value;
}
return 'viewer';
};
const handleInvite = async () => {
if (!inviteEmail.trim()) {
if (!inviteEmail.trim() || !inviteName.trim() || !invitePassword.trim()) {
toast.error(t('validation.required'));
return;
}
if (invitePassword.length < 8) {
toast.error(t('auth.passwordTooShort'));
return;
}
try {
await createUserMutation.mutateAsync({
const payload: any = {
email: inviteEmail.trim(),
role: inviteRole,
first_name: '',
last_name: '',
});
name: inviteName.trim(),
password: invitePassword,
is_active: true,
};
if (inviteRoleId.startsWith('role_id:')) {
payload.role_id = inviteRoleId.substring('role_id:'.length);
payload.role = 'viewer';
} else {
payload.role = inviteRoleId || 'viewer';
}
await createUserMutation.mutateAsync(payload);
toast.success(t('settings.userInvited'));
setInviteEmail('');
setInviteRole('user');
setInviteName('');
setInvitePassword('');
setInviteRoleId('');
setInviteOpen(false);
} catch (err: any) {
toast.error(err.message || t('common.error'));
}
};
const handleRoleChange = async (userId: string, newRole: string) => {
const handleRoleChange = async (userId: string, newRoleValue: string) => {
try {
await updateUserMutation.mutateAsync({ id: userId, data: { role: newRole } });
const data: any = {};
if (newRoleValue.startsWith('role_id:')) {
data.role_id = newRoleValue.substring('role_id:'.length);
data.role = 'viewer';
} else {
data.role = newRoleValue;
data.role_id = null;
}
await updateUserMutation.mutateAsync({ id: userId, data });
toast.success(t('settings.assignRole') + ' — OK');
} catch (err: any) {
toast.error(err.message || t('common.error'));
@@ -81,6 +136,17 @@ export function SettingsUsersPage() {
}
};
const handleDeleteUser = async () => {
if (!confirmDelete) return;
try {
await deleteUserMutation.mutateAsync(confirmDelete.id);
toast.success(t('settings.userDeleted'));
setConfirmDelete(null);
} catch (err: any) {
toast.error(err.message || t('common.error'));
}
};
if (isLoading) {
return (
<div className="p-6 max-w-4xl mx-auto" data-testid="settings-users-page">
@@ -115,13 +181,13 @@ export function SettingsUsersPage() {
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3 flex-1 min-w-0">
<Avatar
name={`${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email}
name={user.name || user.email}
src={user.avatar_url}
size="sm"
/>
<div className="flex-1 min-w-0">
<p className="font-medium text-secondary-900 truncate">
{user.first_name} {user.last_name}
{user.name}
</p>
<p className="text-sm text-secondary-500 truncate">{user.email}</p>
</div>
@@ -131,8 +197,8 @@ export function SettingsUsersPage() {
</div>
<div className="flex items-center gap-2">
<Select
options={ROLES}
value={user.role || 'user'}
options={roleOptions}
value={getRoleValueForUser(user)}
onChange={(e) => handleRoleChange(user.id, e.target.value)}
aria-label={t('settings.assignRole')}
className="w-36"
@@ -145,6 +211,15 @@ export function SettingsUsersPage() {
>
{user.is_active === false ? t('settings.activate') : t('settings.deactivate')}
</Button>
<Button
variant="danger"
size="sm"
onClick={() => setConfirmDelete(user)}
data-testid={`delete-user-${user.id}`}
aria-label={t('common.delete')}
>
{t('common.delete')}
</Button>
</div>
</div>
</Card>
@@ -154,6 +229,14 @@ export function SettingsUsersPage() {
<Modal open={inviteOpen} onClose={() => setInviteOpen(false)} title={t('settings.inviteUser')}>
<div className="space-y-4" data-testid="invite-user-form">
<Input
label={t('settings.name')}
type="text"
value={inviteName}
onChange={(e) => setInviteName(e.target.value)}
placeholder={t('settings.name')}
data-testid="invite-name"
/>
<Input
label={t('settings.inviteEmail')}
type="email"
@@ -162,11 +245,19 @@ export function SettingsUsersPage() {
placeholder="neu.mitarbeiter@firma.de"
data-testid="invite-email"
/>
<Input
label={t('auth.password')}
type="password"
value={invitePassword}
onChange={(e) => setInvitePassword(e.target.value)}
placeholder="********"
data-testid="invite-password"
/>
<Select
label={t('settings.inviteRole')}
options={ROLES}
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value)}
options={roleOptions}
value={inviteRoleId}
onChange={(e) => setInviteRoleId(e.target.value)}
/>
<div className="flex justify-end gap-3">
<Button variant="secondary" onClick={() => setInviteOpen(false)}>{t('common.cancel')}</Button>
@@ -193,6 +284,15 @@ export function SettingsUsersPage() {
onConfirm={handleToggleActive}
onCancel={() => setConfirmDeactivate(null)}
/>
<ConfirmDialog
open={!!confirmDelete}
title={t('common.delete')}
message={`${t('settings.deleteUserConfirm')}: ${confirmDelete?.name || confirmDelete?.email}?`}
variant="danger"
onConfirm={handleDeleteUser}
onCancel={() => setConfirmDelete(null)}
/>
</div>
);
}