import { useEffect, useState } from 'react'; import type { Task } from 'fhir/r4'; import { useQuestionnairePacketService, usePacketPractice, type PacketSummary } from '../services/QuestionnairePacketService'; import { onViewerChange, viewerEpoch } from '../utils/writeThroughHold'; import { SaveRetiredError } from '../utils/saveOperation'; import { packetOfTask } from '../utils/packetMembers'; export interface PatientPacketSummaries { /** Undefined until this patient's packets have been read for this viewer, or when the last read failed. */ packets?: PacketSummary[]; complete: boolean; failed: boolean; } const UNREAD: PatientPacketSummaries = { complete: true, failed: false }; // The last admitted answer per patient, practice and viewer, so Home and Tasks show one summary rather than two. const known = new Map(); onViewerChange(() => known.clear()); /** * The patient's assigned packets as the server projects them for this reader. Read again whenever `refresh` changes, * and forgotten at once when the patient, practice or viewer changes, so one patient's packets never show under * another's; an answer that arrives after its read was retired is never used. */ export const usePatientPacketSummaries = (patientId: string | undefined, enabled: boolean, refresh: string): PatientPacketSummaries => { const { listPacketSummaries } = useQuestionnairePacketService(); const practice = usePacketPractice(); const [viewer, setViewer] = useState(viewerEpoch); useEffect(() => onViewerChange(() => setViewer(viewerEpoch())), []); const key = enabled && patientId ? `${patientId}|${practice ?? ''}|${viewer}` : undefined; const [read, setRead] = useState<{ key: string; value: PatientPacketSummaries }>(); useEffect(() => { if (!key || !patientId) return; const controller = new AbortController(); const startedFor = viewerEpoch(); const current = () => !controller.signal.aborted && viewerEpoch() === startedFor; const admit = () => { if (!current()) throw new SaveRetiredError(); }; listPacketSummaries(patientId, { admit, signal: controller.signal, organizationId: practice }) .then((answer) => { if (!current()) return; const packets = Array.isArray(answer?.assignments) ? answer.assignments : []; const value = { packets, complete: answer?.complete !== false, failed: false }; known.set(key, value); setRead({ key, value }); }) .catch(() => { if (!current()) return; setRead({ key, value: { complete: false, failed: true } }); }); return () => controller.abort(); }, [key, refresh, patientId, practice, listPacketSummaries]); if (!key) return UNREAD; return read?.key === key ? read.value : known.get(key) ?? UNREAD; }; /** Reads the packets only while it is mounted, and takes back what it reported when it goes. */ export const PacketSummaryReader = ({ patientId, refresh, onRead }: { patientId: string | undefined; refresh: string; onRead: (read: PatientPacketSummaries) => void; }) => { const read = usePatientPacketSummaries(patientId, true, refresh); useEffect(() => { onRead(read); }, [read, onRead]); useEffect(() => () => onRead(UNREAD), [onRead]); return null; }; export const NO_PACKETS = UNREAD; /** What a packet read follows: every listed packet form, by id, status and version. Empty when none is listed. */ export const packetRefreshKey = (tasks: Task[]): string => tasks .filter((task) => packetOfTask(task)) .map((task) => `${task.id}:${task.status}:${task.meta?.versionId ?? ''}`).join(',');