import React, { useState, useEffect, useCallback } 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 { Procedure } from 'fhir/r4'; import { useProcedureServiceContext, useAuthServiceContext, usePatientServiceContext } from '../services'; import { FhirProcedure, FhirProcedureDisplay } from '../components'; import { PrintableRegion, PrintRecord } from '../components/ui/print-record'; import { useFhirProcedureTargetedBroadcast, useActiveEncounterReference } from '../hooks'; import { describeWriteFailure } from '../utils/fetchFailureMessage'; import { invalidSubmitMessage } from '../utils/forms/invalidSubmitMessage'; interface ProcedureModalProps { isOpen: boolean; onClose: () => void; procedure?: Procedure; onSuccess?: (procedure: Procedure) => void; onError?: (error: string) => void; title?: string; } export const ProcedureModal: React.FC = ({ isOpen, onClose, procedure, onSuccess, onError, title }) => { const { currentUser, userRole, USER_ROLES } = useAuthServiceContext(); // THIS MODAL HAD NO PATIENT GUARD AT ALL - see the note in ClinicalImpressionModal. const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes(userRole as string); const { patientState, encounterState } = usePatientServiceContext(); const activeEncounter = useActiveEncounterReference(); const { transformToFhirProcedure, transformFromFhirProcedure, createProcedure, updateProcedure } = useProcedureServiceContext(); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(); const [error, setError] = useState(); const isNew = !procedure; const modalTitle = title || `${isNew ? 'Create' : 'Edit'} Procedure`; const methods = useForm({ defaultValues: { encounter: activeEncounter, status: '', category: null, code: null, bodySite: [], outcome: null, followUp: [], performedType: '', performedDateTime: '', performedString: '', performedAge: null, performedRange: { low: null, high: null }, recorder: null, asserter: null, performer: [], reasonCode: [], reasonReference: [], note: [] } }); 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)); const init = useCallback((procedure: Procedure) => { reset(transformFromFhirProcedure(procedure)); }, [transformFromFhirProcedure, reset]); useFhirProcedureTargetedBroadcast({ procedure, isOpen, onUpdate: init, onDelete: onClose }); useEffect(() => { if (procedure) { init(procedure); } }, [procedure, init]); const onSubmit = async (data: any) => { if (!patientState) { const errorMsg = 'Patient information is missing. Please try again.'; setError(errorMsg); onError?.(errorMsg); return; } setSubmitting(true); setSuccess(undefined); setError(undefined); try { let result: Procedure; if (isNew) { result = await createProcedure(transformToFhirProcedure(data, patientState, encounterState, currentUser?.fhir)); } else { result = await updateProcedure({ id: procedure?.id, ...transformToFhirProcedure(data) }); } const successMsg = `Procedure ${isNew ? 'created' : 'updated'} successfully.`; setSuccess(successMsg); onSuccess?.(result); setTimeout(() => { setSuccess(undefined); onClose(); }, 1500); } catch (err: any) { console.error(`Error ${isNew ? 'creating' : 'updating'} procedure:`, err); const errorMsg = describeWriteFailure(err, `Failed to ${isNew ? 'create' : 'update'} procedure`); setError(errorMsg); onError?.(errorMsg); } finally { setSubmitting(false); } }; if (!isOpen) return null; return ( {/* A READER GETS PRINT WHERE A WRITER GETS SAVE. A patient reading a record has no use for a Create/Update button - live it lies, disabled it tells them what they may not do, and neither answers the question they have: how do I get this to somebody who is not on Akashic. */} {isPatient ? ( ) : ( )} ]} >
{success && ( setSuccess(undefined)} className="mb-4"> {success} )} {error && ( setError(undefined)} className="mb-4"> {error} )}
{/* THE READER GETS THE RECORD, NOT THE FORM SWITCHED OFF. `FhirProcedureDisplay` already renders this resource for the history viewer, the reference preview and the patient snapshot, so a patient was the one audience shown the edit form instead of the view written for reading it. The old shape wrapped the form in `inert`, which is worse than it looks: the fields stay in the accessibility tree, so a screen reader still reads them as inputs, and a text range over the subtree returns an EMPTY STRING - a patient could not select or copy their own record. */} {isPatient && procedure ? ( undefined}/> ) : ( )}
); };