import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { SelectControl } from "../ui/select-control"; import { Spinner } from "../ui/spinner"; import { DismissibleAlert } from '../ui/dismissible-alert'; import { Flex, FlexItem } from '../ui/flex'; import { Modal } from '../ui/wp-modal-shim'; import { Patient, Questionnaire, Task } from 'fhir/r4'; import { useFhirQuestionnaires } from '../../hooks'; import { usePublicQuestionnaireIds } from '../../hooks/usePublicQuestionnaireIds'; import { useTaskServiceContext } from '../../services/TaskService.tsx'; import { operationStatusToast } from '../OperationStatusToast/OperationStatusToast.tsx'; import { relayServerFailure } from '../../utils/fetchFailureMessage'; import { TaskPrioritySelect, type TaskPriority } from '../TaskPrioritySelect/TaskPrioritySelect'; interface QuestionnaireTaskAssignmentModalProps { isOpen: boolean; patient: Patient; onClose: () => void; onAssigned?: (task: Task) => void; onPrepared?: (questionnaire: Questionnaire, priority: TaskPriority) => void | Promise; presentation?: 'modal' | 'inline'; submitLabel?: string; excludedQuestionnaireIds?: string[]; initialQuestionnaireId?: string; initialPriority?: TaskPriority; formTitle?: string; formDescription?: string; } const getQuestionnaireLabel = (questionnaire: Questionnaire): string => questionnaire.title?.trim() || questionnaire.name?.trim() || 'Untitled questionnaire'; /** * ONLY AN ACTIVE QUESTIONNAIRE MAY BE SENT TO A PATIENT. * * MEASURED walking B8: this picker offered a DRAFT questionnaire for assignment. A draft is the * library saying, in its own vocabulary, that the form is not finished - and the person choosing here * is choosing what a patient will be asked to fill in before a visit. * * `retired` is excluded for the mirror reason: the practice has said to stop using it, and a new * assignment is exactly using it. Neither exclusion touches a questionnaire ALREADY assigned - this * is the list of what may be sent next, and a form retired after somebody was asked to complete it * still has to render for them. */ const ASSIGNABLE_STATUS = 'active'; export const QuestionnaireTaskAssignmentModal = ({ isOpen, patient, onClose, onAssigned, onPrepared, presentation = 'modal', submitLabel, excludedQuestionnaireIds = [], initialQuestionnaireId = '', initialPriority = 'routine', formTitle, formDescription }: QuestionnaireTaskAssignmentModalProps) => { const { createQuestionnaireTask } = useTaskServiceContext(); const { loading, error: loadError, questionnaireList, loadQuestionnaires, clearError } = useFhirQuestionnaires({ autoLoad: isOpen, pageSize: 100, errorMessage: 'Failed to load questionnaires' }); // Public forms are website content and are never offered here; nothing is offered until that is known. const { publicIds, publicRead, retryPublicRead } = usePublicQuestionnaireIds(isOpen); const [selectedQuestionnaireId, setSelectedQuestionnaireId] = useState( initialQuestionnaireId ); const [priority, setPriority] = useState(initialPriority); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(); const availableQuestionnaires = useMemo( () => questionnaireList .filter((questionnaire): questionnaire is Questionnaire & { id: string } => Boolean( questionnaire.id && questionnaire.status === ASSIGNABLE_STATUS && !publicIds.has(questionnaire.id) && !excludedQuestionnaireIds.includes(questionnaire.id) ) ) .sort((left, right) => getQuestionnaireLabel(left).localeCompare( getQuestionnaireLabel(right) ) ), [excludedQuestionnaireIds, publicIds, questionnaireList] ); /** * DOES THE LIBRARY HOLD ANYTHING SENDABLE AT ALL? * * Without this the empty state has two branches for three states, and the new one lands on the * wrong branch: a library of nothing but drafts would report *"All available questionnaires have * already been added"* - which is false, and false in the direction that stops somebody looking. * Nothing was added; the forms are not finished. */ const assignableExist = questionnaireList.some( (questionnaire) => questionnaire.id && questionnaire.status === ASSIGNABLE_STATUS && !publicIds.has(questionnaire.id), ); // A preselected form is not sendable until the public-forms read says it is not website content. const selectedQuestionnaire = publicRead === 'ready' ? availableQuestionnaires.find(questionnaire => questionnaire.id === selectedQuestionnaireId) : undefined; const resetModal = useCallback(() => { setSelectedQuestionnaireId(initialQuestionnaireId); setPriority(initialPriority); setError(undefined); clearError(); }, [clearError, initialPriority, initialQuestionnaireId]); useEffect(() => { if (!isOpen) { resetModal(); } }, [isOpen, resetModal]); if (!isOpen) { return null; } const handleClose = () => { if (submitting) { return; } resetModal(); onClose(); }; const handleSubmit = async (event: FormEvent) => { event.preventDefault(); if (!selectedQuestionnaire) { setError('Select a questionnaire to assign.'); return; } if (onPrepared) { setSubmitting(true); setError(undefined); try { await onPrepared(selectedQuestionnaire, priority); resetModal(); onClose(); } catch (err) { console.error('Failed to save Questionnaire task:', err); setError( relayServerFailure(err, 'Failed to save the Questionnaire task.') ); } finally { setSubmitting(false); } return; } setSubmitting(true); setError(undefined); const operationToastId = operationStatusToast.saving(); let createdTask: Task | undefined; try { createdTask = await createQuestionnaireTask({ patient, questionnaire: selectedQuestionnaire, priority }); } catch (err) { console.error('Failed to assign Questionnaire:', err); setError( relayServerFailure(err, 'Failed to assign the Questionnaire.') ); } finally { operationStatusToast.dismiss(operationToastId); setSubmitting(false); } if (createdTask) { onAssigned?.(createdTask); resetModal(); onClose(); } }; const resolvedSubmitLabel = submitLabel || (onPrepared ? 'Add Questionnaire' : 'Assign Questionnaire'); const resolvedFormTitle = formTitle || (presentation === 'modal' ? 'Assign Questionnaire' : 'Questionnaire'); const resolvedFormDescription = formDescription || 'Choose the questionnaire now. It will be assigned only after the appointment is booked.'; const form = (
{(error || loadError) && ( {error || loadError} )} {loading || publicRead === 'loading' ? (
Loading questionnaires…
) : publicRead === 'failed' ? (

We could not confirm which forms are public, so none can be offered yet.

) : loadError && questionnaireList.length === 0 ? null : availableQuestionnaires.length === 0 ? ( {/* Three states, three sentences. Each names what is true and what to do about it, because "nothing to choose" has three different causes here and only one of them is the reader's to fix. */} {!questionnaireList.some(questionnaire => Boolean(questionnaire.id)) ? 'No questionnaires are available to assign. A Platform Manager must create one first.' : !assignableExist ? 'No questionnaire in this practice\u2019s library is active yet. A draft is not finished ' + 'and a retired one is no longer in use, so neither can be sent \u2014 a Platform ' + 'Manager sets a questionnaire to Active when it is ready.' : 'All available questionnaires have already been added.'} ) : ( ({ label: getQuestionnaireLabel(questionnaire), value: questionnaire.id })) ]} onChange={(value) => { setSelectedQuestionnaireId(value); setError(undefined); }} disabled={submitting} /> )} {availableQuestionnaires.length > 0 && ( )} {loadError && ( )} {/* Cancel removed: the Modal's onRequestClose IS `handleClose`, the same function reference this called. */}
); if (presentation === 'inline') { return (

{resolvedFormTitle}

{resolvedFormDescription}

{form}
); } return ( {form} ); };