import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { Organization } from "fhir/r4"; import { ExternalLink } from "lucide-react"; import { useFhirTasks } from "../../../hooks/useFhirTasks.tsx"; import { useFhirAppointments } from "../../../hooks/useFhirAppointments.tsx"; import { useOrganizationServiceContext } from "../../../services/OrganizationService.tsx"; import { formatCustomTaskDueDate } from "../../../services/TaskService.tsx"; import { isUnreadableOrganization } from "../../../utils/patientOrganizations.ts"; import { taskName } from "../../../utils/taskName.ts"; import { agreementsUnknown, practicesWaitingOnYou, type PracticeWaitingOnYou } from "./pendingPracticeWork.ts"; import { PRACTICES_CHANGED_EVENT, type PracticesChanged, useRereadOnReturn } from "../../../hooks/useRereadOnReturn.ts"; import { useViewerScope } from "../../../hooks/useViewerScope.ts"; import { useAuthServiceContext } from "../../../services/AuthService.tsx"; import { useMyProxies } from "../../../hooks/useMyProxies"; /** One of the patient's own reads behind this section: still asking, answered, or failed with a retry. */ export interface WorkRead { status: "loading" | "ready" | "failed"; retry: () => void; } const workRead = (answered: boolean, error: string | undefined, retry: () => void): WorkRead => ({ status: error ? "failed" : answered ? "ready" : "loading", retry, }); const ReadNotice = ({ read, what }: { read: WorkRead; what: string }) => read.status === "failed" ? (
We could not load its {what}.
) : read.status === "loading" ? ( Checking its {what}… ) : null; /** A fresh read of the practices waiting on the patient, asked for from a card that could not be read. */ export interface PracticeRecheck { checking: boolean; failed: boolean; retry: () => void; } /** * A practice the patient belongs to but has not yet answered the agreements of. It is shown as waiting, * never as a practice with access to the record, and its agreements are answered at its own portal. */ export const PracticesWaitingOnYouList = ({ items, tasksRead = { status: "ready", retry: () => undefined }, visitsRead = { status: "ready", retry: () => undefined }, recheck, checkAgain, actingFor, actingForMinor, }: { items: PracticeWaitingOnYou[]; tasksRead?: WorkRead; visitsRead?: WorkRead; recheck?: PracticeRecheck; checkAgain?: PracticeRecheck; /** Whose record a helper has switched into; their agreements are not offered from here. */ actingFor?: string; /** Whether the person acted for is a minor, when known; unknown makes no claim about who may answer. */ actingForMinor?: boolean; }) => { if (items.length === 0) return null; return (

Waiting on you

{items.map(({ organization, portalUrl, tasks, visits }) => { const name = organization.name?.trim() || "A practice"; const degraded = isUnreadableOrganization(organization) || agreementsUnknown(organization); return (
{name} {isUnreadableOrganization(organization) && ( We could not load this practice’s details. )} {agreementsUnknown(organization) ? "We could not check which of its agreements are still needed." : actingFor ? name + " has not been given access to " + actingFor + "\u2019s record yet." : checkAgain?.failed && !checkAgain.checking ? name + " was still waiting on you when we last checked. Review its agreements to continue." : name + " has not been given access to your record yet. Review its agreements to continue."} {degraded && recheck && (
{recheck.failed && !recheck.checking && We still could not check this practice.}
)} {(tasks.length > 0 || visits.length > 0) && (
    {tasks.map((task) => (
  • {taskName(task) || task.description?.trim() || "Something to complete"}
  • ))} {visits.map((visit) => (
  • {visit.description?.trim() || visit.serviceType?.[0]?.text?.trim() || "Appointment"} {visit.start && (  · {formatCustomTaskDueDate(visit.start)} )}
  • ))}
)} {/* The switch into someone else's record stays on this site, and an assist-only helper may not answer, so no link sends a helper off to answer for them. */} {actingFor ? ( {actingForMinor === true ? <>Its agreements are answered at its own portal by an established guardian of {actingFor}, signed in there. : actingForMinor === false ? <>Its agreements are answered at its own portal by {actingFor}, or by someone {actingFor} has given full access, signed in there. : <>Its agreements are answered at its own portal by whoever is entitled to answer for {actingFor}.} {" "}Your switch into {actingFor}’s record here does not carry over. ) : portalUrl ? ( Review agreements ) : ( Contact {name} to review its agreements. )}
); })}
{checkAgain && (
{checkAgain.failed && !checkAgain.checking ? "We could not check again just now." : "Already answered at the practice?"}
)}
); }; // A re-check keeps what was already read on screen and reports its own failure beside it. type Discovery = | { status: "loading" } | { status: "ready"; practices: Organization[]; checking?: boolean; checkFailed?: boolean } | { status: "failed" }; // While a practice is shown as waiting and this page is in view, it is checked again on this interval, this many // times; after that the patient is offered a check of their own. Coming back to the page starts the count again. export const RECHECK_WHILE_WAITING_MS = 15_000; export const RECHECKS_WHILE_WAITING = 8; const PracticesWaitingOnYou = () => { const { fetchPendingPractices } = useOrganizationServiceContext(); const { actingAs } = useAuthServiceContext(); const { proxies } = useMyProxies(); const actingForMinor = actingAs ? proxies.find((proxy) => proxy.relatedPersonId === actingAs.relatedPersonId)?.subjectIsMinor : undefined; const [discovery, setDiscovery] = useState({ status: "loading" }); const [attempt, setAttempt] = useState(0); const retryDiscovery = useCallback(() => setAttempt((n) => n + 1), []); const [autoChecks, setAutoChecks] = useState(0); const shownIds = useRef([]); const scope = useViewerScope(); const readFor = useRef(scope); useEffect(() => { let active = true; // Another viewer's practices are never shown while this one's are read. if (readFor.current !== scope) { readFor.current = scope; shownIds.current = []; setAutoChecks(0); setDiscovery({ status: "loading" }); } setDiscovery((current) => current.status === "ready" ? { ...current, checking: true, checkFailed: false } : { status: "loading" }); fetchPendingPractices() .then((practices) => { if (!active) return; const ids = practices.map((practice) => practice.id ?? ""); // A practice that stopped waiting may now be one of the patient's practices, which is read separately. const cleared = shownIds.current.filter((id) => !ids.includes(id)); if (cleared.length) { const detail: PracticesChanged = { ids: cleared, scope }; window.dispatchEvent(new CustomEvent(PRACTICES_CHANGED_EVENT, { detail })); } shownIds.current = ids; setDiscovery({ status: "ready", practices }); }) .catch((error) => { console.error("Could not read the practices waiting on you:", error); if (active) { setDiscovery((current) => current.status === "ready" ? { ...current, checking: false, checkFailed: true } : { status: "failed" }); } }); return () => { active = false; }; }, [fetchPendingPractices, attempt, scope]); useRereadOnReturn(useCallback(() => { setAutoChecks(0); retryDiscovery(); }, [retryDiscovery])); const practices = discovery.status === "ready" ? discovery.practices : []; const settled = discovery.status === "ready" && !discovery.checking; const checksLeft = autoChecks < RECHECKS_WHILE_WAITING; useEffect(() => { if (!settled || practices.length === 0 || !checksLeft) return; const timer = window.setTimeout(() => { if (document.visibilityState !== "visible") return; setAutoChecks((n) => n + 1); retryDiscovery(); }, RECHECK_WHILE_WAITING_MS); return () => window.clearTimeout(timer); }, [settled, practices.length, checksLeft, autoChecks, retryDiscovery]); // The patient's own reads, asked only when a practice is waiting. const waiting = practices.length > 0; const tasks = useFhirTasks({ autoLoad: waiting }); const appointments = useFhirAppointments({ autoLoad: waiting }); const items = useMemo( () => practicesWaitingOnYou(practices, tasks.tasksList ?? [], appointments.appointmentsList ?? [], new Date()), [practices, tasks.tasksList, appointments.appointmentsList], ); if (discovery.status === "failed") { return (

Waiting on you

We could not check whether a practice is waiting on you.
); } return ( { void tasks.loadTasks(); })} visitsRead={workRead(appointments.answered, appointments.error, () => { void appointments.loadAppointments(); })} recheck={discovery.status === "ready" ? { checking: !!discovery.checking, failed: !!discovery.checkFailed, retry: retryDiscovery } : undefined} checkAgain={discovery.status === "ready" && practices.length > 0 && (!checksLeft || discovery.checkFailed) ? { checking: !!discovery.checking, failed: !!discovery.checkFailed, retry: retryDiscovery } : undefined} actingFor={actingAs ? actingAs.subjectName?.trim() || "the person you are acting for" : undefined} actingForMinor={actingForMinor} /> ); }; export default PracticesWaitingOnYou;