// Groups management page (US-031) - Updated for RBAC import { useState, useEffect, useCallback } from 'react' import apiClient from '@/api/client' import { useWorkspace } from '@/contexts/WorkspaceContext' import Button from '@/components/Button' import Input from '@/components/Input' import PermissionEditor from '@/components/PermissionEditor' import type { Group } from '@/types' interface GroupFormData { name: string permissions: string[] parent_group_id?: string } function GroupModal({ group, workspaceId, availableGroups, onClose, onSave, }: { group: Group | null workspaceId: string availableGroups: Group[] onClose: () => void onSave: () => void }) { const [formData, setFormData] = useState({ name: group?.name || '', permissions: group?.permissions || [], parent_group_id: group?.parent_group_id || '', }) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) // Filter out the current group from parent options (can't be parent of itself) const parentOptions = availableGroups.filter(g => g.id !== group?.id) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setIsLoading(true) setError(null) try { const payload = { name: formData.name, permissions: formData.permissions, parent_group_id: formData.parent_group_id || undefined, } if (group) { await apiClient.patch(`/workspaces/${workspaceId}/groups/${group.id}`, payload) } else { await apiClient.post(`/workspaces/${workspaceId}/groups`, payload) } onSave() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save group') } finally { setIsLoading(false) } } return (

{group ? 'Edit Group' : 'Create Group'}

{error && (
{error}
)} setFormData({ ...formData, name: e.target.value })} placeholder="Enter group name" required autoFocus /> {/* Parent Group Selection */}

Child groups inherit permissions from their parent group.

{/* Permission Editor */} setFormData({ ...formData, permissions })} workspaceId={workspaceId} />
) } function DeleteConfirmModal({ group, workspaceId, onClose, onConfirm, }: { group: Group workspaceId: string onClose: () => void onConfirm: () => void }) { const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const handleDelete = async () => { setIsLoading(true) setError(null) try { await apiClient.delete(`/workspaces/${workspaceId}/groups/${group.id}`) onConfirm() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to delete group') setIsLoading(false) } } return (

Delete Group

Are you sure you want to delete {group.name}? Users assigned to this group will lose these permissions.

{error && (
{error}
)}
) } function Groups() { const { currentWorkspace } = useWorkspace() const [groups, setGroups] = useState([]) const [filteredGroups, setFilteredGroups] = useState([]) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [showCreateModal, setShowCreateModal] = useState(false) const [editingGroup, setEditingGroup] = useState(null) const [deletingGroup, setDeletingGroup] = useState(null) const [showSystemGroups, setShowSystemGroups] = useState(false) const fetchGroups = useCallback(async () => { if (!currentWorkspace) { setGroups([]) setIsLoading(false) return } setIsLoading(true) setError(null) try { let url = `/workspaces/${currentWorkspace.id}/groups` if (showSystemGroups) { url += '?include_system=true' } const response = await apiClient.get<{ groups: Group[] }>(url) setGroups(response.groups || []) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch groups') } finally { setIsLoading(false) } }, [currentWorkspace, showSystemGroups]) useEffect(() => { fetchGroups() }, [fetchGroups]) useEffect(() => { const query = searchQuery.toLowerCase() let filtered = groups.filter( (g) => g.name.toLowerCase().includes(query) || g.permissions.some((p) => p.toLowerCase().includes(query)) ) // Filter out system groups unless showing them if (!showSystemGroups) { filtered = filtered.filter((g) => !g.is_system) } setFilteredGroups(filtered) }, [groups, searchQuery, showSystemGroups]) const handleSave = () => { setShowCreateModal(false) setEditingGroup(null) fetchGroups() } const handleDelete = () => { setDeletingGroup(null) fetchGroups() } const getParentGroupName = (parentId?: string): string | null => { if (!parentId) return null const parent = groups.find((g) => g.id === parentId) return parent?.name || null } // Check if any system groups exist const hasSystemGroups = groups.some((g) => g.is_system) if (!currentWorkspace) { return (

Groups

Manage permission groups

Please select a workspace to manage groups.

) } return (

Groups

Manage permission groups in {currentWorkspace.name}

{/* Search and filters */}
setSearchQuery(e.target.value)} />
{hasSystemGroups && ( )}
{/* Error state */} {error &&
{error}
} {/* Loading state */} {isLoading && (
)} {/* Group list */} {!isLoading && !error && (
{filteredGroups.length === 0 ? (

{searchQuery ? 'No groups match your search' : 'No groups found'}

) : ( {filteredGroups.map((group) => ( ))}
Name Permissions Parent Created Actions
{group.name}
{group.is_system && ( System )}
{group.permissions.includes('*') ? ( Superadmin (*) ) : ( <> {group.permissions.slice(0, 3).map((perm) => ( {perm.length > 25 ? `${perm.substring(0, 22)}...` : perm} ))} {group.permissions.length > 3 && ( +{group.permissions.length - 3} more )} )} {group.permissions.length === 0 && ( No permissions )}
{getParentGroupName(group.parent_group_id) || ( - )} {new Date(group.created_at).toLocaleDateString()} {group.is_system ? ( Protected ) : (
)}
)}
)} {/* Modals */} {showCreateModal && ( !g.is_system)} onClose={() => setShowCreateModal(false)} onSave={handleSave} /> )} {editingGroup && !editingGroup.is_system && ( !g.is_system)} onClose={() => setEditingGroup(null)} onSave={handleSave} /> )} {deletingGroup && !deletingGroup.is_system && ( setDeletingGroup(null)} onConfirm={handleDelete} /> )}
) } export default Groups