import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { DismissibleAlert } from '../../../../components/ui/dismissible-alert'; import { Flex, FlexItem } from '../../../../components/ui/flex'; import { Text } from '../../../../components/ui/text'; import { Spacer } from '../../../../components/ui/spacer'; import { Modal } from '../../../../components/ui/wp-modal-shim'; import { useForm, FormProvider } from 'react-hook-form'; import { RelatedPerson, Attachment } from 'fhir/r4'; import { Check, FileText, Trash2 } from 'lucide-react'; import { useFhirServiceContext, usePatientServiceContext, useRelatedPersonServiceContext } from '../../../../services'; import { AppHeader, AppSpinner, HistoryNavigator, ProfileLayoutContainer, ScrollableTab } from '../../../../components'; import { FhirHumanNameDisplay, formatFhirHumanName } from '../../../../utils/fhirHumanName.tsx'; import FhirRelatedPerson from '../../../../components/FhirRelatedPerson/FhirRelatedPerson.tsx'; import { useFhirRelatedPersonTargetedBroadcast } from '../../../../hooks'; import { useRelatedPersonAuthorityService } from '../../../../services/RelatedPersonAuthorityService.tsx'; import type { AuthorityTier } from '../../../../services/relatedPersonAuthority'; import { useInviteAuthority } from '../../../../hooks/useInviteAuthority'; import { onViewerChange } from '../../../../utils/writeThroughHold'; import { describeWriteFailure } from '../../../../utils/fetchFailureMessage'; import { patientCacheGuard } from '../../../../utils/cacheOwner'; import { GUARDIAN_KEEPS_GRANT_MESSAGE, guardianKeepsGrant } from '../../../../utils/guardianKeepsGrant'; const EditRelatedPersonPage: React.FC = () => { const { relatedPersonId } = useParams<{ relatedPersonId: string }>(); const navigate = useNavigate(); const { patientState } = usePatientServiceContext(); const { cachedForPatientId: relatedPersonsOwner, createRelatedPerson, updateRelatedPerson, fetchRelatedPerson, deleteRelatedPerson, transformToFhirRelatedPerson, transformFromFhirRelatedPerson, relatedPersonsList: rawRelatedPersonsList } = useRelatedPersonServiceContext(); // The cache outlives a change of patient: read only the patient on screen. const relatedPersonsGuard = patientCacheGuard(relatedPersonsOwner, patientState?.id); const relatedPersonsList = relatedPersonsGuard.list(rawRelatedPersonsList); const { createReferenceObject, transformToCodeableConcept } = useFhirServiceContext(); const { putAuthority } = useRelatedPersonAuthorityService(); const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(); const [error, setError] = useState(); const [photo, setPhoto] = useState(); const [relatedPerson, setRelatedPerson] = useState(); const [showDeleteModal, setShowDeleteModal] = useState(false); const kept = guardianKeepsGrant(relatedPerson); // A delete confirmation already open when the grant turns out to be the guardian's closes. useEffect(() => { if (kept) setShowDeleteModal(false); }, [kept]); // The link, then its access. Once the link exists, a retry re-sends only the access the server did not // confirm (a full resubmit would mint a duplicate link), and an access decision that ended here creates // nothing again. Each person this page shows is its own opening. Shared with RelatedPersonModal.tsx / // RelatedPersonsPage.tsx. const invite = useInviteAuthority(putAuthority); const resetInvite = invite.reset; // Each person this page shows starts not busy: an earlier one's submit belongs to that one only. useEffect(() => { resetInvite(); setSubmitting(false); }, [relatedPersonId, resetInvite]); // Another viewer, practice or acted-as link is another opening too: the form is usable again, and says // nothing of a submit an earlier viewer left running. useEffect(() => onViewerChange(() => { setSubmitting(false); setError(undefined); setSuccess(undefined); }), []); const isNew = relatedPersonId === 'new'; const spacerMargin = '24px'; const methods = useForm({ defaultValues: { email: '', firstname: '', lastname: '', active: true, gender: '', birthDate: '', name: [{ family: '', given: [''], prefix: [], suffix: [], use: '' }], relationship: [], communication: [], identifier: [], address: [], telecom: [] } }); const { handleSubmit, reset } = methods; const init = useCallback((relatedPerson: RelatedPerson) => { const { photo: relatedPersonPhoto, formData } = transformFromFhirRelatedPerson(relatedPerson); reset(formData); setPhoto(relatedPersonPhoto); setRelatedPerson(relatedPerson); }, [transformFromFhirRelatedPerson]); useFhirRelatedPersonTargetedBroadcast({ relatedPerson, relatedPersonId, onUpdate: init, onDelete: () => navigate('..', { relative: 'path' }) }); useEffect(() => { const loadRelatedPerson = async () => { if (isNew || relatedPerson) return; const foundRelatedPerson = relatedPersonsList?.find(rp => rp.id === relatedPersonId); if (foundRelatedPerson) { init(foundRelatedPerson); return; } setLoading(true); try { const fetchedRelatedPerson = await fetchRelatedPerson(relatedPersonId as string); init(fetchedRelatedPerson); } catch (err) { console.error('Error loading related person:', err); setError('Could not load related person. Try again.'); } finally { setLoading(false); } }; loadRelatedPerson(); }, [relatedPerson, relatedPersonId, relatedPersonsList, fetchRelatedPerson, init]); const onSubmit = async (data: any) => { if (kept) return; if (!patientState) { setError('Patient information is missing. Please try again.'); return; } // Everything this submit does later belongs to this person's opening only. const current = invite.begin(); setSubmitting(true); setSuccess(undefined); setError(undefined); try { if (isNew) { // The name inputs write to name.0.given.0 / name.0.family (see FhirRelatedPerson.tsx) // - NOT flat firstname/lastname fields, matching RelatedPersonModal.tsx's create // submitter. const nameData = data.name?.[0] ?? {}; const firstname = nameData.given?.[0] || ''; const lastname = nameData.family || ''; const tier: AuthorityTier = (data.tier as AuthorityTier) || 'assist-only'; const step = await invite.submit( { email: data.email, firstname, lastname, tier, patient: createReferenceObject('Patient', patientState.id as string, formatFhirHumanName(patientState.name)), relationship: transformToCodeableConcept(data.relationship) as any, }, createRelatedPerson, current, ); if (step.kind === 'gone') return; if (step.kind === 'retry') { setError(step.message); return; } // The link exists either way; an access decision that ended here is reviewed on their own page. if (step.kind === 'review') setError(step.message); else setSuccess('Related person created successfully.'); const result = step.relatedPerson; if (step.kind === 'done') setTimeout(() => { if (current()) setSuccess(undefined); }, 3_000); navigate(`../${result.id}`, { relative: 'path' }); } else { const { email, firstname, lastname, ...withoutUser } = data; await updateRelatedPerson({ id: relatedPerson?.id, ...transformToFhirRelatedPerson(withoutUser, patientState, photo) }); if (!current()) return; setSuccess('Related person updated successfully.'); setTimeout(() => { if (current()) setSuccess(undefined); }, 3_000); } } catch (err: any) { if (!current()) return; console.error(`Error ${isNew ? 'creating' : 'updating'} related person:`, err); setError(describeWriteFailure(err, `Failed to ${isNew ? 'create' : 'update'} related person`)); } finally { if (current()) setSubmitting(false); } }; const handlePhotoChange = (newPhoto: Attachment | undefined) => { setPhoto(newPhoto); }; const handleDeleteRelatedPerson = async () => { if (!relatedPerson || kept) return; try { await deleteRelatedPerson(relatedPerson); } catch (error) { console.error('Failed to delete related person:', error); setError('Failed to delete related person. Please try again.'); } finally { setShowDeleteModal(false); } }; if (loading) { return ( ); } return ( resourceType="RelatedPerson" resourceId={relatedPersonId || ''}> {(showHistory) => (
{!isNew && ( )} {!kept && ( )} {!isNew && !kept && ( )} {kept &&

{GUARDIAN_KEEPS_GRANT_MESSAGE}

} {success && ( <> setSuccess(undefined)}> {success} )} {error && ( <> setError(undefined)}>
{error}
)} {/* Delete Confirmation Modal */} {showDeleteModal && !kept && ( setShowDeleteModal(false)}> Are you sure you want to delete the related person ? {/* Cancel removed: the Modal's own X calls the same setShowDeleteModal(false), so it was redundant. */} )}
)} ); }; export default EditRelatedPersonPage;