import { useEffect, useState } from 'react'; import type { Bundle, ClinicalImpression, Encounter, Resource } from 'fhir/r4'; import { environment } from '../../environments/environment'; import { useHttpService } from '../../services'; import { fetchAllPagesAsBundle, isSearchTruncated } from '../../utils/fetchAllPagesAsBundle'; import { searchPageSize } from '../../utils/pageSizeOverride'; import { isArchived } from '../../utils/archivedResource'; import { overlayHeldBundle, refersToPatient } from '../../utils/writeThroughHold'; // `incomplete`: not every note could be read, so an empty list is not "no note". export type VisitNoteState = | { status: 'loading' } | { status: 'failed' } | { status: 'ready'; texts: string[]; incomplete: boolean }; // Where the dialog's note comes from: the current record, or notes its caller already resolved // (a snapshot or anchor), which must never be swapped for today's note. export type VisitNoteSource = | { kind: 'current' } | { kind: 'resolved'; status: 'idle' | 'loading' | 'ready' | 'error'; resources: Resource[]; incomplete: boolean }; const writtenTextOf = (note: ClinicalImpression): string => [note.summary, ...(note.note ?? []).map((n) => n.text)] .filter((t): t is string => Boolean(t?.trim())) .join('\n\n'); export const visitNoteTexts = (resources: (Resource | undefined)[], encounterId: string | undefined): string[] => resources .filter((r): r is ClinicalImpression => r?.resourceType === 'ClinicalImpression') .filter((r) => !isArchived(r) && r.encounter?.reference === `Encounter/${encounterId}`) .map(writtenTextOf) .filter(Boolean); export const resolvedVisitNote = (source: Extract, encounterId: string | undefined): VisitNoteState => { if (source.status === 'error') return { status: 'failed' }; if (source.status !== 'ready') return { status: 'loading' }; return { status: 'ready', texts: visitNoteTexts(source.resources, encounterId), incomplete: source.incomplete }; }; // Searched by subject, as the note editor does, then narrowed to this encounter. export const useCurrentVisitNote = (encounter: Encounter): VisitNoteState => { const { get } = useHttpService(); const [state, setState] = useState({ status: 'loading' }); const patientId = encounter.subject?.reference?.startsWith('Patient/') ? encounter.subject.reference.slice('Patient/'.length) : undefined; useEffect(() => { if (!patientId || !encounter.id) { setState({ status: 'ready', texts: [], incomplete: false }); return; } let live = true; setState({ status: 'loading' }); fetchAllPagesAsBundle((pageParam) => { const params = new URLSearchParams(); params.append('_count', searchPageSize('100')); params.append('subject', patientId); if (pageParam) params.set('page', pageParam); return get>(`${environment.fhirClinicalApi}/ClinicalImpression?${params.toString()}`); }) .then((searched) => { if (!live) return; // A note this viewer just saved shows before the search has it. const bundle = overlayHeldBundle('ClinicalImpression', searched, (note) => refersToPatient(note, patientId)) as Bundle; const texts = visitNoteTexts((bundle.entry ?? []).map((e) => e.resource), encounter.id); setState({ status: 'ready', texts, incomplete: isSearchTruncated(bundle) }); }) .catch(() => { if (live) setState({ status: 'failed' }); }); return () => { live = false; }; }, [get, patientId, encounter.id]); return state; };