import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { AlertCircle, ExternalLink, FileText, Loader2 } from "lucide-react"; import { Alert, AlertDescription } from "../../../components/ui/alert.tsx"; import { Button } from "../../../components/ui/button.tsx"; import { CheckboxControl } from "../../../components/ui/checkbox-control.tsx"; import { Input } from "../../../components/ui/input.tsx"; import { Label } from "../../../components/ui/label.tsx"; import { useListOwner } from "../../../hooks/useListOwner"; import { useStaffPermissionServiceContext } from "../../../services"; import { newOperationId, recordsConflict, type RecordsRequest } from "../../../services/RecordsService.tsx"; import { HttpError } from "../../../services/HttpService.tsx"; import { HandOverRecordedWithoutLink, ReleaseCallUnanswered, STRUCTURED_FORMAT, useRecordsReleaseService, type RecordsCategory, type RecordsFaxAttempt, type RecordsReleased, type RecordsHandOverStatus, type RecordsManifestSummary, type RecordsManualReceipt, type RecordsPinnedScope, type ReleaseCallOwner, type ReleaseWrite, type AthenaAnswer, } from "../../../services/RecordsReleaseService.tsx"; import AthenaRecipientChooser from "./AthenaRecipientChooser.tsx"; import { formatDate, formatDateTime } from "../../../utils/dateUtils"; import { relayServerFailure } from "../../../utils/fetchFailureMessage"; import { NOT_OFFERED_NOTE, OFFERED, CATEGORY_LABEL, athenaWords, categoryLines, leftOutReasons } from "./releaseWords.ts"; import { releaseBasisRefusal } from "./recordsWords.ts"; import "./RecordsRelease.css"; interface Props { request: RecordsRequest; canStep: boolean; onChanged: (request: RecordsRequest) => void; /** How this version's release went, once its receipts are read: fax, download or print. */ onReleased?: (seen: { version: number; method?: string }) => void; } const RELEASED_STATES = new Set(["delivering", "delivery-unconfirmed", "delivered"]); /** Who a release went to, from its own record; a different recipient in today's details is said, never shown as it. */ const releasedWords = (released: RecordsReleased | undefined, now: RecordsRequest["recipient"]): string | undefined => { if (released?.channel !== "fax" && released?.channel !== "manual") return undefined; const how = released.channel === "fax" ? "fax to " + released.attempt.toFax : released.kind; if (released.target.proof === "not-recorded") return `Released by ${how}. Who it went to was not recorded with it.`; if (released.target.proof !== "proved") return undefined; const to = released.target.recipient.name; const differs = now && now.name !== to ? ` The request's details now name ${now.name}; this release went to ${to}.` : ""; return `Released to ${to} by ${how}.${differs}`; }; /** Why a release cannot be confirmed from here: its record is still being read, could not be read, or is held. */ const releaseHeldWords = (read: { released?: RecordsReleased; unreadable?: boolean } | undefined): string => { if (!read) return "Checking what was released."; if (read.unreadable || !read.released) return "What was released could not be read, so it cannot be confirmed from here. Close the request and open it again."; if (read.released.channel === "held") return "This release's record does not say one way it went, so it cannot be confirmed from here."; return "Who this release went to could not be read, so it cannot be confirmed from here. Close the request and open it again."; }; const BUILDABLE = new Set(["in-progress", "awaiting-approval"]); const SHOWN = new Set(["in-progress", "awaiting-approval", "approved", "delivering", "delivery-unconfirmed", "delivered"]); // A convenience only: the link is also shown, so a blocked window still leaves a way to the packet. const openLink = (url: string) => { window.open(url, "_blank", "noopener"); }; /** What a fax answer says about the attempt, never more than the server recorded. */ const faxWords = (receipt?: RecordsFaxAttempt, owed?: string): string | undefined => { if (receipt?.state === "submitted") return owed ? "The fax service took the packet; its answer was recorded late. That is not delivery." : undefined; if (receipt?.state === "failed") return "The fax was not sent."; if (receipt?.state === "unconfirmed") return "The fax service did not say whether it took the packet. Ask the recipient; it is never sent twice by itself."; if (owed || receipt?.state === "intent") return "It is not known yet whether the fax service took the packet. Ask the recipient before doing anything else; it is never sent twice by itself."; return undefined; }; // A link ready in this view is not the recipient having the records; that is only ever confirmed by a person. const LINK_READY_NOTE = "Link ready. That is not delivery: confirm below once you know the recipient has the records."; /** What a hand-over gave: the packet by its method, or the structured release, which only ever goes by download. */ const handedWhat = (receipt: RecordsManualReceipt): string => receipt.format === STRUCTURED_FORMAT ? "download of the structured release" : `${receipt.kind} hand-over`; /** A hand-over the server recorded with no link: when, what, and the server's reason, never "delivered". */ const noLinkWords = (receipt: RecordsManualReceipt, reason: string): string => `The ${handedWhat(receipt)} was recorded on ${formatDateTime(receipt.at)}, but no link was given: ${reason}`; const formatOf = (receipt?: RecordsManualReceipt): "pdf" | "fhir" => (receipt?.format === STRUCTURED_FORMAT ? "fhir" : "pdf"); /** Whether a person may attest delivery: a link ready here, or one the server read back as prepared or issued. */ const mayAttest = (read: { status?: RecordsHandOverStatus } | undefined, linkHere: boolean): boolean => linkHere || read?.status?.linkState === "prepared" || read?.status?.linkState === "issued"; /** What the read-back of the latest hand-over says, for a download or print waiting for confirmation. */ const handOverWords = (read: { status?: RecordsHandOverStatus; unreadable?: boolean } | undefined, linkHere: boolean): string => { if (linkHere) return LINK_READY_NOTE; if (!read) return "Checking what became of this hand-over's link."; if (read.unreadable || !read.status) return "What became of this hand-over's link could not be read. Close the request and open it again."; const { receipt, linkState, lastLink } = read.status; if (linkState === "unknown") { return `The ${handedWhat(receipt)} was recorded on ${formatDateTime(receipt.at)}, but what became of its link could not be fully read. ` + "It cannot be confirmed as delivered from here; get a fresh link or open the request again."; } if (linkState === "issued") { return "A link was issued for this hand-over. That is not delivery: confirm below once you know the recipient has the records."; } if (linkState === "prepared") { return `The ${handedWhat(receipt)} was recorded on ${formatDateTime(receipt.at)} and a link was prepared, but whether it reached anyone ` + "is not known. Confirm delivery only if you know the recipient has the records; otherwise get a fresh link."; } const why = linkState === "withheld" && lastLink?.message ? lastLink.message : "Its link was not made."; return `${noLinkWords(receipt, why)} It cannot be confirmed as delivered without evidence that a link went out.`; }; // The release half of an incoming request. What the packet holds, whether it may go and where, are all decided // by the server; this shows what it decided and offers only the step the request is at. Every call belongs to this // opening of this request: once the view closes or moves to another request, viewer or session, a late answer // changes nothing and opens nothing. /** What a pinned packet holds at this request version: its scope, only its chosen categories, a newer request, or unread. */ type PinRead = { key: string; of: unknown } & ( | { state: "read"; scope: RecordsPinnedScope } | { state: "access-changed"; categories: RecordsCategory[] } | { state: "newer" } | { state: "unread" } ); /** The pinned categories as a draft: the offered ones in their order, then any other the packet was built with. */ const draftFrom = (categories: RecordsCategory[]): RecordsCategory[] => [...OFFERED.filter((c) => categories.includes(c)), ...categories.filter((c) => !OFFERED.includes(c))]; const shortDigest = (digest: string) => digest.replace(/^sha256:/, "").slice(0, 12); const itemsWord = (n: number) => `${n} ${n === 1 ? "item" : "items"}`; const RecordsReleasePanel = ({ request, canStep, onChanged, onReleased }: Props) => { const { canManage } = useStaffPermissionServiceContext(); const { buildPacket, previewPacket, readPinnedScope, approveRelease, handOver, readHandOvers, linkForHandOver, sendFax, confirmDelivered, athenaSend, athenaReadBack, athenaAttest, } = useRecordsReleaseService(); // An athena send is read from its own attempt, never from the hand-over record or today's details. const athenaDispatch = request.dispatch?.channel === "athena-document" ? request.dispatch : undefined; const [athenaRead, setAthenaRead] = useState<{ attemptId: string; words: string }>(); const [choosing, setChoosing] = useState(false); // The latest recorded hand-over as the server reads it back, for a download or print once it is handed over. const [handOverRead, setHandOverRead] = useState<{ key: string; status?: RecordsHandOverStatus; released?: RecordsReleased; unreadable?: boolean }>(); // The hand-over this view itself recorded or read a link for: what a confirmation from this view is bound to. const [receiptOf, setReceiptOf] = useState<{ key: string; receipt: RecordsManualReceipt }>(); const [selected, setSelected] = useState(OFFERED); const [manifest, setManifest] = useState(); const [busy, setBusy] = useState(); const [error, setError] = useState(); const [notice, setNotice] = useState(); const [confirmation, setConfirmation] = useState(""); const [link, setLink] = useState<{ url: string; format: "pdf" | "fhir" }>(); const [relink, setRelink] = useState<{ write: ReleaseWrite; kind: "download" | "print"; format: "pdf" | "fhir" }>(); // The structured release reviewed in this view, and whether it is chosen for approval: each holds only for the opening, // version and packet it was made for, so a new one of any of them needs it reviewed and chosen again. const [structuredReview, setStructuredReview] = useState<{ key: string; sha256: string; of: unknown }>(); const [structuredChosen, setStructuredChosen] = useState(); const pending = useRef<{ key: string; operationId: string }>(); // The step whose answer was lost: its inputs stay as sent until a retry learns what happened to them. const lost = useRef(); const [frozenNote, setFrozenNote] = useState(); const { owner, live } = useListOwner(buildPacket); const opening = useRef(null); const openingKey = request.organizationId + "/" + request.requestId; useLayoutEffect(() => { const controller = new AbortController(); opening.current = controller; return () => { controller.abort(); if (opening.current === controller) opening.current = null; }; }, [openingKey, owner]); // Once a release went out, how it went is read from its own receipts, never from today's details. const handedOverState = request.direction === "incoming" && RELEASED_STATES.has(request.state); // Read again whenever the request is answered at a new version, so a reopened or second view says what is recorded. const handOverKey = openingKey + "@" + request.version; const handOverKeyNow = useRef(handOverKey); handOverKeyNow.current = handOverKey; useEffect(() => { if (!handedOverState || athenaDispatch) return; const mine = opening.current; if (!mine) return; const of = owner; const forKey = handOverKey; // An answer is taken only for the version still shown, so an older read finishing late never replaces a newer one. const alive = () => opening.current === mine && !mine.signal.aborted && live(of) && handOverKeyNow.current === forKey; const call: ReleaseCallOwner = { organizationId: request.organizationId, signal: mine.signal, admit: () => { if (!alive()) throw new DOMException("This release view was closed.", "AbortError"); }, }; readHandOvers(request.requestId, call) .then((answer) => { if (alive()) setHandOverRead({ key: forKey, status: answer.handOvers[0], released: answer.released }); }) .catch(() => { if (alive()) setHandOverRead({ key: forKey, unreadable: true }); }); }, [handOverKey, handedOverState, owner]); // eslint-disable-line react-hooks/exhaustive-deps const handOverNow = handOverRead?.key === handOverKey ? handOverRead : undefined; const released = handedOverState ? handOverNow?.released : undefined; const liveManual = request.recipient?.method === "download" || request.recipient?.method === "print"; const manual = handedOverState ? released?.channel === "manual" : liveManual; // A release is confirmed only once its record says one way it went and who it was for, or that nobody was recorded. const releaseKnown = !handedOverState || ((released?.channel === "fax" || released?.channel === "manual") && released.target.proof !== "unavailable"); const releasedMethod = released?.channel === "fax" ? "fax" : released?.channel === "manual" ? released.kind : undefined; useEffect(() => { if (handedOverState) onReleased?.({ version: request.version, method: releasedMethod }); }, [handedOverState, request.version, releasedMethod]); // eslint-disable-line react-hooks/exhaustive-deps const reviewKey = handOverKey + "#" + (request.manifest?.digest ?? ""); // The pinned packet this opening shows, read once per pin and request version; a draft is set from it once per // opening, or by the person, and is kept while a newer version is read again. const pin = request.manifest; const pinKey = pin ? openingKey + "@" + request.version + "#" + pin.manifestId + "#" + pin.digest : undefined; const pinKeyNow = useRef(pinKey); pinKeyNow.current = pinKey; const [pinRead, setPinRead] = useState(); const draftSettledIn = useRef(null); const settleDraft = (mine: AbortController | null, categories?: RecordsCategory[]) => { if (!mine || draftSettledIn.current === mine) return; draftSettledIn.current = mine; if (categories) setSelected(draftFrom(categories)); }; // A preview of the current pin says what it holds as surely as the scope read does. const settleFromPreview = (previewed: RecordsManifestSummary) => { if (previewed.digest === request.manifest?.digest) settleDraft(opening.current, previewed.categories.map((c) => c.category)); }; useEffect(() => { if (request.direction !== "incoming" || !pin || !pinKey) return; const mine = opening.current; if (!mine) return; const of = owner; const forKey = pinKey; const expected = { manifestId: pin.manifestId, digest: pin.digest, version: request.version }; const current = () => opening.current === mine && !mine.signal.aborted && live(of) && pinKeyNow.current === forKey; const call: ReleaseCallOwner = { organizationId: request.organizationId, signal: mine.signal, admit: () => { if (!current()) throw new DOMException("This release view was closed.", "AbortError"); }, }; const same = (named?: { manifestId?: string; digest?: string }) => named?.manifestId === expected.manifestId && named?.digest === expected.digest; // Only an answer about this request at exactly this version is taken; a newer one is said, never mixed in. const versionOf = (answered?: { requestId?: string; version?: number }): "same" | "newer" | "other" => answered?.requestId !== request.requestId || typeof answered.version !== "number" ? "other" : answered.version === expected.version ? "same" : answered.version > expected.version ? "newer" : "other"; readPinnedScope(request.requestId, call) .then((scope) => { if (!current()) return; const at = versionOf(scope?.request); if (!same(scope?.pin) || at !== "same") { setPinRead({ key: forKey, of, state: at === "newer" ? "newer" : "unread" }); return; } setPinRead({ key: forKey, of, state: "read", scope }); settleDraft(mine, scope.pin.categories.map((c) => c.category)); }) .catch((failure) => { if (!current()) return; const body = failure instanceof HttpError && failure.status === 409 ? failure.body as { code?: string; request?: { requestId?: string; version?: number }; pin?: { manifestId?: string; digest?: string; categories?: Array<{ category: RecordsCategory }> } } | undefined : undefined; if (body?.code === "access-changed" && versionOf(body.request) === "newer") { setPinRead({ key: forKey, of, state: "newer" }); return; } if (body?.code === "access-changed" && same(body.pin) && versionOf(body.request) === "same" && body.pin?.categories) { const categories = body.pin.categories.map((c) => c.category); setPinRead({ key: forKey, of, state: "access-changed", categories }); settleDraft(mine, categories); return; } setPinRead({ key: forKey, of, state: "unread" }); }); }, [pinKey, owner]); // eslint-disable-line react-hooks/exhaustive-deps const pinNow = pinRead && pinKey && pinRead.key === pinKey && pinRead.of === owner ? pinRead : undefined; const reviewKeyNow = useRef(reviewKey); reviewKeyNow.current = reviewKey; const reviewNow = structuredReview?.key === reviewKey && structuredReview.of === owner ? structuredReview : undefined; const chosenNow = Boolean(reviewNow) && structuredChosen === reviewKey; const receiptHere = receiptOf?.key === openingKey ? receiptOf.receipt : undefined; const setReceiptHere = (receipt: RecordsManualReceipt) => setReceiptOf({ key: openingKey, receipt }); if (request.direction !== "incoming" || !SHOWN.has(request.state)) return null; const mayWork = canManage("records"); const mayRelease = mayWork && canManage("records-release"); // A proposal may be built, but only a supported basis permits preview or approval. const cannotRelease = releaseBasisRefusal(request.basis); const basisNoteId = `records-basis-${request.requestId}`; // The build form carries the basis note when it is shown; otherwise the note sits beside Open packet. const buildFormShown = BUILDABLE.has(request.state) && mayWork; const missing = [!request.patient && "match the patient", !request.recipient && "verify the recipient", !request.basis && "record the basis"] .filter(Boolean) as string[]; /** This opening's call owner, and whether it is still the opening shown, asked at the moment of use. */ const begin = () => { const mine = opening.current; if (!mine) return undefined; const of = owner; const alive = () => opening.current === mine && !mine.signal.aborted && live(of); const call: ReleaseCallOwner = { organizationId: request.organizationId, signal: mine.signal, admit: () => { if (!alive()) throw new DOMException("This release view was closed.", "AbortError"); }, }; return { call, alive }; }; /** * Runs one write. A retry of the same step, at the same version and with the same inputs, reuses its operation id, * so it is applied once; changed inputs are a new operation, which the server refuses if the first one landed. * A frozen write is retried exactly as first sent. */ const step = async ( name: string, inputs: string, call: (write: ReleaseWrite, owner: ReleaseCallOwner) => Promise, frozen?: ReleaseWrite, onRecorded?: (outcome: HandOverRecordedWithoutLink, write: ReleaseWrite) => void, ) => { const run = begin(); if (!run) return undefined; const key = name + "|" + request.version + "|" + inputs; if (pending.current?.key !== key) pending.current = { key, operationId: newOperationId() }; const write = frozen ?? { operationId: pending.current.operationId, expectedVersion: request.version }; setBusy(name); setError(undefined); setNotice(undefined); lost.current = undefined; try { const answer = await call(write, run.call); if (!run.alive()) return undefined; pending.current = undefined; return { answer, write, alive: run.alive }; } catch (failure) { if (!run.alive()) return undefined; // The server recorded the hand-over before it refused the link: an outcome, never a step that did not happen. if (failure instanceof HandOverRecordedWithoutLink && onRecorded) { pending.current = undefined; onRecorded(failure, write); return undefined; } const conflict = recordsConflict(failure); if (conflict?.current) { pending.current = undefined; onChanged(conflict.current); } if (conflict?.code === "structured-changed" || conflict?.code === "no-structured-export") { setStructuredReview(undefined); setStructuredChosen(undefined); } if (conflict) setNotice(conflict.message); else { // Unanswered or lost: its write and inputs are kept, so a retry is the same operation, never a second one. lost.current = name; setError(failure instanceof ReleaseCallUnanswered ? failure.message : relayServerFailure(failure, "That step was not confirmed. Try again; it will not be applied twice.")); } return undefined; } finally { if (run.alive()) setBusy(undefined); } }; const build = async () => { const done = await step("build", selected.join(","), (write, call) => buildPacket(request.requestId, write, selected, call)); // What was just built is this opening's draft: a later read of the new pin does not replace it. if (done?.alive()) { settleDraft(opening.current); setManifest(done.answer.manifest); onChanged(done.answer.request); } }; const review = async () => { if (releaseBasisRefusal(request.basis)) return; const run = begin(); if (!run) return; setBusy("review"); setError(undefined); try { const answer = await previewPacket(request.requestId, run.call); if (!run.alive()) return; settleFromPreview(answer.manifest); setManifest(answer.manifest); setLink({ url: answer.url, format: "pdf" }); openLink(answer.url); } catch (failure) { if (!run.alive()) return; const conflict = recordsConflict(failure); if (conflict) setNotice(conflict.message); else setError(failure instanceof ReleaseCallUnanswered ? "The packet got no answer in time. Try opening it again." : relayServerFailure(failure, "The packet could not be opened.")); } finally { if (run.alive()) setBusy(undefined); } }; /** The structured release's own preview: its exact file, which is what an approval may then name. */ const reviewStructured = async () => { if (releaseBasisRefusal(request.basis)) return; const run = begin(); if (!run) return; const forKey = reviewKey; const of = owner; // An answer for an earlier version, packet or opening changes nothing here. const current = () => run.alive() && reviewKeyNow.current === forKey; setBusy("review-structured"); setError(undefined); setNotice(undefined); try { const answer = await previewPacket(request.requestId, run.call, "fhir"); if (!current()) return; settleFromPreview(answer.manifest); setManifest(answer.manifest); const structured = answer.manifest.structured; if (structured && "sha256" in structured) setStructuredReview({ key: forKey, sha256: structured.sha256, of }); setLink({ url: answer.url, format: "fhir" }); openLink(answer.url); } catch (failure) { if (!current()) return; const conflict = recordsConflict(failure); if (conflict) setNotice(conflict.message); else setError(failure instanceof ReleaseCallUnanswered ? "The structured release got no answer in time. Try opening it again." : relayServerFailure(failure, "The structured release could not be opened.")); } finally { if (run.alive()) setBusy(undefined); } }; const approve = async () => { if (releaseBasisRefusal(request.basis)) return; // The structured release goes only by the exact file reviewed in this opening; otherwise the packet alone. const sha256 = chosenNow ? reviewNow?.sha256 : undefined; const done = await step("approve", sha256 ?? "", (write, call) => approveRelease(request.requestId, write, call, sha256)); if (done?.alive()) onChanged(done.answer.request); }; /** The hand-over is recorded; its link is shown, or asked for again under the very write that recorded it. */ const handedOver = ( answer: { url?: string; linkOwed?: string; receipt?: RecordsManualReceipt }, write: ReleaseWrite, kind: "download" | "print", format: "pdf" | "fhir", ) => { if (answer.receipt) setReceiptHere(answer.receipt); if (answer.url) { setRelink(undefined); setLink({ url: answer.url, format }); openLink(answer.url); setNotice(LINK_READY_NOTE); } else { setRelink({ write, kind, format }); setNotice(answer.linkOwed ?? "The hand-over is recorded. Get the link again."); } }; /** Recorded, with no link given: the request as the server wrote it, and the link asked for again under the same write. */ const recordedNoLink = (outcome: HandOverRecordedWithoutLink, write: ReleaseWrite | undefined, kind: "download" | "print", format: "pdf" | "fhir") => { if (outcome.request) onChanged(outcome.request); setReceiptHere(outcome.receipt); setLink(undefined); setRelink(write ? { write, kind, format } : undefined); setNotice(noLinkWords(outcome.receipt, outcome.reason)); }; /** A fresh link for the hand-over the server reads back, after a reopen or in another view: decided again, never re-sent. */ const linkForRecorded = async (receiptId: string, kind: "download" | "print") => { const run = begin(); if (!run) return; setBusy("relink"); setError(undefined); setNotice(undefined); try { const answer = await linkForHandOver(request.requestId, receiptId, run.call); if (!run.alive()) return; setReceiptHere(answer.receipt); setLink({ url: answer.url, format: formatOf(answer.receipt) }); openLink(answer.url); setNotice(LINK_READY_NOTE); onChanged(answer.request); } catch (failure) { if (!run.alive()) return; if (failure instanceof HandOverRecordedWithoutLink) recordedNoLink(failure, undefined, kind, formatOf(failure.receipt)); else { const conflict = recordsConflict(failure); if (conflict) setNotice(conflict.message); else setError(failure instanceof ReleaseCallUnanswered ? "The link got no answer in time. Ask for it again." : relayServerFailure(failure, "The link could not be given.")); } } finally { if (run.alive()) setBusy(undefined); } }; /** An athena answer: its attempt's own words, and the request it carries, else the request read in the same step. */ const tookAthena = (answer: AthenaAnswer) => { if (answer.kind === "refused") { setNotice(answer.message); return; } if (answer.kind === "unanswered") { setError("athena's answer could not be read, so what happened is not known here. Read it back; nothing is sent again."); return; } if (answer.kind === "receipt") { const words = athenaWords(answer.receipt, answer.owed, answer.message); setAthenaRead({ attemptId: answer.receipt.attemptId, words }); setNotice(words); } const current = answer.request ?? (answer.kind === "receipt" ? answer.current : undefined); if (current) onChanged(current); else setError("The request could not be read again, so what is shown may be out of date. Close it and open it again."); }; // An answer athena gave that cannot be read is lost, so a retry is the same send and never a second one. const answered = (answer: AthenaAnswer) => { if (answer.kind === "unanswered") throw new ReleaseCallUnanswered(); return answer; }; /** Reads back the attempt the request names; it never sends. */ const readBack = async () => { const attemptId = athenaDispatch?.attemptId; const run = begin(); if (!attemptId || !run) return; setBusy("read-back"); setError(undefined); setNotice(undefined); try { const answer = await athenaReadBack(request.requestId, attemptId, run.call); if (run.alive()) tookAthena(answer); } catch { if (run.alive()) setError("Reading it back got no answer. Read it back again; nothing is sent."); } finally { if (run.alive()) setBusy(undefined); } }; /** Asks the server to record delivery as attested; whether the attempt can be attested is the server's to say. */ const attest = async () => { const attemptId = athenaDispatch?.attemptId; if (!attemptId) return; const note = frozenNote ?? confirmation.trim(); const done = await step("attest", note, async (write, call) => answered(await athenaAttest(request.requestId, write, note, attemptId, call))); if (lost.current === "attest") setFrozenNote(note); else setFrozenNote(undefined); if (done?.alive()) tookAthena(done.answer); }; /** Sends the approved packet by the recipient's method, or hands over the approved structured release by download. */ const deliver = async (format: "pdf" | "fhir" = "pdf") => { const method = request.recipient?.method; if (method === "athena-document") { const done = await step("send", "athena", async (write, call) => answered(await athenaSend(request.requestId, write, call))); if (done?.alive()) tookAthena(done.answer); } else if (method === "fax") { const done = await step("send", "fax", (write, call) => sendFax(request.requestId, write, call)); if (!done?.alive()) return; onChanged(done.answer.request); const words = faxWords(done.answer.receipt, done.answer.owed); if (words) setNotice(words); } else if (method === "download" || method === "print") { const as = method === "download" ? format : "pdf"; const done = await step(as === "fhir" ? "send-structured" : "send", method, (write, call) => handOver(request.requestId, write, method, call, as), undefined, (outcome, write) => recordedNoLink(outcome, write, method, as)); if (!done?.alive()) return; onChanged(done.answer.request); handedOver(done.answer, done.write, method, as); } }; const linkAgain = async () => { if (!relink) return; const { write: frozen, kind, format } = relink; const done = await step("relink", kind + "|" + format, (write, call) => handOver(request.requestId, write, kind, call, format), frozen, (outcome) => recordedNoLink(outcome, frozen, kind, format)); if (done?.alive()) { onChanged(done.answer.request); handedOver(done.answer, frozen, kind, format); } }; const delivered = async () => { const note = frozenNote ?? confirmation.trim(); // A manual hand-over is confirmed by its own receipt: the one handled in this view, else the one read back. const receiptId = manual ? (receiptHere?.receiptId ?? (released?.channel === "manual" ? released.receiptId : handOverNow?.status?.receipt.receiptId)) : undefined; const done = await step("delivered", note + "|" + (receiptId ?? ""), (write, call) => confirmDelivered(request.requestId, write, note, call, receiptId)); if (done?.alive()) onChanged(done.answer.request); if (lost.current === "delivered") setFrozenNote(note); else setFrozenNote(undefined); }; // A pinned packet is known once this opening read it, or built or previewed it here; until then nothing is offered on it. // A summary built or previewed here is shown until the request names another pin. const shownManifest = manifest && (!pin || manifest.digest === pin.digest) ? manifest : undefined; const builtHere = Boolean(pin && shownManifest); // The draft may be shown from a build or preview here; building and approving wait for this version's own read. const buildable = !pin || pinNow?.state === "read" || pinNow?.state === "access-changed"; const pinKnown = buildable || builtHere; const approvable = !pin || pinNow?.state === "read"; const pinScope = pinNow?.state === "read" ? { ...pinNow.scope.pin, version: pinNow.scope.request.version, covers: pinNow.scope.pin.categories.map((c) => ({ label: CATEGORY_LABEL[c.category] ?? c.category, included: c.included ?? 0, status: c.status })) } : builtHere && shownManifest ? { ...shownManifest, version: request.version, covers: categoryLines(shownManifest).map((l) => ({ label: l.label, included: l.included, status: l.status })) } : undefined; const lines = shownManifest ? categoryLines(shownManifest) : []; const leftOut = shownManifest ? leftOutReasons(shownManifest) : []; const sendLabel = request.recipient?.method === "athena-document" ? `Send to ${request.recipient.name} through athena` : request.recipient?.method === "fax" ? `Send fax to ${request.recipient.fax ?? "the recipient"}` : request.recipient?.method === "print" ? "Print packet" : "Download packet"; const spin = (name: string) => busy === name &&