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 { Practitioner, VisionPrescription } from 'fhir/r4'; import { Check, FileText } from 'lucide-react'; import { useAuthServiceContext, useVisionPrescriptionServiceContext, usePatientServiceContext } from '../../../../../services'; import { AppHeader, AppSpinner, ProfileLayoutContainer, ScrollableTab, HistoryNavigator, FhirVisionPrescription, FhirVisionPrescriptionDisplay } from '../../../../../components'; import { PrintableRegion, PrintRecord } from '../../../../../components/ui/print-record'; import { useFhirVisionPrescriptionTargetedBroadcast } from '../../../../../hooks'; import { useArchive } from '../../../../../hooks/useArchive'; import ArchiveConfirmModal from '../../../../../components/ArchiveConfirmModal/ArchiveConfirmModal'; import { describeWriteFailure } from '../../../../../utils/fetchFailureMessage'; import { mayEditRecord } from '../../../../../utils/recordEditability'; import { useSeatStanding } from '../../../../../utils/seatStanding'; import { patientCacheGuard } from '../../../../../utils/cacheOwner'; const EditVisionPrescriptionPage: React.FC = () => { const { visionPrescriptionId } = useParams<{ visionPrescriptionId: string }>(); const navigate = useNavigate(); const { currentUser, userRole, USER_ROLES, practitionerRoles } = useAuthServiceContext(); const seatStanding = useSeatStanding(); const { patientState, encounterState } = usePatientServiceContext(); const { cachedForPatientId: visionPrescriptionsOwner, fetchVisionPrescription, transformToFhirVisionPrescription, transformFromFhirVisionPrescription, createVisionPrescription, updateVisionPrescription, archiveVisionPrescription, visionPrescriptionsList: rawVisionPrescriptionsList } = useVisionPrescriptionServiceContext(); // The cache outlives a change of patient: read only the patient on screen. const visionPrescriptionsGuard = patientCacheGuard(visionPrescriptionsOwner, patientState?.id); const visionPrescriptionsList = visionPrescriptionsGuard.list(rawVisionPrescriptionsList); const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(); const [error, setError] = useState(); const [visionPrescription, setVisionPrescription] = useState(); const archive = useArchive((reason, note) => archiveVisionPrescription(visionPrescription!, reason, note)); const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes(userRole as string); /* * MAY THIS VIEWER EDIT THIS RECORD - the question the page below is drawn from. * * It used to ask `isPatient`, which is one case of it. Writing a VisionPrescription needs a paid clinician seat, * resolved per request by the server's `callerMayWrite`, and an unseated practitioner passed every * check here: the form opened populated, with Save and Archive, and the write came back refused. A * refusal that arrives after the typing has already spent the work. * * THE LIST IS NOT ENOUGH ON ITS OWN. A control removed only from the list is a client gate with a * door beside it - this page is reachable by URL - so the same question is asked again here. */ const seat = mayEditRecord({ seatStanding, resourceType: 'VisionPrescription', viewerIsStaff: !isPatient, practitionerRoles, }); const mayWrite = !isPatient && seat.mayEdit; const isNew = visionPrescriptionId === 'new'; const spacerMargin = '24px'; const methods = useForm({ defaultValues: { identifier: [], status: '', created: '', patient: null, encounter: null, dateWritten: '', prescriber: null, lensSpecification: [{ product: null, eye: null, sphere: 0, cylinder: 0, axis: 0, prism: [], add: 0, power: 0, backCurve: 0, diameter: 0, duration: null, color: '', brand: '', note: [] }] } }); const { handleSubmit, reset } = methods; const init = useCallback((visionPrescription: VisionPrescription) => { reset(transformFromFhirVisionPrescription(visionPrescription)); setVisionPrescription(visionPrescription); }, [transformFromFhirVisionPrescription]); useFhirVisionPrescriptionTargetedBroadcast({ visionPrescription, visionPrescriptionId, onUpdate: init, onDelete: () => navigate('..', { relative: 'path' }) }); useEffect(() => { const loadVisionPrescription = async () => { if (isNew || visionPrescription) return; const foundVisionPrescription = visionPrescriptionsList?.find(vp => vp.id === visionPrescriptionId); if (foundVisionPrescription) { init(foundVisionPrescription); return; } setLoading(true); try { const fetchedVisionPrescription = await fetchVisionPrescription(visionPrescriptionId as string); init(fetchedVisionPrescription); } catch (err) { console.error('Error loading vision prescription:', err); setError('Could not load vision prescription. Try again.'); } finally { setLoading(false); } }; loadVisionPrescription(); }, [visionPrescription, visionPrescriptionsList, visionPrescriptionId, fetchVisionPrescription, init]); const onSubmit = async (data: any) => { if (isPatient) return; if (!patientState) { setError('Patient information is missing. Please try again.'); return; } setSubmitting(true); setSuccess(undefined); setError(undefined); try { const practitioner: Practitioner = currentUser?.fhir as Practitioner if (isNew) { await createVisionPrescription(transformToFhirVisionPrescription(data, patientState, practitioner, encounterState)); navigate('..', { relative: 'path' }); } else { await updateVisionPrescription({ id: visionPrescription?.id, ...transformToFhirVisionPrescription(data, patientState, practitioner, encounterState) }); } setSuccess(`Vision prescription ${isNew ? 'created' : 'updated'} successfully.`); setTimeout(() => setSuccess(undefined), 3_000); } catch (err: any) { console.error(`Error ${isNew ? 'creating' : 'updating'} vision prescription:`, err); setError(describeWriteFailure(err, `Failed to ${isNew ? 'create' : 'update'} vision prescription`)); } finally { setSubmitting(false); } }; if (loading) { return ( ); } return ( resourceType="VisionPrescription" resourceId={visionPrescriptionId || ''}> {(showHistory) => (
{!isNew && ( )} {/* SAID, NOT WITHHELD. A Save that simply disappears reads as a broken screen or as a permission the reader has lost, and both send them to an administrator to check permissions that are already correct. A seat is nameable, and a practice can enable one in a minute. */} {!isPatient && !seat.mayEdit && (

{seat.reason}

)} {mayWrite && ( )} {!isNew && mayWrite && ( )} {/* 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. */} {!mayWrite && visionPrescription && }
{success && ( <> setSuccess(undefined)}> {success} )} {error && ( <> setError(undefined)}>
{error}
)} {/* THE READER GETS THE RECORD, NOT THE FORM SWITCHED OFF. `FhirVisionPrescriptionDisplay` 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. */} {!mayWrite && visionPrescription ? ( undefined}/> ) : ( )} {mayWrite && ( { if (!o) archive.close(); }} resourceLabel="Vision Prescription" submitting={archive.submitting} error={archive.error} onConfirm={async (reason, note) => { if (await archive.submit(reason, note)) { navigate('..', { relative: 'path' }); } }} /> )}
)} ); }; export default EditVisionPrescriptionPage;