import { useCallback, useEffect, useState } from 'react'; import type { Appointment } from 'fhir/r4'; import { useAppointmentServiceContext } from '../../../services/AppointmentService'; import { appointmentFollowUpLines, appointmentVersion, visitEncounterLineOf } from '../../../utils/appointmentVersion'; import { writeOutcomeUnknown } from '../../../utils/appointmentWriteOutcome'; import { isRecordChanged } from '../../../utils/recordChanged'; import { isSaveRetired, useSaveScope } from '../../../utils/saveOperation'; import { onViewerChange } from '../../../utils/writeThroughHold'; export const CANCELLED_VISIT_RESAVED = 'The cancelled visit was saved again, which closes any of its visit records the cancellation left open.'; export const CANCELLED_VISIT_RESAVE_CHANGED = 'Someone else changed this appointment, so it was not saved again. Load it as it is now before trying again.'; export const CANCELLED_VISIT_RESAVE_UNCONFIRMED = 'Whether the visit was saved again could not be confirmed. Load it as it is now before trying again.'; export const CANCELLED_VISIT_RESAVE_FAILED = 'The visit could not be saved again.'; export const CANCELLED_VISIT_RELOAD_FAILED = 'We could not load this appointment as it is now. Try again.'; /** Said in a later opening of a visit whose earlier save was never settled: why it waits, not that earlier opening's words. */ export const CANCELLED_VISIT_EARLIER_UNSETTLED = 'An earlier attempt to save this visit again was not settled. Load it as it is now before trying again.'; /** Why a visit may not be saved again until it is loaded as it is now. */ export type ResaveStanding = 'unconfirmed' | 'changed'; export interface CancelledVisitResave { saving: boolean; /** What a saved re-save said; the visit itself is unchanged. */ note?: string[]; error?: string; /** What a re-save that did not go through did to the visit's own record, when it changed one. */ visitRecordLine?: string; /** Set after an unconfirmed or refused-as-changed save of this visit: nothing more is sent until it is reloaded. */ standing?: ResaveStanding; resave: () => Promise; /** The deliberate way out of a standing: read the visit as stored now and show that. */ reload: () => Promise; } type Said = Pick; // What holds a visit back from another save: a settled standing, or a save handed to the network (`sent`) whose answer // has not been taken by the opening that sent it. A sent save outlives its opening: it may reach the server whatever // becomes of the details, so a later opening waits for the visit to be loaded as it is now. type Hold = { kind: ResaveStanding | 'sent'; opening?: string }; // Re-saves a cancelled visit unchanged so the server closes its leftover unstarted records. What a save said lives // only in its details opening; a blocked re-save stays blocked per visit and viewer; only a definite answer settles it. export const useCancelledVisitResave = ( visit: Appointment | undefined, opening: string | undefined, { onSaved, onReloaded }: { onSaved: (saved: Appointment) => void; onReloaded: (current: Appointment) => void }, ): CancelledVisitResave => { const { updateAppointment, fetchAppointment } = useAppointmentServiceContext(); const [saving, setSaving] = useState(false); const [said, setSaidFor] = useState({}); const [holds, setHolds] = useState>({}); // An opening that ends, or a new viewer, retires what it was doing and what it said. const scope = useSaveScope(opening, () => { setSaving(false); setSaidFor({}); }); useEffect(() => onViewerChange(() => setHolds({})), []); const shown: Said = said.opening !== undefined && said.opening === opening ? said : {}; const hold = visit?.id ? holds[visit.id] : undefined; // The save this opening is waiting on is its own business while it waits; any other hold is said as a standing. const waitingHere = hold?.kind === 'sent' && hold.opening === opening && saving; // Said only in an opening: with the details closed nothing is shown, though the visit keeps its hold. const standing: ResaveStanding | undefined = opening && hold && !waitingHere ? (hold.kind === 'sent' ? 'unconfirmed' : hold.kind) : undefined; const holdVisit = (visitId: string, next: Hold | undefined) => setHolds((current) => { const { [visitId]: _settled, ...rest } = current; return next ? { ...rest, [visitId]: next } : rest; }); const resave = useCallback(async () => { if (!opening || !visit?.id || visit.status !== 'cancelled' || saving || hold) return; const visitId = visit.id; const say = (next: Said) => setSaidFor({ ...next, opening }); setSaving(true); say({}); const operation = scope.start(); let followUps: string[] = []; try { const basedOn = appointmentVersion(visit); const saved = await operation.step(() => updateAppointment( { ...visit }, { basedOn, onFollowUp: (reported) => { followUps = reported; }, // From the moment it leaves, the visit is held until its answer is taken here or it is reloaded. transport: { ...operation.transport, onSend: () => holdVisit(visitId, { kind: 'sent', opening }) }, }, ), true); if (!operation.belongs()) return; holdVisit(visitId, undefined); say({ note: [CANCELLED_VISIT_RESAVED, ...appointmentFollowUpLines(followUps)] }); onSaved(saved); } catch (error) { // Nothing is shown for an opening no longer on screen; retiring it says nothing about what the server did, and a // save it had already sent keeps the visit held. if (isSaveRetired(error) || !operation.belongs()) return; const visitRecordLine = visitEncounterLineOf(error); if (isRecordChanged(error)) { holdVisit(visitId, { kind: 'changed' }); say({ error: CANCELLED_VISIT_RESAVE_CHANGED, visitRecordLine }); } else if (writeOutcomeUnknown(error)) { holdVisit(visitId, { kind: 'unconfirmed' }); say({ error: CANCELLED_VISIT_RESAVE_UNCONFIRMED, visitRecordLine }); } else { // A definite refusal: nothing was saved, so nothing holds the visit. holdVisit(visitId, undefined); say({ error: CANCELLED_VISIT_RESAVE_FAILED, visitRecordLine }); } } finally { if (operation.belongs()) setSaving(false); } }, [opening, visit, saving, hold, scope, updateAppointment, onSaved]); const reload = useCallback(async () => { if (!opening || !visit?.id || saving || !hold) return; const visitId = visit.id; const say = (next: Said) => setSaidFor({ ...next, opening }); setSaving(true); const recovery = scope.start(); try { const current = await recovery.step(() => fetchAppointment(visitId, true, recovery.transport)); if (!recovery.belongs()) return; // The visit as stored now is on screen; it may be acted on again. onReloaded(current); holdVisit(visitId, undefined); say({}); } catch (error) { if (isSaveRetired(error) || !recovery.belongs()) return; say({ error: CANCELLED_VISIT_RELOAD_FAILED }); } finally { if (recovery.belongs()) setSaving(false); } }, [opening, visit, saving, hold, scope, fetchAppointment, onReloaded]); return { saving, ...shown, // A standing carried into a later opening is said there in its own words, not the earlier opening's. ...(standing && !shown.error ? { error: CANCELLED_VISIT_EARLIER_UNSETTLED } : {}), standing, resave, reload, }; };