refactor: SettingsRoles.tsx - use API hooks instead of hardcoded data, add delete role
This commit is contained in:
@@ -1,17 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRoles, useCreateRole, useUpdateRole, useDeleteRole } from '@/api/hooks';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { Skeleton } from '@/components/ui/Skeleton';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
|
||||
|
||||
interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
permissions: string[];
|
||||
permissions: Record<string, any>;
|
||||
field_permissions?: Record<string, any>;
|
||||
}
|
||||
|
||||
const ALL_PERMISSIONS = [
|
||||
@@ -25,59 +29,142 @@ const ALL_PERMISSIONS = [
|
||||
{ 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() {
|
||||
const { t } = useTranslation();
|
||||
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 [newRoleName, setNewRoleName] = useState('');
|
||||
const [newRolePermissions, setNewRolePermissions] = useState<string[]>([]);
|
||||
const [newRolePermissions, setNewRolePermissions] = useState<Record<string, any>>({});
|
||||
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()) {
|
||||
toast.error(t('validation.required'));
|
||||
return;
|
||||
}
|
||||
const role: Role = {
|
||||
id: Date.now().toString(),
|
||||
name: newRoleName.trim(),
|
||||
permissions: newRolePermissions,
|
||||
};
|
||||
setRoles((prev) => [...prev, role]);
|
||||
toast.success(t('settings.roleCreated'));
|
||||
setNewRoleName('');
|
||||
setNewRolePermissions([]);
|
||||
setCreateOpen(false);
|
||||
try {
|
||||
await createRoleMutation.mutateAsync({
|
||||
name: newRoleName.trim(),
|
||||
permissions: newRolePermissions,
|
||||
field_permissions: {},
|
||||
});
|
||||
toast.success(t('settings.roleCreated'));
|
||||
setNewRoleName('');
|
||||
setNewRolePermissions({});
|
||||
setCreateOpen(false);
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || t('common.error'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleTogglePermission = (permKey: string, target: 'new' | 'edit') => {
|
||||
if (target === 'new') {
|
||||
setNewRolePermissions((prev) =>
|
||||
prev.includes(permKey) ? prev.filter((p) => p !== permKey) : [...prev, permKey]
|
||||
);
|
||||
setNewRolePermissions((prev) => togglePermission(prev, permKey));
|
||||
} else if (editingRole) {
|
||||
setEditingRole({
|
||||
...editingRole,
|
||||
permissions: editingRole.permissions.includes(permKey)
|
||||
? editingRole.permissions.filter((p) => p !== permKey)
|
||||
: [...editingRole.permissions, permKey],
|
||||
permissions: togglePermission(editingRole.permissions, permKey),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveRole = () => {
|
||||
const handleSaveRole = async () => {
|
||||
if (!editingRole) return;
|
||||
setRoles((prev) => prev.map((r) => (r.id === editingRole.id ? editingRole : r)));
|
||||
toast.success(t('settings.roleUpdated'));
|
||||
setEditingRole(null);
|
||||
try {
|
||||
await updateRoleMutation.mutateAsync({
|
||||
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 (
|
||||
<div className="p-6 max-w-4xl mx-auto" data-testid="settings-roles-page">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
@@ -94,31 +181,44 @@ export function SettingsRolesPage() {
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{roles.map((role) => (
|
||||
<Card key={role.id}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-secondary-900">{role.name}</h3>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setEditingRole(role)}
|
||||
data-testid={`edit-role-${role.id}`}
|
||||
>
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{role.permissions.length === 0 ? (
|
||||
<span className="text-sm text-secondary-500">—</span>
|
||||
) : (
|
||||
role.permissions.map((perm) => {
|
||||
const label = ALL_PERMISSIONS.find((p) => p.key === perm)?.label || perm;
|
||||
return <Badge key={perm} variant="primary">{label}</Badge>;
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{roles.map((role) => {
|
||||
const grantedKeys = getGrantedPermissionKeys(role.permissions);
|
||||
return (
|
||||
<Card key={role.id}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-secondary-900">{role.name}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setEditingRole(role)}
|
||||
data-testid={`edit-role-${role.id}`}
|
||||
>
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={() => setConfirmDelete(role)}
|
||||
data-testid={`delete-role-${role.id}`}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newRolePermissions.includes(perm.key)}
|
||||
checked={isPermissionGranted(newRolePermissions, perm.key)}
|
||||
onChange={() => handleTogglePermission(perm.key, 'new')}
|
||||
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 className="flex justify-end gap-3">
|
||||
<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>
|
||||
</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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editingRole.permissions.includes(perm.key)}
|
||||
checked={isPermissionGranted(editingRole.permissions, perm.key)}
|
||||
onChange={() => handleTogglePermission(perm.key, 'edit')}
|
||||
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 className="flex justify-end gap-3">
|
||||
<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>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!confirmDelete}
|
||||
title={t('common.delete')}
|
||||
message={`${t('common.delete')}: ${confirmDelete?.name}?`}
|
||||
variant="danger"
|
||||
onConfirm={handleDeleteRole}
|
||||
onCancel={() => setConfirmDelete(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user