import type { Encounter } from 'fhir/r4'; import type { QueueItem } from './encounterLifecycle'; import { deriveStatus } from './encounterLifecycle'; /** * - VISITS THIS CLINICIAN LEFT OPEN ON AN EARLIER DAY. * * MEASURED 2026-09-14. The board read *"IN PROGRESS 0 - No visit is open right now"* while six visits * stood open on one patient, the oldest since August 2. The board is built from TODAY's appointments and * then asks for encounters per appointment, so a visit that outlives its day cannot appear in any of its * four cells whatever its status - nothing ever asks for it. And the chart's own entry for one is a * read-only field panel: no Finish, no way in. **There was no sequence of clicks that closed it.** * * An encounter that never closes holds a patient's visit open in the record indefinitely, and the record * is the product. * * THIS IS A SEPARATE LIST AND NOT A WIDER DAY, deliberately. The day's board answers *"who is here now"* * and is ordered by that; folding older visits into it would turn the one surface a clinician reads * between patients into a list of everything ever, and the oldest of these is six weeks old. They are * named as what they are and dated. */ /** Open means the visit is still running or parked. A finished or cancelled one is not this list's business. */ const OPEN_STATUSES = new Set(['in-progress', 'onleave']); const startOf = (encounter: Encounter): string | undefined => encounter.period?.start || encounter.meta?.lastUpdated; /** * Local midnight today, as an instant. * * LOCAL rather than UTC, and that is the whole reason this takes a clock instead of reading one: the * day's board already draws its boundary at the practice's own midnight, and a visit started at 9pm * would otherwise appear in "earlier days" from the moment UTC rolled over - on the same evening it * opened, to the clinician who is still in it. */ const startOfToday = (now: Date): number => new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); /** * The caller's own still-open visits that began before today, newest first. * * PURE, AND IT TAKES ENCOUNTERS RATHER THAN FETCHING THEM: the boundary rule is the part worth testing, * and it is the part that would otherwise need a clock, a network and a signed-in user to exercise. */ export const openVisitsFromEarlierDays = ( encounters: readonly Encounter[], now: Date, patientNames: Record = {}, ): QueueItem[] => { const midnight = startOfToday(now); return encounters .filter((encounter) => encounter.id && OPEN_STATUSES.has(encounter.status as string)) .filter((encounter) => { const start = startOf(encounter); // No start at all: KEPT. A visit whose period never got written is exactly the kind that gets // stranded, and dropping it here would hide the clearest case of the defect this exists for. if (!start) return true; const startedAt = new Date(start).getTime(); return Number.isFinite(startedAt) && startedAt < midnight; }) .map((encounter) => { const patientId = encounter.subject?.reference?.split('/').pop() ?? ''; return { // Keyed on the ENCOUNTER, not the patient: six of these can belong to one person, which // is how the defect was found. key: `open-visit-${encounter.id}`, patientId, patientName: patientNames[patientId], encounter, status: deriveStatus(undefined, encounter), atISO: startOf(encounter), } satisfies QueueItem; }) .sort((a, b) => new Date(b.atISO ?? 0).getTime() - new Date(a.atISO ?? 0).getTime()); };