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 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user