import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '../components/ui/dialog'; import { DismissibleAlert } from '../components/ui/dismissible-alert'; import { describeWriteFailure, ownFailureSentence } from '../utils/fetchFailureMessage'; import { useForm, FormProvider } from 'react-hook-form'; import { AllergyIntolerance } from 'fhir/r4'; import { useAllergyIntoleranceServiceContext, useAuthServiceContext, usePatientServiceContext } from '../services'; import { FhirAllergyIntolerance } from '../components'; import { useFhirAllergyIntoleranceTargetedBroadcast, useActiveEncounterReference } from '../hooks'; import { useEditBaseline } from '../hooks/useEditBaseline'; import { isSaveRetired, isSaveTimedOut, outcomeUnknown, useSaveScope } from '../utils/saveOperation'; import { reloadBaseline, saveOverBaseline } from '../utils/saveOverBaseline'; import { StaleRecordNotice } from '../components/StaleRecordNotice/StaleRecordNotice'; interface AllergyIntoleranceModalProps { isOpen: boolean; onClose: () => void; allergyIntolerance?: AllergyIntolerance; onSuccess?: (allergyIntolerance: AllergyIntolerance) => void; onError?: (error: string) => void; title?: string; } export const AllergyIntoleranceModal: React.FC = ({ isOpen, onClose, allergyIntolerance, onSuccess, onError, title }) => { const { currentUser } = useAuthServiceContext(); const { patientState, encounterState } = usePatientServiceContext(); const activeEncounter = useActiveEncounterReference(); const { transformToFhirAllergyIntolerance, transformFromFhirAllergyIntolerance, createAllergyIntolerance, updateAllergyIntolerance, fetchAllergyIntolerance } = useAllergyIntoleranceServiceContext(); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(); const [error, setError] = useState(); const [reloading, setReloading] = useState(false); // A create whose save could not be confirmed may exist already: not offered again, so it is never made twice. const [createUnconfirmed, setCreateUnconfirmed] = useState(false); // A create that was answered is done for this opening: nothing is created again while it closes. const [created, setCreated] = useState(false); const isNew = !allergyIntolerance; const modalTitle = title || `${isNew ? 'Create' : 'Edit'} Allergy/Intolerance`; const methods = useForm({ defaultValues: { encounter: activeEncounter, clinicalStatus: null, verificationStatus: null, type: '', category: [], criticality: '', code: null, onsetType: '', onsetDateTime: '', onsetString: '', recordedDate: '', recorder: null, asserter: null, lastOccurrence: '', note: [], reaction: [], } }); const { handleSubmit, reset, getValues } = methods; const formOf = useCallback((record: AllergyIntolerance) => transformFromFhirAllergyIntolerance(record) as Record, [transformFromFhirAllergyIntolerance]); // The form's values, as plain data the baseline can compare. const read = useCallback(() => getValues() as Record, [getValues]); const write = useCallback((values: Record) => reset(values as never), [reset]); // The version this opening of the form was filled from, and the only one it saves against. A newer one arriving // (by broadcast or a refreshed prop) while the form holds changes is never filled over them: the opening is marked // changed, and Reload record is the way on. See useEditBaseline. const baseline = useEditBaseline>({ formOf, read, write }); const { open: openBaseline, arrive, close: closeBaseline } = baseline; // Each save - an update or a create - belongs to this opening of the modal: closing it (or another person signing // in) retires it, and everything it would still say, close or hand on is dropped with its busy flag. const saveScope = useSaveScope(isOpen ? allergyIntolerance?.id ?? 'new' : undefined, () => { closeBaseline(); setSubmitting(false); setReloading(false); setSuccess(undefined); setError(undefined); setCreateUnconfirmed(false); setCreated(false); }); useFhirAllergyIntoleranceTargetedBroadcast({ allergyIntolerance, isOpen, onUpdate: arrive, onDelete: onClose }); // Each opening starts from the record handed in; a newer one handed in while open arrives like a broadcast. const filledThisOpening = useRef(false); useEffect(() => { if (!isOpen) { filledThisOpening.current = false; return; } if (!allergyIntolerance) return; if (!filledThisOpening.current) { filledThisOpening.current = true; openBaseline(allergyIntolerance); return; } arrive(allergyIntolerance); }, [isOpen, allergyIntolerance, openBaseline, arrive]); const onSubmit = async (data: any) => { if (!patientState) { const errorMsg = 'Patient information is missing. Please try again.'; setError(errorMsg); onError?.(errorMsg); return; } if (baseline.stale || createUnconfirmed || created) return; const operation = saveScope.start(); setSubmitting(true); setSuccess(undefined); setError(undefined); try { let result: AllergyIntolerance; if (isNew) { result = await operation.step(() => createAllergyIntolerance( transformToFhirAllergyIntolerance(data, patientState, encounterState, currentUser?.fhir), operation.transport, ), true); } else { const outcome = await saveOverBaseline({ baseline, operation, write: (basedOn, transport) => updateAllergyIntolerance({ id: allergyIntolerance?.id, ...transformToFhirAllergyIntolerance(data) }, basedOn, transport), }); if ('retired' in outcome) return; if ('refused' in outcome) { // A finished opening says so in its own notice; only a save that sent nothing says it here. if (!outcome.stale) setError(outcome.refused); onError?.(outcome.refused); return; } result = outcome.saved; } if (isNew) setCreated(true); const successMsg = `Allergy/intolerance ${isNew ? 'created' : 'updated'} successfully.`; setSuccess(successMsg); onSuccess?.(result); // Closes this opening only: if the modal was closed (and perhaps opened again) meanwhile, it is left alone. setTimeout(() => { if (!operation.belongs()) return; setSuccess(undefined); onClose(); }, 1500); } catch (err: any) { if (isSaveRetired(err)) return; // An answer that never came - out of time, or the connection failed after the request may have left - // may mean it was created: not offered again until what is saved is looked at. if (isNew && outcomeUnknown(err)) { setCreateUnconfirmed(true); const unconfirmed = 'Whether it was created could not be confirmed. Close this and check the list before trying again.'; setError(unconfirmed); onError?.(unconfirmed); return; } if (isSaveTimedOut(err)) { setError(ownFailureSentence(err)); onError?.(ownFailureSentence(err)); return; } console.error(`Error ${isNew ? 'creating' : 'updating'} allergy/intolerance:`, err); const errorMsg = describeWriteFailure( err, `Failed to ${isNew ? 'create' : 'update'} allergy/intolerance`, ); setError(errorMsg); onError?.(errorMsg); } finally { if (operation.belongs()) setSubmitting(false); } }; // Reload record: what is saved now, read fresh, becomes the opening; the draft is discarded (the notice says so). const reload = async () => { const id = baseline.opening?.id; if (!id) return; const operation = saveScope.start(); setReloading(true); setError(undefined); try { await reloadBaseline({ baseline, operation, readNow: (transport) => fetchAllergyIntolerance(id, true, transport) }); } catch (reloadError) { if (isSaveRetired(reloadError)) return; setError('The record could not be read. Try Reload record again.'); } finally { if (operation.belongs()) setReloading(false); } }; if (!isOpen) return null; return ( { if (!next) onClose(); }}> {modalTitle}
{success && ( setSuccess(undefined)} className="mb-4"> {success} )} {error && ( setError(undefined)} className="mb-4"> {error} )} void reload()} reloading={reloading} />
); };