// NUCC Provider Taxonomy: https://www.nucc.org/index.php/code-sets-mainmenu-41/provider-taxonomy-mainmenu-40 // SNOMED CT specialties: https://browser.ihtsdotools.org/ // // THE SNOMED CODES HAVE BEEN VERIFIED, AND 19 OF THE 40 ARE WRONG. Read the block on // `snomedSpecialty` below before you touch, render, or "finish" any specialty coding in this file. // (This line used to read "Verify codes against current releases before production deployment." It was // done - on 2026-07-28, against our own Snowstorm - and it failed. An unmet to-do and a failed check are // different facts, and the to-do wording made the second look like the first.) import type { Coding, PractitionerRole } from 'fhir/r4'; // --------------------------------------------------------------------------- // System URLs // --------------------------------------------------------------------------- export const SNOMED_CT_SYSTEM = 'http://snomed.info/sct'; export const NUCC_TAXONOMY_SYSTEM = 'http://nucc.org/provider-taxonomy'; export const PRACTICE_TYPE_SYSTEM = 'https://akashichealthrecords.com/fhir/CodeSystem/practice-type'; export const STAFF_ORGANIZATION_ROLE_SYSTEM = 'https://akashichealthrecords.com/fhir/CodeSystem/organization-role'; export const STAFF_LEADERSHIP_DESIGNATION_SYSTEM = 'https://akashichealthrecords.com/fhir/CodeSystem/leadership-designation'; export const STAFF_TRAINING_LEVEL_SYSTEM = 'https://akashichealthrecords.com/fhir/CodeSystem/training-level'; export const STAFF_PRIMARY_ROLE_EXTENSION_URL = 'https://akashichealthrecords.com/fhir/StructureDefinition/practitioner-role-primary-role'; export const STAFF_TRAINING_LEVEL_EXTENSION_URL = 'https://akashichealthrecords.com/fhir/StructureDefinition/practitioner-role-training-level'; export const STAFF_WORKSPACE_EXTENSION_URL = 'https://akashichealthrecords.com/fhir/StructureDefinition/practitioner-role-workspace'; export const STAFF_DEFAULT_WORKSPACE_EXTENSION_URL = 'https://akashichealthrecords.com/fhir/StructureDefinition/practitioner-role-default-workspace'; export const STAFF_AUTHENTICATION_GROUP = 'Staff'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type StaffRoleOption = { system: string; code: string; display: string; // NUCC Provider Taxonomy code - emitted as secondary coding for interoperability. nucc?: string; // SNOMED CT specialty code - auto-populated to PractitionerRole.specialty on save. // // ============================================================================================ // 13 OF THESE 40 CODES ARE STILL KNOWN-WRONG. THE ONLY THING KEEPING THAT INVISIBLE IS THAT // NOTHING EVER RENDERS THEM. DO NOT POPULATE `coding[0].display`. (.) // ============================================================================================ // // Every distinct code was looked up against our own Snowstorm (SNOMED CT release 2026-03-01, // FHIR `CodeSystem/$lookup`) on 2026-07-28, and the bad ones were RE-CONFIRMED on 2026-08-19 // against the same release - every one still said the same wrong thing. Snowstorm answered // correctly every time, including the 404s. The audit target is THIS CATALOG, not the // terminology server (Spencer, 2026-07-28). // // CORRECTED 2026-08-19 - six where the right concept needed no judgement, only a lookup. // Each replacement was itself looked up first: all six resolve AND are active. The old value // is recorded beside each entry below, because the record of a bad code is what stops the // next reader taking it for a deliberate choice and putting it back. // // dentist 160696009 "Transport distance too great" 722163006 |Dentistry| // pediatric-dentist 394608004 "Orthodontics" 394607009 |Pediatric dentistry| // orthodontist 394821009 "Occupational medicine" 394608004 |Orthodontics| // endodontist 394733009 "Medical specialty" 408441001 |Endodontics - specialty| // prosthodontist 394732004 "Surgical specialty" 408460008 |Prosthodontics| // dietitian 394590007 "Thoracic medicine" (retired) 722164000 |Dietetics and nutrition| // // `orthodontist` and `pediatric-dentist` were SWAPPED - each held a real dental specialty // belonging to the other. That is why both read as plausible. // // STILL WRONG - 3 that need a DECISION, not a lookup. Candidates are tabled in // `akashic-docs/_CLAUDE/2026-08-19-snomed-specialty-candidates.md`; do not pick one here. // // orthopedic-surgeon 394617004 "Result" (retired). Three defensible successors exist and // they are not interchangeable. // psychologist 394913002 "Psychotherapy (specialty)" - a THERAPY, not the profession. // optometrist 394813003 "Medical ophthalmology" - a different profession, and // SNOMED has NO optometry entry in the clinical-specialty // hierarchy at all (checked 2026-08-19). Leaving it unset is // a legitimate answer, which is exactly why it is a decision. // // STILL WRONG - 10 roles carrying a code that does not resolve AT ALL (HTTP 404), and these // are the ones genuinely blocked on Toncy's SNOMED-vs-HAPI decision: "absent from OUR release" // is a statement about this server, not about SNOMED CT. // anesthesiologist AND certified-registered-nurse-anesthetist (both 394577003) - // audiologist (310179007) - certified-nursing-assistant (224542000) - // occupational-therapist (80467003) - periodontist (394613001) - pharmacist (394662009) - // podiatrist (394539000) - speech-language-pathologist (159026002) - urologist (394612006) // // WHY THE REST IS STILL HERE, rather than fixed: choosing the concept that correctly // represents "Periodontist" or "Certified Registered Nurse Anesthetist" is a TERMINOLOGY // decision with clinical meaning, not an engineering one - and getting it subtly wrong // (optometrist "Medical ophthalmology") is exactly how the values here came to look // plausible. Several of these professions may have no SNOMED *specialty* concept at all, only // a practitioner-TYPE concept in another hierarchy; leaving `snomedSpecialty` unset for those // roles is a legitimate outcome, because `text` already carries the human-readable role. // // AND WHY THE SIX ABOVE WERE *NOT* HELD FOR THE SAME REASON - the distinction is the whole // reason this card moved at all. The 404 group waits on Toncy's possible SNOMED-vs-HAPI split, // because "no such code in our release" is a claim about the SERVER. But a server does not // change what a code MEANS: 160696009 is "Transport distance too great" in SNOMED CT itself, // and will still be after any migration. Correcting a provably-wrong code is not // server-dependent work and cannot be invalidated by the split, so holding it behind that // decision was caution applied to the wrong group. // // WHY IT IS NOT USER-VISIBLE, AND WHAT WOULD MAKE IT SO - this is the load-bearing part. // `deriveAutoSpecialtyCodings` emits `{ coding: [{ system, code }], text: }`. // The SNOMED coding carries NO `display`, and every consumer reads `text || coding[0].display` // - so the role's own words win and the concept name is never read. Populating // `coding[0].display` from these codes looks like an obvious improvement and finishes a field // that reads as merely unfinished. It would put "Transport distance too great" on a dentist's // booking profile, in front of patients. The blank has been masking this for as long as this // catalog has existed; the guard in `src/utils/__tests__/autoSpecialtyDisplayMask.test.ts` // makes removing the mask a deliberate act instead of an accident. // // The codes are ALREADY written into every PractitionerRole we save and are readable by any // external FHIR consumer. The mask hides them from OUR screens; it does not make them right. snomedSpecialty?: string; // When true, the specialty picker is surfaced for this role (used for generic roles // like 'physician' where the subspecialty cannot be inferred from the role alone). specialtyRequired?: boolean; }; export type StaffRoleGroup = { label: string; items: StaffRoleOption[]; }; // Extended group type used for patient-facing role catalog. // Structurally compatible with StaffRoleGroup and RelationValueSetList. export type StaffRoleDisciplineGroup = StaffRoleGroup & { discipline: string; patientFacing: boolean; }; export type StaffWorkspaceOption = { key: string; name: string; description?: string; }; // Practice type stored in Organization.type using the PRACTICE_TYPE_SYSTEM. // disciplines lists discipline codes in priority order for the role picker UI. export type PracticeTypeOption = { system: string; code: string; display: string; disciplines: string[]; }; // --------------------------------------------------------------------------- // Workspace constants (unchanged) // --------------------------------------------------------------------------- export const STAFF_WORKSPACES = { PRACTITIONER: 'Practitioner', RECEPTIONIST: 'Receptionist', ADMIN: 'Admin', PLATFORM_MANAGER: 'PlatformManager', GENERIC: 'Generic', HR: 'HR', BILLING: 'Billing', // S4c final review, I4. 'Marketing' was added to staffAccess.ts's // PERMISSION_DRIVEN_WORKSPACES but never to this union, so no caller could ever legally pass // it - the entry was dead, and its test only passed because resolveStaffPermissionLevel takes // `workspace?: string` and so accepted a value the type system said could not exist. MARKETING: 'Marketing', // -- the referral coordinator's workspace. A workspace is registered in SEVEN places // and five fail silently; `everyWorkspaceIsFullyRegistered` and `everyWorkspaceBuildsANav` name any // one that is missed, and two Records keyed on the union make the other two compile errors. REFERRALS: 'Referrals', // The two diagnostic hats. THEY ARE A LENS, NOT A FENCE, and that distinction is the whole // design: both resolve to the same `diagnostics` permission, so what separates them is which // work each one is shown, never what either is allowed to reach. A tech granted diagnostics can // switch workspace and see the other kind -- and should be able to, because the section is what // the practice actually granted. Splitting the fence would mean two permission sections and a // server change; this is deliberately not that. LAB_TECH: 'LabTech', IMAGING_TECH: 'ImagingTech', // Records requests; what it shows is whatever records grant the practice gave. RECORDS: 'Records', } as const; export type StaffWorkspace = typeof STAFF_WORKSPACES[keyof typeof STAFF_WORKSPACES]; export const getOrganizationDefinedRoleSystem = (organizationId?: string) => organizationId ? `${STAFF_ORGANIZATION_ROLE_SYSTEM}/${encodeURIComponent(organizationId)}` : `${STAFF_ORGANIZATION_ROLE_SYSTEM}/custom`; export const BUILT_IN_WORKSPACE_OPTIONS: StaffWorkspaceOption[] = [ { name: '', key: '' }, { name: 'Practitioner', key: 'Practitioners', description: 'Clinical workspace for physicians, nurses, and other care-team members.', }, { name: 'Receptionist', key: 'Receptionists', description: 'Front-desk workspace for appointments, patients, and communications.', }, { name: 'Admin', key: 'Admins', description: 'Organization administration workspace for staff, services, and settings.', }, { name: 'Platform Manager', key: 'PlatformManagers', description: 'Platform-level workspace for shared content and platform operations.', }, { name: 'Permission-based', key: 'Generic', description: 'Starts with Profile only. Organization role permissions determine which other sections are available.', }, { name: 'HR', key: 'HumanResources', description: 'Human-resources workspace for managing staff and schedules.', }, { name: 'Billing', key: 'Billing', description: 'Billing workspace. Payment and billing tools are coming soon.', }, // - exposed for beta at Spencer's direction (2026-08-02). The description is // deliberately explicit that the tools are still landing, mirroring Billing above: // S4c shipped the SERVER side (segmentation + the counts-only audience preview) but S4d (the // campaign entity and sender) and S4e (reporting) are not built, so a user who selects this // today gets the workspace and its permissions, not a campaign composer. Saying so here is the // difference between a known placeholder and an apparently broken feature. { name: 'Referrals', key: 'Referrals', description: 'Referral coordination workspace - referrals this practice sends and receives.', }, { name: 'Lab technician', key: 'LabTechnicians', description: 'Specimen workspace - lab orders this practice is working, and their results.', }, { name: 'Imaging technician', key: 'ImagingTechnicians', description: 'Imaging workspace - studies this practice is working, and their results.', }, { name: 'Records', key: 'Records', description: 'Records workspace - requests for records held here, and records this practice has asked for.', }, { name: 'Marketing', key: 'Marketing', description: 'Marketing workspace for patient campaigns. Campaign tools are coming soon.', }, ]; const STAFF_WORKSPACE_DISPLAY_ORDER: StaffWorkspace[] = [ STAFF_WORKSPACES.PRACTITIONER, STAFF_WORKSPACES.RECEPTIONIST, STAFF_WORKSPACES.ADMIN, STAFF_WORKSPACES.PLATFORM_MANAGER, STAFF_WORKSPACES.GENERIC, STAFF_WORKSPACES.HR, STAFF_WORKSPACES.BILLING, STAFF_WORKSPACES.MARKETING, STAFF_WORKSPACES.REFERRALS, STAFF_WORKSPACES.LAB_TECH, STAFF_WORKSPACES.IMAGING_TECH, STAFF_WORKSPACES.RECORDS, ]; export const sortStaffWorkspaces = ( workspaces: StaffWorkspace[], ): StaffWorkspace[] => [...workspaces].sort((left, right) => { const leftIndex = STAFF_WORKSPACE_DISPLAY_ORDER.indexOf(left); const rightIndex = STAFF_WORKSPACE_DISPLAY_ORDER.indexOf(right); if (leftIndex === -1 && rightIndex === -1) return left.localeCompare(right); if (leftIndex === -1) return 1; if (rightIndex === -1) return -1; return leftIndex - rightIndex; }); const workspaceFromFormValue: Record = { Practitioners: STAFF_WORKSPACES.PRACTITIONER, Practitioner: STAFF_WORKSPACES.PRACTITIONER, Receptionists: STAFF_WORKSPACES.RECEPTIONIST, Receptionist: STAFF_WORKSPACES.RECEPTIONIST, Admins: STAFF_WORKSPACES.ADMIN, Admin: STAFF_WORKSPACES.ADMIN, PlatformManagers: STAFF_WORKSPACES.PLATFORM_MANAGER, PlatformManager: STAFF_WORKSPACES.PLATFORM_MANAGER, Generic: STAFF_WORKSPACES.GENERIC, Staff: STAFF_WORKSPACES.GENERIC, HumanResources: STAFF_WORKSPACES.HR, HR: STAFF_WORKSPACES.HR, Billing: STAFF_WORKSPACES.BILLING, Marketing: STAFF_WORKSPACES.MARKETING, Referrals: STAFF_WORKSPACES.REFERRALS, LabTechnicians: STAFF_WORKSPACES.LAB_TECH, ImagingTechnicians: STAFF_WORKSPACES.IMAGING_TECH, Records: STAFF_WORKSPACES.RECORDS, }; const formValueFromWorkspace: Record = { [STAFF_WORKSPACES.PRACTITIONER]: 'Practitioners', [STAFF_WORKSPACES.RECEPTIONIST]: 'Receptionists', [STAFF_WORKSPACES.ADMIN]: 'Admins', [STAFF_WORKSPACES.PLATFORM_MANAGER]: 'PlatformManagers', [STAFF_WORKSPACES.GENERIC]: 'Generic', [STAFF_WORKSPACES.HR]: 'HumanResources', [STAFF_WORKSPACES.BILLING]: 'Billing', [STAFF_WORKSPACES.MARKETING]: 'Marketing', [STAFF_WORKSPACES.REFERRALS]: 'Referrals', [STAFF_WORKSPACES.LAB_TECH]: 'LabTechnicians', [STAFF_WORKSPACES.IMAGING_TECH]: 'ImagingTechnicians', [STAFF_WORKSPACES.RECORDS]: 'Records', }; export const getWorkspaceFromFormValue = (value?: string): StaffWorkspace | undefined => value ? workspaceFromFormValue[value] : undefined; export const getWorkspaceFormValue = (workspace?: string): string => { if (!workspace) return ''; return formValueFromWorkspace[workspace as StaffWorkspace] || ''; }; export const getAuthenticationGroupForWorkspace = (value?: string): string => { const workspace = getWorkspaceFromFormValue(value); if ( workspace === STAFF_WORKSPACES.GENERIC || workspace === STAFF_WORKSPACES.HR || workspace === STAFF_WORKSPACES.BILLING || // Marketing joined the union in S4c's final review (I4). Like HR/Billing/Generic // it is a permission-driven back-office workspace, NOT a Cognito group of its own - without // this branch it would fall through and return the literal 'Marketing' as an auth group. workspace === STAFF_WORKSPACES.MARKETING || // -- Referrals is permission-driven like the four above: it is not a Cognito group of its // own. Without this branch it falls through and returns the literal 'Referrals' as an // authentication group, which is a group that does not exist. workspace === STAFF_WORKSPACES.REFERRALS || // Permission-driven like every workspace above: neither is a Cognito group. Without these // branches the literal 'LabTech' is returned as an authentication group, and no such group // exists. workspace === STAFF_WORKSPACES.LAB_TECH || workspace === STAFF_WORKSPACES.IMAGING_TECH || workspace === STAFF_WORKSPACES.RECORDS ) { return STAFF_AUTHENTICATION_GROUP; } return value || ''; }; // --------------------------------------------------------------------------- // Practice type definitions // disciplines lists discipline codes in UI priority order for this org type. // All other disciplines are still accessible but collapsed/secondary. // --------------------------------------------------------------------------- // @deprecated DEAD - the practice-type / service-category taxonomy is now server-owned // (config/serviceCategories usePracticeTypeOptions). Nothing imports this anymore; remove in a cleanup pass. export const PRACTICE_TYPE_OPTIONS: PracticeTypeOption[] = [ { system: PRACTICE_TYPE_SYSTEM, code: 'primary-care', display: 'Primary Care / Family Practice', disciplines: ['medical', 'nursing', 'allied-health', 'pharmacy', 'mental-health', 'rehabilitation', 'administrative'], }, { system: PRACTICE_TYPE_SYSTEM, code: 'hospital', display: 'Hospital / Medical Center', disciplines: ['medical', 'nursing', 'allied-health', 'pharmacy', 'mental-health', 'rehabilitation', 'specialty-medical', 'administrative'], }, { system: PRACTICE_TYPE_SYSTEM, code: 'dental', display: 'Dental / Oral Health', disciplines: ['dental', 'administrative'], }, { system: PRACTICE_TYPE_SYSTEM, code: 'chiropractic', display: 'Chiropractic', disciplines: ['chiropractic', 'allied-health', 'administrative'], }, { system: PRACTICE_TYPE_SYSTEM, code: 'optometry', display: 'Optometry / Eye Care', disciplines: ['optometry', 'allied-health', 'administrative'], }, { system: PRACTICE_TYPE_SYSTEM, code: 'mental-health', display: 'Mental Health & Behavioral Health', disciplines: ['mental-health', 'allied-health', 'administrative'], }, { system: PRACTICE_TYPE_SYSTEM, code: 'physical-therapy', display: 'Physical Therapy & Rehabilitation', disciplines: ['rehabilitation', 'allied-health', 'administrative'], }, { system: PRACTICE_TYPE_SYSTEM, code: 'pharmacy', display: 'Pharmacy', disciplines: ['pharmacy', 'administrative'], }, { system: PRACTICE_TYPE_SYSTEM, code: 'specialty-medical', display: 'Specialty Medical Practice', disciplines: ['specialty-medical', 'medical', 'nursing', 'allied-health', 'administrative'], }, { system: PRACTICE_TYPE_SYSTEM, code: 'urgent-care', display: 'Urgent Care', disciplines: ['medical', 'nursing', 'allied-health', 'administrative'], }, { system: PRACTICE_TYPE_SYSTEM, code: 'home-health', display: 'Home Health & Hospice', disciplines: ['nursing', 'rehabilitation', 'allied-health', 'administrative'], }, { system: PRACTICE_TYPE_SYSTEM, code: 'integrative', display: 'Integrative & Alternative Medicine', disciplines: ['integrative', 'allied-health', 'administrative'], }, { system: PRACTICE_TYPE_SYSTEM, code: 'multi-specialty', display: 'Multi-Specialty Practice', disciplines: [ 'medical', 'nursing', 'specialty-medical', 'dental', 'chiropractic', 'optometry', 'mental-health', 'rehabilitation', 'pharmacy', 'integrative', 'allied-health', 'administrative', ], }, ]; // --------------------------------------------------------------------------- // Private role builder helpers // --------------------------------------------------------------------------- const orgRole = ( code: string, display: string, extra?: Pick, ): StaffRoleOption => ({ system: STAFF_ORGANIZATION_ROLE_SYSTEM, code, display, ...extra, }); const leadershipDesignation = (code: string, display: string): StaffRoleOption => ({ system: STAFF_LEADERSHIP_DESIGNATION_SYSTEM, code, display, }); const trainingLevel = (code: string, display: string): StaffRoleOption => ({ system: STAFF_TRAINING_LEVEL_SYSTEM, code, display, }); // --------------------------------------------------------------------------- // Organization role catalog // --------------------------------------------------------------------------- export const ORGANIZATION_ROLE_GROUPS: StaffRoleDisciplineGroup[] = [ // ------------------------------------------------------------------------- // Medical - Physicians // ------------------------------------------------------------------------- { label: 'Medical — Physicians', discipline: 'medical', patientFacing: true, items: [ // Generic physician: specialtyRequired surfaces the specialty picker so // the user can specify Family Medicine, Internal Medicine, etc. orgRole('physician', 'Physician', { nucc: '208D00000X', snomedSpecialty: '394814009', specialtyRequired: true, }), orgRole('attending-physician', 'Attending Physician', { nucc: '208D00000X', snomedSpecialty: '394814009', specialtyRequired: true, }), orgRole('family-medicine-physician', 'Family Medicine Physician', { nucc: '207Q00000X', snomedSpecialty: '394814009', }), orgRole('internal-medicine-physician', 'Internal Medicine Physician', { nucc: '207R00000X', snomedSpecialty: '419192003', }), orgRole('pediatrician', 'Pediatrician', { nucc: '208000000X', snomedSpecialty: '394537008', }), orgRole('obgyn', 'OB/GYN', { nucc: '207V00000X', snomedSpecialty: '394586005', }), orgRole('hospitalist', 'Hospitalist', { nucc: '208M00000X', snomedSpecialty: '394814009', }), orgRole('fellow', 'Fellow', { nucc: '390200000X', specialtyRequired: true }), orgRole('resident-physician', 'Resident Physician', { nucc: '390200000X', specialtyRequired: true }), orgRole('medical-student', 'Medical Student', { nucc: '390200000X' }), ], }, // ------------------------------------------------------------------------- // Nursing & Advanced Practice // ------------------------------------------------------------------------- { label: 'Nursing & Advanced Practice', discipline: 'nursing', patientFacing: true, items: [ orgRole('nurse-practitioner', 'Nurse Practitioner (APRN)', { nucc: '363L00000X', snomedSpecialty: '224571005', }), orgRole('certified-nurse-midwife', 'Certified Nurse-Midwife (CNM)', { nucc: '367A00000X', snomedSpecialty: '394586005', }), orgRole('certified-registered-nurse-anesthetist', 'Certified Registered Nurse Anesthetist (CRNA)', { nucc: '367500000X', snomedSpecialty: '394577003', }), orgRole('physician-assistant', 'Physician Assistant (PA)', { nucc: '363A00000X', }), orgRole('registered-nurse', 'Registered Nurse (RN)', { nucc: '163W00000X', snomedSpecialty: '224535009', }), orgRole('licensed-practical-nurse', 'Licensed Practical Nurse (LPN)', { nucc: '164W00000X', snomedSpecialty: '224534008', }), orgRole('licensed-vocational-nurse', 'Licensed Vocational Nurse (LVN)', { nucc: '164X00000X', snomedSpecialty: '224534008', }), orgRole('certified-nursing-assistant', 'Certified Nursing Assistant (CNA)', { nucc: '376K00000X', snomedSpecialty: '224542000', }), ], }, // ------------------------------------------------------------------------- // Medical Specialties // ------------------------------------------------------------------------- { label: 'Medical Specialties', discipline: 'specialty-medical', patientFacing: true, items: [ orgRole('anesthesiologist', 'Anesthesiologist', { nucc: '207L00000X', snomedSpecialty: '394577003', }), orgRole('cardiologist', 'Cardiologist', { nucc: '207RC0000X', snomedSpecialty: '394579002', }), orgRole('dermatologist', 'Dermatologist', { nucc: '207N00000X', snomedSpecialty: '394582007', }), orgRole('emergency-medicine-physician', 'Emergency Medicine Physician', { nucc: '207P00000X', snomedSpecialty: '773568002', }), orgRole('endocrinologist', 'Endocrinologist', { nucc: '207RE0101X', snomedSpecialty: '394583002', }), orgRole('gastroenterologist', 'Gastroenterologist', { nucc: '207RG0100X', snomedSpecialty: '394584008', }), orgRole('hematologist-oncologist', 'Hematologist / Oncologist', { nucc: '207RX0202X', snomedSpecialty: '394593009', }), orgRole('neurologist', 'Neurologist', { nucc: '2084N0400X', snomedSpecialty: '394591006', }), orgRole('ophthalmologist', 'Ophthalmologist', { nucc: '207W00000X', snomedSpecialty: '422234006', }), orgRole('orthopedic-surgeon', 'Orthopedic Surgeon', { nucc: '207X00000X', snomedSpecialty: '394617004', }), orgRole('podiatrist', 'Podiatrist', { nucc: '213E00000X', snomedSpecialty: '394539000', }), orgRole('psychiatrist', 'Psychiatrist', { nucc: '2084P0800X', snomedSpecialty: '394587001', }), orgRole('pulmonologist', 'Pulmonologist', { nucc: '207RP1001X', snomedSpecialty: '418112009', }), orgRole('radiologist', 'Radiologist', { nucc: '2085R0202X', snomedSpecialty: '394914008', }), orgRole('rheumatologist', 'Rheumatologist', { nucc: '207RR0500X', snomedSpecialty: '394810000', }), orgRole('urologist', 'Urologist', { nucc: '208800000X', snomedSpecialty: '394612006', }), ], }, // ------------------------------------------------------------------------- // Dental & Oral Health // ------------------------------------------------------------------------- { label: 'Dental & Oral Health', discipline: 'dental', patientFacing: true, items: [ orgRole('dentist', 'Dentist (General)', { nucc: '122300000X', // 722163006 |Dentistry|. WAS 160696009, which is "Transport distance too great" - not a // near-miss, a different branch of the ontology entirely, and the headline of. snomedSpecialty: '722163006', }), orgRole('pediatric-dentist', 'Pediatric Dentist', { nucc: '1223P0221X', // 394607009 |Pediatric dentistry|. WAS 394608004, which is "Orthodontics". // THESE TWO WERE SWAPPED: 394608004 is the correct code for the ORTHODONTIST below, and // it was sitting here. That is why both looked plausible - each was a real dental specialty, // just on the wrong role. snomedSpecialty: '394607009', }), orgRole('orthodontist', 'Orthodontist', { nucc: '1223X0400X', // 394608004 |Orthodontics| - the code that was wrongly on pediatric-dentist above. // WAS 394821009, which is "Occupational medicine". snomedSpecialty: '394608004', }), orgRole('oral-surgeon', 'Oral Surgeon', { nucc: '1223S0112X', snomedSpecialty: '394605001', }), orgRole('periodontist', 'Periodontist', { nucc: '1223P0300X', snomedSpecialty: '394613001', }), orgRole('endodontist', 'Endodontist', { nucc: '1223E0200X', // 408441001 |Endodontics - specialty|. WAS 394733009, "Medical specialty" - a generic // placeholder that resolves cleanly and says nothing, which is the worst kind of wrong here. snomedSpecialty: '408441001', }), orgRole('prosthodontist', 'Prosthodontist', { nucc: '1223P0700X', // 408460008 |Prosthodontics|. WAS 394732004, "Surgical specialty" - the same generic // placeholder problem as endodontist above. snomedSpecialty: '408460008', }), orgRole('dental-hygienist', 'Dental Hygienist', { nucc: '124Q00000X' }), orgRole('dental-assistant', 'Dental Assistant', { nucc: '125J00000X' }), ], }, // ------------------------------------------------------------------------- // Chiropractic // ------------------------------------------------------------------------- { label: 'Chiropractic', discipline: 'chiropractic', patientFacing: true, items: [ orgRole('chiropractor', 'Chiropractor (DC)', { nucc: '111N00000X', snomedSpecialty: '3842006', }), orgRole('chiropractic-assistant', 'Chiropractic Assistant'), ], }, // ------------------------------------------------------------------------- // Vision & Eye Care // ------------------------------------------------------------------------- { label: 'Vision & Eye Care', discipline: 'optometry', patientFacing: true, items: [ orgRole('optometrist', 'Optometrist (OD)', { nucc: '152W00000X', snomedSpecialty: '394813003', }), orgRole('optician', 'Optician', { nucc: '156FX1800X' }), orgRole('ophthalmic-technician', 'Ophthalmic Technician', { nucc: '156F00000X' }), ], }, // ------------------------------------------------------------------------- // Mental Health & Behavioral Health // ------------------------------------------------------------------------- { label: 'Mental Health & Behavioral Health', discipline: 'mental-health', patientFacing: true, items: [ orgRole('psychologist', 'Psychologist', { nucc: '103T00000X', snomedSpecialty: '394913002', }), orgRole('licensed-clinical-social-worker', 'Licensed Clinical Social Worker (LCSW)', { nucc: '1041C0700X', }), orgRole('licensed-professional-counselor', 'Licensed Professional Counselor (LPC)', { nucc: '101YM0800X', }), orgRole('licensed-marriage-family-therapist', 'Licensed Marriage & Family Therapist (LMFT)', { nucc: '106H00000X', }), orgRole('addiction-counselor', 'Addiction Counselor', { nucc: '101YA0400X' }), orgRole('behavioral-health-technician', 'Behavioral Health Technician'), // Legacy code - kept for backward compatibility with existing staff records. orgRole('social-worker', 'Social Worker'), ], }, // ------------------------------------------------------------------------- // Physical, Occupational & Speech Therapy // ------------------------------------------------------------------------- { label: 'Physical, Occupational & Speech Therapy', discipline: 'rehabilitation', patientFacing: true, items: [ orgRole('physical-therapist', 'Physical Therapist (PT)', { nucc: '225100000X', snomedSpecialty: '36682004', }), orgRole('physical-therapy-assistant', 'Physical Therapy Assistant (PTA)', { nucc: '225200000X', }), orgRole('occupational-therapist', 'Occupational Therapist (OT)', { nucc: '224Z00000X', snomedSpecialty: '80467003', }), orgRole('occupational-therapy-assistant', 'Occupational Therapy Assistant (OTA)', { nucc: '224900000X', }), orgRole('speech-language-pathologist', 'Speech-Language Pathologist (SLP)', { nucc: '235Z00000X', snomedSpecialty: '159026002', }), orgRole('respiratory-therapist', 'Respiratory Therapist (RT)', { nucc: '227800000X', }), orgRole('audiologist', 'Audiologist', { nucc: '231H00000X', snomedSpecialty: '310179007', }), // Legacy code - kept for backward compatibility with existing staff records. orgRole('therapist', 'Therapist (General)'), ], }, // ------------------------------------------------------------------------- // Pharmacy // ------------------------------------------------------------------------- { label: 'Pharmacy', discipline: 'pharmacy', patientFacing: true, items: [ orgRole('pharmacist', 'Pharmacist', { nucc: '183500000X', snomedSpecialty: '394662009', }), orgRole('pharmacy-technician', 'Pharmacy Technician', { nucc: '183700000X' }), ], }, // ------------------------------------------------------------------------- // Integrative & Alternative Medicine // ------------------------------------------------------------------------- { label: 'Integrative & Alternative Medicine', discipline: 'integrative', patientFacing: true, items: [ orgRole('naturopathic-doctor', 'Naturopathic Doctor (ND)', { nucc: '175L00000X' }), orgRole('acupuncturist', 'Acupuncturist', { nucc: '171100000X' }), orgRole('massage-therapist', 'Massage Therapist', { nucc: '225700000X' }), ], }, // ------------------------------------------------------------------------- // Allied Health & Clinical Support // ------------------------------------------------------------------------- { label: 'Allied Health & Clinical Support', discipline: 'allied-health', patientFacing: true, items: [ orgRole('medical-assistant', 'Medical Assistant (MA)'), orgRole('dietitian', 'Dietitian / Nutritionist', { nucc: '133V00000X', // 722164000 |Dietetics and nutrition|. WAS 394590007, "Thoracic medicine" - and RETIRED, // so it was both the wrong concept and one no current release vouches for. snomedSpecialty: '722164000', }), orgRole('laboratory-professional', 'Laboratory Professional'), orgRole('radiology-professional', 'Radiology Technician'), orgRole('surgical-technician', 'Surgical Technician'), orgRole('phlebotomist', 'Phlebotomist'), orgRole('case-manager', 'Case Manager'), orgRole('care-coordinator', 'Care Coordinator'), orgRole('medical-interpreter', 'Medical Interpreter'), orgRole('medical-scribe', 'Medical Scribe'), ], }, // ------------------------------------------------------------------------- // Administrative & Operations (patientFacing: false - excluded from booking) // ------------------------------------------------------------------------- { label: 'Administrative & Operations', discipline: 'administrative', patientFacing: false, items: [ orgRole('receptionist', 'Receptionist'), orgRole('healthcare-administrator', 'Healthcare Administrator'), orgRole('practice-manager', 'Practice Manager'), orgRole('office-manager', 'Office Manager'), orgRole('human-resources', 'Human Resources'), orgRole('scheduling-coordinator', 'Scheduling Coordinator'), // -- the referral coordinator. Spencer, 2026-08-21: *"My initial instinct was to lump // it all into the front desk system for the receptionists but that won't work in a large // medical center network."* In a two-person practice this person IS the receptionist; in a // medical centre it is a department. Same role, different number of people in it -- which is // why the ROLE is the thing that exists, and the workspace merely appears when it is held. // // IT DELIBERATELY HAS NO PERMISSION PROFILE, and that is the card's control rather than an // omission. A coordinator triages, verifies insurance and schedules; they do not need the // chart. An unlisted code contributes nothing in either permission resolver, so this role // grants exactly the baseline every staff role has -- their own schedule -- and nothing else. // Adding a profile for it later is a decision to widen reach, not housekeeping. orgRole('referral-coordinator', 'Referral Coordinator'), orgRole('billing-specialist', 'Billing Specialist'), orgRole('medical-records-coordinator', 'Medical Records Coordinator'), orgRole('information-technology', 'Information Technology'), orgRole('marketing-communications', 'Marketing & Communications'), ], }, ]; // --------------------------------------------------------------------------- // Leadership designations (unchanged) // --------------------------------------------------------------------------- export const LEADERSHIP_DESIGNATION_GROUPS: StaffRoleGroup[] = [ { label: 'Leadership designations', items: [ leadershipDesignation('medical-director', 'Medical Director'), leadershipDesignation('department-head', 'Head of Department'), leadershipDesignation('lead-physician', 'Lead Physician'), leadershipDesignation('chief-resident', 'Chief Resident'), leadershipDesignation('nurse-administrator', 'Nurse Administrator'), leadershipDesignation('clinical-nurse-manager', 'Clinical Nurse Manager'), ], }, ]; // --------------------------------------------------------------------------- // Training levels (unchanged) // --------------------------------------------------------------------------- export const TRAINING_LEVEL_OPTIONS: StaffRoleOption[] = [ trainingLevel('pgy-1', 'PGY-1 (Intern)'), trainingLevel('pgy-2', 'PGY-2'), trainingLevel('pgy-3', 'PGY-3'), trainingLevel('pgy-4', 'PGY-4'), trainingLevel('pgy-5', 'PGY-5'), trainingLevel('pgy-6', 'PGY-6'), trainingLevel('pgy-7', 'PGY-7'), trainingLevel('pgy-8', 'PGY-8'), trainingLevel('pgy-9-plus', 'PGY-9+'), ]; // --------------------------------------------------------------------------- // Derived catalog lookups // --------------------------------------------------------------------------- // Roles belonging to patient-facing disciplines. Used to determine clinical // staff and to populate the appointment booking role filter. export const CLINICAL_ORGANIZATION_ROLE_CODES = new Set( ORGANIZATION_ROLE_GROUPS .filter((group) => group.patientFacing) .flatMap((group) => group.items.map((role) => role.code)), ); export const flattenOrganizationRoleOptions = (): StaffRoleOption[] => ORGANIZATION_ROLE_GROUPS.flatMap((group) => group.items); export const flattenLeadershipDesignationOptions = (): StaffRoleOption[] => LEADERSHIP_DESIGNATION_GROUPS.flatMap((group) => group.items); export const slugifyStaffRoleCode = (value: string) => value .trim() .toLocaleLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); export const isClinicalOrganizationRole = (role?: Pick) => Boolean(role?.code && CLINICAL_ORGANIZATION_ROLE_CODES.has(role.code)); // Org-role codes from non-patient-facing support disciplines (reception, administration). These are // never offered as bookable patient-facing services. export const SUPPORT_ORGANIZATION_ROLE_CODES = new Set( ORGANIZATION_ROLE_GROUPS .filter((group) => !group.patientFacing) .flatMap((group) => group.items.map((role) => role.code)), ); // Whether a role should appear as a bookable patient-facing service. Resilient by design: a built-in // clinical role qualifies, AND so does a practice's CUSTOM role (not in our catalog) - only known // support roles are excluded. The patient booking service picker derives from this rather than the // strict clinical check, so custom roles aren't silently dropped. export const isBookableServiceRole = (role?: Pick) => Boolean(role?.code) && !SUPPORT_ORGANIZATION_ROLE_CODES.has(role?.code ?? ''); // Returns the full StaffRoleOption for a given org role code, or undefined. export const getOrgRoleByCode = (code: string): StaffRoleOption | undefined => { for (const group of ORGANIZATION_ROLE_GROUPS) { const found = group.items.find((item) => item.code === code); if (found) return found; } return undefined; }; // Assigning a workspace auto-attaches (and locks) its matching org-role coding, so a permission-driven // workspace always carries the coding its permissions resolve from. Only these support/back-office // workspaces map 1:1 to a single org-role; Admin/Practitioner/Generic are intentionally uncoupled. export const WORKSPACE_REQUIRED_ORG_ROLE: Partial> = { [STAFF_WORKSPACES.RECEPTIONIST]: 'receptionist', [STAFF_WORKSPACES.HR]: 'human-resources', [STAFF_WORKSPACES.PLATFORM_MANAGER]: 'information-technology', [STAFF_WORKSPACES.BILLING]: 'billing-specialist', //. The coding MUST be `marketing-communications` - that is the catalog's entry and the // key the permission profile is stored under on BOTH sides (staffAccess.ts and the server's // staff-permissions.ts). S4c's review found this profile keyed on `marketing-manager`, // which no catalog coding produced, so the workspace resolved to no permissions at all through // the intended path. Without this line the workspace is selectable but grants nothing. [STAFF_WORKSPACES.MARKETING]: 'marketing-communications', // /608 -- assigning the workspace attaches the coordinator coding, so the surface and the // role cannot come apart. That coding carries NO permission profile by design: the workspace is // workflow, and access stays exactly where it was. [STAFF_WORKSPACES.REFERRALS]: 'referral-coordinator', [STAFF_WORKSPACES.RECORDS]: 'medical-records-coordinator', // THE CATALOG'S OWN CODINGS, and the note on Marketing above is why that matters rather than // being a naming preference: a coding no catalog entry produces attaches a role the directory // cannot name and no permission profile can ever be keyed to. These two roles already existed -- // Laboratory Professional and Radiology Technician, both allied health -- so inventing // `lab-technician` and `imaging-technician` beside them would have been a second name for a // person the practice can already describe. // // Neither carries a permission profile, exactly like the coordinator above. Assigning the // workspace attaches the coding so the surface and the role cannot come apart, and grants // nothing on its own -- the practice still decides `diagnostics` separately. A profile here // would make the workspace itself the grant, which is the fence moving into the client. [STAFF_WORKSPACES.LAB_TECH]: 'laboratory-professional', [STAFF_WORKSPACES.IMAGING_TECH]: 'radiology-professional', }; // The org-role codes that must be present while the given (canonical) workspaces are assigned. export const getRequiredOrgRoleCodesForWorkspaces = ( workspaces: StaffWorkspace[], ): string[] => Array.from(new Set( workspaces .map((workspace) => WORKSPACE_REQUIRED_ORG_ROLE[workspace]) .filter((code): code is string => Boolean(code)), )); // Org-role codes managed by workspace assignment (auto-attached + auto-removed with their workspace). // These ARE selectable in the org-role picker - a coding that grants a permission floor must never // exist without the workspace that authorizes it, so selecting either half (the role or the // workspace) auto-assigns the other, keeping the pair a single unit rather than hiding one side. export const WORKSPACE_COUPLED_ORG_ROLE_CODES = new Set( Object.values(WORKSPACE_REQUIRED_ORG_ROLE), ); // The INVERSE of WORKSPACE_REQUIRED_ORG_ROLE, derived (never hand-written) so the two directions // cannot drift. Selecting one of these org roles must assign its workspace, exactly as assigning the // workspace already attaches the role - the coupling is a unit in both directions, on add and remove. export const ORG_ROLE_REQUIRED_WORKSPACE: Record = Object.fromEntries( Object.entries(WORKSPACE_REQUIRED_ORG_ROLE) .filter(([, roleCode]) => Boolean(roleCode)) .map(([workspace, roleCode]) => [roleCode as string, workspace as StaffWorkspace]), ); export const getCoupledWorkspaceForOrgRole = (code: string): StaffWorkspace | undefined => ORG_ROLE_REQUIRED_WORKSPACE[code]; export const getCoupledOrgRoleCodeForWorkspace = (workspace: StaffWorkspace): string | undefined => WORKSPACE_REQUIRED_ORG_ROLE[workspace]; // The workspaces that must be present while the given org-role codes are assigned. export const getRequiredWorkspacesForOrgRoleCodes = (codes: string[]): StaffWorkspace[] => Array.from(new Set( codes .map((code) => ORG_ROLE_REQUIRED_WORKSPACE[code]) .filter((workspace): workspace is StaffWorkspace => Boolean(workspace)), )); // Display label for a canonical workspace. BUILT_IN_WORKSPACE_OPTIONS is keyed by the FORM value // ('Receptionists'), not the canonical workspace ('Receptionist'), so route through // getWorkspaceFormValue rather than adding a second label source. export const getWorkspaceLabel = (workspace: StaffWorkspace): string => BUILT_IN_WORKSPACE_OPTIONS.find((option) => option.key === getWorkspaceFormValue(workspace))?.name ?? workspace; // Picker copy. Both notes come from the same map, so the two sides can never describe different pairs. export const getOrgRoleCouplingNote = (code: string): string | undefined => { const workspace = ORG_ROLE_REQUIRED_WORKSPACE[code]; if (!workspace) return undefined; return `Also assigns the ${getWorkspaceLabel(workspace)} workspace`; }; export const getWorkspaceCouplingNote = (workspace: StaffWorkspace): string | undefined => { const code = WORKSPACE_REQUIRED_ORG_ROLE[workspace]; if (!code) return undefined; return `Also assigns the ${getOrgRoleByCode(code)?.display ?? code} role`; }; export interface CoupledAssignments { orgRoleCodes: string[]; workspaces: StaffWorkspace[]; removedOrgRoleCodes?: string[]; removedWorkspaces?: StaffWorkspace[]; } // THE single reconciler - both save paths (create and edit) call this one function, so the two // surfaces cannot diverge. Explicit removals win over the additive coupling, so un-ticking either half // of a pair takes the other half with it instead of the coupling immediately re-adding it. export const reconcileCoupledAssignments = ( { orgRoleCodes, workspaces, removedOrgRoleCodes = [], removedWorkspaces = [] }: CoupledAssignments, ): { orgRoleCodes: string[]; workspaces: StaffWorkspace[] } => { const removedRoles = new Set(removedOrgRoleCodes); const removedWs = new Set(removedWorkspaces); // A removal on either side removes its partner. for (const code of removedOrgRoleCodes) { const ws = ORG_ROLE_REQUIRED_WORKSPACE[code]; if (ws) removedWs.add(ws); } for (const ws of removedWorkspaces) { const code = WORKSPACE_REQUIRED_ORG_ROLE[ws]; if (code) removedRoles.add(code); } const roles = new Set(orgRoleCodes.filter((c) => !removedRoles.has(c))); const ws = new Set(workspaces.filter((w) => !removedWs.has(w))); for (const code of Array.from(roles)) { const required = ORG_ROLE_REQUIRED_WORKSPACE[code]; if (required && !removedWs.has(required)) ws.add(required); } for (const workspace of Array.from(ws)) { const required = WORKSPACE_REQUIRED_ORG_ROLE[workspace]; if (required && !removedRoles.has(required)) roles.add(required); } return { orgRoleCodes: Array.from(roles), workspaces: Array.from(ws) }; }; // Returns the discipline code for a given org role code, or undefined. export const getOrgRoleDiscipline = (code: string): string | undefined => { for (const group of ORGANIZATION_ROLE_GROUPS) { if (group.items.some((item) => item.code === code)) return group.discipline; } return undefined; }; // Returns the ORGANIZATION_ROLE_GROUPS group (category) a given org role code belongs to, or // undefined for custom/unknown codes - callers should bucket those under a fallback like "Other". export const getOrganizationRoleGroupForCode = ( code?: string, ): StaffRoleDisciplineGroup | undefined => { if (!code) return undefined; for (const group of ORGANIZATION_ROLE_GROUPS) { if (group.items.some((item) => item.code === code)) return group; } return undefined; }; // Short display labels for role categories, used where horizontal space is tight (e.g. the // location list-item category chips). Any category without an entry - including short ones // (Chiropractic, Pharmacy) and the custom "Other" bucket - falls back to its full label. const ORGANIZATION_ROLE_GROUP_SHORT_LABELS: Record = { 'Medical — Physicians': 'Physicians', 'Nursing & Advanced Practice': 'Nursing', 'Medical Specialties': 'Specialists', 'Dental & Oral Health': 'Dental', 'Vision & Eye Care': 'Vision', 'Mental Health & Behavioral Health': 'Behavioral', 'Physical, Occupational & Speech Therapy': 'Therapy', 'Integrative & Alternative Medicine': 'Alternative', 'Allied Health & Clinical Support': 'Clinical Support', 'Administrative & Operations': 'Admin', 'Leadership designations': 'Leadership', }; // Returns a compact label for a role-category (ORGANIZATION_ROLE_GROUPS label), falling back to // the full label when no abbreviation is defined. export const getOrganizationRoleGroupShortLabel = (label: string): string => ORGANIZATION_ROLE_GROUP_SHORT_LABELS[label] ?? label; // Returns ORGANIZATION_ROLE_GROUPS sorted so that disciplines matching the // given practice type codes appear first. Used by the role picker to surface // the most relevant groups for the org's configured practice type(s). export const sortRoleGroupsByPracticeTypes = ( practiceTypeCodes: string[], practiceTypes: { code: string; disciplines: string[] }[], ): StaffRoleDisciplineGroup[] => { if (practiceTypeCodes.length === 0) return ORGANIZATION_ROLE_GROUPS; const priorityDisciplines = practiceTypeCodes.flatMap((code) => { const practiceType = practiceTypes.find((pt) => pt.code === code); return practiceType?.disciplines ?? []; }); // De-duplicate while preserving first-occurrence order. const seen = new Set(); const orderedDisciplines: string[] = []; for (const d of priorityDisciplines) { if (!seen.has(d)) { seen.add(d); orderedDisciplines.push(d); } } const priorityIndex = (group: StaffRoleDisciplineGroup) => { const idx = orderedDisciplines.indexOf(group.discipline); return idx === -1 ? orderedDisciplines.length : idx; }; return [...ORGANIZATION_ROLE_GROUPS].sort( (a, b) => priorityIndex(a) - priorityIndex(b), ); }; // --------------------------------------------------------------------------- // PractitionerRole read helpers (all unchanged) // --------------------------------------------------------------------------- const getCodingFromRoleConcept = (role: NonNullable[number] | undefined) => role?.coding?.[0]; export const getPractitionerRoleOrganizationCodings = (practitionerRole?: PractitionerRole): Coding[] => (practitionerRole?.code ?? []) .map(getCodingFromRoleConcept) .filter((coding): coding is Coding => { if (!coding?.code) return false; return coding.system !== STAFF_LEADERSHIP_DESIGNATION_SYSTEM; }); /** * Is this coding drawn from OUR organization-role vocabulary - the one the catalog is written in? * * - `getPractitionerRoleOrganizationCodings` above excludes only the LEADERSHIP system, so * SNOMED, NUCC and every other stratum flow straight through it. That is harmless where the caller only * matches a known code, and it is a disclosure defect where the caller asks "is this code NOT in a * denylist" - because a code from another vocabulary can never be in a denylist built from ours, so the * answer is always yes. `Medical record administrator` and `Administrative healthcare staff` were being * offered to patients as people to message on exactly that arithmetic. * * It is NOT folded into the function above, which has ~14 call sites including one that decides * PERMISSION floors and several that drive booking filters and role labels. Narrowing shared behaviour * for one surface's benefit is how a fix becomes an outage somewhere nobody was looking. * * LENIENT ABOUT A MISSING SYSTEM, deliberately, and the same way `staffAccess.isBuiltInRoleCoding` and * its server twin already are: a legacy role may carry a bare code, while a custom role always carries * its own org-scoped system. A second, stricter answer to "is this one of ours" is the divergence those * comments warn about. * * THE ORG-SCOPED CHILD SYSTEM COUNTS. A practice-defined role is written under * `/`, so an exact `===` would exclude every custom role - which is the one * property the denylist design exists to protect. */ export const isOrganizationRoleCoding = (coding?: Pick): boolean => { if (!coding?.code?.trim()) return false; const system = coding.system?.trim(); if (!system) return true; return system === STAFF_ORGANIZATION_ROLE_SYSTEM || system.startsWith(`${STAFF_ORGANIZATION_ROLE_SYSTEM}/`); }; /** * The role's codes, from our organization-role vocabulary only, trimmed. * * The trim is not cosmetic: staging holds `"224608005 "` with a trailing space, and any exact-match * `Set.has()` misses it. (That particular code is SNOMED and is now excluded for a different reason - * so the trim is protecting the general case, not that one.) */ export const getOrganizationRoleCodes = (practitionerRole?: PractitionerRole): string[] => getPractitionerRoleOrganizationCodings(practitionerRole) .filter(isOrganizationRoleCoding) .map((coding) => (coding.code ?? '').trim()) .filter((code) => code.length > 0); export const getPractitionerRoleLeadershipCodings = (practitionerRole?: PractitionerRole): Coding[] => (practitionerRole?.code ?? []) .map(getCodingFromRoleConcept) .filter((coding): coding is Coding => { if (!coding?.code) return false; return coding.system === STAFF_LEADERSHIP_DESIGNATION_SYSTEM; }); export const getPractitionerRolePrimaryCoding = (practitionerRole?: PractitionerRole): Coding | undefined => { const primaryExtensionCoding = practitionerRole?.extension?.find( (extension) => extension.url === STAFF_PRIMARY_ROLE_EXTENSION_URL, )?.valueCoding; if (primaryExtensionCoding?.code) return primaryExtensionCoding; return getPractitionerRoleOrganizationCodings(practitionerRole)[0]; }; export const getPractitionerRoleTrainingLevelCoding = (practitionerRole?: PractitionerRole): Coding | undefined => practitionerRole?.extension?.find( (extension) => extension.url === STAFF_TRAINING_LEVEL_EXTENSION_URL, )?.valueCoding; export const getPractitionerRolePrimaryDisplay = ( practitionerRole?: PractitionerRole, fallback = 'Staff', ) => { const primaryCoding = getPractitionerRolePrimaryCoding(practitionerRole); return primaryCoding?.display || primaryCoding?.code || practitionerRole?.code?.[0]?.text || fallback; }; const normalizeWorkspaceValue = (value?: string): StaffWorkspace | undefined => { if (!value) return undefined; return getWorkspaceFromFormValue(value) || (Object.values(STAFF_WORKSPACES).includes(value as StaffWorkspace) ? value as StaffWorkspace : undefined); }; export const getPractitionerRoleWorkspaces = ( practitionerRole?: PractitionerRole, fallbackGroup?: string, ): StaffWorkspace[] => { const extensionWorkspaces = (practitionerRole?.extension ?? []) .filter((extension) => extension.url === STAFF_WORKSPACE_EXTENSION_URL) .map((extension) => normalizeWorkspaceValue(extension.valueCode)) .filter((workspace): workspace is StaffWorkspace => Boolean(workspace)); const uniqueWorkspaces = sortStaffWorkspaces(Array.from(new Set(extensionWorkspaces))); if (uniqueWorkspaces.length > 0) return uniqueWorkspaces; const fallbackWorkspace = normalizeWorkspaceValue(fallbackGroup); return fallbackWorkspace ? [fallbackWorkspace] : []; }; /** * Does this person hold a LIVE clinical role at this organization - i.e. can a patient visit be theirs? * * EXTRACTED BECAUSE IT HAD ALREADY BEEN WRITTEN FOUR TIMES, in the booking readiness resolver, the * readiness facts, the provider setup chain and the staff record's tab gate - and a fifth copy was about * to go into the deactivation reassign picker. They agreed, which is exactly how this kind of thing * survives: nothing fails while they agree, and nothing notices the day one of them stops. * * WORKSPACE, NOT `code`. `PractitionerRole.code` carries the role's title/specialty; the workspace is * an extension. Reading `code` compiles, type-checks and answers "not clinical" for everybody. * And an INACTIVE role does not count - a departed clinician is not somebody a visit can be moved to. */ export const holdsClinicalRole = (roles: readonly PractitionerRole[] = []): boolean => roles.some( (role) => role.active !== false && getPractitionerRoleWorkspaces(role).includes(STAFF_WORKSPACES.PRACTITIONER), ); export const getPractitionerRoleDefaultWorkspace = ( practitionerRole?: PractitionerRole, fallbackGroup?: string, ): StaffWorkspace | undefined => { const explicitDefault = (practitionerRole?.extension ?? []) .filter((extension) => extension.url === STAFF_DEFAULT_WORKSPACE_EXTENSION_URL) .map((extension) => normalizeWorkspaceValue(extension.valueCode)) .find((workspace): workspace is StaffWorkspace => Boolean(workspace)); const workspaces = getPractitionerRoleWorkspaces(practitionerRole, fallbackGroup); if (explicitDefault && workspaces.includes(explicitDefault)) return explicitDefault; return workspaces[0]; }; /** The default somebody set on the role, if it is one of the role's own workspaces; never an inferred one. */ export const getExplicitDefaultWorkspace = ( practitionerRole?: PractitionerRole, fallbackGroup?: string, ): StaffWorkspace | undefined => { const explicitDefault = (practitionerRole?.extension ?? []) .filter((extension) => extension.url === STAFF_DEFAULT_WORKSPACE_EXTENSION_URL) .map((extension) => normalizeWorkspaceValue(extension.valueCode)) .find((workspace): workspace is StaffWorkspace => Boolean(workspace)); return explicitDefault && getPractitionerRoleWorkspaces(practitionerRole, fallbackGroup).includes(explicitDefault) ? explicitDefault : undefined; }; export const getPractitionerRoleWorkspace = ( practitionerRole?: PractitionerRole, fallbackGroup?: string, ): StaffWorkspace | undefined => getPractitionerRoleDefaultWorkspace( practitionerRole, fallbackGroup, ); export const getWorkspaceFormValues = ( workspaces: StaffWorkspace[], ): string[] => workspaces .map((workspace) => getWorkspaceFormValue(workspace)) .filter(Boolean); export const setPractitionerRoleWorkspaceAssignments = ( practitionerRole: PractitionerRole, workspaces: StaffWorkspace[], defaultWorkspace?: StaffWorkspace, ): PractitionerRole => { const uniqueWorkspaces = sortStaffWorkspaces(Array.from(new Set(workspaces))); const resolvedDefault = defaultWorkspace && uniqueWorkspaces.includes(defaultWorkspace) ? defaultWorkspace : uniqueWorkspaces[0]; const preservedExtensions = (practitionerRole.extension ?? []).filter( (extension) => extension.url !== STAFF_WORKSPACE_EXTENSION_URL && extension.url !== STAFF_DEFAULT_WORKSPACE_EXTENSION_URL, ); const workspaceExtensions = uniqueWorkspaces.map((workspace) => ({ url: STAFF_WORKSPACE_EXTENSION_URL, valueCode: workspace, })); const defaultExtension = resolvedDefault ? [{ url: STAFF_DEFAULT_WORKSPACE_EXTENSION_URL, valueCode: resolvedDefault }] : []; const extensions = [...preservedExtensions, ...workspaceExtensions, ...defaultExtension]; return { ...practitionerRole, ...(extensions.length > 0 ? { extension: extensions } : { extension: undefined }), }; }; // - THIS IS A `Record` ON PURPOSE. DO NOT TURN IT BACK INTO A // SWITCH WITH A `default`. // // It was a switch, and Marketing joined StaffWorkspace without gaining a case, so it silently fell // through and rendered "Unknown Workspace" in the role switcher and on the dashboard. The compiler // could not help: a `default` arm makes EVERY missing case type-check cleanly, so the omission was // invisible until someone looked at the screen. // // As a Record keyed on the union, omitting a workspace is a COMPILE ERROR. The next person to add // one cannot ship this bug - which matters because the failure mode is silent and cosmetic enough // to survive review. const WORKSPACE_DISPLAY_LABELS: Record = { [STAFF_WORKSPACES.PRACTITIONER]: 'Practitioner', [STAFF_WORKSPACES.RECEPTIONIST]: 'Receptionist', [STAFF_WORKSPACES.ADMIN]: 'Admin', [STAFF_WORKSPACES.PLATFORM_MANAGER]: 'Platform Manager', [STAFF_WORKSPACES.GENERIC]: 'Permission-based', [STAFF_WORKSPACES.HR]: 'HR', [STAFF_WORKSPACES.BILLING]: 'Billing', [STAFF_WORKSPACES.MARKETING]: 'Marketing', [STAFF_WORKSPACES.REFERRALS]: 'Referrals', [STAFF_WORKSPACES.LAB_TECH]: 'Lab technician', [STAFF_WORKSPACES.IMAGING_TECH]: 'Imaging technician', [STAFF_WORKSPACES.RECORDS]: 'Records', }; // Takes `string` rather than StaffWorkspace because callers pass raw stored/claim values, so the // runtime fallback still has to exist for a value that is not a workspace at all. That fallback is // for UNKNOWN INPUT - it is no longer load-bearing for a workspace we simply forgot to label. export const getWorkspaceDisplay = (workspace?: string) => WORKSPACE_DISPLAY_LABELS[workspace as StaffWorkspace] ?? 'Unknown Workspace'; export const isPatientFacingPractitionerRole = ( practitionerRole?: PractitionerRole, fallbackGroup?: string, ) => { const workspace = getPractitionerRoleWorkspace(practitionerRole, fallbackGroup); if (workspace === STAFF_WORKSPACES.PRACTITIONER) return true; if (workspace !== STAFF_WORKSPACES.GENERIC) return false; return getPractitionerRoleOrganizationCodings(practitionerRole) .some((coding) => isClinicalOrganizationRole({ code: coding.code || '' })); };