import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Flex, FlexItem } from '../components/ui/flex'; import { DismissibleAlert } from '../components/ui/dismissible-alert'; import { Modal } from '../components/ui/wp-modal-shim'; import { useForm, FormProvider } from 'react-hook-form'; import { Observation } from 'fhir/r4'; import { useAuthServiceContext, useObservationServiceContext, usePatientServiceContext } from '../services'; import { EarlierSaveNotice } from '../components/EarlierSaveNotice/EarlierSaveNotice'; import { operationPartiesOf, useObservationOperation, type EarlierOutcome } from '../hooks/useObservationOperation'; import { OPERATION_NOT_SAVED_MESSAGE, OPERATION_REUSED_MESSAGE, OPERATION_UNAVAILABLE_MESSAGE, isWithheldSave, operationNotYetAdmitted, operationReused, operationUnconfirmed, } from '../utils/observationOperation'; import { RecoveryFull, RecoveryUnavailable } from '../utils/messaging/sendRecovery'; import { isSaveRetired, useSaveScope } from '../utils/saveOperation'; import { FhirObservation } from '../components'; import { useFhirObservationTargetedBroadcast } from '../hooks'; import { describeWriteFailure } from '../utils/fetchFailureMessage'; import { invalidSubmitMessage } from '../utils/forms/invalidSubmitMessage'; import { toLocalDateTimeInput } from '../utils/dateUtils'; import { GENERIC_ENTRY_HELD_NOTE, editDropsSourceNote, releasableOriginOf } from '../utils/releaseOrigin'; interface ObservationModalProps { isOpen: boolean; onClose: () => void; observation?: Observation; onSuccess?: (observation: Observation) => void; onError?: (error: string) => void; title?: string; } export const ObservationModal: React.FC = ({ isOpen, onClose, observation, onSuccess, onError, title }) => { const { patientState, encounterState } = usePatientServiceContext(); const { transformToFhirObservation, transformFromFhirObservation, createObservationOnce, updateObservation, // US Core Observation Profiles usCoreAverageBloodPressureProfile, usCoreCareExperiencePreferenceProfile, usCoreObservationAdiDocumentationProfile, usCoreObservationLabProfile, usCoreObservationOccupationProfile, usCoreObservationPregnancyintentProfile, usCoreObservationPregnancystatusProfile, usCoreObservationScreeningAssessmentProfile, usCoreObservationSexualOrientationProfile, usCoreSimpleObservationProfile, usCoreSmokingstatusProfile, usCoreTreatmentInterventionPreferenceProfile, // Vital Signs usCoreVitalSignsProfile, usCoreBloodPressureProfile, usCorePediatricMmiForAgeProfile, pediatricWeightForHeightProfile, usCoreBmiProfile, usCoreBodyHeightProfile, usCoreBodyTemperatureProfile, usCoreBodyWeightProfile, usCoreHeadCircumferenceProfile, usCoreHeartRateProfile, usCorePulseOximetryProfile, usCoreRespiratoryRateProfile, headOccipitalFrontalCircumferencePercentileProfile } = useObservationServiceContext(); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(); const [error, setError] = useState(); const [observationProfile, setObservationProfile] = useState(); const isNew = !observation; // A new reading is one save under one operation id; one never confirmed is checked when the modal opens. const reading = useObservationOperation('observation-modal', operationPartiesOf(useAuthServiceContext(), patientState?.id), useObservationServiceContext()); // Each opening owns its saves: closing it, another patient or another person retires them before they are sent. const saveScope = useSaveScope(patientState?.id); const submittingNow = useRef(false); useEffect(() => { saveScope.renew(); // A new opening starts idle: whatever the retired one was doing no longer holds its controls. submittingNow.current = false; setSubmitting(false); setSuccess(undefined); setError(undefined); if (isOpen && isNew) void reading.opening(); else reading.retire(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen, isNew, patientState?.id]); const settleEarlier = async (outcome: Promise) => { const result = await outcome; if (result?.saved) { const shownFor = saveScope.start(); setSuccess('Observation created successfully.'); if (result.observation) onSuccess?.(result.observation); setTimeout(() => { if (!shownFor.belongs()) return; setSuccess(undefined); onClose(); }, 1500); } else if (result) { setError('The earlier save was refused, so nothing was saved from it.'); } }; const modalTitle = title || `${isNew ? 'Create' : 'Edit'} Observation`; const baseObservation = { identifier: [], basedOn: [], partOf: [], status: '', category: [], code: null, focus: [], encounter: null, // A new observation starts at now: the server refuses a vital sign with no time taken. effectiveType: 'dateTime', effectiveDateTime: toLocalDateTimeInput(new Date()), effectiveTiming: null, effectiveInstant: '', issued: '', performer: [], valueType: '', valueQuantity: null, valueCodeableConcept: null, valueString: '', valueBoolean: false, valueInteger: 0, valueRange: { low: null, high: null }, valueRatio: null, valueSampledData: null, valueTime: '', valueDateTime: '', dataAbsentReason: null, interpretation: [], note: [], bodySite: null, method: null, specimen: null, device: null, referenceRange: [], hasMember: [], derivedFrom: [], component: [] }; const methods = useForm({ defaultValues: {} }); const { handleSubmit, reset } = methods; // A REFUSED CLICK SAYS SO WHERE IT HAPPENED. The button is in the modal header, above a form that // can be several screens tall, and react-hook-form reports each problem inline beside its field - // so somebody scrolled past the first card saw nothing change at all. This fills the error slot // this modal already has, at the top of the form, next to the button they pressed. const onInvalid = (errors: unknown) => setError(invalidSubmitMessage(errors)); useEffect(() => { if (observationProfile === 'us-core-average-blood-pressure') { if (isNew) { reset({ ...baseObservation, ...usCoreAverageBloodPressureProfile }); } } else if (observationProfile === 'us-core-care-experience-preference') { if (isNew) { reset({ ...baseObservation, ...usCoreCareExperiencePreferenceProfile }); } } else if (observationProfile === 'us-core-observation-adi-documentation') { if (isNew) { reset({ ...baseObservation, ...usCoreObservationAdiDocumentationProfile }); } } else if (observationProfile === 'us-core-observation-lab') { if (isNew) { reset({ ...baseObservation, ...usCoreObservationLabProfile }); } } else if (observationProfile === 'us-core-observation-occupation') { if (isNew) { reset({ ...baseObservation, ...usCoreObservationOccupationProfile }); } } else if (observationProfile === 'us-core-observation-pregnancyintent') { if (isNew) { reset({ ...baseObservation, ...usCoreObservationPregnancyintentProfile }); } } else if (observationProfile === 'us-core-observation-pregnancystatus') { if (isNew) { reset({ ...baseObservation, ...usCoreObservationPregnancystatusProfile }); } } else if (observationProfile === 'us-core-observation-screening-assessment') { if (isNew) { reset({ ...baseObservation, ...usCoreObservationScreeningAssessmentProfile }); } } else if (observationProfile === 'us-core-observation-sexual-orientation') { if (isNew) { reset({ ...baseObservation, ...usCoreObservationSexualOrientationProfile }); } } else if (observationProfile === 'us-core-smokingstatus') { if (isNew) { reset({ ...baseObservation, ...usCoreSmokingstatusProfile }); } } else if (observationProfile === 'us-core-treatment-intervention-preference') { if (isNew) { reset({ ...baseObservation, ...usCoreTreatmentInterventionPreferenceProfile }); } } else if (observationProfile === 'us-core-vital-signs') { if (isNew) { reset({ ...baseObservation, ...usCoreVitalSignsProfile }); } } else if (observationProfile === 'pediatric-bmi-for-age') { if (isNew) { reset({ ...baseObservation, ...usCorePediatricMmiForAgeProfile }); } } else if (observationProfile === 'pediatric-weight-for-height') { if (isNew) { reset({ ...baseObservation, ...pediatricWeightForHeightProfile }); } } else if (observationProfile === 'us-core-blood-pressure') { if (isNew) { reset({ ...baseObservation, ...usCoreBloodPressureProfile }); } } else if (observationProfile === 'us-core-bmi') { if (isNew) { reset({ ...baseObservation, ...usCoreBmiProfile }); } } else if (observationProfile === 'us-core-body-height') { if (isNew) { reset({ ...baseObservation, ...usCoreBodyHeightProfile }); } } else if (observationProfile === 'us-core-body-temperature') { if (isNew) { reset({ ...baseObservation, ...usCoreBodyTemperatureProfile }); } } else if (observationProfile === 'us-core-body-weight') { if (isNew) { reset({ ...baseObservation, ...usCoreBodyWeightProfile }); } } else if (observationProfile === 'us-core-head-circumference') { if (isNew) { reset({ ...baseObservation, ...usCoreHeadCircumferenceProfile }); } } else if (observationProfile === 'us-core-heart-rate') { if (isNew) { reset({ ...baseObservation, ...usCoreHeartRateProfile }); } } else if (observationProfile === 'us-core-pulse-oximetry') { if (isNew) { reset({ ...baseObservation, ...usCorePulseOximetryProfile }); } } else if (observationProfile === 'us-core-respiratory-rate') { if (isNew) { reset({ ...baseObservation, ...usCoreRespiratoryRateProfile }); } } else if (observationProfile === 'head-occipital-frontal-circumference-percentile') { if (isNew) { reset({ ...baseObservation, ...headOccipitalFrontalCircumferencePercentileProfile }); } } }, [observationProfile, reset]); const init = useCallback((observation: Observation) => { const formData = transformFromFhirObservation(observation); reset(formData); setObservationProfile(formData.observationProfileSlug); }, [transformFromFhirObservation, reset]); useFhirObservationTargetedBroadcast({ observation, isOpen, onUpdate: init, onDelete: onClose }); useEffect(() => { if (observation) { init(observation); } }, [observation, init]); const onSubmit = async (data: any) => { if (!patientState) { const errorMsg = 'Patient information is missing. Please try again.'; setError(errorMsg); onError?.(errorMsg); return; } if (submittingNow.current) return; submittingNow.current = true; const operation = saveScope.start(); setSubmitting(true); setSuccess(undefined); setError(undefined); try { let result: Observation | undefined; if (isNew) { let operationId: string; try { operationId = await reading.begin(); } catch (holdError) { if (holdError instanceof RecoveryUnavailable || holdError instanceof RecoveryFull) { setError(OPERATION_UNAVAILABLE_MESSAGE); onError?.(OPERATION_UNAVAILABLE_MESSAGE); return; } throw holdError; } try { // A confirmed answer settles its own operation, even one that arrives after the modal closed. const created = await operation.step(async () => { const answer = await createObservationOnce(transformToFhirObservation(data, patientState, encounterState), operationId, operation.transport); reading.settled(operationId); return answer; }, true); // Saved but not shown to this viewer now: saved all the same, with no record to hand on. result = isWithheldSave(created) ? undefined : created; } catch (createError) { if (isSaveRetired(createError) || !operation.belongs()) return; // Not confirmed, fenced unsaved, or its id already used: kept, and checked before another is entered. if (operationUnconfirmed(createError) || operationReused(createError) || operationNotYetAdmitted(createError)) { reading.unconfirmed(operationId); if (operationReused(createError)) setError(OPERATION_REUSED_MESSAGE); if (operationNotYetAdmitted(createError)) setError(OPERATION_NOT_SAVED_MESSAGE); return; } reading.settled(operationId); throw createError; } } else { result = await updateObservation({ id: observation?.id, ...transformToFhirObservation(data) // Every edit names the version it was made on. Today's callers open this modal only to create. }, observation?.meta?.versionId); } if (!operation.belongs()) return; const successMsg = `Observation ${isNew ? 'created' : 'updated'} successfully.`; setSuccess(successMsg); if (result) onSuccess?.(result); setTimeout(() => { if (!operation.belongs()) return; setSuccess(undefined); onClose(); }, 1500); } catch (err: any) { if (isSaveRetired(err) || !operation.belongs()) return; console.error(`Error ${isNew ? 'creating' : 'updating'} observation:`, err); const errorMsg = describeWriteFailure(err, `Failed to ${isNew ? 'create' : 'update'} observation`); setError(errorMsg); onError?.(errorMsg); } finally { if (operation.belongs()) { submittingNow.current = false; setSubmitting(false); } } }; if (!isOpen) return null; return ( ]} >
{success && ( setSuccess(undefined)} className="mb-4"> {success} )} {error && ( setError(undefined)} className="mb-4"> {error} )} {isNew && reading.earlier && ( void settleEarlier(reading.check())} onFinish={() => void settleEarlier(reading.finish())} onStartNew={() => { setError(undefined); reading.startNew(); }} /> )} {(() => { const origin = isNew ? undefined : releasableOriginOf(observation); const note = isNew ? GENERIC_ENTRY_HELD_NOTE : origin ? editDropsSourceNote(origin) : undefined; return note &&

{note}

; })()}
setObservationProfile(profile)} elevated={false} disabled={submitting} enableInlineActions={false}/>
); };