import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { DismissibleAlert } from '../../../../../components/ui/dismissible-alert'; import { Spacer } from '../../../../../components/ui/spacer'; import { useForm, FormProvider } from 'react-hook-form'; import { Procedure } from 'fhir/r4'; import { Check, FileText } from 'lucide-react'; import { useAuthServiceContext, usePatientServiceContext, useProcedureServiceContext } from '../../../../../services'; import { AppHeader, AppSpinner, FhirProcedure, HistoryNavigator, ProfileLayoutContainer, ScrollableTab, FhirProcedureDisplay } from '../../../../../components'; import { PrintableRegion, PrintRecord } from '../../../../../components/ui/print-record'; import { useFhirProcedureBroadcast, useFhirProcedures, useFhirProcedureTargetedBroadcast, useActiveEncounterReference, useRequestRecordCorrection } from '../../../../../hooks'; import { resolveProcedureAuthorReference } from '../../../../../utils/messaging/requestCorrection'; import { useArchive } from '../../../../../hooks/useArchive'; import ArchiveConfirmModal from '../../../../../components/ArchiveConfirmModal/ArchiveConfirmModal'; import { describeWriteFailure } from '../../../../../utils/fetchFailureMessage'; import { formatFhirCodeableConcept } from '../../../../../utils/fhirCodeableConcept.tsx'; import { patientCacheGuard } from '../../../../../utils/cacheOwner'; const EditProcedurePage: React.FC = () => { const { procedureId } = useParams<{ procedureId: string }>(); const navigate = useNavigate(); const { currentUser, userRole, USER_ROLES } = useAuthServiceContext(); const { patientState, encounterState } = usePatientServiceContext(); const { cachedForPatientId: proceduresOwner, fetchProcedure, transformToFhirProcedure, transformFromFhirProcedure, createProcedure, updateProcedure, archiveProcedure, proceduresList: rawProceduresList } = useProcedureServiceContext(); // The cache outlives a change of patient: read only the patient on screen. const proceduresGuard = patientCacheGuard(proceduresOwner, patientState?.id); const proceduresList = proceduresGuard.list(rawProceduresList); const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(); const [error, setError] = useState(); const [procedure, setProcedure] = useState(undefined); const isNew = procedureId === 'new'; const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes(userRole as string); const spacerMargin = '24px'; useFhirProcedureBroadcast(); useFhirProcedures(); // THE FIELD AGREES WITH THE SAVE NOW. The service already applied the active encounter whether or // not this field was touched, so the record filed against the visit correctly while the control // read "Select...". A clinician reading that is told their record is unattached when it is not, and // the natural correction is to open a list of every past visit and pick one - which is how a wrong // visit gets attached to a clinical fact. // // The same hook the eight clinical modals use, so there is one rule rather than a second copy. It // returns undefined outside a visit, which is what keeps the plain chart route opening empty. const activeEncounter = useActiveEncounterReference(); 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; const init = useCallback((procedure: Procedure) => { reset(transformFromFhirProcedure(procedure)); setProcedure(procedure); }, [transformFromFhirProcedure, reset]); useFhirProcedureTargetedBroadcast({ procedure, procedureId, onUpdate: init, onDelete: () => navigate('..', { relative: 'path' }) }); const archive = useArchive((reason, note) => archiveProcedure(procedure!, reason, note)); const { requestCorrection, error: correctionError, recovery: correctionRecovery } = useRequestRecordCorrection( procedure ? resolveProcedureAuthorReference(procedure) : undefined); const handleRequestCorrection = async (note: string) => { if (!procedure) return { ok: false }; const label = formatFhirCodeableConcept(procedure.code) || 'this procedure'; return requestCorrection(resolveProcedureAuthorReference(procedure), label, note); }; useEffect(() => { const loadProcedure = async () => { if (isNew || procedure) return; const foundProcedure = proceduresList?.find(p => p.id === procedureId); if (foundProcedure) { init(foundProcedure); return; } setLoading(true); try { const fetchedProcedure = await fetchProcedure(procedureId as string); init(fetchedProcedure); } catch (err) { console.error('Error loading procedure:', err); setError('Could not load procedure. Try again.'); } finally { setLoading(false); } }; loadProcedure(); }, [procedure, proceduresList, procedureId, fetchProcedure, init]); const onSubmit = async (data: any) => { if (!patientState) { setError('Patient information is missing. Please try again.'); return; } setSubmitting(true); setSuccess(undefined); setError(undefined); try { if (isNew) { await createProcedure(transformToFhirProcedure(data, patientState, encounterState, currentUser?.fhir)); navigate('..', { relative: 'path' }); } else { await updateProcedure({ id: procedure?.id, ...transformToFhirProcedure(data) }); } setSuccess(`Procedure ${isNew ? 'created' : 'updated'} successfully.`); setTimeout(() => setSuccess(undefined), 3_000); } catch (err: any) { console.error(`Error ${isNew ? 'creating' : 'updating'} procedure:`, err); setError(describeWriteFailure(err, `Failed to ${isNew ? 'create' : 'update'} procedure`)); } finally { setSubmitting(false); } }; if (loading) { return ; } return ( resourceType="Procedure" resourceId={procedureId || ''}> {(showHistory) => (
{!isNew && ( )} {!isPatient && ( )} {!isNew && !isPatient && ( )} {/* The record has to be able to leave the screen. Without this a patient sending it to somebody not on Akashic photographs a scrolling page or prints the browser window, navigation and all. */} {isPatient && procedure && } {success && ( <> setSuccess(undefined)}> {success} )} {error && ( <> setError(undefined)}>
{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} onRequestCorrection={handleRequestCorrection} correctionError={correctionError} correctionRecovery={correctionRecovery}/> ) : ( )} {!isPatient && ( { if (!o) archive.close(); }} resourceLabel="Procedure" submitting={archive.submitting} error={archive.error} onConfirm={async (reason, note) => { if (await archive.submit(reason, note)) { navigate('..', { relative: 'path' }); } }} /> )}
)} ); }; export default EditProcedurePage;