import { useRef, useState } from "react"; import { AlertCircle, 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 { useReferralWorkbenchService, type ReferralContactAttempt } from "../../../services/ReferralWorkbenchService"; import { ReferralContactHistory } from "./ReferralContactHistory"; import { stillOpen, type ReferralRequestScope } from "../../../utils/referralRequestScope"; import { relayServerFailure } from "../../../utils/fetchFailureMessage"; import { useReferralContactRecovery } from "../../../hooks/useReferralContactRecovery"; import { ReferralContactRecovery } from "./ReferralContactRecovery"; import { outcomeUnknown } from "../../../utils/saveOperation"; import "./referralDialog.css"; import type { ReferralQueueRow } from "../../../hooks/useReferralQueue"; import { REFERRAL_CLOSURE_REASON_CHOICES, REFERRAL_CLOSURE_REASON_LABELS, type ReferralClosureReason, } from "../../../utils/referralClosureReason"; /** * - THE ONE THING AN INBOUND REFERRAL NEEDS A COORDINATOR TO DO: reach the patient. * * WHY THE INBOUND ROW HAD NO ACTION AT ALL, and why the answer is NOT the send workbench. * ruled that workbench must never open on a referral somebody else sent - *"offering 'record a phone call' * on it would invite a coordinator to log the other practice's work as our own."* That reasoning is * right and is not reversed here. **What it withheld was the only action inbound had, and nothing * replaced it.** * * THE WORK IS DIFFERENT, SO THE CONTROL IS DIFFERENT. Outbound, a coordinator contacts a PRACTICE to * send a referral. Inbound, the referral is already here and the job is getting the patient through the * door - ringing them, leaving a message, ringing again. This records OUR practice contacting OUR * patient. No cross-organization write exists in it: the attempt lives on this practice's own Task, which * is the org boundary the contact record was designed around. * * LOGGING A CALL DOES NOT CLOSE THE REFERRAL, AND THE COPY SAYS SO OUT LOUD. Spencer ruled 2026-08-24 * that **booking the patient is what closes it** - the appointment is the evidence the coordinator did * their job, and a "mark it done" button beside it would be a hand-kept record of the same fact that * drifts from it. A coordinator who logs three calls and books nobody still has an open referral. * * ADDED THE OTHER EXIT, and it is NOT that button. Spencer ruled (b): a referral this practice * will never book is **closed with a reason the SENDING practice can read**, because *"they declined"* and * *"we sent it to the wrong place"* lead them to do different things for a patient who still needs care. * It lives in this dialog rather than behind a control of its own because a coordinator opens this row * asking one question - *what happened with this referral?* - and "I chased them" and "this is never going * to happen" are two answers to it, not two features. * Still a FIXED LIST, never free text: the reason crosses into another organization. */ export type LogPatientContactDialogProps = { /** The inbound queue row being worked, or `null` when the dialog is closed. */ row: ReferralQueueRow | null; onOpenChange: (open: boolean) => void; onRecorded: () => void; /** The opening this dialog belongs to: its requests are admitted by it, and nothing is shown once it retires. */ scope?: ReferralRequestScope; }; export const LogPatientContactDialog = ({ row, onOpenChange, onRecorded, scope, }: LogPatientContactDialogProps) => { const { readContactHistory, recordContact, closeReferral } = useReferralWorkbenchService(); const [recipient, setRecipient] = useState(""); const [spokeWith, setSpokeWith] = useState(""); const [outcome, setOutcome] = useState(""); const [busy, setBusy] = useState(false); const [saved, setSaved] = useState(false); const [error, setError] = useState(null); /** - the closure reason, chosen from a fixed list. `null` until the coordinator picks one. */ const [reason, setReason] = useState(null); /** Which outcome the confirmation is about, so it does not tell a coordinator the wrong thing. */ const [closed, setClosed] = useState(false); const recovery = useReferralContactRecovery(row, scope); const contactUnconfirmed = recovery.held; const [recordedContacts, setRecordedContacts] = useState<{ taskId: string; contacts?: ReferralContactAttempt[] }>(); const contacts = recovery.contacts ?? (recordedContacts && recordedContacts.taskId === row?.taskId ? recordedContacts.contacts : row?.contacts); const linkedSender = Boolean(row?.requesterOrganizationId?.trim() || /^Organization\/[^/?#\s]+$/.test(row?.requesterReference ?? "")); const bodyRef = useRef(null); // Scheduled and expired are terminal: the server refuses to close them, so the exit is not offered. const finished = row?.state === "scheduled" || row?.state === "expired"; const reset = () => { setRecipient(""); setSpokeWith(""); setOutcome(""); setBusy(false); setSaved(false); setError(null); setReason(null); setClosed(false); setRecordedContacts(undefined); }; const close = (open: boolean) => { if (!open) reset(); onOpenChange(open); }; const logCall = async () => { if (!row?.taskId || busy || contactUnconfirmed) return; setBusy(true); setError(null); try { const contacts = await recovery.record({ method: "phone", recipient: recipient.trim() || undefined, spokeWith: spokeWith.trim() || undefined, outcome: outcome.trim() || undefined, // A FULL ISO INSTANT WITH AN OFFSET. FHIR's dateTime grammar allows a naked local time and the // resource INVARIANT does not, so HealthLake 400s it - `toISOString` is always UTC-qualified //. occurredAt: new Date().toISOString(), }, scope, recordContact); if (!stillOpen(scope)) return; setRecordedContacts({ taskId: row.taskId, contacts }); setSaved(true); onRecorded(); } catch (failure: unknown) { // Never closes on failure: a coordinator who saw it vanish would believe the attempt was on the // record, and the next one would ring a patient who has already been rung three times today. if (!stillOpen(scope)) return; setError(outcomeUnknown(failure) ? null : relayServerFailure(failure, "That call could not be recorded. Please try again.")); } finally { setBusy(false); } }; /** * - close it, with the reason the sender will read. * * IT SENDS THE ORDER ID, NOT THE TASK ID, unlike the contact log above - and the difference is the * org boundary. A contact attempt is our own account of our own work; the state and its reason live on * the SHARED order because the sending practice reads the same fact we wrote. */ const closeIt = async () => { if (!row?.orderId || !reason) return; setBusy(true); setError(null); try { await closeReferral(row.orderId, reason, scope); if (!stillOpen(scope)) return; setClosed(true); setSaved(true); onRecorded(); } catch (failure: unknown) { // Never closes the dialog on failure, same as the contact path: a coordinator who saw it vanish // would believe the sending practice had been told, and nobody would chase it again. if (!stillOpen(scope)) return; setError(relayServerFailure(failure, "That referral could not be closed. Nothing was changed.")); } finally { setBusy(false); } }; return ( { if (contacts?.length) { event.preventDefault(); bodyRef.current?.querySelector('[data-contact-history-heading]')?.focus(); } }}> Record contact with the patient {row?.requesterDisplay ? `Referral from ${row.requesterDisplay}. Log what happened when you tried to reach the patient, so the next person knows.` : "Log what happened when you tried to reach the patient, so the next person knows."}
{ if (!row?.taskId) return; setRecordedContacts({ taskId: row.taskId, contacts }); setRecipient(""); setSpokeWith(""); setOutcome(""); setError(null); setSaved(false); onRecorded(); }} /> {saved ? ( {/* ⛔ SAYS WHAT IS STILL OUTSTANDING. A coordinator who reads "recorded" as "handled" stops chasing, and a referral nobody chases is a patient who never reaches the specialist — the exact failure this queue exists to prevent. */} {closed ? linkedSender ? 'Closed. The practice that sent this referral can see the reason. No notice was sent from here.' : 'Closed. Sender access to this reason is not confirmed. No notice was sent from here; contact the sender separately if needed.' : 'Recorded against this referral. It stays in the queue until the patient has an appointment.'} ) : (
setRecipient(event.target.value)} placeholder="(555) 555-0123" />
{/* ⭐ Not always the patient — a spouse, a parent, a care home. Naming who answered is what lets the next coordinator pick the call back up. */} setSpokeWith(event.target.value)} placeholder="The patient, or whoever answered" />