'use client'; import { useEffect, useMemo, useState } from 'react'; type PermissionKey = 'view' | 'edit' | 'delete' | 'manage'; type RoleId = string; type PermissionValue = boolean | null; type PermissionRow = { id: string; label: string; icon: string; description: string; values: Record; }; type RoleDefinition = { id: RoleId; name: string; badge: string; tone: 'primary' | 'secondary' | 'tertiary'; summary: string; usersAssigned: number; icon: string; permissionRows: PermissionRow[]; }; const permissionColumns: Array<{ key: PermissionKey; label: string }> = [ { key: 'view', label: 'View' }, { key: 'edit', label: 'Create/Edit' }, { key: 'delete', label: 'Delete' }, { key: 'manage', label: 'Manage All' }, ]; function cloneRoleSet(roles: RoleDefinition[]) { return roles.map((role) => ({ ...role, permissionRows: role.permissionRows.map((row) => ({ ...row, values: { ...row.values }, })), })); } function roleBadge(role: RoleDefinition, isEditing: boolean) { if (isEditing) { return { label: 'Editing Now', tone: 'editing' as const }; } return { label: role.badge, tone: role.tone }; } type AuditHighlight = { id: string; icon: string; title: string; description: string; time: string; }; type Props = { initialRoles: RoleDefinition[]; initialAuditHighlights: AuditHighlight[]; }; export function RolesPermissionsBoard({ initialRoles, initialAuditHighlights }: Props) { const [roles, setRoles] = useState(() => cloneRoleSet(initialRoles)); const [savedRoles, setSavedRoles] = useState(() => cloneRoleSet(initialRoles)); const [selectedRoleId, setSelectedRoleId] = useState(initialRoles[0]?.id || ''); const [editingRoleId, setEditingRoleId] = useState(null); const [saveMessage, setSaveMessage] = useState(''); const [isCreateOpen, setIsCreateOpen] = useState(false); const [newRoleName, setNewRoleName] = useState(''); const [newRoleSummary, setNewRoleSummary] = useState(''); const [isSaving, setIsSaving] = useState(false); const selectedRole = useMemo( () => roles.find((role) => role.id === selectedRoleId) ?? roles[0], [roles, selectedRoleId], ); const isEditingSelectedRole = selectedRole ? editingRoleId === selectedRole.id : false; useEffect(() => { const nextRoles = cloneRoleSet(initialRoles); setRoles(nextRoles); setSavedRoles(nextRoles); setSelectedRoleId((current) => nextRoles.some((role) => role.id === current) ? current : nextRoles[0]?.id || '', ); setEditingRoleId((current) => nextRoles.some((role) => role.id === current) ? current : null, ); }, [initialRoles]); function updatePermission(rowId: string, permission: PermissionKey, nextValue: boolean) { if (!isEditingSelectedRole) { return; } setRoles((currentRoles) => currentRoles.map((role) => role.id !== selectedRoleId ? role : { ...role, permissionRows: role.permissionRows.map((row) => row.id === rowId ? { ...row, values: { ...row.values, [permission]: nextValue } } : row, ), }, ), ); setSaveMessage(''); } function beginEdit(roleId: RoleId) { setSelectedRoleId(roleId); setEditingRoleId(roleId); setSaveMessage(''); } function resetChanges() { setRoles(cloneRoleSet(savedRoles)); setEditingRoleId(null); setSaveMessage('Changes discarded.'); } async function saveChanges() { if (!selectedRole) { return; } setIsSaving(true); try { const response = await fetch(`/api/roles/${selectedRole.id}`, { method: 'PATCH', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ permissions: selectedRole.permissionRows, summary: selectedRole.summary, badge: selectedRole.badge, tone: selectedRole.tone, icon: selectedRole.icon, }), }); const payload = await response.json(); if (!response.ok) { setSaveMessage(payload.message || 'Failed to save role'); return; } setRoles((current) => current.map((role) => role.id === payload.id ? { ...role, permissionRows: payload.permissions, usersAssigned: payload.usersAssigned, summary: payload.summary, badge: payload.badge, tone: payload.tone, icon: payload.icon, } : role, ), ); setSavedRoles((current) => current.map((role) => role.id === payload.id ? { ...role, permissionRows: payload.permissions, usersAssigned: payload.usersAssigned, summary: payload.summary, badge: payload.badge, tone: payload.tone, icon: payload.icon, } : role, ), ); setEditingRoleId(null); setSaveMessage(`Permission matrix for ${payload.name} saved.`); } finally { setIsSaving(false); } } async function createRole() { const trimmedName = newRoleName.trim(); const trimmedSummary = newRoleSummary.trim(); if (!trimmedName) { setSaveMessage('Role name is required.'); return; } const id = trimmedName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') || `role-${Date.now()}`; const nextRole: RoleDefinition = { id, name: trimmedName, badge: 'Custom', tone: 'secondary', summary: trimmedSummary || 'Custom role for a focused operational access policy.', usersAssigned: 0, icon: 'verified_user', permissionRows: [ { id: 'campaigns', label: 'Manage Campaigns', icon: 'campaign', description: 'Broadcasts and outbound campaign controls.', values: { view: true, edit: false, delete: false, manage: false } }, { id: 'analytics', label: 'View Analytics', icon: 'monitoring', description: 'KPI, trends, and performance dashboards.', values: { view: true, edit: null, delete: null, manage: false } }, { id: 'settings', label: 'Edit Settings', icon: 'settings', description: 'Providers, secrets, and environment-facing settings.', values: { view: false, edit: false, delete: false, manage: false } }, { id: 'billing', label: 'Billing & Invoices', icon: 'payments', description: 'Plan usage, invoices, and billing visibility.', values: { view: false, edit: null, delete: null, manage: false } }, ], }; setIsSaving(true); try { const response = await fetch('/api/roles', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: trimmedName, summary: trimmedSummary || 'Custom role for a focused operational access policy.', badge: 'Custom', tone: 'secondary', icon: 'verified_user', permissions: nextRole.permissionRows, }), }); const payload = await response.json(); if (!response.ok) { setSaveMessage(payload.message || 'Failed to create role'); return; } const createdRole = { id: payload.id, name: payload.name, badge: payload.badge, tone: payload.tone, summary: payload.summary, usersAssigned: payload.usersAssigned, icon: payload.icon, permissionRows: payload.permissions, } satisfies RoleDefinition; const nextRoles = [createdRole, ...cloneRoleSet(roles)]; setRoles(nextRoles); setSavedRoles(cloneRoleSet(nextRoles)); setSelectedRoleId(payload.id); setEditingRoleId(payload.id); setIsCreateOpen(false); setNewRoleName(''); setNewRoleSummary(''); setSaveMessage(`Role ${payload.name} created and opened for editing.`); } finally { setIsSaving(false); } } if (!selectedRole) { return (

Roles

No roles available

Create the first role to start defining access permissions.

); } return ( <>
{isCreateOpen ? (

New Role

Create a custom access profile

Define a clear role name and a short summary so your team can understand the scope immediately.