import { useEffect, useRef, useState } from "react"; import { AlertCircle, CheckCircle2, 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 { Label } from "../../../components/ui/label"; import { useReferralWorkbenchService } from "../../../services/ReferralWorkbenchService"; import { stillOpen, type ReferralRequestScope } from "../../../utils/referralRequestScope"; import { relayServerFailure } from "../../../utils/fetchFailureMessage"; import "./referralDialog.css"; /** * TYPE IN A CLAIM CODE THE SENDING PRACTICE READ OUT. * * THE OTHER HALF OF A PROMISE THE PRODUCT WAS ALREADY MAKING. The sending screen has always said "give * the receiving practice this code, they enter it when they sign in to Akashic, and the referral opens on * their side" - and there was nowhere to enter it. Measured with a real code minted at one practice and a * signed-in coordinator at another: the only inbound control was for fax and phone arrivals, and the * redeem route had no caller anywhere in the client. * * -> So the failure landed on somebody at ANOTHER organization, following an instruction our screen gave * them, with no way to know it was never supported. * * THE REFUSALS ARE THE SERVER'S AND ARE SHOWN VERBATIM. Malformed, unknown, already spent, and the sender * claiming their own referral all answer with ONE sentence, deliberately - "that code is real but spent" * would confirm a live referral had been found, and the route would become a way to probe for them. An * EXPIRED code is the single distinction the server draws, because a coordinator who cannot tell expired * from mistyped retypes a dead code until they give up on the referral. * * -> This dialog therefore adds NO interpretation of its own. It relays what the server said. Inventing a * "wrong code" versus "already used" distinction here would undo the reasoning on the route. */ export type RedeemClaimCodeDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; /** * Re-read the queue rather than patch a row in: the server placed the referral and composed the notice. * Given the claimed referral's id, so the page can wait for that one to be listed. */ onRedeemed: (referralId?: string) => void; /** The opening this dialog belongs to: its requests are admitted by it, and nothing is shown once it retires. */ scope?: ReferralRequestScope; }; export const RedeemClaimCodeDialog = ({ open, onOpenChange, onRedeemed, scope, }: RedeemClaimCodeDialogProps) => { const { redeemClaimCode } = useReferralWorkbenchService(); const [code, setCode] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(undefined); const [outcome, setOutcome] = useState(undefined); // The page keys this dialog by practice, so a switch or leaving the page ends it. A redemption that // answers after that is still the server's committed claim, but its words and wait belong to a page // that is gone, so they are dropped. const alive = useRef(true); useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []); const reset = () => { setCode(""); setError(undefined); setOutcome(undefined); }; const close = (next: boolean) => { if (!next) reset(); onOpenChange(next); }; const submit = async () => { setBusy(true); setError(undefined); setOutcome(undefined); try { const result = await redeemClaimCode(code.trim(), scope); if (!alive.current || !stillOpen(scope)) return; /* * ALREADY CLAIMED IS A SUCCESS AND IS WORDED AS ONE. The referral is here; the coordinator either * typed the code twice or a colleague got there first. Telling them something went wrong would send * them back to the sending practice to ask for a code they do not need. */ /* * NONE OF THESE SAY THE REFERRAL IS ALREADY VISIBLE, and that is the correction. * * MEASURED: the queue row is written immediately - the POST lands in the same second the code is * redeemed - but HealthLake's SEARCH index took about two and a half minutes to make it findable, * and the queue is a search. So the coordinator was told "it is in your inbound queue", looked at * the queue, saw nothing, and reloaded. I did exactly that during the walk and concluded the * product was broken. * * A coordinator who reaches that conclusion rings the sending practice, or redeems the code again, * or gives up on a referral that is sitting there safely. -> The claim is what has to change, not * the write: say it is theirs, and that the list takes a moment to catch up. */ if (result.alreadyClaimed) { setOutcome("That referral is already yours — nothing further to do."); } else if (result.arrival?.delivered) { setOutcome( "Referral claimed and sent to the front desk. It appears in your inbound queue shortly —" + " the list can take a couple of minutes to catch up.", ); } else if (result.arrival?.destination === "referral-workspace") { // The coordinator queue is the delivery, so the server reports nothing further delivered. setOutcome( "Referral claimed for your referral coordinator. It appears in your inbound queue shortly —" + " the list can take a couple of minutes to catch up.", ); } else { /* * It arrived, but this practice has not said where referrals should go. Worth saying, because * the two states need different things from a human: one is done, the other means somebody * should look at the queue rather than wait to be told. */ setOutcome( "Referral claimed. It appears in your inbound queue shortly — the list can take a couple of" + " minutes to catch up. Nobody was notified — this practice has not set where incoming" + " referrals are sent.", ); } setCode(""); onRedeemed(result.referralId); } catch (failure: unknown) { if (!alive.current) return; // The server's sentence, verbatim - the dialog heading already says what was being attempted, so // there is no context to prefix and the server's words are the most useful thing on the screen. if (!stillOpen(scope)) return; setError(relayServerFailure(failure, "That code could not be redeemed. Please try again.")); } finally { setBusy(false); } }; return ( Enter a claim code {/* THE SAME CORRECTION AS THE OUTCOME MESSAGES BELOW, and it belongs here more than there: this is read BEFORE the code is entered, so it is what sets the expectation the outcome would otherwise have to walk back. It used to say the referral "opens on your side, in your inbound queue" - present tense, and the queue is a search that takes about two and a half minutes to catch up with the write. */} {/* CAPPED BECAUSE DIALOG CONTENT IS `width: max-content` (id-scoped, src/index.css:3511), so the dialog is exactly as wide as its widest single line. Uncapped, this sentence is the widest thing the dialog can hold and it drew the whole dialog at 1306px in a 1400px viewport - an 8-character code field stretched to 1256px. A max-w-* on DialogContent cannot fix it; the id-scoped rule wins on specificity, so the cap has to sit on the content. */} Enter the code from the referring practice to claim the referral. It becomes yours immediately and appears in your inbound queue within a couple of minutes.
setCode(event.target.value)} placeholder="XXXX-XXXX" autoComplete="off" spellCheck={false} maxLength={20} /> {/* The alphabet has no I, L, O or U in it, so the characters that get misheard on a telephone cannot occur. Saying so is what stops a coordinator "correcting" a zero into a letter O and then believing they were given a bad code. */} {/* Capped to the same 28rem for the same reason: with the description capped, this line becomes the widest one and takes over sizing the dialog. */}

Codes never contain the letters I, L, O or U — so 0 is always a zero and 1 is always a one.

{error && ( {error} )} {outcome && ( {outcome} )}
); }; export default RedeemClaimCodeDialog;