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 { PractitionerRole, Practitioner } from 'fhir/r4'; import { usePractitionerRoleServiceContext, useAuthServiceContext, usePractitionerServiceContext, useValueSetServiceContext } from '../services'; import { FhirPractitionerRole } from '../components'; import { useFhirPractitionerRoleTargetedBroadcast } from '../hooks'; import { getAuthenticationGroupForWorkspace } from '../utils/staffRoleCatalog.ts'; import { describeWriteFailure } from '../utils/fetchFailureMessage'; import { invalidSubmitMessage } from '../utils/forms/invalidSubmitMessage'; import { defaultHoursRows } from '../utils/defaultHoursRows'; import { useClinicianSeatCreate } from '../hooks/useClinicianSeatCreate'; import { SavedClinicianSeatActions } from '../components/ClinicianSeat/SavedClinicianSeatActions'; import { isSaveRetired } from '../utils/saveOperation'; interface PractitionerRoleModalProps { isOpen: boolean; onClose: () => void; practitionerRole?: PractitionerRole; onSuccess?: (practitionerRole: PractitionerRole) => void; onError?: (error: string) => void; title?: string; } export const PractitionerRoleModal: React.FC = ({ isOpen, onClose, practitionerRole, onSuccess, onError, title }) => { const { wpOrganization } = useAuthServiceContext(); const { practitionerState, setPractitionerState, setPractitionersList } = usePractitionerServiceContext(); const { daysOfWeek } = useValueSetServiceContext(); const { transformToFhirPractitionerRole, transformFromFhirPractitionerRole, updatePractitionerRole, checkClinicianSeat } = usePractitionerRoleServiceContext(); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(); const [error, setError] = useState(); const [progress, setProgress] = useState(); const seatCreation = useClinicianSeatCreate({ active: isOpen, practitionerId: practitionerState?.practitioner?.id, roleId: practitionerRole?.id, editor: 'role-modal' }); const savedRoleId = practitionerRole?.id ?? (seatCreation.retained?.created ? seatCreation.retained.roleId : undefined); useEffect(() => { setProgress(undefined); setSuccess(undefined); setError(undefined); setSubmitting(false); }, [isOpen, practitionerRole?.id, practitionerState?.practitioner?.id, wpOrganization.current?.id]); const isNew = !practitionerRole; const modalTitle = title || `${isNew ? 'Create' : 'Edit'} Staff Role`; const methods = useForm({ defaultValues: { role: '', active: true, organization: null, code: [], primaryOrganizationRole: '', leadershipDesignations: [], trainingLevel: null, specialty: [], extension: [], location: [], healthcareService: [], telecom: [], availabilityExceptions: '', availableTime: defaultHoursRows(daysOfWeek), notAvailable: [] } }); 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((practitionerRole: PractitionerRole) => { reset(transformFromFhirPractitionerRole(practitionerRole)); }, [transformFromFhirPractitionerRole, reset]); useFhirPractitionerRoleTargetedBroadcast({ practitionerRole, isOpen, onUpdate: init, onDelete: onClose }); useEffect(() => { if (practitionerRole) { init(practitionerRole); } }, [practitionerRole, init]); const onSubmit = async (data: Record, mode: 'check' | 'resume' = 'resume') => { const owner = seatCreation.beginSave(); let closeScheduled = false; try { owner.admit(); if (!wpOrganization.current || !practitionerState) throw new Error('Practice and staff information are required.'); setSubmitting(true); setSuccess(undefined); setError(undefined); setProgress(undefined); const { role, ...withoutUser } = data; const workspace = typeof role === 'string' ? role : ''; const body = transformToFhirPractitionerRole({ ...withoutUser, workspace }, wpOrganization.current, practitionerState.practitioner); let result: PractitionerRole | undefined; let ready = true; if (isNew) { const outcome = await owner.step(() => seatCreation.create({ practitioner: practitionerState.practitioner as Practitioner, practitionerRole: body, userGroup: getAuthenticationGroupForWorkspace(workspace), }, owner, mode), mode !== 'check'); owner.admit(); result = outcome.role; ready = !outcome.keyed || outcome.standing === 'enabled' || outcome.standing === 'created'; if (!ready) setProgress(outcome.standing === 'checkout-required' ? 'The staff role is saved without a clinician seat. Continue to checkout when you are ready.' : 'This role creation is not confirmed. Check or resume the original creation.'); } else { if (!practitionerRole?.meta?.versionId || practitionerRole.practitioner?.reference !== `Practitioner/${owner.clientScope.practitionerId}` || practitionerRole.organization?.reference !== `Organization/${owner.clientScope.organizationId}`) { throw new Error('Reload this staff role before saving its current version.'); } result = await owner.step(() => updatePractitionerRole({ ...body, id: practitionerRole?.id }, { transport: owner.transport, organizationId: owner.clientScope.organizationId, expectedVersion: practitionerRole?.meta?.versionId, }), true); } owner.admit(); if (result) { const saved = result; const merge = (roles: PractitionerRole[]) => [...roles.filter(item => item.id !== saved.id), saved]; setPractitionerState(previous => owner.isCurrentScope() && previous?.practitioner.id === owner.clientScope.practitionerId ? { ...previous, roles: merge(previous.roles) } : previous); setPractitionersList(previous => owner.isCurrentScope() ? previous.map(item => item.practitioner.id === owner.clientScope.practitionerId ? { ...item, roles: merge(item.roles) } : item) : previous); } if (!ready || !result) return; setSuccess(`Staff role ${isNew ? 'created' : 'updated'} successfully.`); owner.admit(); onSuccess?.(result); closeScheduled = true; setTimeout(() => { try { owner.admit(); setSuccess(undefined); onClose(); } catch { /* A retired editor does not close a later opening. */ } finally { owner.retire(); } }, 1500); } catch (error: unknown) { if (!owner.belongs() || isSaveRetired(error)) return; const message = describeWriteFailure(error, `Failed to ${isNew ? 'create' : 'update'} staff role`); setError(message); onError?.(message); } finally { if (owner.belongs()) setSubmitting(false); if (!closeScheduled) owner.retire(); } }; const checkSavedRole = async () => { const owner = seatCreation.beginSave(); try { owner.admit(); if (!savedRoleId) throw new Error('A saved role is required.'); setSubmitting(true); setError(undefined); setSuccess(undefined); setProgress(undefined); const result = await owner.step(() => checkClinicianSeat(savedRoleId, { transport: owner.transport, organizationId: owner.clientScope.organizationId, practitionerId: owner.clientScope.practitionerId, })); owner.admit(); if (result.role) { const saved = result.role; const merge = (roles: PractitionerRole[]) => [...roles.filter(role => role.id !== saved.id), saved]; setPractitionerState(previous => owner.isCurrentScope() && previous?.practitioner.id === owner.clientScope.practitionerId ? { ...previous, roles: merge(previous.roles) } : previous); setPractitionersList(previous => owner.isCurrentScope() ? previous.map(item => item.practitioner.id === owner.clientScope.practitionerId ? { ...item, roles: merge(item.roles) } : item) : previous); } setProgress(result.standing === 'enabled' ? 'This saved role has an enabled clinician seat.' : result.standing === 'disabled' ? 'This saved role does not have an enabled clinician seat.' : 'The clinician seat is not confirmed. Check this saved role again.'); } catch (error: unknown) { if (owner.belongs() && !isSaveRetired(error)) setError(describeWriteFailure(error, 'Could not check the saved role')); } finally { if (owner.belongs()) setSubmitting(false); owner.retire(); } }; if (!isOpen) return null; return ( {isNew && seatCreation.retained && } {savedRoleId && } ]} >
{savedRoleId && { owner.admit(); await seatCreation.observeEnabledRole(saved, owner); owner.admit(); const merge = (roles: PractitionerRole[]) => [...roles.filter(role => role.id !== saved.id), saved]; setPractitionerState(previous => owner.isCurrentScope() && previous?.practitioner.id === owner.clientScope.practitionerId ? { ...previous, roles: merge(previous.roles) } : previous); setPractitionersList(previous => owner.isCurrentScope() ? previous.map(item => item.practitioner.id === owner.clientScope.practitionerId ? { ...item, roles: merge(item.roles) } : item) : previous); }} />} {progress && setProgress(undefined)} className="mb-4">{progress}} {seatCreation.needsReentry &&

Re-enter the original role details to check or resume this creation.

} {success && ( setSuccess(undefined)} className="mb-4"> {success} )} {error && ( setError(undefined)} className="mb-4"> {error} )}
); };