refactor: SettingsRoles.tsx - use API hooks instead of hardcoded data, add delete role

This commit is contained in:
2026-07-03 15:23:57 +00:00
parent d26efa5bb7
commit 1aa6d1ffa9
+178 -57
View File
@@ -1,17 +1,21 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRoles, useCreateRole, useUpdateRole, useDeleteRole } from '@/api/hooks';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { EmptyState } from '@/components/ui/EmptyState'; import { EmptyState } from '@/components/ui/EmptyState';
import { Skeleton } from '@/components/ui/Skeleton';
import { useToast } from '@/components/ui/Toast'; import { useToast } from '@/components/ui/Toast';
import { Modal } from '@/components/ui/Modal'; import { Modal } from '@/components/ui/Modal';
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
interface Role { interface Role {
id: string; id: string;
name: string; name: string;
permissions: string[]; permissions: Record<string, any>;
field_permissions?: Record<string, any>;
} }
const ALL_PERMISSIONS = [ const ALL_PERMISSIONS = [
@@ -25,59 +29,142 @@ const ALL_PERMISSIONS = [
{ key: 'settings:write', label: 'Einstellungen bearbeiten' }, { key: 'settings:write', label: 'Einstellungen bearbeiten' },
]; ];
function isPermissionGranted(permissions: Record<string, any>, permKey: string): boolean {
if (!permissions) return false;
if (typeof permissions === 'boolean') return permissions;
if (Array.isArray(permissions)) return permissions.includes(permKey);
if (typeof permissions === 'object') {
if (permKey in permissions) {
const val = permissions[permKey];
return val === true || val === 'true' || val === 1;
}
if ('all' in permissions && permissions.all === true) return true;
}
return false;
}
function togglePermission(
permissions: Record<string, any>,
permKey: string
): Record<string, any> {
const current = isPermissionGranted(permissions, permKey);
return { ...permissions, [permKey]: !current };
}
function getGrantedPermissionKeys(permissions: Record<string, any>): string[] {
if (!permissions) return [];
if (Array.isArray(permissions)) return permissions as string[];
if (typeof permissions === 'object') {
return Object.keys(permissions).filter(
(k) => k !== 'all' && permissions[k] === true
);
}
return [];
}
export function SettingsRolesPage() { export function SettingsRolesPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const toast = useToast(); const toast = useToast();
const { data, isLoading, isError, error, refetch } = useRoles();
const createRoleMutation = useCreateRole();
const updateRoleMutation = useUpdateRole();
const deleteRoleMutation = useDeleteRole();
const [roles, setRoles] = useState<Role[]>([
{ id: '1', name: 'Administrator', permissions: ALL_PERMISSIONS.map((p) => p.key) },
{ id: '2', name: 'Mitarbeiter', permissions: ['companies:read', 'companies:write', 'contacts:read', 'contacts:write'] },
{ id: '3', name: 'Gast', permissions: ['companies:read', 'contacts:read'] },
]);
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [newRoleName, setNewRoleName] = useState(''); const [newRoleName, setNewRoleName] = useState('');
const [newRolePermissions, setNewRolePermissions] = useState<string[]>([]); const [newRolePermissions, setNewRolePermissions] = useState<Record<string, any>>({});
const [editingRole, setEditingRole] = useState<Role | null>(null); const [editingRole, setEditingRole] = useState<Role | null>(null);
const [confirmDelete, setConfirmDelete] = useState<Role | null>(null);
const handleCreateRole = () => { const roles: Role[] = data?.items ?? [];
const handleCreateRole = async () => {
if (!newRoleName.trim()) { if (!newRoleName.trim()) {
toast.error(t('validation.required')); toast.error(t('validation.required'));
return; return;
} }
const role: Role = { try {
id: Date.now().toString(), await createRoleMutation.mutateAsync({
name: newRoleName.trim(), name: newRoleName.trim(),
permissions: newRolePermissions, permissions: newRolePermissions,
}; field_permissions: {},
setRoles((prev) => [...prev, role]); });
toast.success(t('settings.roleCreated')); toast.success(t('settings.roleCreated'));
setNewRoleName(''); setNewRoleName('');
setNewRolePermissions([]); setNewRolePermissions({});
setCreateOpen(false); setCreateOpen(false);
} catch (err: any) {
toast.error(err.message || t('common.error'));
}
}; };
const handleTogglePermission = (permKey: string, target: 'new' | 'edit') => { const handleTogglePermission = (permKey: string, target: 'new' | 'edit') => {
if (target === 'new') { if (target === 'new') {
setNewRolePermissions((prev) => setNewRolePermissions((prev) => togglePermission(prev, permKey));
prev.includes(permKey) ? prev.filter((p) => p !== permKey) : [...prev, permKey]
);
} else if (editingRole) { } else if (editingRole) {
setEditingRole({ setEditingRole({
...editingRole, ...editingRole,
permissions: editingRole.permissions.includes(permKey) permissions: togglePermission(editingRole.permissions, permKey),
? editingRole.permissions.filter((p) => p !== permKey)
: [...editingRole.permissions, permKey],
}); });
} }
}; };
const handleSaveRole = () => { const handleSaveRole = async () => {
if (!editingRole) return; if (!editingRole) return;
setRoles((prev) => prev.map((r) => (r.id === editingRole.id ? editingRole : r))); try {
toast.success(t('settings.roleUpdated')); await updateRoleMutation.mutateAsync({
setEditingRole(null); id: editingRole.id,
data: {
name: editingRole.name,
permissions: editingRole.permissions,
field_permissions: editingRole.field_permissions || {},
},
});
toast.success(t('settings.roleUpdated'));
setEditingRole(null);
} catch (err: any) {
toast.error(err.message || t('common.error'));
}
}; };
const handleDeleteRole = async () => {
if (!confirmDelete) return;
try {
await deleteRoleMutation.mutateAsync(confirmDelete.id);
toast.success(t('settings.roleDeleted'));
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-roles-page">
<Skeleton className="h-8 w-48 mb-6" />
<div className="space-y-3">
<Skeleton className="h-16" />
<Skeleton className="h-16" />
<Skeleton className="h-16" />
</div>
</div>
);
}
if (isError) {
return (
<div className="p-6 max-w-4xl mx-auto" data-testid="settings-roles-page">
<h1 className="text-2xl font-bold text-secondary-900 mb-6">{t('settings.roles')}</h1>
<EmptyState
title={t('common.error')}
action={<Button onClick={() => refetch()}>{t('common.reset')}</Button>}
>
<p className="text-sm text-secondary-500">{error?.message || t('common.error')}</p>
</EmptyState>
</div>
);
}
return ( return (
<div className="p-6 max-w-4xl mx-auto" data-testid="settings-roles-page"> <div className="p-6 max-w-4xl mx-auto" data-testid="settings-roles-page">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
@@ -94,31 +181,44 @@ export function SettingsRolesPage() {
/> />
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{roles.map((role) => ( {roles.map((role) => {
<Card key={role.id}> const grantedKeys = getGrantedPermissionKeys(role.permissions);
<div className="flex items-center justify-between mb-3"> return (
<h3 className="text-lg font-semibold text-secondary-900">{role.name}</h3> <Card key={role.id}>
<Button <div className="flex items-center justify-between mb-3">
variant="secondary" <h3 className="text-lg font-semibold text-secondary-900">{role.name}</h3>
size="sm" <div className="flex items-center gap-2">
onClick={() => setEditingRole(role)} <Button
data-testid={`edit-role-${role.id}`} variant="secondary"
> size="sm"
{t('common.edit')} onClick={() => setEditingRole(role)}
</Button> data-testid={`edit-role-${role.id}`}
</div> >
<div className="flex flex-wrap gap-2"> {t('common.edit')}
{role.permissions.length === 0 ? ( </Button>
<span className="text-sm text-secondary-500"></span> <Button
) : ( variant="danger"
role.permissions.map((perm) => { size="sm"
const label = ALL_PERMISSIONS.find((p) => p.key === perm)?.label || perm; onClick={() => setConfirmDelete(role)}
return <Badge key={perm} variant="primary">{label}</Badge>; data-testid={`delete-role-${role.id}`}
}) >
)} {t('common.delete')}
</div> </Button>
</Card> </div>
))} </div>
<div className="flex flex-wrap gap-2">
{grantedKeys.length === 0 ? (
<span className="text-sm text-secondary-500"></span>
) : (
grantedKeys.map((perm) => {
const label = ALL_PERMISSIONS.find((p) => p.key === perm)?.label || perm;
return <Badge key={perm} variant="primary">{label}</Badge>;
})
)}
</div>
</Card>
);
})}
</div> </div>
)} )}
@@ -138,7 +238,7 @@ export function SettingsRolesPage() {
<label key={perm.key} className="flex items-center gap-2 p-2 rounded-md hover:bg-secondary-50 cursor-pointer min-h-touch"> <label key={perm.key} className="flex items-center gap-2 p-2 rounded-md hover:bg-secondary-50 cursor-pointer min-h-touch">
<input <input
type="checkbox" type="checkbox"
checked={newRolePermissions.includes(perm.key)} checked={isPermissionGranted(newRolePermissions, perm.key)}
onChange={() => handleTogglePermission(perm.key, 'new')} onChange={() => handleTogglePermission(perm.key, 'new')}
className="w-4 h-4 rounded border-secondary-300 text-primary-600 focus:ring-primary-500" className="w-4 h-4 rounded border-secondary-300 text-primary-600 focus:ring-primary-500"
/> />
@@ -149,7 +249,13 @@ export function SettingsRolesPage() {
</div> </div>
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<Button variant="secondary" onClick={() => setCreateOpen(false)}>{t('common.cancel')}</Button> <Button variant="secondary" onClick={() => setCreateOpen(false)}>{t('common.cancel')}</Button>
<Button onClick={handleCreateRole} data-testid="save-role-btn">{t('common.save')}</Button> <Button
onClick={handleCreateRole}
isLoading={createRoleMutation.isPending}
data-testid="save-role-btn"
>
{t('common.save')}
</Button>
</div> </div>
</div> </div>
</Modal> </Modal>
@@ -169,7 +275,7 @@ export function SettingsRolesPage() {
<label key={perm.key} className="flex items-center gap-2 p-2 rounded-md hover:bg-secondary-50 cursor-pointer min-h-touch"> <label key={perm.key} className="flex items-center gap-2 p-2 rounded-md hover:bg-secondary-50 cursor-pointer min-h-touch">
<input <input
type="checkbox" type="checkbox"
checked={editingRole.permissions.includes(perm.key)} checked={isPermissionGranted(editingRole.permissions, perm.key)}
onChange={() => handleTogglePermission(perm.key, 'edit')} onChange={() => handleTogglePermission(perm.key, 'edit')}
className="w-4 h-4 rounded border-secondary-300 text-primary-600 focus:ring-primary-500" className="w-4 h-4 rounded border-secondary-300 text-primary-600 focus:ring-primary-500"
/> />
@@ -180,11 +286,26 @@ export function SettingsRolesPage() {
</div> </div>
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<Button variant="secondary" onClick={() => setEditingRole(null)}>{t('common.cancel')}</Button> <Button variant="secondary" onClick={() => setEditingRole(null)}>{t('common.cancel')}</Button>
<Button onClick={handleSaveRole} data-testid="update-role-btn">{t('common.save')}</Button> <Button
onClick={handleSaveRole}
isLoading={updateRoleMutation.isPending}
data-testid="update-role-btn"
>
{t('common.save')}
</Button>
</div> </div>
</div> </div>
)} )}
</Modal> </Modal>
<ConfirmDialog
open={!!confirmDelete}
title={t('common.delete')}
message={`${t('common.delete')}: ${confirmDelete?.name}?`}
variant="danger"
onConfirm={handleDeleteRole}
onCancel={() => setConfirmDelete(null)}
/>
</div> </div>
); );
} }