import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { AlertCircle, Inbox, Loader2, Plus, Send, Ticket } from "lucide-react"; import { useAuthServiceContext } from "../../../services/AuthService.tsx"; import { Card, CardContent } from "../../../components/ui/card.tsx"; import { Alert, AlertDescription } from "../../../components/ui/alert.tsx"; import { Button } from "../../../components/ui/button.tsx"; import { directionOf, useReferralQueue, type ReferralQueueRow, } from "../../../hooks/useReferralQueue.ts"; import { patientBookedLine, REFERRAL_DELIVERY_LABELS, referralClosureReasonLabel, sortReferrals, referralPillClass, type ReferralState, } from "../../../utils/referralQueue.ts"; import { referralClosureReasonLabel as ownClosureReasonLabel } from "../../../utils/referralClosureReason"; import LogInboundReferralDialog from "./LogInboundReferralDialog"; import RedeemClaimCodeDialog from "./RedeemClaimCodeDialog.tsx"; import PendingBeforeJoining from "./PendingBeforeJoining.tsx"; import { useCapabilityServiceContext } from "../../../services/CapabilityService.tsx"; import ReferralWorkbenchDialog from "./ReferralWorkbenchDialog.tsx"; import WithdrawReferralDialog, { WITHDRAWAL_REASON_LABELS } from "./WithdrawReferralDialog.tsx"; import LogPatientContactDialog from "./LogPatientContactDialog.tsx"; import MatchReferralToPatientDialog from "./MatchReferralToPatientDialog.tsx"; import { describeDrill, matchesDrill, referralDrillFrom } from "./referralDrill"; import { useReferralWorkbenchService } from "../../../services/ReferralWorkbenchService.tsx"; import { useRecordsIntakeService } from "../../../services/RecordsIntakeService.tsx"; import { actionAttempt, type ActionAttempt, type ReferralRequestScope } from "../../../utils/referralRequestScope"; import { useSaveScope, type SaveOperation } from "../../../utils/saveOperation"; import { viewerEpoch } from "../../../utils/writeThroughHold"; import { useClaimedReferralArrival } from "../../../hooks/useClaimedReferralArrival.ts"; import { useQueueCatchUpOnReturn } from "../../../hooks/useQueueCatchUpOnReturn.ts"; import { openReferralClaimHints, type ReferralClaimHint } from "../../../utils/referralClaimHint"; import "./referralsQueue.css"; // - THE REFERRAL COORDINATOR'S WORKSPACE. Two columns: what we are sending, and what is arriving. // // Spencer's reasoning for it being its own workspace rather than part of the front desk: *"My initial // instinct was to lump it all into the front desk system for the receptionists but that won't work in a // large medical center network."* In a two-person practice the coordinator IS the receptionist; in a // medical centre it is a department. Same role, different number of people in it. // // This is the READ half. The send workbench - deliver in-system, fax, or record a phone call - is the // next slice, and there the phone call is a first-class outcome rather than a fallback: *"however they // want to do it as long as it's all logged."* // `sent` means placed: it reads Placed, and delivery is tracked separately. const STATE_LABELS: Record = { sent: "Placed", delivered: "Delivered", claimed: "Claimed", scheduled: "Scheduled", expired: "Expired", withdrawn: "Withdrawn", }; /** * NO STATE IS ITS OWN THING, NOT A BLANK. A referral nobody has tracked is one somebody has to chase, * and an empty cell reads as a row that merely has less information in it. */ const stateLabel = (state?: ReferralState) => (state ? STATE_LABELS[state] : "Not tracked"); /** * A CLOSED REFERRAL AND AN EXPIRED ONE REACH THE SAME STATE AND ARE NOT THE SAME EVENT. * * Both carry `expired`, because the state machine has five values and closing is how a referral leaves * it. So a referral the receiving practice closed saying the patient declined rendered Expired, beside * one that simply ran out of time - and the coordinator deciding what to do next for that patient could * not tell which was which. * * The reason REPLACES the word rather than sitting beside it: the machine's word is the misleading one * here, and a row has space for one answer. */ // The sender reads the receiving practice's reason as theirs; on Inbound the reason is this practice's own. const closureOrStateLabel = (state?: ReferralState, closureReason?: string, direction: "outbound" | "inbound" = "outbound") => (direction === "inbound" ? ownClosureReasonLabel(closureReason) : referralClosureReasonLabel(closureReason)) ?? stateLabel(state); /** * A REFERRAL THE RECEIVING PRACTICE CANNOT SEE IS NEVER "SENT". One routed to a practice on Akashic is * saved before it is delivered, and its state stays `sent` whichever way delivery went - so on the * sender's row the delivery outcome replaces the state word until delivery is confirmed * (referrals.html#s-delivery-failed). A stored `pending` is a delivery that never reported back. */ const undelivered = (row: ReferralQueueRow) => row.delivery === "failed" || row.delivery === "pending"; const deliveryLabel = (row: ReferralQueueRow): string | undefined => { if (row.delivery === "failed" || row.delivery === "pending") return REFERRAL_DELIVERY_LABELS[row.delivery]; if (row.delivery === "delivered" && row.state === "sent") return REFERRAL_DELIVERY_LABELS.delivered; return undefined; }; const recipientOf = (row: ReferralQueueRow) => row.performerDisplay ?? "The receiving practice"; /** * A REFERRAL ITS SENDER WITHDREW READS "Withdrawn" ON BOTH SIDES, never the "Expired" its closed state would * print, with who and why (referrals.html#s-withdraw-referral). The sender may withdraw anything nobody has * booked yet; a booked or closed referral is the receiver's to close. */ const WITHDRAWABLE_STATES = new Set(["sent", "delivered", "claimed"]); const withdrawalNote = (row: ReferralQueueRow, direction: "outbound" | "inbound"): string | undefined => { if (!row.withdrawal) return undefined; const why = WITHDRAWAL_REASON_LABELS[row.withdrawal.reason]; return direction === "outbound" ? `Withdrawn by ${row.withdrawal.byDisplay ?? "your practice"} · ${why}. Contact ${recipientOf(row)} if they already received a copy.` : `The sending practice withdrew this referral · ${why}.`; }; /** Above the Outbound list, so a failed delivery is seen before anybody scans for it. */ const UndeliveredNotice = ({ rows }: { rows: ReferralQueueRow[] }) => { if (rows.length === 0) return null; const [only] = rows; return (
Referral not delivered

{rows.length === 1 ? `The referral for ${only.patientDisplay ?? "this patient"} is saved, but ${recipientOf(only)} has not received it. Retry from its Outbound row.` : `${rows.length} referrals are saved but have not reached the practice they were sent to. Retry each from its Outbound row.`}

); }; /** * - an OUTBOUND row opens the SEND workbench, and an inbound one must never. * * THE ASYMMETRY IS THE POINT, not an omission. The workbench records how WE sent a referral, and a * referral arriving here was sent by somebody else - offering "record a phone call" on it would invite a * coordinator to log the other practice's work as our own. * * CORRECTED BY, and the correction is what the original ruling left behind: withholding the * workbench withheld the only action inbound had, and **nothing replaced it**. The row was plain `div`s - * no button, no role, no handler - so a practice could look at its inbound queue and not work it. * Both columns are openable now, onto **different dialogs**. Outbound contacts a PRACTICE to send a * referral; inbound contacts the PATIENT to get them through the door. Same row shape, opposite work. * The rule that has not moved: **an inbound row never opens the send workbench.** * * AND A ROW WITH NO TASK IS NOT OPENABLE EITHER WAY. The contact record lives on the Task - whichever * practice's own account of its own work that is - so a row with an order and no Task has nothing to * write to. A button that opens a dialog which then cannot save is the shape second control * warns about. */ const ReferralRow = ({ row, direction, onOpen, canOpen, onRetry, onWithdraw, }: { row: ReferralQueueRow; /** EXPLICIT, not inferred from whether `onOpen` was passed. The two columns name different parties, * and tying that to the presence of a handler makes a display rule depend on an interaction rule. */ direction: "outbound" | "inbound"; onOpen?: (row: ReferralQueueRow) => void; /** Whether this row's action is this person's to take; a row they may not work is shown, not opened. */ canOpen?: (row: ReferralQueueRow) => boolean; /** Redeliver an outbound referral that did not arrive. Only the sender is offered it. */ onRetry?: (row: ReferralQueueRow) => Promise; /** Open the withdrawal confirmation for an outbound referral nobody has booked. */ onWithdraw?: (row: ReferralQueueRow) => void; }) => { const openable = Boolean(onOpen && (row.taskId || row.loggedReferralId) && (canOpen?.(row) ?? true)); const [retrying, setRetrying] = useState(false); const [retryFailed, setRetryFailed] = useState(false); const withdrawn = Boolean(row.withdrawal); const delivery = direction === "outbound" && !withdrawn ? deliveryLabel(row) : undefined; const retryable = direction === "outbound" && !withdrawn && undelivered(row) && Boolean(onRetry && row.orderId); const withdrawable = direction === "outbound" && !withdrawn && Boolean(onWithdraw && row.orderId && row.taskId) && WITHDRAWABLE_STATES.has(row.state ?? ""); const note = withdrawalNote(row, direction); const retry = async () => { if (!onRetry) return; setRetrying(true); setRetryFailed(false); try { await onRetry(row); } catch { setRetryFailed(true); } finally { setRetrying(false); } }; const body = ( <>
{/* 🩺 CARD 608 — THE TWO COLUMNS NAME DIFFERENT PARTIES, and rendering one field for both was a real defect rather than a simplification. On an OUTBOUND row the useful name is the performer, the practice we are sending to. On an INBOUND one the performer is US — so that column was showing a coordinator their own practice's name on every row, which is no information at all. ⇒ Inbound names the SENDER. ⭐ It is a display string with no reference, because the ordinary inbound sender is a practice that is not on Akashic and faxed us like it faxes everyone. */}

{/* A performer reference with no name is a practice on Akashic that claimed the referral with its code: the claim writes the reference alone, so the referral has a recipient. */} {direction === "outbound" ? row.performerDisplay ?? (row.performerReference ? "A practice on Akashic" : "No recipient chosen yet") : row.requesterDisplay ?? "Sender not recorded"}

{/* 🔻 THE PATIENT IS NAMED HERE NOW, and this reverses what this comment used to say. 🗣️ Spencer, 2026-08-26: *"What if someone asks ‘has Steve Smith’s referral been sent?’, how would they know if they can’t see the names on the referrals they’re sending out?"* ⚠️ The old rule — *"a coordinator … does not need a roomful of names on screen"* — was about SCANNABILITY rather than privacy, and it measured wrong: walked 2026-08-26, two referrals to one destination rendered character-for-character identical. The ABSENCE of names is what made the queue unscannable. ✅ Privacy was settled the same way on 2026-08-25 (a referral thread may name the patient), and this audience is NARROWER than that one — the practice that placed it already holds the record. */} {row.patientDisplay && (

{row.patientDisplay}

)} {row.reasonText &&

{row.reasonText}

} {patientBookedLine(row.patientBookedAt) && (

{patientBookedLine(row.patientBookedAt)}

)}

{row.authoredOn ? new Date(row.authoredOn).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "No date recorded"}

{withdrawn ? "Withdrawn" : row.loggedReferralId && !row.taskId ? "Needs a patient" : delivery ?? closureOrStateLabel(row.state, row.closureReason, direction)} ); const shared = "ak-row referrals-queue-row flex w-full items-start justify-between text-left"; // Retry sits BESIDE the row's own button, never inside it: a button in a button is one control to a // screen reader and cannot be pressed on its own. return (
{openable ? ( ) : (
{body}
)} {retryable && (
{retryFailed && (

The retry did not go through. {recipientOf(row)} still has not received this referral. Try again shortly.

)}
)} {withdrawable && (
)} {note &&

{note}

}
); }; const Column = ({ title, description, icon, rows, empty, direction, onOpen, canOpen, onRetry, onWithdraw, notice, incomplete, unreadable, }: { title: string; description: string; icon: React.ReactNode; rows: ReferralQueueRow[]; empty: string; direction: "outbound" | "inbound"; onOpen?: (row: ReferralQueueRow) => void; canOpen?: (row: ReferralQueueRow) => boolean; onRetry?: (row: ReferralQueueRow) => Promise; onWithdraw?: (row: ReferralQueueRow) => void; /** Said above the rows, under the column's heading. */ notice?: React.ReactNode; incomplete?: boolean; /** The whole read failed (ReferralsPage's `error`). Suppresses the day-one empty copy below, which would otherwise sit under the error banner claiming this column has nothing in it - a fact the failed read never established. */ unreadable?: boolean; }) => (

{icon} {title} {/* NO NUMBER WHEN THE READ WAS INCOMPLETE. A count is an assertion about how much work is waiting, and a coordinator finishes their day on it. Printing the length of a partial list is the half of this failure that made it invisible: the screen looked finished. */} {!incomplete && ( ({rows.length}) )}

{description}

{incomplete && (

This list is incomplete — some referrals could not be read. Reload, and if it keeps happening tell whoever supports this practice before working from it.

)}
{notice} {/* ⭐ Each empty state says what WILL appear here rather than asserting nothing is wrong. A coordinator opening this on day one has never seen a referral row and does not yet know what this column is for. */} {rows.length === 0 ? (incomplete || unreadable) ? null :

{empty}

: rows.map((row) => ( ))}
); /** How long a dialog may stay open before it is retired; a write inside it still has its own deadline. */ const OPENING_LIFETIME_MS = 8 * 60 * 60 * 1000; type DialogKind = "work" | "log" | "claim" | "contact" | "match" | "withdraw"; /** One open dialog: what it was opened on, and the lifetime its requests, answers and callbacks are admitted by. */ interface Opening { id: number; value: T; life: SaveOperation; scope: ReferralRequestScope; } interface Openings { work?: Opening; log?: Opening; claim?: Opening; contact?: Opening; match?: Opening; withdraw?: Opening; } const ReferralsPage = () => { const { currentOrganization } = useAuthServiceContext(); const { classifyFax } = useRecordsIntakeService(); // The server's answer to who works referrals: the front desk, clinicians, admins and the referral coordinator. // `undefined` is not known yet, and the server still refuses anyone else. An answer about another practice is // not one about this practice, so it reads as not known until this practice's arrives. const capability = useCapabilityServiceContext(); const capabilityHere = !capability.organizationId || capability.organizationId === currentOrganization?.id; const mayCall = (method: string, path: string) => (capabilityHere ? capability.mayCall(method, path) : undefined); const may = (method: string, path: string) => mayCall(method, path) !== false; const deniedByCapability = mayCall("GET", "/referral/queue") === false; const { rows, loading, error, truncated, refused, reload, refreshQuietly } = useReferralQueue(currentOrganization?.id, !deniedByCapability); // The server refusing the read is the answer, whatever the capability snapshot still says. const denied = deniedByCapability || refused; // Each write control, by the route it calls. const mayClaim = may("POST", "/referral-claim/redeem"); const mayLog = may("POST", "/referral/inbound"); const mayWithdraw = may("POST", "/referral/*/withdraw"); const mayContact = may("POST", "/referral/*/contact"); const mayMatch = may("GET", "/referral/*/patient-candidates") && may("POST", "/referral/*/link"); // Moving a received fax to Records needs Records manage as well as referral work; the server checks both. const mayMoveToRecords = may("POST", "/records/fax-items/*/classify"); const mayRedeliver = may("POST", "/referral/*/redeliver"); const workbenchWrites = { contact: mayContact, state: may("PUT", "/referral/*/state"), claimCode: may("POST", "/referral/*/claim-code"), fax: may("POST", "/referral/*/fax"), contactBook: may("PUT", "/referral-contacts/*"), }; const allowed: Record = { work: true, log: mayLog, claim: mayClaim, contact: mayContact, match: mayMatch, withdraw: mayWithdraw, }; // The permissions as they stand now, read by requests already under way rather than as they stood when they began. const allowedNow = useRef({ ...allowed, redeliver: mayRedeliver, moveToRecords: mayMoveToRecords }); allowedNow.current = { ...allowed, redeliver: mayRedeliver, moveToRecords: mayMoveToRecords }; // Every open dialog has a lifetime. A switch of practice or of the person viewing, or this page going away, // retires them all for good; they are cleared rather than hidden, so returning to a practice brings none back. const [openings, setOpenings] = useState({}); const openingsRef = useRef(openings); openingsRef.current = openings; const lives = useSaveScope(currentOrganization?.id, () => setOpenings({})); const nextOpening = useRef(0); const scopeOf = (life: SaveOperation): ReferralRequestScope => ({ admit: life.admit, signal: life.transport.signal, organizationId: currentOrganization?.id }); const open = (kind: K, value: NonNullable["value"]) => { openingsRef.current[kind]?.life.retire(); const life = lives.start(OPENING_LIFETIME_MS); const opening = { id: ++nextOpening.current, value, life, scope: actionAttempt(scopeOf(life), () => allowedNow.current[kind]) }; setOpenings((current) => ({ ...current, [kind]: opening })); }; // Closes this opening only: a late close from one already replaced leaves its replacement alone. const close = (kind: DialogKind, opening: Opening | undefined) => { if (!opening) return; opening.life.retire(); setOpenings((current) => (current[kind] === opening ? { ...current, [kind]: undefined } : current)); }; // A completion re-reads only while its own opening still holds. const completed = (opening: Opening | undefined) => { if (opening?.life.belongs()) void reload(); }; // Losing one action's authority retires that action's opening for good; losing the queue retires them all. useEffect(() => { if (denied) { lives.renew(); setOpenings({}); return; } (Object.keys(openingsRef.current) as DialogKind[]).forEach((kind) => { if (!allowed[kind]) close(kind, openingsRef.current[kind]); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [denied, mayLog, mayClaim, mayContact, mayMatch, mayWithdraw, lives]); // Shown only while it holds, so the render that loses an authority does not wait for the effect above. const shown = (kind: K): Openings[K] | undefined => { const opening = openings[kind]; return opening && allowed[kind] && !denied && opening.life.belongs() ? opening : undefined; }; const work = shown("work"); const logging = shown("log"); const claiming = shown("claim"); const chasing = shown("contact"); const matching = shown("match"); const withdrawing = shown("withdraw"); const [pendingReads, setPendingReads] = useState(0); const rereadPending = useCallback(() => setPendingReads((n) => n + 1), []); // A closed queue looks for no claim, and a claim belongs to the viewer and practice it was made under. const arrivalScope = denied ? undefined : `${viewerEpoch()}|${currentOrganization?.id ?? ""}`; const arrival = useClaimedReferralArrival(rows, refreshQuietly, arrivalScope, rereadPending); // A match retires the logged entry at once, but its order reaches the queue search minutes later. const matchArrival = useClaimedReferralArrival(rows, refreshQuietly, arrivalScope); // A claim or match made on another page of this practice in this browser is waited for here too, the same // bounded way; a hint for another practice, or to a page that may not read the queue, starts nothing. const hear = useRef<(hint: ReferralClaimHint) => void>(() => undefined); hear.current = (hint) => { if (denied || !arrivalScope || hint.practice !== currentOrganization?.id) return; if (hint.kind === "matched") matchArrival.awaitClaim(hint.referralId, arrivalScope); else if (arrival.awaitClaim(hint.referralId, arrivalScope)) rereadPending(); }; const hints = useRef>(); useEffect(() => { const opened = openReferralClaimHints((hint) => hear.current(hint)); hints.current = opened; return () => { opened.close(); hints.current = undefined; }; }, []); const tellOtherPages = (kind: ReferralClaimHint["kind"], referralId: string) => { if (currentOrganization?.id) hints.current?.post({ kind, practice: currentOrganization.id, referralId }); }; // Coming back to the page reads the queue and the notice again, and keeps catching up for a bounded while, so a // claim whose hint this page missed is found even when the list's index lists it minutes later. const catchUp = useQueueCatchUpOnReturn({ rows, refresh: refreshQuietly, scope: arrivalScope, enabled: !denied, waiting: arrival.waiting.length > 0 || matchArrival.waiting.length > 0, onCheck: rereadPending, }); const { redeliverReferral } = useReferralWorkbenchService(); // Re-read whatever the retry answered: the server wrote the outcome onto the order, and the queue is // the only honest account of it. A refusal is thrown back to the row, which says so beside its Retry. const retries = useRef(new Set()); const retryDelivery = async (row: ReferralQueueRow) => { if (!row.orderId) return; const life = lives.start(); try { const retry = actionAttempt(scopeOf(life), () => allowedNow.current.redeliver); retries.current.add(retry); await redeliverReferral(row.orderId, retry); if (life.belongs()) void reload(); } finally { life.retire(); } }; // Losing the permission to move a fax to Records retires every move under way, for good. const moves = useRef(new Set()); useEffect(() => { if (mayMoveToRecords) return; moves.current.forEach((move) => move.retire()); moves.current.clear(); }, [mayMoveToRecords]); // Losing the redeliver permission retires every retry under way, for good. useEffect(() => { if (mayRedeliver) return; retries.current.forEach((retry) => retry.retire()); retries.current.clear(); }, [mayRedeliver]); const needsMatch = (row: ReferralQueueRow) => Boolean(row.loggedReferralId && !row.taskId); const mayOpenInbound = (row: ReferralQueueRow) => (needsMatch(row) ? mayMatch : mayContact); const [searchParams, setSearchParams] = useSearchParams(); const drill = useMemo(() => referralDrillFrom(searchParams), [searchParams]); const columns = useMemo(() => { const withDirection = rows.map((row) => ({ row, direction: directionOf(row, currentOrganization?.id), })); const pick = (want: "outbound" | "inbound") => sortReferrals( withDirection .filter((item) => item.direction === want && (!drill || matchesDrill(item.row, want, drill))) .map((item) => item.row), ) as ReferralQueueRow[]; return { outbound: pick("outbound"), inbound: pick("inbound") }; }, [rows, currentOrganization?.id, drill]); if (denied) { return (

Referrals

); } return (

Referrals

Referrals this practice is sending, and referrals arriving here.

{/* 📥 CARD 684 — the only way a faxed or phoned referral gets tracked at all. Every other arrival path starts with the SENDER being on Akashic, so without this the queue holds only the referrals that needed tracking least. ⭐ On the page rather than inside the Inbound column: a coordinator reaches for this while holding a piece of paper, before they have looked at any column. */} {/* 🎟️ THE TWO WAYS A REFERRAL ARRIVES, SIDE BY SIDE, because a coordinator holding something has to pick one and the difference is only where it came from. ⭐ The claim code is the SECONDARY action deliberately: paper and telephone are still how most referrals arrive, and the practice sending a code is the minority case. */}
{mayClaim && ( )} {mayLog && ( )}
{error && ( {error} )} {arrival.waiting.length > 0 && (

)} {arrival.late.length > 0 && (

{arrival.late.length === 1 ? "The referral you claimed is yours, but it is not listed yet. Reload this page in a few minutes; the claim is kept." : `${arrival.late.length} referrals you claimed are yours, but are not listed yet. Reload this page in a few minutes; the claims are kept.`}

)} {matchArrival.waiting.length > 0 && (

)} {matchArrival.late.length > 0 && (

{matchArrival.late.length === 1 ? "The referral you matched is on the patient's record, but it is not listed yet. Reload this page in a few minutes." : `${matchArrival.late.length} referrals you matched are on their patients' records, but are not listed yet. Reload this page in a few minutes.`}

)} {arrival.refused && (

Your claim is kept, but this account can no longer read the referral list, so it cannot show it here.

)} {catchUp.failed && !denied && (

We could not check this list for new referrals.{" "}

)} {drill && (

{describeDrill(drill)}.{" "}

)} {loading ? (

Loading referrals…

) : (
)} { if (!next) close("log", logging); }} // Re-read rather than push a row in: the SERVER minted the id and composed what the desk // reads, so the queue is the only honest account of what was just created. onLogged={() => completed(logging)} /> { if (!next) close("claim", claiming); }} // Same reason as the dialog above: the server placed the referral in the queue and notified the // desk, so re-reading is the only account of what actually arrived. // Then waits for the claimed one: the queue is a search that catches up minutes after the write. onRedeemed={(referralId) => { if (!claiming?.life.belongs()) return; if (!referralId) { void reload(); return; } if (!arrival.awaitClaim(referralId, arrivalScope)) return; rereadPending(); tellOtherPages("claimed", referralId); }} /> { if (!next) close("withdraw", withdrawing); }} // Re-read: the server closed the order and wrote who and why, and the queue is its only account. onWithdrawn={() => completed(withdrawing)} /> { if (!next) close("work", work); }} // Re-read rather than patch the row in place: the contact is appended server-side and the // referral's own state may move with it, so the queue is the only honest source of what happened. onRecorded={() => completed(work)} /> { if (!next) close("contact", chasing); }} onRecorded={() => completed(chasing)} /> {matching?.value.loggedReferralId && ( { if (!next) close("match", matching); }} referral={{ referralId: matching.value.loggedReferralId, label: "Referral from " + (matching.value.requesterDisplay ?? "another practice") }} fax={matching.value.fax} onMoveToRecords={matching.value.fax && mayMoveToRecords ? async () => { const move = actionAttempt(matching.scope, () => allowedNow.current.moveToRecords); moves.current.add(move); try { await classifyFax(matching.value.loggedReferralId as string, "referral", "records", move); move.admit(); if (matching.life.belongs()) void reload(); } finally { moves.current.delete(move); } } : undefined} onMatched={() => { if (!matching.life.belongs()) return; if (!matchArrival.awaitClaim(matching.value.loggedReferralId as string, arrivalScope)) void reload(); else tellOtherPages("matched", matching.value.loggedReferralId as string); }} /> )}
); }; export default ReferralsPage;