191 lines
7.7 KiB
TypeScript
191 lines
7.7 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
||
|
|
import { useTranslation } from 'react-i18next';
|
||
|
|
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 { useToast } from '@/components/ui/Toast';
|
||
|
|
import { Modal } from '@/components/ui/Modal';
|
||
|
|
|
||
|
|
interface Role {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
permissions: string[];
|
||
|
|
}
|
||
|
|
|
||
|
|
const ALL_PERMISSIONS = [
|
||
|
|
{ key: 'companies:read', label: 'Firmen lesen' },
|
||
|
|
{ key: 'companies:write', label: 'Firmen bearbeiten' },
|
||
|
|
{ key: 'contacts:read', label: 'Kontakte lesen' },
|
||
|
|
{ key: 'contacts:write', label: 'Kontakte bearbeiten' },
|
||
|
|
{ key: 'users:read', label: 'Benutzer lesen' },
|
||
|
|
{ key: 'users:write', label: 'Benutzer bearbeiten' },
|
||
|
|
{ key: 'audit:read', label: 'Audit-Log lesen' },
|
||
|
|
{ key: 'settings:write', label: 'Einstellungen bearbeiten' },
|
||
|
|
];
|
||
|
|
|
||
|
|
export function SettingsRolesPage() {
|
||
|
|
const { t } = useTranslation();
|
||
|
|
const toast = useToast();
|
||
|
|
|
||
|
|
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 [editingRole, setEditingRole] = useState<Role | null>(null);
|
||
|
|
|
||
|
|
const handleCreateRole = () => {
|
||
|
|
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);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleTogglePermission = (permKey: string, target: 'new' | 'edit') => {
|
||
|
|
if (target === 'new') {
|
||
|
|
setNewRolePermissions((prev) =>
|
||
|
|
prev.includes(permKey) ? prev.filter((p) => p !== permKey) : [...prev, permKey]
|
||
|
|
);
|
||
|
|
} else if (editingRole) {
|
||
|
|
setEditingRole({
|
||
|
|
...editingRole,
|
||
|
|
permissions: editingRole.permissions.includes(permKey)
|
||
|
|
? editingRole.permissions.filter((p) => p !== permKey)
|
||
|
|
: [...editingRole.permissions, permKey],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSaveRole = () => {
|
||
|
|
if (!editingRole) return;
|
||
|
|
setRoles((prev) => prev.map((r) => (r.id === editingRole.id ? editingRole : r)));
|
||
|
|
toast.success(t('settings.roleUpdated'));
|
||
|
|
setEditingRole(null);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="p-6 max-w-4xl mx-auto" data-testid="settings-roles-page">
|
||
|
|
<div className="flex items-center justify-between mb-6">
|
||
|
|
<h1 className="text-2xl font-bold text-secondary-900">{t('settings.roles')}</h1>
|
||
|
|
<Button onClick={() => setCreateOpen(true)} data-testid="create-role-btn">
|
||
|
|
{t('settings.createRole')}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{roles.length === 0 ? (
|
||
|
|
<EmptyState
|
||
|
|
title={t('settings.noRoles')}
|
||
|
|
action={<Button onClick={() => setCreateOpen(true)}>{t('settings.createRole')}</Button>}
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<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>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<Modal open={createOpen} onClose={() => setCreateOpen(false)} title={t('settings.createRole')}>
|
||
|
|
<div className="space-y-4" data-testid="create-role-form">
|
||
|
|
<Input
|
||
|
|
label={t('settings.roleName')}
|
||
|
|
value={newRoleName}
|
||
|
|
onChange={(e) => setNewRoleName(e.target.value)}
|
||
|
|
placeholder="z.B. Vertrieb"
|
||
|
|
data-testid="new-role-name"
|
||
|
|
/>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-secondary-700 mb-2">{t('settings.permissions')}</label>
|
||
|
|
<div className="space-y-2">
|
||
|
|
{ALL_PERMISSIONS.map((perm) => (
|
||
|
|
<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)}
|
||
|
|
onChange={() => handleTogglePermission(perm.key, 'new')}
|
||
|
|
className="w-4 h-4 rounded border-secondary-300 text-primary-600 focus:ring-primary-500"
|
||
|
|
/>
|
||
|
|
<span className="text-sm text-secondary-900">{perm.label}</span>
|
||
|
|
</label>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</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>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Modal>
|
||
|
|
|
||
|
|
<Modal open={!!editingRole} onClose={() => setEditingRole(null)} title={t('settings.roles') + ' — ' + (editingRole?.name || '')}>
|
||
|
|
{editingRole && (
|
||
|
|
<div className="space-y-4" data-testid="edit-role-form">
|
||
|
|
<Input
|
||
|
|
label={t('settings.roleName')}
|
||
|
|
value={editingRole.name}
|
||
|
|
onChange={(e) => setEditingRole({ ...editingRole, name: e.target.value })}
|
||
|
|
/>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-secondary-700 mb-2">{t('settings.permissions')}</label>
|
||
|
|
<div className="space-y-2">
|
||
|
|
{ALL_PERMISSIONS.map((perm) => (
|
||
|
|
<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)}
|
||
|
|
onChange={() => handleTogglePermission(perm.key, 'edit')}
|
||
|
|
className="w-4 h-4 rounded border-secondary-300 text-primary-600 focus:ring-primary-500"
|
||
|
|
/>
|
||
|
|
<span className="text-sm text-secondary-900">{perm.label}</span>
|
||
|
|
</label>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</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>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</Modal>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|