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 (
{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"}
)}
{/* 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.