import { useState } from "react"; import { AlertCircle, ArrowLeft, Loader2 } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../../../components/ui/dialog"; import { Alert, AlertDescription } from "../../../../components/ui/alert"; import { Button } from "../../../../components/ui/button"; import { Input } from "../../../../components/ui/input"; import { Textarea } from "../../../../components/ui/textarea"; import ReferralDirectorySearch, { type ReferralTarget, } from "../../../../components/ReferralDirectorySearch/ReferralDirectorySearch"; import { useReferralWorkbenchService } from "../../../../services/ReferralWorkbenchService"; import { usePatientServiceContext } from "../../../../services"; import { relayServerFailure } from "../../../../utils/fetchFailureMessage"; import DiagnosisPicker from "../../../../components/DiagnosisPicker/DiagnosisPicker"; import type { IValueSet } from "../../../../services"; import { contactFromDirectoryLocation } from "../../../../utils/patientReferrals"; /** * - WHERE A PRACTITIONER PLACES A REFERRAL. The first step of the outbound half, and the one * thing nothing in the product could do: the send paths, the queue and the directory all existed, and * **no screen anywhere POSTed a `ServiceRequest`.** * * IT IS A PRACTITIONER SURFACE AND MUST STAY ONE. `POST /R4/ServiceRequest` allows `[PRACTITIONERS]` * alone while the queue routes beside it also allow Admins and Receptionists - *"the practitioner places * the order; staff executes. Two actions, two people."* Mounting this in the coordinator workspace * would 403 for that workspace's only audience. * * WHY THE CHART RATHER THAN THE ENCOUNTER, traced rather than assumed (the card asked for exactly * this): a practitioner refers **from a visit and between visits**, and only the chart covers both. The * encounter surface has no ordering area at all - it is notes, the scribe and the visit lifecycle - so * building here reuses the section where the other request-family resources already live (Care Plans, * Nutrition Orders, Vision Prescriptions) instead of inventing an orders area inside a visit. */ export type PlaceReferralDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; patientId: string; patientName?: string; /** Carries the id the server minted, so the chart can read that one record rather than search for it. */ onPlaced: (placedOrderId?: string) => void; }; export const PlaceReferralDialog = ({ open, onOpenChange, patientId, patientName, onPlaced, }: PlaceReferralDialogProps) => { const { placeReferral } = useReferralWorkbenchService(); // STEP 4 - the visit this referral came out of. `encounterState` is set in exactly two // places: entering a visit from the clinician queue, and the chart mounted INSIDE an encounter route. const { encounterState } = usePatientServiceContext(); /** * The chosen destination, or `null` while the practitioner is still searching. * * TWO STEPS RATHER THAN ONE FORM, because the two questions are answered from different places: who * should see this patient comes out of a registry search, and why comes out of the practitioner's head. * Showing an empty reason box beside a search that has not been run yet asks both at once and gets a * worse answer to each. */ const [target, setTarget] = useState(null); /** Set when the registry could not answer and the practitioner typed the practice themselves. */ const [manualName, setManualName] = useState(null); const [reason, setReason] = useState(""); const [diagnosis, setDiagnosis] = useState(null); const [note, setNote] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const reset = () => { setTarget(null); setManualName(null); setReason(""); setDiagnosis(null); setNote(""); setBusy(false); setError(null); }; const close = (next: boolean) => { if (!next) reset(); onOpenChange(next); }; /** * Who the referral is addressed to, as one string. * * THE PRACTITIONER'S NAME IS APPENDED, NOT SUBSTITUTED. A referral addressed to a named person is * still addressed to their practice - the receiving org triages internally and the named clinician may * be away. `ReferralDirectorySearch` already treats "refer to this organization" as a first-class * ending rather than a failed practitioner search, and this preserves that. */ const destination = (): string | undefined => { if (manualName?.trim()) return manualName.trim(); if (!target) return undefined; return target.practitioner ? `${target.practitioner.name} — ${target.organization.name}` : target.organization.name; }; /** * The ORGANISATION's NPI, even when a practitioner was chosen. * * The shared referral-contact book is keyed on the organisation deliberately - "so three practices * referring to one health system do not create three records". What it holds is how to REACH a place: * the fax number, the preferred method, which desk referrals go to. That is a property of the * organisation, not of the person named on the letter. * * UNDEFINED FOR A HAND-TYPED RECIPIENT, and that path must never break. The manual name exists for * when the registry cannot answer, so there is no NPI to have. */ const destinationNpi = (): string | undefined => { if (manualName?.trim()) return undefined; return target?.organization.npi; }; const place = async () => { const toDisplay = destination(); if (!toDisplay || busy) return; setBusy(true); setError(undefined as unknown as null); try { // THE RETURNED ID IS THE POINT, AND IT USED TO BE DISCARDED. The server mints it and the // chart's list is a SEARCH, which cannot see the new order for minutes. Handing the id back // lets the chart read that one record directly instead of asking a question with no answer yet. const placed = await placeReferral({ patientId, toDisplay, toNpi: destinationNpi(), toContact: manualName?.trim() ? undefined : contactFromDirectoryLocation(target?.location ?? target?.practitioner?.location ?? target?.organization.location), // The one link that cannot be recovered later: *"ordered at this visit"* is what puts a // referral into the visit container that produced it, and a referral placed on the DAY of a // visit is not evidence it came out of one. // Undefined here is CORRECT and ordinary - a referral placed from a chart opened directly did // not come out of a visit, and stamping one would be a lie the container would then repeat. encounterId: encounterState?.id, diagnosis: diagnosis ?? undefined, reason: reason.trim() || undefined, note: note.trim() || undefined, }); reset(); onOpenChange(false); onPlaced(placed?.id); } catch (failure: unknown) { // Never closes on failure: a practitioner who saw it vanish would believe the referral was // placed, and nobody downstream would ever chase a referral that does not exist. setError( relayServerFailure(failure, "That referral could not be placed. Nothing was sent."), ); } finally { setBusy(false); } }; const chosen = destination(); return ( {/* A directory search returns as many organizations as the registry has, so this dialog has to bound its own height. Without it the results list grew past the viewport and the footer landed over the FIRST result - the one a practitioner reaches for - while every other row stayed clickable, so the search read as broken rather than as needing a scroll. Capping the dialog was not enough on its own, and the reason is the grid. DialogContent lays its header, body and footer out as three auto rows; give the container a max-height and the grid compresses the middle row BELOW its content rather than letting the row scroll, so the body kept its full-height children inside a short box and they painted straight over the footer. Measured: the body ended at 407 and its last row ran 407 to 494, with the footer at 423 to 459 and the hit test landing on the footer. So the BODY scrolls, not the dialog. min-h-0 is the half that is easy to miss - a grid item will not shrink below its content without it, which is what let the row escape in the first place. Header and footer then stay put, which is also what a long result list wants. */} Refer this patient {patientName ? `Find the practice or specialist ${patientName} should see.` : "Find the practice or specialist this patient should see."}
{!chosen ? ( /* THE CONTROL THAT TRAVELS FROM AND MUST SURVIVE THIS MOUNT: the directory badges organizations already on Akashic and must NEVER be REORDERED by it. Ranking specialists by our own adoption is steering a clinical referral on a commercial basis. Nothing here sorts, filters or re-groups what the component returns - it is rendered exactly as given. */ setManualName("")} // A practice missing from the registry is not a reason to refuse the referral. manualOnNoMatch /> ) : (

Referring to

{chosen}

{/* ⭐ Changing your mind is one click, not a cancel-and-start-again. A practitioner who picked the wrong branch of a group practice should not lose the reason they typed. */}
{/* THE CODE AND THE WORDS, IN THAT ORDER AND BOTH OPTIONAL. reasonCode is a coded field and always was; a referral carrying only free text left its coding empty, which is the one shape the record model refuses - free text sits beside a code, never instead of it. The coordinator's queue reads the coding when there are no words, which is what made a coded-only referral show a blank reason there. */}
{/* A SPAN AND NOT A LABEL, because the control below already carries its own aria-label - the picker is a search box plus a result list, and a label element bound to nothing is a promise to a screen reader that is not kept. */} Diagnosis (optional)