import type { Reference, Schedule } from 'fhir/r4'; // S4 - reading a block's Location actors when there can be MORE THAN ONE. // // A Schedule now carries the practitioner's physical site AND, when the block offers video, the org's // virtual (telehealth) Location. That co-actor arrangement is what makes a video booking share the // block's existing slot pool instead of forking it into a second Schedule. // // THE BUG THIS EXISTS TO FIX: `ScheduleService.getScheduleLocationReference` is `.find(...)` - the // FIRST Location actor only. The patient booking flow derived its entire `locationIds` accessor from it, // so a block carrying [physical, virtual] reported ONLY the physical one and the telehealth gate then // raised NO_VIRTUAL_LOCATION_ON_BLOCK for a block that had explicitly opted into video. Patient video // booking was impossible. (The staff context was already correct - it filters all Location actors.) // // Lives here rather than beside the singular reader because `ScheduleService.tsx` cannot be imported by // a test: it pulls in `environment.ts`, which throws at module load outside the app. A pure module is // the difference between this rule being pinned by tests and not. // // The singular reader is deliberately NOT changed. Plenty of callers legitimately want "the location of // this block" for a label or a single-select, and it remains exactly `getScheduleLocationReferences(s)[0]`. /** EVERY `Location/` actor on the schedule, in actor order. */ export const getScheduleLocationReferences = (schedule?: Schedule): Reference[] => (schedule?.actor ?? []).filter((ref) => ref.reference?.startsWith('Location/')); /** Their bare ids, for the accessors that compare against a location id. */ export const getScheduleLocationIds = (schedule?: Schedule): string[] => getScheduleLocationReferences(schedule) .map((ref) => ref.reference?.split('/').pop()) .filter((id): id is string => Boolean(id)); /** * The Location a block should be LABELLED with, which is not simply its first actor. * * Spencer ruled on 2026-09-14 that a block whose only location is virtual is a VIDEO-ONLY block, * and that a pattern which is both in person and by video names its site. That makes the label * load-bearing: it is the only place a reader is told which of the two shapes a block is. * * THE FIRST ACTOR IS THE WRONG ANSWER FOR THAT QUESTION. `buildScheduleLocationActors` writes * [physical, virtual], so first-actor happens to be right for anything saved through the current * editor - but it is right by accident of ORDER, and any block whose stored order puts the virtual * one first reads as "Video Visit" while it is genuinely bookable at a site. One exists on the * staging practice today. Under the ruling that is no longer a cosmetic mislabel: it tells the * reader the block is video only when it is not. * * So the label is derived from the RULING - the physical site if there is one, the virtual one only * when there is nothing else - rather than from the order the actors happen to sit in. * * `virtualLocationId` undefined means the practice has no virtual Location, so nothing can be * virtual and the first actor is the whole answer. */ export const scheduleLocationLabelRef = ( schedule: Schedule | undefined, virtualLocationId: string | undefined, ): Reference | undefined => { const refs = getScheduleLocationReferences(schedule); if (!virtualLocationId) return refs[0]; const physical = refs.find((ref) => ref.reference !== `Location/${virtualLocationId}`); return physical ?? refs[0]; }; /** * Which site to SEED the block editor's location picker with. * * THE ROLE FALLBACK IS FOR BLOCKS THAT PREDATE THE MOVE, when location lived on the * PractitionerRole rather than on the block. Those carry no Location actor at all, and seeding * from the role is how they keep working. * * IT MUST NOT REACH A VIDEO-ONLY BLOCK. Such a block DOES carry a Location actor - the virtual * one - and deliberately carries no site, which since Spencer's 2026-09-14 ruling is a meaningful * shape rather than an omission. Letting the role fallback fire there seeds the picker with a * hospital the block is not at, tells the administrator the wrong thing, and - because the save * writes whatever the picker holds - silently converts a video-only block into a site-plus-video * one on any save that did not touch the field. * * Measured on the staging practice: a Sunday video-only block opened reading "West Burlington * Hospital" while the same dialog's own summary, and the schedules list, both read "Video Visit". * * THE DISTINCTION IS WHETHER THE BLOCK NAMES ANY LOCATION AT ALL, not whether it names a site. */ export const seedScheduleLocationId = ( schedule: Schedule | undefined, virtualLocationId: string | undefined, roleLocationId: string | undefined, ): string => { const ids = getScheduleLocationIds(schedule); const physical = ids.find((id) => id !== virtualLocationId); if (physical) return physical; // It named its locations and none of them is a site: that is the video-only shape, and an // empty picker is the truth about it. if (ids.length > 0) return ''; // Hours on the Schedule mean it was saved after the move, so no Location is "All locations". const ownsHours = (schedule?.extension ?? []).some((ext) => /\/schedule-(available-time|not-available)$/.test(ext.url)); if (ownsHours) return ''; return roleLocationId ?? ''; };