import type { BookabilityBlocker } from './bookabilityReasons'; // WHO can fix a cause, and WHERE - so a viewer who cannot act is told who can, instead of being handed a link // they will be denied. // // This is the dead end the whole placement decision exists to prevent. HR authors schedules and staff // services (the fragile heart of the chain) but may hold only VIEW on the organization section - so it will // routinely see causes it cannot fix. Dangling a link at that user is worse than saying nothing: it wastes a // click and teaches them the panel is unreliable. // // The sections match the dashboard's own nav sections, which is also what `canViewStaffSection` / // `canManage` are keyed on - so "can I act on this?" is answered with the same vocabulary the app already uses. export type FixSection = 'organization' | 'staff' | 'schedules'; export interface FixOwner { section: FixSection; /** Human-facing path, matching the dashboard's own labels. */ where: string; } const ORGANIZATION_SERVICES: FixOwner = { section: 'organization', where: 'Organization → Healthcare Services', }; const ORGANIZATION_POLICIES: FixOwner = { section: 'organization', where: 'Organization → appointment policies', }; const STAFF_ROLES: FixOwner = { section: 'staff', where: 'Staff → the provider → Roles' }; // Named separately from STAFF_ROLES because the seat is a DIFFERENT tab, and sending somebody to Roles // for it is the specific dead end this avoids: the guidance named a screen where the // control is not. const STAFF_WORKSPACES: FixOwner = { section: 'staff', where: 'Staff → the provider → Workspaces → Enable as a clinician', }; const STAFF_SERVICES: FixOwner = { section: 'staff', where: 'Staff → the provider → Services' }; const SCHEDULES: FixOwner = { section: 'schedules', where: 'Schedules → the schedule block' }; const OWNERS: Record = { ROLE_INACTIVE: STAFF_ROLES, NO_CLINICIAN_SEAT: STAFF_WORKSPACES, BLOCK_BOOKING_DISABLED: SCHEDULES, BLOCK_SELF_BOOKING_DISABLED: SCHEDULES, SERVICE_INACTIVE: ORGANIZATION_SERVICES, SERVICE_NOT_OFFERED_AT_LOCATION: ORGANIZATION_SERVICES, NOT_SELF_BOOKABLE_ORG: ORGANIZATION_SERVICES, NOT_SELF_BOOKABLE_PRACTITIONER: STAFF_SERVICES, NOT_SELF_BOOKABLE_BLOCK: SCHEDULES, PRACTITIONER_NOT_OFFERING_SERVICE: STAFF_SERVICES, BLOCK_DOES_NOT_OFFER_SERVICE: SCHEDULES, NO_SCHEDULE_AT_LOCATION: SCHEDULES, NEW_PATIENT_EXAM_NO_ELIGIBLE_BLOCK: ORGANIZATION_POLICIES, BLOCK_NO_HOURS: SCHEDULES, BLOCK_NO_PLANNING_HORIZON: SCHEDULES, BLOCK_HORIZON_EXPIRED: SCHEDULES, // TELEHEALTH'S THREE GATES, EACH OWNED BY THE SCREEN THAT ACTUALLY HOLDS ITS CONTROL. They were added // to the blocker union and given patient-facing copy without being given an owner here, so every one // of them fell through `fixOwnerFor`'s default and sent the reader to the schedule block - which holds // exactly one of the three. Naming a screen where the control is not is the dead end this table exists // to prevent, and it is worse than saying nothing. // // Verified against the running product rather than the design alone: the org toggle is the "Offer // video visits" checkbox under Organization -> How this practice takes appointments; per-service video // is the practitioner's own "Do you deliver this service by video?" on their Services Offered tab; and // the block's is "Also offer this block by video" on the schedule block itself. ORG_TELEHEALTH_DISABLED: ORGANIZATION_POLICIES, SERVICE_NOT_TELEHEALTH_ELIGIBLE: STAFF_SERVICES, NO_VIRTUAL_LOCATION_ON_BLOCK: SCHEDULES, }; export const fixOwnerFor = (code: BookabilityBlocker): FixOwner => OWNERS[code] ?? SCHEDULES; export interface FixGuidance { /** True when the viewer can go and change it themselves. */ actionable: boolean; text: string; } export function fixGuidance(code: BookabilityBlocker, viewerCanManageSection: boolean): FixGuidance { const owner = fixOwnerFor(code); return viewerCanManageSection ? { actionable: true, text: `Change this under ${owner.where}.` } : { actionable: false, text: `An administrator can change this under ${owner.where}.` }; }