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 { CareTeam, Organization } from 'fhir/r4'; import { Check, FileText } from 'lucide-react'; import { useAuthServiceContext, useCareTeamServiceContext, usePatientServiceContext } from '../../../../../services'; import { AppHeader, AppSpinner, ProfileLayoutContainer, ScrollableTab, HistoryNavigator, FhirCareTeam, FhirCareTeamDisplay } from '../../../../../components'; import { PrintableRegion, PrintRecord } from '../../../../../components/ui/print-record'; import { useFhirCareTeamTargetedBroadcast } from '../../../../../hooks'; import { useArchive } from '../../../../../hooks/useArchive'; import { careTeamScopeAdmitted, useCareTeamReadScope } from '../../../../../hooks/useCareTeamReadScope'; import { useScopeLifetime } from '../../../../../hooks/useScopeLifetime'; import ArchiveConfirmModal from '../../../../../components/ArchiveConfirmModal/ArchiveConfirmModal'; import { describeWriteFailure } from '../../../../../utils/fetchFailureMessage'; const EditCareTeamPage: React.FC = () => { const { careTeamId } = useParams<{ careTeamId: string }>(); const navigate = useNavigate(); const { userRole, USER_ROLES, wpOrganization, currentOrganization } = useAuthServiceContext(); const { patientState, encounterState } = usePatientServiceContext(); const { fetchCareTeam, transformToFhirCareTeam, transformFromFhirCareTeam, createCareTeam, updateCareTeam, archiveCareTeam, careTeamsList, ownedScope } = useCareTeamServiceContext(); const readScope = useCareTeamReadScope(patientState?.id); const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(); const [error, setError] = useState(); const { generation, isLive } = useScopeLifetime(readScope); // The open team belongs to the viewer, practice and patient it was opened for, and to that opening's lifetime. const [opened, setOpened] = useState<{ generation: number; team: CareTeam } | undefined>(); const careTeam = opened?.generation === generation ? opened.team : undefined; // A write is sent only for the practice and opening it was made in; a switch, even back again, refuses it. const writeTransport = () => { const organization = currentOrganization; if (!organization) throw new Error('No practice in context'); const opening = generation; return { organization, admission: { isLive: () => isLive(opening) && wpOrganization.current?.id === organization.id } }; }; const archive = useArchive((reason, note) => archiveCareTeam(careTeam!, reason, note, writeTransport())); const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes(userRole as string); const isNew = careTeamId === 'new'; const spacerMargin = '24px'; const methods = useForm({ defaultValues: { identifier: [], status: '', category: [], name: '', subject: null, encounter: null, participant: [{ member: [{}] }], reasonCode: [], reasonReference: [], managingOrganization: [], telecom: [], note: [] } }); const { handleSubmit, reset } = methods; const init = useCallback((team: CareTeam) => { reset(transformFromFhirCareTeam(team)); setOpened({ generation, team }); }, [transformFromFhirCareTeam, generation]); useFhirCareTeamTargetedBroadcast({ careTeam, careTeamId, onUpdate: init, onDelete: () => navigate('..', { relative: 'path' }) }); useEffect(() => { const loadCareTeam = async () => { if (isNew || careTeam || !careTeamScopeAdmitted(readScope)) return; const opening = generation; // The shared list may still be another patient's, viewer's or practice's until this scope's read answers. const foundCareTeam = ownedScope === readScope ? careTeamsList?.find(ct => ct.id === careTeamId) : undefined; if (foundCareTeam) { init(foundCareTeam); return; } setLoading(true); try { const fetchedCareTeam = await fetchCareTeam(careTeamId as string, readScope); if (isLive(opening)) init(fetchedCareTeam); } catch (err) { console.error('Error loading care team:', err); setError('Could not load care team. Try again.'); } finally { setLoading(false); } }; loadCareTeam(); }, [careTeam, careTeamsList, ownedScope, readScope, careTeamId, fetchCareTeam, init, generation, isLive]); 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 { if (isNew) { await createCareTeam(transformToFhirCareTeam(data, patientState, wpOrganization.current as Organization, encounterState)); navigate('..', { relative: 'path' }); } else { if (!careTeam) return; const opening = generation; const saved = await updateCareTeam({ id: careTeam.id, ...transformToFhirCareTeam(data, patientState) }, careTeam.meta?.versionId, writeTransport()); // The saved version is the next edit's baseline. if (isLive(opening)) init(saved); } setSuccess(`Care team ${isNew ? 'created' : 'updated'} successfully.`); setTimeout(() => setSuccess(undefined), 3_000); } catch (err: any) { console.error(`Error ${isNew ? 'creating' : 'updating'} care team:`, err); setError(describeWriteFailure(err, `Failed to ${isNew ? 'create' : 'update'} care team`)); } finally { setSubmitting(false); } }; if (loading) { return ( ); } return ( resourceType="CareTeam" resourceId={careTeamId || ''}> {(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 && careTeam && } {success && ( <> setSuccess(undefined)}> {success} )} {error && ( <> setError(undefined)}>
{error}
)} {/* THE READER GETS THE RECORD, NOT THE FORM SWITCHED OFF. `FhirCareTeamDisplay` 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 && careTeam ? ( undefined}/> ) : ( )} {!isPatient && ( { if (!o) archive.close(); }} resourceLabel="Care Team" submitting={archive.submitting} error={archive.error} onConfirm={async (reason, note) => { if (await archive.submit(reason, note)) { navigate('..', { relative: 'path' }); } }} /> )}
)} ); }; export default EditCareTeamPage;