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 { RelatedPerson, Attachment } from 'fhir/r4'; import { useRelatedPersonServiceContext, useFhirServiceContext, usePatientServiceContext } from '../services'; import { useRelatedPersonAuthorityService } from '../services/RelatedPersonAuthorityService.tsx'; import type { AuthorityTier } from '../services/relatedPersonAuthority'; import { useInviteAuthority } from '../hooks/useInviteAuthority'; import { onViewerChange } from '../utils/writeThroughHold'; import { FhirRelatedPerson } from '../components'; import { formatFhirHumanName } from '../utils/fhirHumanName.tsx'; import { useFhirRelatedPersonTargetedBroadcast } from '../hooks'; import { describeWriteFailure } from '../utils/fetchFailureMessage'; import { invalidSubmitMessage } from '../utils/forms/invalidSubmitMessage'; interface RelatedPersonModalProps { isOpen: boolean; onClose: () => void; relatedPerson?: RelatedPerson; onSuccess?: (relatedPerson: RelatedPerson) => void; onError?: (error: string) => void; title?: string; } export const RelatedPersonModal: React.FC = ({ isOpen, onClose, relatedPerson, onSuccess, onError, title }) => { const { patientState } = usePatientServiceContext(); const { createReferenceObject, transformToCodeableConcept } = useFhirServiceContext(); const { transformToFhirRelatedPerson, transformFromFhirRelatedPerson, createRelatedPerson, updateRelatedPerson } = useRelatedPersonServiceContext(); const { putAuthority } = useRelatedPersonAuthorityService(); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(); const [error, setError] = useState(); const [photo, setPhoto] = useState(); // The link, then its access; a retry of an access never confirmed; and an access decision that ended here. const invite = useInviteAuthority(putAuthority); const resetInvite = invite.reset; const isNew = !relatedPerson; const modalTitle = title || `${isNew ? 'Create' : 'Edit'} Related Person`; const methods = useForm({ defaultValues: { email: '', firstname: '', lastname: '', active: true, gender: '', birthDate: '', tier: 'assist-only', name: [{ family: '', given: [''], prefix: [], suffix: [], use: '' }], relationship: [], communication: [], identifier: [], address: [], telecom: [] } }); 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((relatedPerson: RelatedPerson) => { const { photo: relatedPersonPhoto, formData } = transformFromFhirRelatedPerson(relatedPerson); reset(formData); setPhoto(relatedPersonPhoto); }, [transformFromFhirRelatedPerson, reset]); useFhirRelatedPersonTargetedBroadcast({ relatedPerson, isOpen, onUpdate: init, onDelete: onClose }); useEffect(() => { if (relatedPerson) { init(relatedPerson); } }, [relatedPerson, init]); // A fresh open must not carry a stale retry pointer from a previous invite in this same modal // instance (the modal isn't unmounted between opens - see FhirReference.tsx). // Each opening starts not busy: an earlier opening's submit, still running, belongs to that opening only. useEffect(() => { resetInvite(); setSubmitting(false); }, [isOpen, 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 handlePhotoChange = (newPhoto: Attachment | undefined) => { setPhoto(newPhoto); }; const onSubmit = async (data: any) => { if (!patientState) { const errorMsg = 'Patient information is missing. Please try again.'; setError(errorMsg); onError?.(errorMsg); return; } // Everything this submit does later belongs to this opening only. const current = invite.begin(); setSubmitting(true); setSuccess(undefined); setError(undefined); try { let result: RelatedPerson; if (isNew) { const { email, relationship, tier } = data; // The name inputs write to name.0.given.0 / name.0.family (see FhirRelatedPerson.tsx) - // NOT flat firstname/lastname fields, so those must be read from there, matching // RelatedPersonsPage.tsx's create submitter. const nameData = data.name?.[0] ?? {}; const firstname = nameData.given?.[0] || ''; const lastname = nameData.family || ''; // Once the link exists, a retry only re-sends the access never confirmed (resubmitting the create // would mint a duplicate link), and an access decision that ended here creates nothing again. const step = await invite.submit( { email, firstname, lastname, tier: (tier as AuthorityTier) || 'assist-only', patient: createReferenceObject('Patient', patientState.id as string, formatFhirHumanName(patientState.name)), relationship: transformToCodeableConcept(relationship) as any, }, createRelatedPerson, current, ); if (step.kind === 'gone') return; if (step.kind !== 'done') { setError(step.message); onError?.(step.message); // The link exists either way: the list shows them. if (step.kind === 'review') onSuccess?.(step.relatedPerson); return; } result = step.relatedPerson; } else { const { email, firstname, lastname, ...withoutUser } = data; result = await updateRelatedPerson({ id: relatedPerson?.id, ...transformToFhirRelatedPerson(withoutUser, patientState, photo) }); if (!current()) return; } const successMsg = `Related person ${isNew ? 'created' : 'updated'} successfully.`; setSuccess(successMsg); onSuccess?.(result); setTimeout(() => { // A later opening of this modal is not closed by an earlier one's success. if (!current()) return; setSuccess(undefined); onClose(); }, 1500); } catch (err: any) { if (!current()) return; console.error(`Error ${isNew ? 'creating' : 'updating'} related person:`, err); const errorMsg = describeWriteFailure(err, `Failed to ${isNew ? 'create' : 'update'} related person`); setError(errorMsg); onError?.(errorMsg); } finally { if (current()) setSubmitting(false); } }; if (!isOpen) return null; return ( ]} >
{success && ( setSuccess(undefined)} className="mb-4"> {success} )} {error && ( setError(undefined)} className="mb-4"> {error} )}
); };