import type { Appointment } from 'fhir/r4'; import { taskCountIndicator, type PatientTaskCount } from '../../../utils/patientTaskCount'; import { AWAITING_APPROVAL_STATUSES, statusToBucket, type BucketKey, type QueueItem, } from '../ClinicianOverviewPage/encounterLifecycle'; import { readCheckoutDone } from '../../../utils/appointmentCheckout'; import { isToday } from './frontDeskDay'; /** * - the four priority queues of the settled receptionist overview. * * THE FINDING THAT SHAPED THIS FILE: the front desk board has never READ an Encounter. The server * creates one when an appointment is checked in, so every state on screen came from * `Appointment.status` - which cannot express *roomed* vs *with provider* vs *on hold*. Those three words * exist only on the Encounter, and telling them apart is the entire value of the "In the rooms" band: a * patient sitting alone in Room 3 for 22 minutes is the most useful thing that band can say, and a flat * "in visit" hides it completely. * * SO THE FRONT DESK RE-BUCKETS THE ONE QUEUE. The design's closing note is explicit - "the front desk is * a third bucketing of the one queue, not a second system. Give the receptionist her own status model * instead and 'arrived' means two different things in two places, which they will discover on a busy * morning." Everything below therefore derives from `statusToBucket(status, 'support')` rather than * restating encounter-status literals. Do not add a `status === 'triaged'` test to this file; add it to * the shared lifecycle module or nowhere. * * PURE ON PURPOSE, and it cannot call a hook. Names are resolved once at the component (through the * shared `appointmentParticipants` resolvers) and handed in - the same reason * `mapFhirAppointmentToReceptionistAppointment` takes its resolution rather than fetching it. */ /** The board's four queues, in the priority order they are rendered. */ export type FrontDeskQueueKey = 'needs-you' | 'waiting-room' | 'in-rooms' | 'still-to-come'; /** * ONLY EXCEPTIONS EARN COLOUR. `plain` is the default and the absence of a marker is the cleanest way * to say nothing is wrong - colouring every state makes colour mean nothing. */ export type StateTone = 'plain' | 'warn' | 'bad' | 'good'; /** The row's left rail + tint. One visual device, one meaning: this one is PATIENT STATE, nothing else. */ export type RowRail = 'none' | 'late' | 'here' | 'in-encounter' | 'checkout'; export type FrontDeskActionKind = 'check-out' | 'call' | 'review' | 'room' | 'send'; export interface FrontDeskRow { /** Stable react key. Never a display string - a name is neither stable nor unique. */ key: string; queue: FrontDeskQueueKey; patientId: string; patientName: string; /** Left-hand time column: the clock time today, or a weekday for something further out. */ time: string; /** The muted detail line. Never carries a clinical reason - see `roomsDetail` below. */ detail: string; state: { label: string; tone: StateTone }; /** * The instant the elapsed column counts from, or undefined when there is nothing to measure. * Not a formatted string: the board re-renders on its own clock, and a string baked here would * freeze at whatever minute this ran. */ elapsedSince?: number; action?: { kind: FrontDeskActionKind; label: string }; rail: RowRail; /** The patient carries an organisation flag (the authorization marker on the detail line). */ flagged?: boolean; item: QueueItem; } export interface FrontDeskQueues { needsYou: FrontDeskRow[]; waitingRoom: FrontDeskRow[]; inRooms: FrontDeskRow[]; stillToCome: FrontDeskRow[]; /** * "Dr. Reyes is running ~25 minutes behind", derived from the LONGEST roomed-and-waiting wait. * Measured from roomed-and-waiting, never from how long a visit has RUN - a 50-minute consultation * with the clinician in the room is a normal appointment, and reporting it here sends the desk chasing * the wrong person. */ runningBehind?: { room?: string; waitingMinutes: number }; /** A visit booked for today has already left Scheduled today, so an empty section means none are left. */ visitedToday?: boolean; } /** * SUPERSEDED BY SPENCER'S 2026-08-15 RULING - this is now an alias of the SHARED vocabulary. * * It used to be this board's own private three-state type, counting only the tasks attached to the * appointment. Both surfaces now show **every** task the patient has not completed plus the subset due * by this visit, and both resolve it through `utils/patientTaskCount.ts`. Do not re-grow a local * shape here: two implementations of one sentence is the drift the shared file exists to stop, and they * agreed only by coincidence the last time there were two. */ export type TaskSummary = PatientTaskCount; export interface FrontDeskInputs { items: readonly QueueItem[]; now: number; patientLabel: (item: QueueItem) => string; practitionerLabel: (item: QueueItem) => string; /** Per-appointment task completion. `undefined` means NOTHING WAS ASSIGNED - distinct from 'unknown'. */ taskSummary?: (item: QueueItem) => TaskSummary | undefined; /** A balance owed at checkout, already formatted (e.g. "$40 copay"). Absent when not known. */ balanceLabel?: (item: QueueItem) => string | undefined; /** * An organisation-level flag about this patient that the front desk must not lose sight of - today * its only consumer is the "not authorized" marker the old Upcoming list carried. * * IT EXISTS BECAUSE THE REDESIGN WOULD OTHERWISE HAVE DROPPED THAT SIGNAL SILENTLY. The design's * row vocabulary enumerates three things Needs you can contain, and an unauthorised patient is none of * them - but the old list showed it and a redesign is not a licence to delete a disclosure marker. * It rides the DETAIL line rather than becoming a fourth Needs-you row type: inventing a row the * spec does not name is the failure §4.6 exists to prevent, and this is a fact about a patient rather * than a job of its own. */ flagLabel?: (item: QueueItem) => string | undefined; /** False for a practice that does not room patients: no Roomed band and no Room action. */ rooming?: boolean; } /** * THE BOARD'S ONE ESCALATION LADDER, and it lives here so there is exactly one of it. * * These three numbers were already shipped - they are the tiers the late-appointment toast escalates * through. `ReceptionistHomePage` now imports them from here rather than declaring its own copies, because * §4.7 of the spec says a value that differs between two similar things must be derived or defended, and a * board that called someone late at a different minute from the toast that chases them is two answers to * one question. Do not re-declare any of these anywhere on this surface. */ export const LATE_LADDER_MINUTES = { /** Past this, a booked appointment nobody arrived for is late. */ warning: 15, /** Past this, the wait has stopped being routine - it is also what makes a WAITING patient an exception. */ escalation: 30, /** Past this, it is a no-show risk. */ overdue: 60, } as const; /** How far past its start a booked appointment must sit before the board calls it late. */ export const BOARD_LATE_MINUTES = LATE_LADDER_MINUTES.warning; /** * MOVED to `src/utils/formatElapsedSince.ts` 2026-08-22 and re-exported here, so every existing import * and test keeps working untouched. * * Its own reason for existing is what moved it: *"three spellings of that is three answers to one * question."* That argument never stopped at the front desk - the clinician overview had a **fourth** * spelling, raw minutes with no hour rollover, and an in-progress visit read `405m`. A shared answer that * lives inside one role's folder is one the next role does not find. */ export { formatElapsedSince } from '../../../utils/formatElapsedSince'; /** * The four queues in the order the board renders them, which is also the order they are FUNDED in. * "Needs you" first is not a preference: the waiting room is self-announcing - a receptionist can see * those people over the counter - while a patient who is 70 minutes late exists only on this screen. * "Still to come" is deliberately last, and the spec says why: it is the only queue with a door out * ("Open the full day ") and the only one whose contents are neither in the building nor already wrong. */ export const FRONT_DESK_QUEUE_PRIORITY = [ 'needsYou', 'waitingRoom', 'inRooms', 'stillToCome', ] as const; export type QueueRowCounts = Record<(typeof FRONT_DESK_QUEUE_PRIORITY)[number], number>; /** * CAPS ARE ADAPTIVE, NOT LITERAL - and this is the invariant that keeps the page from scrolling at any * volume while still using the whole screen on a quiet one. * * Every non-empty queue gets a floor, then the leftover goes to whichever queue actually OVERFLOWS, in * priority order. AN EMPTY SECTION RETURNS ITS ROWS TO THE POOL, which is the whole point: a quiet * morning with nothing late and nobody roomed hands most of the screen to the day ahead, and a taller * monitor simply shows more of everything. * * The result caps how much is VISIBLE, never how much EXISTS. The label's count is always the total - * a cap that also narrowed the count would quietly hide work, which is the defect this whole surface was * redesigned to remove. * * ON A SHORT SCREEN "STILL TO COME" YIELDS FIRST AND LAST. It is last in the funding order, so it is * the one starved of a floor when the budget cannot cover four of them - and the spec chose it because * it is the only queue you can open in full elsewhere. */ export const allocateQueueRows = ( demand: QueueRowCounts, budgetRows: number, floorRows = 2, ): QueueRowCounts => { const allocated: QueueRowCounts = { needsYou: 0, waitingRoom: 0, inRooms: 0, stillToCome: 0 }; let left = Math.max(0, Math.floor(budgetRows)); // Pass 1 - a floor for every queue that has anything in it, in priority order so a budget too small // for four floors starves the last one rather than shortchanging all four. // // NO `if (demand[key] === 0) continue` GUARD, and no `if (left <= 0)` either - both were here and a // mutation pass showed them INERT. An empty section takes nothing BY CONSTRUCTION (the `min` sees a // demand of 0) and an exhausted budget gives nothing for the same reason, with `left` clamped at zero // on the way in. Worse, the two guards made each OTHER untestable: with both present, removing either // changed no answer. A line that reads as load-bearing and is not is worse than no line. for (const key of FRONT_DESK_QUEUE_PRIORITY) { const give = Math.min(demand[key], floorRows, left); allocated[key] = give; left -= give; } // Pass 2 - the leftover, to whoever still overflows, in the same order. for (const key of FRONT_DESK_QUEUE_PRIORITY) { if (left <= 0) break; const want = demand[key] - allocated[key]; if (want <= 0) continue; const give = Math.min(want, left); allocated[key] += give; left -= give; } return allocated; }; /** * Which appointment statuses this board is willing to SHOW. * * Cancelled and entered-in-error stay out - the queues answer "what needs me now", and a cancelled * appointment needs nothing. `fulfilled` is out too: the visit is closed, and a finished ENCOUNTER is what * puts a patient in Checkout. `noshow` is a resolved outcome, not a job. */ const RESOLVED_APPOINTMENT_STATUSES = new Set([ 'cancelled', 'entered-in-error', 'fulfilled', 'noshow', ]); // Never on the day's schedule, so they cannot make Scheduled today say the day has had visits. const UNSCHEDULED_APPOINTMENT_STATUSES = new Set(['cancelled', 'entered-in-error']); /** * The APPOINTMENT says the patient is here. * * - THE ONE ACTION THE ALARM NAMES USED TO BE THE ONE ACTION WITH NO EFFECT ON IT. The board * left the "not arrived" state only when an ENCOUNTER existed. With no encounter the fallback below * recognised only *resolved* and *awaiting-approval* appointment statuses, so `arrived` fell through to the * late check and re-rendered as "not arrived - 249m late" - while the dialog the receptionist had just used * said "Arrived - Late" and the Appointments list rendered it correctly. * * WHY THE ENCOUNTER CANNOT BE THE ONLY SIGNAL, which is the durable half. Checking a patient in writes * TWO things: the appointment status (the source of truth the server returns) and an `arrived` Encounter, * which the server creates after that write and treats as **non-fatal**, logging a failure rather than * rolling back the check-in. A board that * reads only the encounter therefore hangs the busiest screen in the product on the one write that is * allowed to fail silently. * * The consequence was worse than a stuck row: the only statuses that cleared the board were Completed, * Cancelled and No Show, so a receptionist whose count refused to move had three exits and **two of them * write a false record about a patient standing in front of them**. * * Both FHIR spellings, because the status control offers one "arrived" concept and * `receptionistAppointmentStatus` maps `checked-in` onto it - but a record created elsewhere may carry * either, and a board that honoured only one would reproduce this card for the other. */ const ARRIVED_APPOINTMENT_STATUSES = new Set([ 'arrived', 'checked-in', ]); const minutesBetween = (fromMs: number, toMs: number) => Math.floor(Math.max(0, toMs - fromMs) / 60_000); const startMs = (iso?: string): number | undefined => { if (!iso) return undefined; const ms = new Date(iso).getTime(); return Number.isNaN(ms) ? undefined : ms; }; /** Without rooms, a visit past check-in stays under Checked in and says where it is in plain words. */ const ROOMLESS_STATE: Partial> = { triaged: { label: 'ready', tone: 'good' }, 'in-progress': { label: 'with provider', tone: 'plain' }, onleave: { label: 'on hold', tone: 'warn' }, }; /** * THE BUCKET DECIDES, NOT A STATUS LITERAL. The only queue this function chooses on its own is the one * the shared queue has no opinion about - `later`, which covers everything with no encounter yet, where * the APPOINTMENT's own status is the remaining question (booked vs awaiting approval vs already resolved). */ export const frontDeskQueueFor = ( item: QueueItem, now: number, ): FrontDeskQueueKey | undefined => { const bucket: BucketKey = statusToBucket(item.status, 'support', Boolean(item.room)); if (bucket === 'to-room') return 'waiting-room'; if (bucket === 'roomed' || bucket === 'with-provider') return 'in-rooms'; if (bucket === 'seen') return 'needs-you'; // No encounter yet. The appointment answers the rest. const status = item.appointment?.status; if (!status || RESOLVED_APPOINTMENT_STATUSES.has(status)) return undefined; if (AWAITING_APPROVAL_STATUSES.has(status)) return 'needs-you'; // - ABOVE THE LATE CHECK, WHICH IS THE WHOLE POINT. A patient who has checked in is not // late any more, however long ago they were due; leaving this below would keep them in Needs you with a // minutes-late counter, which is the defect. And it is deliberately not a widening of // RESOLVED_APPOINTMENT_STATUSES: `arrived` does not remove the row from the board, it moves it to the // waiting room, where the row still carries its task count and its Room action. if (ARRIVED_APPOINTMENT_STATUSES.has(status)) return 'waiting-room'; const at = startMs(item.atISO); if (at !== undefined && minutesBetween(at, now) >= BOARD_LATE_MINUTES) return 'needs-you'; return 'still-to-come'; }; const formatTime = (iso: string | undefined, now: number): string => { const at = startMs(iso); if (at === undefined) return ''; const dayStart = new Date(now); dayStart.setHours(0, 0, 0, 0); const dayEnd = new Date(dayStart); dayEnd.setDate(dayEnd.getDate() + 1); const date = new Date(at); if (at >= dayStart.getTime() && at < dayEnd.getTime()) { return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); } // Something further out - a self-booking two days away. The weekday is what a front desk reads. return date.toLocaleDateString('en-US', { weekday: 'short' }); }; /** * ALWAYS MINUTES, never rolled into hours - the drawing says "70m late", not "1h 10m late". * * And the reason generalises past this one string: these values sit in a column the eye is meant to run * down looking for the LARGEST number (§4.5 splits elapsed out precisely so it forms a tabular column). * Mixing "95m" and "1h 10m" in that column defeats it - the reader has to convert before comparing, on the * one screen whose whole job is being scannable. */ const formatMinutesLate = (minutes: number): string => `${minutes}m late`; /** Joined with the board's separator, dropping anything absent - never a placeholder for a missing fact. */ const detailOf = (...parts: Array) => parts.filter((part): part is string => Boolean(part && part.trim())).join(' · '); /** * The rooms band's detail line, and the disclosure fence lives here. * * SHOW *THAT*, NEVER *WHY*. Room, provider and elapsed - not the clinical reason. The receptionist's * access is demographic visibility, not the record, and a front-desk monitor is frequently readable from * the counter. She already sees the booked service; she must not start seeing what the clinician has since * entered. `encounter.reasonCode` must never be read in this file. */ const roomsDetail = (item: QueueItem, practitioner: string): string => { if (item.status === 'triaged') { return detailOf(item.room, 'nobody with them yet'); } if (item.status === 'onleave') { return detailOf(item.room, 'stepped out'); } return detailOf(item.room, practitioner); }; const IN_ROOM_STATE: Partial> = { triaged: { label: 'roomed', tone: 'warn' }, 'in-progress': { label: 'with provider', tone: 'plain' }, onleave: { label: 'on hold', tone: 'warn' }, }; /** * How long a patient may sit in the waiting room before the wait itself becomes the exception. * Derived, not chosen: it is the ladder's own second rung - the minute at which this board already * decides a wait has stopped being routine. */ const WAITING_ROOM_PATIENCE_MINUTES = LATE_LADDER_MINUTES.escalation; /** * THE BOARD'S TASK SENTENCE COMES FROM THE SHARED VOCABULARY, not from here (Spencer, 2026-08-15). * * This file used to compute it: an appointment-scoped count with its own three-state handling that * happened to produce the same strings as the practitioner's. They agreed **by coincidence, not by * construction** - and the day one of them was wired to a different source, they said different things * about the same patient. One function now, both boards. */ const taskInfo = (summary: TaskSummary | undefined) => taskCountIndicator(summary); export const buildFrontDeskQueues = (inputs: FrontDeskInputs): FrontDeskQueues => { const { items, now, patientLabel, practitionerLabel, taskSummary = () => undefined, balanceLabel = () => undefined, flagLabel = () => undefined, rooming = true, } = inputs; const queues: FrontDeskQueues = { needsYou: [], waitingRoom: [], inRooms: [], stillToCome: [], }; /** The longest roomed-and-waiting wait on the board, which is what "running behind" is measured from. */ let longestRoomedWait: { room?: string; waitingMinutes: number } | undefined; for (const item of items) { const queue = frontDeskQueueFor(item, now); const booked = item.appointment?.status; if ( queue !== 'still-to-come' && isToday(item.atISO, now) && booked && !UNSCHEDULED_APPOINTMENT_STATUSES.has(booked) && !AWAITING_APPROVAL_STATUSES.has(booked) ) { queues.visitedToday = true; } if (!queue) continue; const patientName = patientLabel(item); const practitioner = practitionerLabel(item); const summary = taskSummary(item); // Appended to whatever the row's detail line already says, so a flag can never displace the fact // the row exists for. `detailOf` drops it when there is none. const flag = flagLabel(item); const base = { key: item.key, queue, patientId: item.patientId, patientName, time: formatTime(item.atISO, now), flagged: Boolean(flag), item, }; if (queue === 'in-rooms') { const state = IN_ROOM_STATE[item.status] ?? { label: 'in a room', tone: 'plain' as StateTone }; const since = startMs(item.encounter?.period?.start); if (item.status === 'triaged' && since !== undefined) { const waitingMinutes = minutesBetween(since, now); if (!longestRoomedWait || waitingMinutes > longestRoomedWait.waitingMinutes) { longestRoomedWait = { room: item.room, waitingMinutes }; } } if (!rooming) { queues.waitingRoom.push({ ...base, queue: 'waiting-room', detail: detailOf(practitioner, taskInfo(summary)?.label, flag), state: ROOMLESS_STATE[item.status] ?? state, elapsedSince: since, rail: 'in-encounter', }); continue; } queues.inRooms.push({ ...base, detail: detailOf(roomsDetail(item, practitioner), flag), state, elapsedSince: since, // No action, deliberately. A visit in progress needs nothing from the front desk - that is // exactly why this band is information and not "Needs you". rail: 'in-encounter', }); continue; } if (queue === 'waiting-room') { // - `since` is undefined for a patient whose APPOINTMENT says arrived but whose // check-in Encounter has not landed, and that is the honest answer rather than a gap. The encounter's // period start is the only record of WHEN they walked in; the appointment start is when they were // DUE, so falling back to it would tell the desk that someone who arrived 60 minutes late has been // sitting there for an hour. No clock, and the state reads "ready" rather than "waiting". const since = startMs(item.encounter?.period?.start); const waitingMinutes = since === undefined ? 0 : minutesBetween(since, now); // An incomplete task list is an EXCEPTION and rises to the state column, in amber as drawn; a complete // one is information and rides the detail line. Reconciles the spec's §3.0 ("every row that names a // patient carries the count, amber while incomplete") with §4.3 ("only exceptions earn colour"). const tasks = taskInfo(summary); // OUTSTANDING WORK IS THE EXCEPTION AND TAKES THE STATE COLUMN; a settled count is information // and rides the detail line. And "due by this visit" outranks the wait itself: a patient sitting // 40 minutes with nothing owed is a queue problem, while one who still owes the intake for the // appointment they are at is a DECISION - finish it here, or rebook. const state = tasks?.outstanding ? { label: tasks.dueHereLabel ?? tasks.label, tone: 'warn' as StateTone, } : waitingMinutes >= WAITING_ROOM_PATIENCE_MINUTES ? { label: 'waiting', tone: 'bad' as StateTone } : rooming ? { label: 'ready', tone: 'good' as StateTone } : { label: 'checked in', tone: 'plain' as StateTone }; queues.waitingRoom.push({ ...base, // The whole picture ALWAYS rides the detail line, even when the state column is shouting about // this visit's share of it - the person at the desk needs both numbers, which is the ruling. detail: detailOf(practitioner, tasks?.label, flag), state, elapsedSince: since, action: tasks?.outstanding ? { kind: 'send', label: 'Send' } : rooming ? { kind: 'room', label: 'Room' } : undefined, rail: 'here', }); continue; } if (queue === 'still-to-come') { const tasks = taskInfo(summary); queues.stillToCome.push({ ...base, // The flag rides the detail line only when outstanding tasks hold the state column; otherwise // the state column carries it and the line would repeat it. detail: detailOf(practitioner, tasks?.label, tasks?.outstanding ? flag : undefined), state: tasks?.outstanding ? { label: tasks.dueHereLabel ?? tasks.label, tone: 'warn', } // Owed agreements are an exception too: check-in will stop on them. The detail line is too // narrow at desk widths to show a marker at its end, so it takes the empty state column. : flag ? { label: flag, tone: 'warn' } // "Booked" carries NO marker at all. It is the default, and the absence of a marker is the // cleanest way to say nothing is wrong. : { label: '', tone: 'plain' }, action: tasks?.outstanding ? { kind: 'send', label: 'Send' } : undefined, rail: 'none', }); continue; } // Needs you - three different jobs, and each names the query that produced it. if (statusToBucket(item.status, 'support') === 'seen') { // The desk said checkout is done; the visit stays in the patient's history, off this board. if (readCheckoutDone(item.appointment)) continue; const balance = balanceLabel(item); queues.needsYou.push({ ...base, detail: detailOf('finished', balance, flag), state: { label: 'Checkout', tone: 'good' }, elapsedSince: startMs(item.encounter?.period?.end), action: { kind: 'check-out', label: 'Check out' }, rail: 'checkout', }); continue; } const status = item.appointment?.status; if (status && AWAITING_APPROVAL_STATUSES.has(status)) { // TWO DIFFERENT ACTS, AND THE ROW USED TO CALL BOTH OF THEM "needs approval". A preferred-slot // request arrives holding a practitioner and a time, so the desk approves what the patient chose. // A time-window request holds neither, so the desk PLACES it - it decides the two things the // patient deliberately did not. The dialog this row opens already knows the difference and says // "Place appointment" on the second one; the row is what disagreed with it, one click apart. const awaitingPlacement = !item.appointment?.slot?.some( (slot) => slot.reference?.startsWith('Slot/'), ); // Request-only arrives `proposed`: the patient asked for this time, so the desk confirms a request. const requested = item.appointment?.status === 'proposed'; queues.needsYou.push({ ...base, detail: detailOf(requested ? 'requested' : 'self-booked', practitioner, flag), state: { label: awaitingPlacement ? 'needs placing' : requested ? 'needs confirming' : 'needs approval', tone: 'warn', }, action: { kind: 'review', label: 'Review' }, rail: 'none', }); continue; } const at = startMs(item.atISO); const lateMinutes = at === undefined ? 0 : minutesBetween(at, now); queues.needsYou.push({ ...base, detail: detailOf(practitioner, 'not arrived', flag), state: { label: formatMinutesLate(lateMinutes), tone: 'bad' }, action: { kind: 'call', label: 'Call' }, rail: 'late', }); } if (longestRoomedWait) queues.runningBehind = longestRoomedWait; return queues; }; /** * - `boardIsAllClear` AND ITS SENTENCE ARE GONE. This note is deliberately what replaces them. * * The board used to say *"Nothing needs you and nobody is waiting."* when every source agreed there was * nothing. Spencer retired it on 2026-08-25: the four sections each state their own zero now, and four * precise statements do not need a fifth, vaguer one sitting on top of them. * * BUT KEEP THE RULE IT ENCODED - it was the expensive part, and it is now enforced in four places * instead of one: **THREE STATES ON EVERY SOURCE - loading, empty, and NOT-FULLY-READ.** landed * it for *"No visits on your schedule today"*; added `boardIncomplete` for an appointment read * that stopped early; put the message reads under it after an unanswered patient message - the * front door for anyone without a named clinician - turned out to be a person waiting on the one screen * that told the receptionist nobody was. * * AND THE FAILURE MODE THAT KILLED IT, which is the part worth not repeating: the sentence was * correctly WITHHELD whenever a message was unanswered - most quiet mornings - and **nothing took its * place**, so the column went blank. A claim that is right to withhold still owes the reader something. */ // `FrontDeskQueuesColumn`'s `countsRead` prop is that rule now, applied per section. A zero is a CLAIM, // and only a source that has actually answered may make one. /** * Whether the board's counts may speak yet. * * IT IS A FUNCTION SO IT CAN BE TESTED, and it exists because the page got it wrong for months in a way * no component test could see. The page passed `!loadingAppointments && !loadingEncounters` - which is * true before anything has started as well as after everything has finished. `FrontDeskQueuesColumn` * then did exactly as it was told and stated a count from empty arrays. * * The tests for that component all pass `countsRead` in directly, so they proved the component honours * the flag and could never have caught the page computing it wrongly. Measured live 2026-08-31: the * board read "NEEDS ATTENTION! 0" for about a second on every in-app return, while eight patients were * over an hour late. * * NOT LOADING IS NOT AN ANSWER. That is the whole rule. */ export const frontDeskCountsAreRead = (state: { appointmentsAnswered: boolean; loadingAppointments: boolean; loadingEncounters: boolean; /** The practice and day the board last fully answered for (`frontDeskBoardKey`). */ answeredFor?: string; /** The practice and day on screen now. */ showingFor?: string; }): boolean => state.appointmentsAnswered && ((!state.loadingAppointments && !state.loadingEncounters) || // A re-read of a board that answered for this same practice and day keeps its counts; blanking // them moved the board on every poll. Another day's or practice's answer never stands in. (state.answeredFor !== undefined && state.answeredFor === state.showingFor)); /** Which practice and local day a board's answer belongs to. */ export const frontDeskBoardKey = (organizationId: string | undefined, now: number): string => { const day = new Date(now); return `${organizationId ?? ""}|${day.getFullYear()}-${day.getMonth() + 1}-${day.getDate()}`; }; /** * Whether the board shows its loading line. Only the FIRST read of each source shows it: a re-read of an * answered board keeps that board on screen, because the line above it pushes the whole board down. */ export const frontDeskBoardIsLoading = (state: { appointmentsAnswered: boolean; loadingAppointments: boolean; encountersAnswered: boolean; loadingEncounters: boolean; }): boolean => (state.loadingAppointments && !state.appointmentsAnswered) || (state.loadingEncounters && !state.encountersAnswered);