T07b: frontend feature pages — companies + contacts + settings + audit + dashboard + search
- 11 new feature pages (CompaniesList/Detail/Form, ContactsList/Detail/Form, SettingsProfile/Roles/Users, AuditLog, GlobalSearchResults) - 3 page updates (Dashboard with StatCard+ActivityFeed, Settings with tree nav+Outlet, TopBar with SearchDropdown) - 13 new routes in routes/index.tsx - i18n updates (de.json + en.json) with companies/contacts/settings/audit/search keys - 12 new test files + 2 existing test fixes (TopBar, AppShell) - 7 shared components (DataGrid, Tabs, SearchDropdown, CsvImportDialog, StatCard, ActivityFeed, UnsavedChangesGuard) - 16 new API hooks in hooks.ts - Verification: 141 tests pass, build succeeds, tsc --noEmit clean
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user