import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useMyOrganizations } from "../hooks/useMyOrganizations"; import { createdAtOrganization } from "../utils/createdAtOrganization"; import "./RecordPractice.css"; // Which practice a patient's record is being read for: All practices, or one they belong to. It is a view over // records already read, chosen on the record shell and shown on every page it governs. It changes no header, // actor, grant, write destination or practice-bound booking, message or billing context. A named practice never // claims a record that does not say where it came from: those are counted, and shown under All practices. export type RecordPractice = { id: string; name: string }; export type PracticeView = { shown: T[]; /** Records that do not say which practice they came from, left out of a named practice's view. */ unattributed: number; /** Records from another practice, left out of a named practice's view. */ elsewhere: number; }; export const viewForPractice = ( items: readonly T[], practice: RecordPractice | undefined, practiceIdOf: (item: T) => string | undefined, ): PracticeView => { if (!practice) return { shown: [...items], unattributed: 0, elsewhere: 0 }; const shown: T[] = []; let unattributed = 0; let elsewhere = 0; for (const item of items) { const id = practiceIdOf(item); if (id === practice.id) shown.push(item); else if (!id) unattributed += 1; else elsewhere += 1; } return { shown, unattributed, elsewhere }; }; // The record pages that list through the context, exactly: the timeline and the details it opens, and each category // page that names a source practice. A page reached from one of these (a report, a prescription) is not among them. const GOVERNED_RECORD_PAGES = [ "", "documents", "medications", "care-provision", "device-data/device-use-statements", ...["family-history", "allergy-intolerances", "conditions", "procedures", "adverse-events", "clinical-impressions", "detected-issues"] .map((page) => `clinical/${page}`), ...["observations", "body-structures", "diagnostics-reports", "media", "imaging-studies"].map((page) => `diagnostics/${page}`), ...["medication-statements", "medication-requests", "medication-administrations", "medication-dispenses", "immunizations", "immunization-evaluations", "immunization-recommendations"].map((page) => `medications/${page}`), ]; /** True on a patient record page that lists through the context; the bar is named on no other. */ export const isRecordReadingPath = (pathname: string) => { if (!/^\/dashboard\/patient(\/|$)/.test(pathname)) return false; const page = pathname.replace(/^\/dashboard\/patient\/?/, "").replace(/\/+$/, ""); return GOVERNED_RECORD_PAGES.includes(page) || /^(visit|result)\/[^/]+$/.test(page); }; /** The practice a stored record was written at, by its own attribution only; undefined when it names none. */ export const recordPracticeId = (resource: Parameters[0]) => createdAtOrganization(resource).id; type RecordPracticeState = { /** True inside a patient's own record shell; elsewhere nothing is governed and every row shows. */ governed: boolean; /** The patient and viewer the context belongs to. */ scope: string; practice: RecordPractice | undefined; /** The practices offered: the patient's own, and one chosen from the record that is no longer among them. */ practices: RecordPractice[]; choose: (practice: RecordPractice | undefined) => void; /** How many pages on screen list through the context. The context is named only while one is. */ pagesInView: number; /** A page that lists through the context says so while it is on screen; returns its release. */ govern: () => () => void; }; const NOT_GOVERNED: RecordPracticeState = { governed: false, scope: "", practice: undefined, practices: [], choose: () => undefined, pagesInView: 0, govern: () => () => undefined, }; const RecordPracticeContext = createContext(NOT_GOVERNED); /** * `scope` names the patient and the person viewing. A new patient, helper or viewer starts again at All practices, * and coming back to an earlier one does too: a choice is retired with the scope it was made in, never kept aside. * `arrival` is a practice the person came to read about, such as one whose notification they opened: it is chosen * once it is found among their practices, and a practice not among them leaves the record at All practices. */ export const RecordPracticeProvider = ({ scope, enabled = true, arrival, children }: { scope: string; enabled?: boolean; arrival?: string; children: ReactNode }) => { const { organizations } = useMyOrganizations({ enabled }); const practices = useMemo( () => organizations .filter((organization) => organization.id) .map((organization) => ({ id: organization.id as string, name: organization.name ?? "A practice" })), [organizations], ); const [chosen, setChosen] = useState<{ scope: string; practice: RecordPractice } | undefined>(); const choose = useCallback((practice: RecordPractice | undefined) => { setChosen(practice ? { scope, practice } : undefined); }, [scope]); useEffect(() => { setChosen(undefined); }, [scope]); const arrived = useRef(); useEffect(() => { if (!arrival) { arrived.current = undefined; return; } const found = practices.find((candidate) => candidate.id === arrival); if (!found || arrived.current === arrival) return; arrived.current = arrival; setChosen({ scope, practice: found }); }, [arrival, practices, scope]); // A choice holds only for the record it was made on. A practice from the record that is not (or no longer) one // of the patient's can still be read, under the name its records carry. const practice = chosen?.scope === scope ? practices.find((candidate) => candidate.id === chosen.practice.id) ?? chosen.practice : undefined; const offered = useMemo( () => (practice && !practices.some((candidate) => candidate.id === practice.id) ? [...practices, practice] : practices), [practice, practices], ); const [pagesInView, setPagesInView] = useState(0); const govern = useCallback(() => { setPagesInView((count) => count + 1); return () => setPagesInView((count) => count - 1); }, []); const value = useMemo( () => (enabled ? { governed: true, scope, practice, practices: offered, choose, pagesInView, govern } : NOT_GOVERNED), [enabled, scope, practice, offered, choose, pagesInView, govern], ); return {children}; }; export const useRecordPractice = () => useContext(RecordPracticeContext); /** * Marks the page on screen as one that lists through the context. Only then is the context named: a page that * does not filter by practice never sits under "Showing records from" one, though the choice is kept for the next. */ export const useGovernedPage = (listsThroughIt = true) => { const { govern } = useRecordPractice(); useEffect(() => (listsThroughIt ? govern() : undefined), [govern, listsThroughIt]); }; /** A governed page's rows for the practice in view, and what it left out. */ export const useRecordPracticeView = (items: readonly T[], practiceIdOf: (item: T) => string | undefined): PracticeView => { useGovernedPage(); const { practice } = useRecordPractice(); return useMemo(() => viewForPractice(items, practice, practiceIdOf), [items, practice, practiceIdOf]); }; /** * Calls `onChange` when the practice in view changes, or the patient or viewer is replaced (All to All too), so a * record opened under one is not left open under another. */ export const useRetireOnPracticeChange = (onChange: () => void) => { const { practice, scope } = useRecordPractice(); const latest = useRef(onChange); latest.current = onChange; const key = `${scope}|${practice?.id ?? ""}`; const shownFor = useRef(key); useEffect(() => { if (shownFor.current === key) return; shownFor.current = key; latest.current(); }, [key]); }; /** * Under a named practice, says what this page left out: records that name no practice, and, when none read here is * from the practice, that there are records from others. It speaks only for the records that were read, so a read * that failed or came back in part never becomes "none from this practice". A page keeps its own empty state and * its own read errors for the whole record. */ export const RecordPracticeNote = ({ view }: { view: PracticeView }) => { const { practice, choose } = useRecordPractice(); if (!practice) return null; const noneHere = view.shown.length === 0 && view.elsewhere + view.unattributed > 0; if (!noneHere && view.unattributed === 0) return null; const unnamed = view.unattributed === 1 ? "1 record read here does not say which practice it came from, so it is shown only under All practices." : `${view.unattributed} records read here do not say which practice they came from, so they are shown only under All practices.`; return (

{noneHere && `None of the records read here is from ${practice.name}. `} {view.unattributed > 0 && unnamed} {" "}

); }; /** The context, named and changeable, on the record shell. Nothing to choose with one practice. */ export const RecordPracticeBar = () => { const { practice, practices, choose, pagesInView } = useRecordPractice(); if (pagesInView === 0 || (practices.length < 2 && !practice)) return null; return (
{practice ? `Showing records from ${practice.name}` : "Showing records from all your practices"} {practice && ( )}
); };