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 { Device } from 'fhir/r4'; import { useDeviceServiceContext, usePatientServiceContext } from '../services'; import { FhirDevice } from '../components'; import { useFhirDeviceTargetedBroadcast } from '../hooks'; import { useEditBaseline } from '../hooks/useEditBaseline'; import { isSaveRetired, isSaveTimedOut, outcomeUnknown, useSaveScope } from '../utils/saveOperation'; import { reloadBaseline, saveOverBaseline } from '../utils/saveOverBaseline'; import { asRecordChanged } from '../utils/recordChanged'; import { StaleRecordNotice } from '../components/StaleRecordNotice/StaleRecordNotice'; interface DeviceModalProps { isOpen: boolean; onClose: () => void; device?: Device; onSuccess?: (device: Device) => void; onError?: (error: string) => void; title?: string; } const emptyEquipment = { status: '', deviceName: [], manufacturer: '', modelNumber: '', serialNumber: '', lotNumber: '', expirationDate: '', udiCarrier: [], identifier: [], }; export const DeviceModal: React.FC = ({ isOpen, onClose, device, onSuccess, onError, title, }) => { const { patientState } = usePatientServiceContext(); const { transformToFhirDevice, transformFromFhirDevice, createDevice, updateDevice, fetchDevice } = useDeviceServiceContext(); 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 = !device; const modalTitle = title || `${isNew ? 'Create' : 'Edit'} Device`; const methods = useForm({ defaultValues: emptyEquipment }); const { handleSubmit, reset, getValues } = methods; const formOf = useCallback((record: Device) => transformFromFhirDevice(record) as Record, [transformFromFhirDevice]); // 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 filledThisOpening = useRef(); const saveScope = useSaveScope(isOpen ? `${patientState?.id ?? ''}/${device?.id ?? 'new'}` : undefined, () => { filledThisOpening.current = undefined; closeBaseline(); reset(emptyEquipment); setSubmitting(false); setReloading(false); setSuccess(undefined); setError(undefined); setCreateUnconfirmed(false); setCreated(false); }); useFhirDeviceTargetedBroadcast({ device, isOpen, onUpdate: (incoming) => { if (isOpen && incoming.id === device?.id && incoming.patient?.reference === `Patient/${patientState?.id}`) arrive(incoming); }, onDelete: onClose }); // Each opening starts from the record handed in; a newer one handed in while open arrives like a broadcast. useEffect(() => { if (!isOpen) { filledThisOpening.current = undefined; return; } if (!device) { const key = `${patientState?.id}/new`; if (filledThisOpening.current !== key) { filledThisOpening.current = key; reset(emptyEquipment); } return; } if (device.patient?.reference !== `Patient/${patientState?.id}`) return; const key = `${patientState?.id}/${device.id}`; if (filledThisOpening.current !== key) { filledThisOpening.current = key; openBaseline(device); } else arrive(device); }, [isOpen, patientState?.id, device, openBaseline, arrive, reset]); const onSubmit = async (data: any) => { if (!isOpen || !patientState || (device && device.patient?.reference !== `Patient/${patientState.id}`)) { const errorMsg = 'Patient information is missing. Please try again.'; setError(errorMsg); onError?.(errorMsg); return; } if (baseline.stale || createUnconfirmed || created || submitting) return; if (!isNew && !baseline.basedOn) { baseline.refuse('outOfDate'); return; } const operation = saveScope.start(); setSubmitting(true); setSuccess(undefined); setError(undefined); try { let result: Device; if (isNew) { result = await operation.step(() => createDevice( transformToFhirDevice(data, patientState), operation.transport, ), true); } else { const outcome = await saveOverBaseline({ baseline, operation, write: (basedOn, transport) => updateDevice({ ...transformToFhirDevice(data, patientState), id: baseline.opening?.id, meta: { versionId: basedOn }, }, transport).catch(error => { throw asRecordChanged(error); }), }); 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 = `Device ${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'} device:`, err); const errorMsg = describeWriteFailure( err, `Failed to ${isNew ? 'create' : 'update'} device`, ); 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: async (transport) => { const fresh = await fetchDevice(id, transport); if (fresh.id !== id || fresh.patient?.reference !== `Patient/${patientState?.id}`) { throw new Error('The equipment record does not belong to this opening.'); } return fresh; } }); } 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} />
); };