import type { PractitionerRole, Schedule } from 'fhir/r4'; import { formatFhirHumanName } from './fhirHumanName'; import { scheduleLocationLabelRef } from './scheduleLocations'; import type { PractitionerWithRoles } from '../services/PractitionerService'; export type StaffScheduleRecord = { schedule: Schedule; role?: PractitionerRole; practitioner?: PractitionerWithRoles; staffName: string; roleLabel: string; locationLabel: string; servicesLabel: string; active: boolean; }; const getReferenceId = (reference?: string) => { if (!reference) { return undefined; } const parts = reference.split('/'); return parts[parts.length - 1] || undefined; }; const normalizeRoleKey = (value: string) => value.toLowerCase().replace(/[^a-z0-9]/g, ''); export const getRoleLabel = (role?: PractitionerRole) => { const rawLabel = role?.code?.[0]?.text?.trim() || role?.code?.[0]?.coding?.[0]?.display?.trim() || role?.code?.[0]?.coding?.[0]?.code?.trim() || 'Staff'; const normalized = normalizeRoleKey(rawLabel); const roleAliases: Record = { practitioners: 'Practitioner', practitioner: 'Practitioner', receptionists: 'Receptionist', receptionist: 'Receptionist', admins: 'Admin', admin: 'Admin', administrators: 'Admin', administrator: 'Admin', platformmanagers: 'Platform Manager', platformmanager: 'Platform Manager', }; return roleAliases[normalized] || rawLabel; }; const getScheduleRoleId = (schedule: Schedule) => getReferenceId( schedule.actor?.find((actor) => actor.reference?.startsWith('PractitionerRole/'))?.reference, ); const getSchedulePractitionerId = (schedule: Schedule) => getReferenceId( schedule.actor?.find((actor) => actor.reference?.startsWith('Practitioner/'))?.reference, ); export const getStaffScheduleRecord = ( schedule: Schedule, practitionersList: PractitionerWithRoles[], /** The practice's virtual Location id, so the label can tell a site from the video one. Optional: * a practice with none has nothing virtual to mistake, and a caller that omits it keeps the old * first-actor behaviour rather than guessing. */ virtualLocationId?: string, ): StaffScheduleRecord => { const roleId = getScheduleRoleId(schedule); const practitionerId = getSchedulePractitionerId(schedule); const practitioner = practitionersList.find((item) => (roleId && item.roles.some((role) => role.id === roleId)) || (practitionerId && item.practitioner.id === practitionerId), ); const role = roleId ? practitioner?.roles.find((candidateRole) => candidateRole.id === roleId) : practitioner?.roles[0]; const practitionerActor = schedule.actor?.find((actor) => actor.reference?.startsWith('Practitioner/'), ); const roleActor = schedule.actor?.find((actor) => actor.reference?.startsWith('PractitionerRole/'), ); const staffName = formatFhirHumanName(practitioner?.practitioner.name, { emptyText: practitionerActor?.display?.trim() || role?.practitioner?.display?.trim() || roleActor?.display?.trim() || 'Unknown staff member', }); const roleLabel = getRoleLabel(role); // THE LABEL IS THE ONLY PLACE A READER LEARNS WHICH SHAPE THIS BLOCK IS, since Spencer's // 2026-09-14 ruling made "video only" a real pattern distinct from "at a site, also by video". // Taking the first actor answers that question by accident of storage order; this answers it by // the rule - the site if there is one, the virtual location only when there is nothing else. const locationLabel = scheduleLocationLabelRef(schedule, virtualLocationId)?.display?.trim() || role?.location?.[0]?.display?.trim() || 'All locations'; const servicesLabel = (schedule.actor ?? []) .filter((ref) => ref.reference?.startsWith('HealthcareService/')) .map((ref) => ref.display) .filter((display): display is string => Boolean(display)) .join(', ') || 'No services set'; const active = schedule.active !== false && role?.active !== false && practitioner?.practitioner.active !== false; return { schedule, role, practitioner, staffName, roleLabel, locationLabel, servicesLabel, active, }; };