import type { Encounter } from 'fhir/r4'; import type { SnapshotItem, SnapshotTypeResult } from '../../../hooks/usePatientSnapshot'; // The categories surfaced in "since last visit" (design decision ยง14.3). export const SINCE_LAST_VISIT_TYPES = [ 'Observation', 'MedicationRequest', 'MedicationStatement', 'AllergyIntolerance', 'Condition', ] as const; export interface SinceGroup { resourceType: string; label: string; items: SnapshotItem[]; } export interface SinceLastVisitResult { /** * `undefined` MEANS TWO DIFFERENT THINGS AND THAT IS WHY `loaded` EXISTS. This comment used to read * *"undefined no prior finished encounter on record"*, and that reading is only safe once the * snapshot has actually come back - it is equally what you get before anything has loaded. The card * rendered the second meaning during the first, and told a clinician **"First visit on record."** about * a patient last seen two weeks earlier. * Read this ONLY when `loaded` is true. */ lastVisitISO?: string; groups: SinceGroup[]; // in SINCE_LAST_VISIT_TYPES order; only non-empty groups totalCount: number; /** * Has the snapshot come back at all? * * It is derived from `results` being non-empty rather than from a loading flag, because it has to * cover the window BEFORE the query starts - measured on the clinician overview, the card's first paint * had `running === false` with nothing loaded, so a `running` guard alone still showed a settled, * wrong answer for the first second. `usePatientSnapshot` returns one entry per configured type once it * resolves, regardless of whether that type holds anything, so empty genuinely means "not yet". */ loaded: boolean; } const parse = (iso?: string): number => (iso ? Date.parse(iso) : NaN); // Most recent finished encounter whose end is strictly before `now`. const findLastVisitISO = (results: SnapshotTypeResult[], now: Date): string | undefined => { const encounters = results.find((r) => r.resourceType === 'Encounter')?.items ?? []; let bestISO: string | undefined; let bestTs = -Infinity; for (const item of encounters) { const enc = item.resource as Encounter; if (enc?.status !== 'finished') continue; const endISO = enc.period?.end ?? item.lastUpdated; const ts = parse(endISO); if (!Number.isFinite(ts) || ts >= now.getTime()) continue; if (ts > bestTs) { bestTs = ts; bestISO = endISO; } } return bestISO; }; /** * From reconstructed snapshot results, derive what changed since the patient's last finished * encounter. Pure: filters in-scope types to items with `lastUpdated` after the anchor (or all, * when there's no prior visit). One snapshot call powers both the full chart and this summary. */ export const deriveSinceLastVisit = ( results: SnapshotTypeResult[], now: Date = new Date(), ): SinceLastVisitResult => { const lastVisitISO = findLastVisitISO(results, now); const anchorTs = lastVisitISO ? parse(lastVisitISO) : -Infinity; const groups: SinceGroup[] = []; let totalCount = 0; for (const type of SINCE_LAST_VISIT_TYPES) { const result = results.find((r) => r.resourceType === type); if (!result) continue; const items = result.items.filter((item) => { const ts = parse(item.lastUpdated); return Number.isFinite(ts) && ts > anchorTs; }); if (items.length > 0) { groups.push({ resourceType: type, label: result.label, items }); totalCount += items.length; } } return { lastVisitISO, groups, totalCount, loaded: results.length > 0 }; };