import { useEffect, useMemo, useState } from "react"; import type { Questionnaire } from "fhir/r4"; import { useQuestionnaireServiceContext } from "../services/QuestionnaireService.tsx"; import { questionnaireIdFromReference } from "../utils/questionnaireReference"; /** * WHAT EACH FORM IS CALLED, read once per form for the whole session. * * A QuestionnaireResponse names its form only by reference, so a list of answers needs the forms' own * titles. Every caller shares one reader keyed by the form's id: ten answers to the same intake form are * one read, and a form already read on another screen is not read again. * * THREE STATES PER FORM, and a caller must draw all three: `pending` (no title yet - draw a placeholder, * never a guess), `ready` with the form's own title, and `failed`. A failed or unresolvable form is never * given somebody else's title. */ export type FormTitle = { state: "pending" } | { state: "failed" } | { state: "ready"; title: string }; const titles = new Map>(); /** Tests only: forget every form's title. */ export const forgetQuestionnaireTitles = () => titles.clear(); const titleOf = (questionnaire: Questionnaire): string | null => questionnaire.title?.trim() || questionnaire.name?.trim() || null; /** `organizationId` names the practice the forms belong to when the page has none of its own. */ export const useQuestionnaireTitles = (references: readonly (string | undefined)[], organizationId?: string): Record => { const { fetchQuestionnaire } = useQuestionnaireServiceContext(); const ids = useMemo( () => [...new Set(references.map((reference) => questionnaireIdFromReference(reference)).filter((id): id is string => Boolean(id)))].sort(), // The set of forms, not the array's identity, is what changes the reads. // eslint-disable-next-line react-hooks/exhaustive-deps [references.join("|")], ); const [answers, setAnswers] = useState>({}); useEffect(() => { let current = true; for (const id of ids) { let asked = titles.get(id); if (!asked) { asked = (organizationId ? fetchQuestionnaire(id, organizationId) : fetchQuestionnaire(id)).then(titleOf); // A failed read is forgotten so a later screen asks again rather than inheriting it. asked.catch(() => titles.delete(id)); titles.set(id, asked); } asked.then( (title) => { if (current) setAnswers((prior) => ({ ...prior, [id]: title })); }, () => { if (current) setAnswers((prior) => ({ ...prior, [id]: null })); }, ); } return () => { current = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ids, organizationId]); const out: Record = {}; for (const reference of references) { if (!reference) continue; const id = questionnaireIdFromReference(reference); if (!id) { out[reference] = { state: "failed" }; continue; } const answer = answers[id]; out[reference] = answer === undefined ? { state: "pending" } : answer === null ? { state: "failed" } : { state: "ready", title: answer }; } return out; }; export default useQuestionnaireTitles;