import React, { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGroups, useCreateGroup, useUpdateGroup, useDeleteGroup, usePermissions, useGroupMembers, useAddGroupMember, useRemoveGroupMember, useUsers, } from '@/api/hooks'; import type { PermissionItem, Group, GroupMember } 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 { Select } from '@/components/ui/Select'; 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'; function isPermissionGranted(permissions: Record, 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, permKey: string ): Record { const current = isPermissionGranted(permissions, permKey); return { ...permissions, [permKey]: !current }; } function getGrantedPermissionKeys(permissions: Record): 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 []; } function toggleDeniedPermission(denied: string[], permKey: string): string[] { if (denied.includes(permKey)) { return denied.filter((k) => k !== permKey); } return [...denied, permKey]; } interface EditingGroup extends Partial { id?: string; name: string; description: string; permissions: Record; denied_permissions: string[]; field_permissions: Record; } export function SettingsGroupsPage() { const { t } = useTranslation(); const toast = useToast(); const { data, isLoading, isError, error, refetch } = useGroups(); const { data: permissionsData, isLoading: permissionsLoading } = usePermissions(); const createGroupMutation = useCreateGroup(); const updateGroupMutation = useUpdateGroup(); const deleteGroupMutation = useDeleteGroup(); const [createOpen, setCreateOpen] = useState(false); const [newGroupName, setNewGroupName] = useState(''); const [newGroupDescription, setNewGroupDescription] = useState(''); const [newGroupPermissions, setNewGroupPermissions] = useState>({}); const [newGroupDenied, setNewGroupDenied] = useState([]); const [editingGroup, setEditingGroup] = useState(null); const [confirmDelete, setConfirmDelete] = useState(null); // Member management state const [memberSearchUserId, setMemberSearchUserId] = useState(''); const groups: Group[] = data?.items ?? []; const allPermissions: PermissionItem[] = useMemo(() => { return permissionsData?.all ?? []; }, [permissionsData]); const permissionLabelMap = useMemo(() => { const map = new Map(); for (const perm of allPermissions) { map.set(perm.key, perm.label); } return map; }, [allPermissions]); const groupedPermissions = useMemo(() => { const groups: Record = {}; for (const perm of allPermissions) { if (!groups[perm.category]) { groups[perm.category] = []; } groups[perm.category].push(perm); } return groups; }, [allPermissions]); const handleCreateGroup = async () => { if (!newGroupName.trim()) { toast.error(t('validation.required', 'Pflichtfeld')); return; } try { await createGroupMutation.mutateAsync({ name: newGroupName.trim(), description: newGroupDescription.trim() || null, permissions: newGroupPermissions, denied_permissions: newGroupDenied, field_permissions: {}, }); toast.success(t('settings.groupCreated', 'Gruppe erstellt')); setNewGroupName(''); setNewGroupDescription(''); setNewGroupPermissions({}); setNewGroupDenied([]); setCreateOpen(false); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleTogglePermission = (permKey: string, target: 'new' | 'edit') => { if (target === 'new') { setNewGroupPermissions((prev) => togglePermission(prev, permKey)); } else if (editingGroup) { setEditingGroup({ ...editingGroup, permissions: togglePermission(editingGroup.permissions || {}, permKey), }); } }; const handleToggleDenied = (permKey: string, target: 'new' | 'edit') => { if (target === 'new') { setNewGroupDenied((prev) => toggleDeniedPermission(prev, permKey)); } else if (editingGroup) { setEditingGroup({ ...editingGroup, denied_permissions: toggleDeniedPermission(editingGroup.denied_permissions || [], permKey), }); } }; const handleSaveGroup = async () => { if (!editingGroup || !editingGroup.id) return; if (!editingGroup.name.trim()) { toast.error(t('validation.required', 'Pflichtfeld')); return; } try { await updateGroupMutation.mutateAsync({ id: editingGroup.id, data: { name: editingGroup.name.trim(), description: editingGroup.description.trim() || null, permissions: editingGroup.permissions, denied_permissions: editingGroup.denied_permissions, field_permissions: editingGroup.field_permissions || {}, }, }); toast.success(t('settings.groupUpdated', 'Gruppe aktualisiert')); setEditingGroup(null); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleDeleteGroup = async () => { if (!confirmDelete) return; try { await deleteGroupMutation.mutateAsync(confirmDelete.id); toast.success(t('settings.groupDeleted', 'Gruppe gelöscht')); setConfirmDelete(null); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const startEdit = (group: Group) => { setEditingGroup({ id: group.id, name: group.name, description: group.description || '', permissions: group.permissions || {}, denied_permissions: group.denied_permissions || [], field_permissions: group.field_permissions || {}, }); }; if (isLoading || permissionsLoading) { return (
); } if (isError) { return (

{t('settings.groups', 'Gruppen')}

refetch()}>{t('common.reset', 'Erneut versuchen')}} />
); } return (

{t('settings.groups', 'Gruppen')}

{groups.length === 0 ? ( setCreateOpen(true)}>{t('settings.createGroup', 'Gruppe erstellen')}} /> ) : (
{groups.map((group) => { const grantedKeys = getGrantedPermissionKeys(group.permissions); const deniedCount = (group.denied_permissions || []).length; return (

{group.name}

{group.description && (

{group.description}

)}
{grantedKeys.length === 0 ? ( ) : ( grantedKeys.slice(0, 10).map((perm) => { const label = permissionLabelMap.get(perm) || perm; return {label}; }) )} {grantedKeys.length > 10 && ( +{grantedKeys.length - 10} )} {deniedCount > 0 && ( {deniedCount} {t('settings.denied', 'verweigert')} )}
); })}
)} {/* Create Group Modal */} setCreateOpen(false)} title={t('settings.createGroup', 'Gruppe erstellen')} size="lg" >
setNewGroupName(e.target.value)} placeholder={t('settings.groupName', 'Gruppenname')} data-testid="new-group-name" /> setNewGroupDescription(e.target.value)} placeholder={t('settings.groupDescription', 'Beschreibung')} data-testid="new-group-description" />
{allPermissions.length === 0 && (

{t('settings.noPermissions', 'Keine Berechtigungen verfügbar')}

)} {Object.entries(groupedPermissions).map(([category, perms]) => (

{category === 'system' ? t('settings.systemPermissions', 'System') : t('settings.pluginPermissions', 'Plugins')}

{perms.map((perm) => ( ))}
))}
{allPermissions.length === 0 && (

{t('settings.noPermissions', 'Keine Berechtigungen verfügbar')}

)} {allPermissions.map((perm) => ( ))}
{/* Edit Group Modal */} setEditingGroup(null)} title={t('settings.groups', 'Gruppen') + ' — ' + (editingGroup?.name || '')} size="xl" > {editingGroup && ( handleTogglePermission(key, 'edit')} onToggleDenied={(key) => handleToggleDenied(key, 'edit')} allPermissions={allPermissions} groupedPermissions={groupedPermissions} permissionLabelMap={permissionLabelMap} onSave={handleSaveGroup} onCancel={() => setEditingGroup(null)} isSaving={updateGroupMutation.isPending} memberSearchUserId={memberSearchUserId} setMemberSearchUserId={setMemberSearchUserId} /> )} setConfirmDelete(null)} />
); } // ─── Edit Group Content with Tabs ───────────────────────────────────────────── interface EditGroupContentProps { group: EditingGroup; onChange: (group: EditingGroup) => void; onTogglePermission: (key: string) => void; onToggleDenied: (key: string) => void; allPermissions: PermissionItem[]; groupedPermissions: Record; permissionLabelMap: Map; onSave: () => void; onCancel: () => void; isSaving: boolean; memberSearchUserId: string; setMemberSearchUserId: (val: string) => void; } function EditGroupContent({ group, onChange, onTogglePermission, onToggleDenied, allPermissions, groupedPermissions, permissionLabelMap, onSave, onCancel, isSaving, memberSearchUserId, setMemberSearchUserId, }: EditGroupContentProps) { const { t } = useTranslation(); const toast = useToast(); const [activeTab, setActiveTab] = useState<'details' | 'permissions' | 'denied' | 'members'>('details'); const groupId = group.id || null; const { data: membersData, isLoading: membersLoading } = useGroupMembers(groupId); const { data: usersData } = useUsers(1, 100); const addMemberMutation = useAddGroupMember(); const removeMemberMutation = useRemoveGroupMember(); const members: GroupMember[] = membersData?.items ?? []; const allUsers = usersData?.items ?? []; // Users not yet in this group const availableUsers = useMemo(() => { const memberIds = new Set(members.map((m) => m.user_id)); return allUsers.filter((u: any) => !memberIds.has(u.id)); }, [allUsers, members]); const userSelectOptions = useMemo(() => { return availableUsers.map((u: any) => ({ value: u.id, label: `${u.name || u.email} (${u.email})`, })); }, [availableUsers]); const handleAddMember = async () => { if (!groupId || !memberSearchUserId) return; try { await addMemberMutation.mutateAsync({ groupId, userId: memberSearchUserId }); toast.success(t('settings.memberAdded', 'Mitglied hinzugefügt')); setMemberSearchUserId(''); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleRemoveMember = async (userId: string) => { if (!groupId) return; try { await removeMemberMutation.mutateAsync({ groupId, userId }); toast.success(t('settings.memberRemoved', 'Mitglied entfernt')); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const tabBtnClass = (tab: string) => `px-4 py-2 text-sm font-medium rounded-md transition-colors ${ activeTab === tab ? 'bg-primary-100 text-primary-700' : 'text-secondary-600 hover:bg-secondary-100 hover:text-secondary-900' }`; return (
{/* Tab Navigation */}
{/* Details Tab */} {activeTab === 'details' && (
onChange({ ...group, name: e.target.value })} data-testid="edit-group-name" /> onChange({ ...group, description: e.target.value })} data-testid="edit-group-description" />
)} {/* Permissions Tab */} {activeTab === 'permissions' && (
{allPermissions.length === 0 && (

{t('settings.noPermissions', 'Keine Berechtigungen verfügbar')}

)} {Object.entries(groupedPermissions).map(([category, perms]) => (

{category === 'system' ? t('settings.systemPermissions', 'System') : t('settings.pluginPermissions', 'Plugins')}

{perms.map((perm) => ( ))}
))}
)} {/* Denied Permissions Tab */} {activeTab === 'denied' && (

{t('settings.deniedPermissionsHint', 'Diese Berechtigungen werden explizit verweigert, auch wenn sie über Rollen gewährt wurden.')}

{allPermissions.length === 0 && (

{t('settings.noPermissions', 'Keine Berechtigungen verfügbar')}

)} {allPermissions.map((perm) => ( ))}
)} {/* Members Tab */} {activeTab === 'members' && (