import React, { useState, useEffect } from 'react'; import { Card, CardHeader, CardBody, CardDivider } from '../ui/wp-card'; import { Elevation } from '../ui/elevation'; import { InputControl } from '../ui/input-control'; import { ExternalLink } from 'lucide-react'; import { Heading } from '../ui/heading'; import { Flex, FlexItem, FlexBlock } from '../ui/flex'; import { Spacer } from '../ui/spacer'; import { TabPanel } from '../ui/tab-panel'; import { Disabled } from '../ui/disabled'; import { Modal } from '../ui/wp-modal-shim'; import { DismissibleAlert } from '../ui/dismissible-alert'; import { describeFetchFailure } from '../../utils/fetchFailureMessage'; import { useFormContext, Controller, useFieldArray, useForm, FormProvider } from 'react-hook-form'; import { IValueSet, useAuthServiceContext, useQuestionnaireServiceContext, useValueSetServiceContext } from '../../services'; import { FhirCode, FhirReference, FhirReferenceCollection } from '../../components'; import { useFhirCarePlans, useFhirEncounters, useFhirObservations, useFhirProcedures, useFhirQuestionnaireResponseBroadcast, useFhirQuestionnaireResponses, useFhirQuestionnaires } from '../../hooks'; import { FhirResource, Questionnaire, QuestionnaireResponse } from 'fhir/r4'; import { ANSWERS_WITHHELD_MESSAGE, answersWithheld } from '../../utils/answersWithheld'; import QuestionnairePreview from '../../pages/DashboardPage/QuestionnairesPage/QuestionnairePage/QuestionnairePreview/QuestionnairePreview.tsx'; interface FhirQuestionnaireResponseProps { /** * OPTIONAL, and the type is the fix rather than a concession to it. * * This was declared required, so TypeScript believed it could never be undefined - while * `PatientQuestionnaireResponse` was handing it exactly that on the create route, where nothing has * been loaded yet. A required prop that is not required is worse than an optional one: it silences the * compiler at precisely the call site that needed checking, and the first thing anybody sees is an * uncaught TypeError inside a click handler. */ questionnaireResponse?: QuestionnaireResponse; elevated?: boolean; disabled?: boolean; enableInlineActions?: boolean; } export const FhirQuestionnaireResponse: React.FC = ({ questionnaireResponse, elevated = true, disabled = false, enableInlineActions = true }) => { const { control } = useFormContext(); const { userRole, USER_ROLES } = useAuthServiceContext(); const { fetchQuestionnaire, transformFromFhirQuestionnaire, } = useQuestionnaireServiceContext(); const { fetchValueSet, valueSetUrls } = useValueSetServiceContext(); const [loading, setLoading] = useState(false); const [showResponse, setShowResponse] = useState(false); // - what View Answers says when it cannot open. const [viewError, setViewError] = useState(null); // value sets const [questionnaireAnswersStatus, setQuestionnaireAnswersStatus] = useState([]); const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes(userRole as string); const spacerMargin = '24px'; useFhirQuestionnaireResponses(); useFhirQuestionnaireResponseBroadcast(); const { encountersList } = useFhirEncounters(); const { questionnaireList } = useFhirQuestionnaires(); const { proceduresList } = useFhirProcedures(); const { observationsList } = useFhirObservations(); const { carePlansList } = useFhirCarePlans(); const responseMethods = useForm({ defaultValues: {} }); const { reset: responseReset, watch: responseWatch } = responseMethods; const basedOnFieldArray = useFieldArray({ control, name: 'basedOn' }); const partOfFieldArray = useFieldArray({ control, name: 'partOf' }); useEffect(() => { const loadValueSets = async () => { setLoading(true); try { const [ questionnaireAnswersStatusValueSet ] = await Promise.all([ fetchValueSet(valueSetUrls.questionnaireAnswersStatus), ]); setQuestionnaireAnswersStatus(questionnaireAnswersStatusValueSet); } catch (err) { console.error('Error loading value sets:', err); } finally { setLoading(false); } }; loadValueSets(); }, [fetchValueSet, valueSetUrls]); /** * - see `PatientQuestionnaireResponses`. Same dead catch, same doubled URL: this is the * third copy of one block, all three calling the one function that built the path. Repairing only * the surface that happened to be reported is how the other two stay silent. */ const viewResponse = async (event: any, questionnaireResponse?: QuestionnaireResponse) => { event.preventDefault(); event.stopPropagation(); setViewError(null); // THE WHOLE OBJECT FIRST, THEN THE PROPERTY. The guard below used to read `.questionnaire` // off a value that is itself undefined on the create route, so it threw one character before it // could help: the message existed, was correctly worded, and was unreachable in exactly the // situation it describes. The button no longer renders in that case at all (see below), and this // stays as the second line of defence - a click handler reached from anywhere must not be the // thing that decides whether the page survives. if (!questionnaireResponse) { setViewError('These answers have not been saved yet, so there is nothing to display.'); return; } if (!questionnaireResponse.questionnaire) { setViewError('These answers are not linked to a questionnaire, so they cannot be displayed.'); return; } try { const questionnaire: Questionnaire = await fetchQuestionnaire(questionnaireResponse.questionnaire); const transformedQuestionnaire = transformFromFhirQuestionnaire(questionnaire, questionnaireResponse); responseReset(transformedQuestionnaire); setShowResponse(true); } catch (error) { console.error('Failed to load questionnaire:', error); setViewError(describeFetchFailure(error, 'We could not open these answers')); } }; return ( {elevated && } Details {/* * Offered only once there is a saved record to open. A button on a record that does * not exist yet cannot do anything but fail, and the honest form of "you cannot view * answers that were never saved" is not to present the control. */} {questionnaireResponse && answersWithheld(questionnaireResponse) && ( {ANSWERS_WITHHELD_MESSAGE} )} {questionnaireResponse && !answersWithheld(questionnaireResponse) && ( )} {/* Card 523 — what View Answers says when it cannot open. */} {viewError && ( setViewError(null)} className="mb-4"> {viewError} )} {/* subject */} {/* status */} {/* authored */} ( )}/> {/* encounter */} {(tab) => { // basedOn if (tab.name === 'basedOn') { return (
{/* basedOn */} { switch (referenceType) { case 'CarePlan': return carePlansList; default: return []; } }} required={false} disabled={disabled} readonly={isPatient} enableInlineActions={enableInlineActions}/>
); } // partOf if (tab.name === 'partOf') { return (
{/* partOf */} { switch (referenceType) { case 'Observation': return observationsList; case 'Procedure': return proceduresList; default: return []; } }} required={false} disabled={disabled} readonly={isPatient} enableInlineActions={enableInlineActions}/>
); } }}
{/* Response Modal */} {showResponse && ( setShowResponse(false)}>
)}
); }; export default FhirQuestionnaireResponse;