import { useEffect, useRef, useState } from "react"; import { Loader2, Search } from "lucide-react"; import { Checkbox } from "../../../components/ui/checkbox"; import { Input } from "../../../components/ui/input"; import { useReferralWorkbenchService, type ReferralConfirmedFaxDetails, type ReferralFaxReadField, type ReferralFaxReadResult, } from "../../../services/ReferralWorkbenchService"; import { eitherAborted, stillOpen, type ReferralRequestScope } from "../../../utils/referralRequestScope"; import { relayServerFailure } from "../../../utils/fetchFailureMessage"; import { isSaveRetired, useSaveScope } from "../../../utils/saveOperation"; import { citedLine, FAX_FIELD_LABEL, SEARCHABLE, senderRead, SHOWN_ONLY } from "./referralFaxWords"; const ASK_AGAIN_MS = 4000; /** One ask may take this long before it is stopped and offered again. */ const ASK_DEADLINE_MS = 30_000; const ASKS_BEFORE_PAUSE = 45; type Ready = Extract["reading"]; type Phase = { at: "idle" } | { at: "reading" } | { at: "paused" } | { at: "failed"; message: string } | { at: "ready"; reading: Ready }; export type ReferralFaxReadingProps = { referralId: string; scope?: ReferralRequestScope; disabled?: boolean; onSearch: (term: string) => void; onConfirmedChange: (details: ReferralConfirmedFaxDetails | undefined) => void; }; const Cited = ({ field }: { field: ReferralFaxReadField }) => ( <> {field.lines.map((line) => ( {citedLine(line)} ))} {field.unassigned ? The fax does not say whose this is. Read it from the fax. : field.uncertain && Hard to read. Check it against the fax.} ); /** A received fax read into suggestions. Only the sender and reason a person ticks are saved, and only with the match. */ export const ReferralFaxReading = ({ referralId, scope, disabled, onSearch, onConfirmedChange }: ReferralFaxReadingProps) => { const { readReferralFax } = useReferralWorkbenchService(); const [phase, setPhase] = useState({ at: "idle" }); const [from, setFrom] = useState({ value: "", checked: false }); const [reason, setReason] = useState({ value: "", checked: false }); const timer = useRef>(); // This referral's own lifetime inside the dialog's: a change of viewer, or this panel going away, retires its asks // and conceals what they read. const lives = useSaveScope(referralId, () => { clearTimeout(timer.current); setPhase({ at: "idle" }); setFrom({ value: "", checked: false }); setReason({ value: "", checked: false }); }); useEffect(() => () => { clearTimeout(timer.current); onConfirmedChange(undefined); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { const details: ReferralConfirmedFaxDetails = { ...(from.checked && from.value.trim() ? { from: from.value.trim() } : {}), ...(reason.checked && reason.value.trim() ? { reason: reason.value.trim() } : {}), }; onConfirmedChange(Object.keys(details).length > 0 ? details : undefined); }, [from, reason, onConfirmedChange]); const ask = async (asked: number) => { if (!stillOpen(scope)) return; setPhase({ at: "reading" }); const operation = lives.start(ASK_DEADLINE_MS); const owned: ReferralRequestScope = { admit: () => { operation.admit(); scope?.admit(); }, signal: scope ? eitherAborted(operation.transport.signal, scope.signal) : operation.transport.signal, organizationId: scope?.organizationId, }; try { const result = await operation.step(() => readReferralFax(referralId, owned)); if (!operation.belongs() || !stillOpen(scope)) return; if (result.state === "reading") { if (asked + 1 >= ASKS_BEFORE_PAUSE) { setPhase({ at: "paused" }); return; } timer.current = setTimeout(() => void ask(asked + 1), ASK_AGAIN_MS); return; } if (result.state === "failed") { setPhase({ at: "failed", message: result.message }); return; } const sender = senderRead(result.reading.fields); setFrom({ value: sender?.value ?? "", checked: false }); setReason({ value: result.reading.fields.reason?.value ?? "", checked: false }); setPhase({ at: "ready", reading: result.reading }); } catch (failure: unknown) { if (isSaveRetired(failure) || !operation.belongs() || !stillOpen(scope)) return; setPhase({ at: "failed", message: relayServerFailure(failure, "We could not read the fax just now. Try again in a moment.") }); } finally { operation.retire(); } }; if (phase.at === "idle" || phase.at === "failed" || phase.at === "paused") { return (

{phase.at === "failed" ? phase.message : phase.at === "paused" ? "The fax is still being read. Try again in a minute." : "This referral came by fax. Read it to suggest the patient, the sender and the reason; you check each one."}

); } if (phase.at === "reading") { return (

Reading the fax. This can take a minute.

); } const { fields, droppedUngrounded, truncated } = phase.reading; const sender = senderRead(fields); const searchable = SEARCHABLE.filter((name) => fields[name]); const shown = SHOWN_ONLY.filter((name) => fields[name]); const editable = [ { key: "from", label: "Sender", read: sender, state: from, set: setFrom }, { key: "reason", label: FAX_FIELD_LABEL.reason, read: fields.reason, state: reason, set: setReason }, ].filter((item) => item.read); return (

Read from the fax

{searchable.length === 0 && editable.length === 0 && shown.length === 0 && (

Nothing on the fax could be read with confidence. Work from the fax itself.

)} {searchable.map((name) => (
{FAX_FIELD_LABEL[name]} {fields[name]!.value} {!fields[name]!.unassigned && }
))} {editable.map((item) => (
item.set({ ...item.state, value: event.target.value })} />
))} {shown.length > 0 && (
{shown.map((name) => (
{FAX_FIELD_LABEL[name]}
{fields[name]!.value}
))}
)} {(droppedUngrounded > 0 || truncated) && (

{truncated ? "Only the first part of a long fax was read. " : ""} {droppedUngrounded > 0 ? "Some values were left out because the fax text did not carry them." : ""}

)}
); }; export default ReferralFaxReading;