import { useState, useEffect, useMemo } from 'react'; import { Title, Text, Card, Stack, Group, Table, Checkbox, Button, Alert, Badge, Tooltip, Divider, Loader, Center, } from '@mantine/core'; import { notifications } from '@mantine/notifications'; import { IconShieldCheck, IconRefresh, IconInfoCircle } from '@tabler/icons-react'; import { useAuthStore } from '../../stores/authStore'; import { CAPABILITY_AREAS } from '../../permissions/capabilities'; import { DEFAULT_ROLE_CAPABILITIES } from '../../permissions/default-role-capabilities'; import api from '../../services/api'; /** Roles shown as columns (homeowner hidden from UI per product decision) */ const DISPLAY_ROLES = [ { value: 'president', label: 'President' }, { value: 'vice_president', label: 'Vice President' }, { value: 'treasurer', label: 'Treasurer' }, { value: 'secretary', label: 'Secretary' }, { value: 'member_at_large', label: 'Member at Large' }, { value: 'manager', label: 'Property Manager' }, { value: 'viewer', label: 'Viewer' }, ]; interface PermissionOverrides { [role: string]: { grant?: string[]; revoke?: string[]; }; } function buildCheckedState(overrides: PermissionOverrides): Record> { const state: Record> = {}; for (const role of DISPLAY_ROLES) { const defaults = new Set(DEFAULT_ROLE_CAPABILITIES[role.value] || []); const roleOverride = overrides[role.value]; if (roleOverride?.grant) { for (const cap of roleOverride.grant) defaults.add(cap); } if (roleOverride?.revoke) { for (const cap of roleOverride.revoke) defaults.delete(cap); } state[role.value] = {}; for (const area of CAPABILITY_AREAS) { for (const cap of area.capabilities) { state[role.value][cap.key] = defaults.has(cap.key); } } } return state; } function buildOverridesFromState(checkedState: Record>): PermissionOverrides { const overrides: PermissionOverrides = {}; for (const role of DISPLAY_ROLES) { const defaults = new Set(DEFAULT_ROLE_CAPABILITIES[role.value] || []); const grant: string[] = []; const revoke: string[] = []; for (const [cap, checked] of Object.entries(checkedState[role.value] || {})) { const isDefault = defaults.has(cap); if (checked && !isDefault) grant.push(cap); if (!checked && isDefault) revoke.push(cap); } if (grant.length > 0 || revoke.length > 0) { overrides[role.value] = {}; if (grant.length > 0) overrides[role.value].grant = grant; if (revoke.length > 0) overrides[role.value].revoke = revoke; } } return overrides; } export function PermissionSettingsPage() { const { currentOrg, setOrgSettings } = useAuthStore(); const [saving, setSaving] = useState(false); const [loaded, setLoaded] = useState(false); const existingOverrides: PermissionOverrides = useMemo( () => currentOrg?.settings?.permissionOverrides || {}, [currentOrg?.settings?.permissionOverrides], ); const [checkedState, setCheckedState] = useState>>(() => buildCheckedState(existingOverrides), ); useEffect(() => { setCheckedState(buildCheckedState(existingOverrides)); setLoaded(true); }, [existingOverrides]); const currentOverrides = useMemo(() => buildOverridesFromState(checkedState), [checkedState]); const hasChanges = JSON.stringify(currentOverrides) !== JSON.stringify(existingOverrides); const toggleCapability = (role: string, cap: string) => { setCheckedState((prev) => ({ ...prev, [role]: { ...prev[role], [cap]: !prev[role]?.[cap], }, })); }; const resetRole = (roleValue: string) => { const defaults = new Set(DEFAULT_ROLE_CAPABILITIES[roleValue] || []); const newRoleState: Record = {}; for (const area of CAPABILITY_AREAS) { for (const cap of area.capabilities) { newRoleState[cap.key] = defaults.has(cap.key); } } setCheckedState((prev) => ({ ...prev, [roleValue]: newRoleState })); }; const handleSave = async () => { setSaving(true); try { const overrides = buildOverridesFromState(checkedState); const res = await api.patch('/organizations/settings', { permissionOverrides: overrides }); setOrgSettings(res.data); notifications.show({ title: 'Saved', message: 'Permission settings updated. Members will see changes on next login or page refresh.', color: 'green' }); } catch (err: any) { notifications.show({ title: 'Error', message: err.response?.data?.message || 'Failed to save', color: 'red' }); } finally { setSaving(false); } }; const isOverridden = (role: string, cap: string) => { const isDefault = (DEFAULT_ROLE_CAPABILITIES[role] || []).includes(cap); const isChecked = checkedState[role]?.[cap] ?? false; return isChecked !== isDefault; }; if (!loaded) { return
; } return ( Role Permissions } color="blue" variant="light"> Customize which capabilities each role has in your organization. Highlighted cells differ from the system defaults. Use "Reset" to revert a role to defaults. The Viewer role is always read-only regardless of settings. Capability {DISPLAY_ROLES.map((role) => ( {role.label} ))} {CAPABILITY_AREAS.map((area) => ( <> {area.label} {area.capabilities.map((cap) => ( {cap.label} {DISPLAY_ROLES.map((role) => { const checked = checkedState[role.value]?.[cap.key] ?? false; const overridden = isOverridden(role.value, cap.key); return ( toggleCapability(role.value, cap.key)} styles={{ input: { cursor: 'pointer' } }} /> ); })} ))} ))}
{hasChanges && ( You have unsaved changes. Click "Save Changes" to apply. )}
); }