import type { Coding, Organization } from 'fhir/r4'; import type { PractitionerWithRoles } from '../services/PractitionerService'; import { DEFAULT_SCHEDULES_SCOPE, STAFF_PERMISSION_DEFINITIONS, floorManagesSchedules, getBuiltInPermissionLevel, getCodingKey, getHigherPermissionLevel, getOrganizationRolePermissionProfile, getOrganizationRolePermissionProfiles, getRoleDefaultPermissionLevel, type OrganizationRolePermissionProfile, type StaffPermissionLevel, type StaffPermissionScope, type StaffPermissionSection, } from './staffAccess'; import { CLINICAL_ORGANIZATION_ROLE_CODES, STAFF_ORGANIZATION_ROLE_SYSTEM, STAFF_WORKSPACES, getCoupledWorkspaceForOrgRole, getPractitionerRoleOrganizationCodings, type StaffWorkspace, } from './staffRoleCatalog'; // The admin-v2 permission matrix, as pure functions over the organization so the page and its // tests read the same model. Two floors meet in a cell and they must not be confused: // - the ROLE default (built-in role codes) is applied by the resolver in every workspace, so it is // safe to store and cannot be lowered; // - the WORKSPACE floor (Admin always manage, front desk always views schedules) applies only while // someone works in that workspace, so it is shown but never written into the role's profile. export const MATRIX_SECTIONS: { key: StaffPermissionSection; label: string }[] = [ { key: 'organization', label: 'Practice' }, { key: 'staff', label: 'Staff' }, { key: 'schedules', label: 'Schedules' }, { key: 'billing', label: 'Billing' }, { key: 'campaigns', label: 'Campaigns' }, { key: 'messages', label: 'Shared inboxes' }, { key: 'diagnostics', label: 'Lab & imaging' }, { key: 'records', label: 'Records requests' }, { key: 'records-release', label: 'May release records' }, { key: 'clinicians', label: 'May enable seats' }, ]; export type RoleRow = { key: string; coding: Coding; people: number; workspace?: StaffWorkspace; }; export type RolePermissionChange = | { kind: 'level'; section: StaffPermissionSection; level: StaffPermissionLevel } | { kind: 'scope'; scope: StaffPermissionScope } | { kind: 'practitionerWorkspace'; value: boolean }; // The coding the server gives a practice's founding admin along with the Admin workspace, so its row // is drawn in that workspace. const ADMINISTRATOR_ROLE_CODE = 'healthcare-administrator'; // What the server grants the Admins master role on every route, whatever the profile says. Lab // results and clinician seats are read from the profile itself, so they stay the practice's choice. const ADMIN_REQUIRED_SECTIONS = new Set(['organization', 'staff', 'schedules', 'billing', 'campaigns', 'messages']); const workspaceOf = (coding: Coding): StaffWorkspace | undefined => coding.code === ADMINISTRATOR_ROLE_CODE && (!coding.system || coding.system === STAFF_ORGANIZATION_ROLE_SYSTEM) ? STAFF_WORKSPACES.ADMIN : getCoupledWorkspaceForOrgRole(coding.code ?? ''); /** Roles someone actually holds, plus any the practice has configured, each with its head count. */ export const buildRoleRows = ( practitioners: PractitionerWithRoles[], organization: Organization | null | undefined, ): RoleRow[] => { const rows = new Map }>(); const add = (coding: Coding, holder?: string) => { const key = getCodingKey(coding); if (!key) return; const row = rows.get(key) ?? { coding, holders: new Set() }; if (holder) row.holders.add(holder); rows.set(key, row); }; practitioners.forEach(({ practitioner, roles }) => { roles .filter((role) => role.active !== false) .forEach((role) => getPractitionerRoleOrganizationCodings(role).forEach((coding) => add(coding, practitioner.id))); }); getOrganizationRolePermissionProfiles(organization).forEach((profile) => add(profile.role)); const workspaceRank = (w?: StaffWorkspace) => (w === STAFF_WORKSPACES.ADMIN ? 0 : 1); return [...rows.entries()] .map(([key, { coding, holders }]) => ({ key, coding, people: holders.size, workspace: workspaceOf(coding) })) .sort((a, b) => workspaceRank(a.workspace) - workspaceRank(b.workspace) || b.people - a.people || (a.coding.display ?? '').localeCompare(b.coding.display ?? '')); }; /** What this role's own setting is, as the resolver reads it: configured or role default, never below it. */ export const roleStoredLevel = ( organization: Organization | null | undefined, coding: Coding, section: StaffPermissionSection, ): StaffPermissionLevel => { const profile = getOrganizationRolePermissionProfile(organization, coding); const configured = getHigherPermissionLevel(profile?.permissions[section] ?? 'none', getRoleDefaultPermissionLevel(coding, section)); // Every staff role may see its own schedule until the practice configures the role. return section === 'schedules' && !profile ? getHigherPermissionLevel(configured, 'view') : configured; }; /** The lowest level the matrix may show for this cell: role default and workspace floor together. */ export const workspaceSectionFloor = (coding: Coding, section: StaffPermissionSection): StaffPermissionLevel => workspaceOf(coding) === STAFF_WORKSPACES.ADMIN && !ADMIN_REQUIRED_SECTIONS.has(section) ? 'none' : getBuiltInPermissionLevel(workspaceOf(coding), section) ?? 'none'; /** The lowest level the matrix may offer for this cell: role default and workspace floor together. */ export const roleSectionFloor = (coding: Coding, section: StaffPermissionSection): StaffPermissionLevel => getHigherPermissionLevel(workspaceSectionFloor(coding, section), getRoleDefaultPermissionLevel(coding, section)); /** What applies to someone holding this role in its own workspace. */ export const roleSectionLevel = ( organization: Organization | null | undefined, coding: Coding, section: StaffPermissionSection, ): StaffPermissionLevel => getHigherPermissionLevel(roleStoredLevel(organization, coding, section), roleSectionFloor(coding, section)); /** Admin and the front desk always see everyone's schedules, so there is no scope to choose for them. */ export const roleSchedulesScopeIsFixed = (coding: Coding): boolean => { const workspace = workspaceOf(coding); return workspace === STAFF_WORKSPACES.ADMIN || workspace === STAFF_WORKSPACES.RECEPTIONIST; }; export const roleSchedulesScope = (organization: Organization | null | undefined, coding: Coding): StaffPermissionScope => { const profile = getOrganizationRolePermissionProfile(organization, coding); if (profile?.schedulesScope) return profile.schedulesScope; if (roleSchedulesScopeIsFixed(coding)) return 'all'; if (!profile && floorManagesSchedules(coding)) return 'all'; // A profile saved before the scope existed reads as `all`; an unconfigured role sees its own. return profile ? DEFAULT_SCHEDULES_SCOPE : 'own'; }; export const rolePractitionerWorkspace = (organization: Organization | null | undefined, coding: Coding): boolean => getOrganizationRolePermissionProfile(organization, coding)?.practitionerWorkspace ?? CLINICAL_ORGANIZATION_ROLE_CODES.has(coding.code ?? ''); /** The complete profile to write for one change, seeded from what currently applies to the role. */ export const nextRoleProfile = ( organization: Organization | null | undefined, coding: Coding, change: RolePermissionChange, ): OrganizationRolePermissionProfile => { const permissions = Object.fromEntries( STAFF_PERMISSION_DEFINITIONS.map((definition) => [definition.key, roleStoredLevel(organization, coding, definition.key)]), ) as Record; const profile: OrganizationRolePermissionProfile = { role: coding, permissions, schedulesScope: roleSchedulesScope(organization, coding), practitionerWorkspace: rolePractitionerWorkspace(organization, coding), }; if (change.kind === 'level') { profile.permissions[change.section] = getHigherPermissionLevel(change.level, getRoleDefaultPermissionLevel(coding, change.section)); } else if (change.kind === 'scope') { profile.schedulesScope = change.scope; } else { profile.practitionerWorkspace = change.value; } return profile; };