import { useCallback, useEffect, useState } from "react"; import type { ServiceRequest } from "fhir/r4"; import { AlertCircle, Loader2, Plus, Send } from "lucide-react"; import { Alert, AlertDescription } from "../../../../components/ui/alert"; import { Badge } from "../../../../components/ui/badge"; import { Button } from "../../../../components/ui/button"; import { Card, CardContent } from "../../../../components/ui/card"; import { EmptyState } from "../../../../components/ui/empty-state"; import { usePatientServiceContext } from "../../../../services"; import { useReferralWorkbenchService } from "../../../../services/ReferralWorkbenchService"; import { formatFhirHumanName } from "../../../../utils/fhirHumanName"; import { readOrderClosureReason, readOrderInSystemDelivery, readOrderReferralState, REFERRAL_DELIVERY_LABELS, referralClosureReasonLabel, type ReferralState, } from "../../../../utils/referralQueue"; import PlaceReferralDialog from "./PlaceReferralDialog"; /** * - THE PRACTITIONER'S SIDE OF A REFERRAL, on the patient it is about. * * THE GAP THIS CLOSES: the whole inbound half was built and proven - typed in by hand, claimed, * matched to its patient, retired on booking, closable with a reason - the three send paths existed, the * directory existed, **and no screen anywhere placed one.** The outbound column was a queue nothing could * put anything into. * * WHY THE CHART AND NOT THE ENCOUNTER, traced rather than assumed. A practitioner refers **from a visit * and between visits**, and only one surface covers both. The encounter screen has no ordering area at * all - it is notes, the scribe and the visit lifecycle - so an encounter-only home would both miss the * between-visits case and require inventing an orders area inside a visit. * Care Provision is where the other request-family resources already live (Care Plans, Nutrition * Orders, Vision Prescriptions), which is the same shelf a referral belongs on. * * AND IT IS A PRACTITIONER SURFACE BY RULING, not by placement. `POST /R4/ServiceRequest` allows * `[PRACTITIONERS]` alone - *"the practitioner places the order; staff executes. Two actions, two * people."* The coordinator workspace deliberately has no authoring affordance, and must not grow one. */ // `sent` means placed: it reads Placed unless the order records how its delivery went (stateLabel). const STATE_LABELS: Record = { sent: "Placed", delivered: "Delivered", claimed: "Claimed", scheduled: "Scheduled", expired: "Closed", withdrawn: "Withdrawn", }; /** * NO STATE IS ITS OWN THING, NOT A BLANK - the same rule the coordinator's queue follows. An empty * cell reads as a row with less information in it; "Not tracked" says what is actually true. */ const stateLabel = (order: ServiceRequest): string => { const closure = referralClosureReasonLabel(readOrderClosureReason(order)); if (closure) return closure; const state = readOrderReferralState(order); // A PLACED REFERRAL SAYS WHETHER IT ARRIVED, when the order records it: the same words as the coordinator's // Outbound row. Only while it is still `sent` - a claim, booking, withdrawal or closure is the newer fact and // keeps its word. No recorded outcome stays Placed; nothing is inferred from a recipient or a fax. const delivery = state === "sent" ? readOrderInSystemDelivery(order) : undefined; if (delivery) return REFERRAL_DELIVERY_LABELS[delivery]; return state ? STATE_LABELS[state] : "Not tracked"; }; /** Who it went to. A display-only performer is the ordinary case: the practice is not on Akashic. */ const destinationOf = (order: ServiceRequest): string => order.performer?.[0]?.display ?? "No recipient recorded"; const Referrals = () => { const { patientState } = usePatientServiceContext(); const { listPatientReferrals, readPlacedReferral } = useReferralWorkbenchService(); const [orders, setOrders] = useState(null); const [error, setError] = useState(null); const [placing, setPlacing] = useState(false); const patientId = patientState?.id; const patientName = formatFhirHumanName(patientState?.name, { emptyText: "" }) || undefined; const load = useCallback(async () => { if (!patientId) return; setError(null); try { const bundle = await listPatientReferrals(patientId); const found = (bundle?.entry ?? []) .map((entry) => entry.resource) .filter((resource): resource is ServiceRequest => resource?.resourceType === "ServiceRequest") // A REFERRAL IS AN ORDER CARRYING A REFERRAL STATE. The same search returns lab and // imaging orders for this patient, and listing those here would tell a practitioner they // had referred somebody they had not ⇒ the same discriminator the booking path uses. .filter((resource) => readOrderReferralState(resource) !== undefined); setOrders(found); } catch { // The error is stated rather than shown as an empty list. "No referrals" and "we could not // ask" look identical on screen and mean opposite things to somebody deciding whether to refer. setError("We could not load this patient's referrals just now."); setOrders(null); } }, [patientId, listPatientReferrals]); /** * The same load, plus the one order that was just placed if the search cannot see it yet. * * A SEARCH INDEX LAGS A WRITE; A READ BY ID DOES NOT. Measured 2026-08-31: a referral was written * and confirmed in the handler's logs in the same second, and was still absent from a subject-scoped * search two and a half minutes later - through a full page reload, which is what separates this from * a stale client cache. So `load()` on its own tells a practitioner their referral was placed and * then shows them a list without it. * * NOT A POLL. The wait is minutes; retrying the search would only lengthen the blank window. This * asks the one question that has an answer, once, and only when the search has not answered it. * And it is still the server's account rather than an invented row - which is what the original * "re-read rather than push the new one in" comment was protecting, and this keeps. */ const loadIncluding = useCallback(async (placedOrderId?: string) => { await load(); if (!placedOrderId) return; try { const placed = await readPlacedReferral(placedOrderId); if (readOrderReferralState(placed) === undefined) return; setOrders((current) => { // Null means the load above FAILED and the error banner is up. A one-row list // painted under it would read as "you have one referral" rather than as the one we // happen to know about, so the banner is left to speak alone. if (current === null) return current; // The search may already have caught up, in which case there is nothing to add and // adding anyway would double the row. if (current.some((order) => order.id === placed.id)) return current; return [placed, ...current]; }); } catch { // Deliberately silent. The referral IS placed - the dialog only calls this after a // success - so a failure to read it back is a list that is briefly short, not an error // worth alarming a practitioner about. `load()` above already reported any real failure. } }, [load, readPlacedReferral]); useEffect(() => { void load(); }, [load]); return (

Referrals

Referrals you have placed for this patient, and where each one got to.

{error && ( {error} )} {orders === null && !error && (

Loading referrals…

)} {orders?.length === 0 && ( /* Says what WILL appear rather than asserting nothing is wrong - the same rule the coordinator's two columns follow. A practitioner seeing this has never placed one. */ )} {orders && orders.length > 0 && ( {orders.map((order) => (

{destinationOf(order)}

{order.reasonCode?.[0]?.text ?? "No reason recorded"}

{stateLabel(order)}
))}
)} { void loadIncluding(placedOrderId); }} />
); }; export default Referrals;