import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { EmptyState } from "../../../components/ui/empty-state"; import { DocumentReference } from "fhir/r4"; import { AppSpinner, FhirDocumentReferenceDisplay, FhirDocumentReference } from "../../../components"; import { formatFhirCodeableConcept } from "../../../utils/fhirCodeableConcept"; import { useFhirDocumentReferenceBroadcast, useFhirDocumentReferences, useFhirDocumentReferenceTargetedBroadcast, } from "../../../hooks"; import { Calendar, ChevronRight, FileText, Lock, Paperclip, Plus, } from "lucide-react"; import { Button } from "../../../components/ui/button"; import { ARCHIVE_ABANDONED, useArchive } from "../../../hooks/useArchive"; import { useLifetime } from "../../../hooks/useLifetime"; import { ArchiveConfirmModal } from "../../../components/ArchiveConfirmModal/ArchiveConfirmModal"; import { Card } from "../../../components/ui/card"; import { formatDateForDisplay, } from "../../../lib/utils"; import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "../../../components/ui/sheet"; import { FormProvider, useForm } from "react-hook-form"; import { DocumentReferenceModal } from "../../../modals"; import { useAuthServiceContext, useDocumentReferenceServiceContext, usePatientServiceContext } from "../../../services"; import { Organization, Practitioner } from "fhir/r4"; import { toast } from "sonner"; import { SAVED_WITHHELD_MESSAGE } from "../../../utils/documentListScope"; import { describeWriteFailure, isRetryableFailure } from "../../../utils/fetchFailureMessage"; import { Alert, AlertDescription } from "../../../components/ui/alert"; import { missingDocumentFile } from "../../../utils/documentNeedsFile"; import { isStaffExperience } from "../../../utils/mountContext"; import { DismissibleAlert } from "../../../components/ui/dismissible-alert"; import { useMyProxies } from "../../../hooks/useMyProxies"; import { describeProxyCannotAddToRecord, mayCompleteFormsAsProxy, proxyStanding } from "../../../utils/proxyFormAuthority"; import { practiceDocumentAddedBy, practiceDocumentAuthors } from "./practiceDocumentAuthor"; import { isArchived } from '../../../utils/archivedResource'; import { filingOwnerOf, useDocumentFiling, type FilingTarget } from '../../../services/DocumentFilingService'; import { useSaveScope } from '../../../utils/saveOperation'; import { OutstandingFilings } from './OutstandingFilings'; import { RecordPracticeNote, recordPracticeId, useRecordPracticeView, useRetireOnPracticeChange } from '../../../contexts/RecordPracticeContext'; // TWO READERS, ONE COMPONENT, AND THE COPY HAS TO KNOW WHICH IT IS TALKING TO. // // isRoot is the practice's own filing drawer at /dashboard/documents, reached from the staff rail: the // things an organization keeps that are neither a patient's record nor an agreement - a licence, an // insurance certificate, a signed policy, a supplier contract. WITHOUT it this is a PATIENT's // documents, at /dashboard/patient/documents in the portal and under /dashboard/patients/:id for staff // reading a chart, and the patient wording is correct there. // // The whole page was addressed to a patient, so an administrator opening their practice's drawer was // asked to upload "anything a practice has asked you for - insurance cards, referral letters". THE FIX // IS THE AUDIENCE RATHER THAN NEW WORDS: rewriting that sentence in place would have moved the defect // onto the patient instead of removing it, since one component serves both. // // The agreements are deliberately not named. The BAA and the three patient agreements live on the // Organization page, and this page must not imply it holds them. const DOCUMENTS_COPY = { practice: { subtitle: "Files this practice keeps on its own account.", emptyDescription: "Keep the practice's own paperwork here - licences, insurance certificates, signed policies and supplier contracts.", }, patient: { subtitle: "Your documents on file.", emptyDescription: "When your care team asks for a document, you can send it from that request in your tasks.", }, // Staff reading a chart are a third reader: the page is about the patient, not addressed to them. chart: { subtitle: "Documents on file for this patient.", emptyDescription: "Nothing is on file for this patient yet. Add a referral letter, an outside result or an insurance card they brought in.", }, } as const; // How often a helper's own access is read again while a document dialog stays open. const PROXY_RECHECK_MS = 20_000; // How long one read of it may take before it is given up, so a stalled answer cannot hold the next one off. const PROXY_READ_DEADLINE_MS = 10_000; const EDIT_WITHDRAWN = 'Your access changed, so this edit was not saved. You can still read it here; copy anything you need before closing.'; const ARCHIVE_WITHDRAWN = 'Your access changed, so this document was not archived.'; const ARCHIVE_UNCONFIRMED = 'Your access changed while this document was being archived. Check the list to see whether it was.'; type WriteAdmission = { kind: 'go' } | { kind: 'refused'; message: string } | { kind: 'stale' }; // What a failed Save says in the open sheet. Only a 4xx (refused or rejected) proves nothing was written; any other // failure, a 5xx with a body included, may follow a write that went through, so it says to check before saving again. type AddNotice = { kind: 'refused' | 'not-saved' | 'uncertain'; message: string }; const ADD_UNCERTAIN = "This document may have been saved, but that could not be confirmed. Check the documents list before saving it again."; const addNoticeFor = (err: unknown): AddNotice => { const status = (err as { status?: unknown } | null | undefined)?.status; if (typeof status !== 'number' || status < 400 || status >= 500) return { kind: 'uncertain', message: ADD_UNCERTAIN }; return { kind: isRetryableFailure(err) ? 'not-saved' : 'refused', message: describeWriteFailure(err, 'Not saved') }; }; const DocumentReferences = ({ isRoot = false }: { isRoot?: boolean }) => { const { loading, error, refused, documentReferencesList, reloadAfterWrite } = useFhirDocumentReferences({ isRoot }); // A save, review or archive that answers after this page was replaced, or the viewer changed, does nothing here. const lifetime = useLifetime(); // Each opening of the edit dialog is its own session; only that session's completion or close may act, and // only while this page is still shown to the same viewer. const editSessionRef = useRef<{ id: number; alive: () => boolean } | null>(null); const [editSessionId, setEditSessionId] = useState(0); const [selectedDocument, setSelectedDocument] = useState(null); const [showDisplayModal, setShowDisplayModal] = useState(false); // The record's practice context: its own documents under a named practice; the empty state speaks for the whole record. const practiceView = useRecordPracticeView(documentReferencesList, recordPracticeId); useRetireOnPracticeChange(() => { setShowDisplayModal(false); setSelectedDocument(null); }); const [editingDocument, setEditingDocument] = useState(null); const [openCreateSheet, setOpenCreateSheet] = useState(false); // The edit opening, and whether an archive confirmation, that were open when access was lowered under them. const [withdrawnEditId, setWithdrawnEditId] = useState(null); const [archiveNotice, setArchiveNotice] = useState(null); // Set once this opening's archive request has been handed to the service, so a change of access is never // described as having stopped it. const archiveSentRef = useRef(false); const formMethods = useForm(); const { createDocumentReference, transformToFhirDocumentReference, archiveDocumentReference, updateDocumentReference, filings } = useDocumentReferenceServiceContext(); /* * THE DOCUMENT IS CAPTURED WHEN DELETE IS PRESSED, not read when Archive is confirmed. * * The first version closed over `selectedDocument` and asserted it with `!`. Walked in the browser * and it threw: "Archive failed: TypeError: Cannot read properties of null (reading 'id')". By the * time the confirm's own button is pressed, the details dialog behind it has given up its selection, * so the archive read a null the `!` had promised could not happen. * * A NON-NULL ASSERTION IS A CLAIM ABOUT A FUTURE MOMENT, and this one was made about a value owned by * a different dialog's lifecycle. Holding the document in its own state makes the claim true by * construction: nothing between the press and the confirm can take it away. * * It throws rather than asserting when there is nothing to archive, so a future rewiring fails with a * sentence instead of a TypeError, and `useArchive` renders it where the person can see it. */ const [documentPendingArchive, setDocumentPendingArchive] = useState(null); const proxyCannotAddRef = useRef(false); // Set below, once the helper's own list is in hand; read through a ref by the writes declared before it. const admitWriteRef = useRef<(stillOpen: () => boolean) => Promise>(async () => ({ kind: 'go' })); // Each opening of the archive confirmation is its own; one asked in an earlier opening sends nothing. const archiveOpeningRef = useRef(0); const archive = useArchive(async (reason, note) => { if (!documentPendingArchive) throw new Error('There is no document waiting to be archived.'); if (proxyCannotAddRef.current) throw new Error('Your access no longer lets you archive this document.'); const opening = archiveOpeningRef.current; const admission = await admitWriteRef.current(() => archiveOpeningRef.current === opening); if (admission.kind === 'stale') return ARCHIVE_ABANDONED; if (admission.kind === 'refused') throw new Error(admission.message); if (proxyCannotAddRef.current) return ARCHIVE_ABANDONED; archiveSentRef.current = true; return archiveDocumentReference(documentPendingArchive, reason, note); }); const { patientState } = usePatientServiceContext(); const { wpOrganization, actingAs, currentUser, userRole, USER_ROLES } = useAuthServiceContext(); const readerIsPatientSide = userRole === USER_ROLES.PATIENT || userRole === USER_ROLES.RELATED_PERSON; const copy = isRoot ? DOCUMENTS_COPY.practice : readerIsPatientSide ? DOCUMENTS_COPY.patient : DOCUMENTS_COPY.chart; // Below the forms tier a helper cannot upload. The tier is known before the press, so the page says so // first, as the task, allergy and medication pages do. const { proxies, listAnswered, refetch: refetchProxies, confirmProxies } = useMyProxies(); const proxyTier = actingAs ? proxyStanding(proxies, actingAs.relatedPersonId, listAnswered) : undefined; const proxyRefused = !isRoot && !!actingAs && !mayCompleteFormsAsProxy(proxyTier); // Whom this page should be showing: the person acted for, or a patient viewing themselves. While the record on // screen is still somebody else's (an exit or a switch still loading), nothing is offered over it: an ended // session must not hand its subject's documents to the viewer's own rights. const expectedPatientId = isRoot ? undefined : actingAs ? actingAs.subjectPatientId : currentUser?.fhir?.resourceType === 'Patient' ? currentUser.fhir.id : undefined; const contextSettling = !!expectedPatientId && !!patientState?.id && patientState.id !== expectedPatientId; const proxyCannotAdd = proxyRefused || contextSettling; proxyCannotAddRef.current = proxyCannotAdd; // Lowered to a tier that still reads this record: an open edit stays readable, says it was not saved and offers // nothing writable. A revoke, or the record on screen changing hands, retires it instead. const demoted = proxyRefused && proxyTier !== 'none' && !contextSettling; // A patient or helper sends a document only in answer to a practice's request, so their record has no Add. const noStandingAdd = !isRoot && readerIsPatientSide; const cannotAdd = proxyCannotAdd || noStandingAdd; // A helper's tier can be lowered from another session with nothing telling this page, so while acting for // somebody their own list is read again (on return, and while a document is open) and confirmed before a write. const actingForSomebody = !isRoot && !!actingAs; // Which context a write was asked in: viewer, practice and acted-as link. Moves on any change, A-B-A included. const scopeKey = `${currentUser?.fhir?.id ?? ''}|${wpOrganization.current?.id ?? ''}|${actingAs?.relatedPersonId ?? ''}|${actingAs?.subjectPatientId ?? ''}`; const scope = useRef({ key: scopeKey, gen: 0 }); if (scope.current.key !== scopeKey) scope.current = { key: scopeKey, gen: scope.current.gen + 1 }; admitWriteRef.current = async (stillOpen) => { const gen = scope.current.gen; const alive = lifetime(); const current = () => alive() && stillOpen() && scope.current.gen === gen; if (!actingForSomebody || !actingAs) return current() ? { kind: 'go' } : { kind: 'stale' }; const subject = actingAs.relatedPersonId; const controller = new AbortController(); const deadline = window.setTimeout(() => controller.abort(), PROXY_READ_DEADLINE_MS); const answer = await confirmProxies(controller.signal, PROXY_READ_DEADLINE_MS).finally(() => window.clearTimeout(deadline)); if (!current()) return { kind: 'stale' }; if (answer.status !== 'confirmed') { return { kind: 'refused', message: "We couldn't confirm your access just now, so nothing was saved. Try again." }; } const row = answer.proxies.find((proxy) => proxy.relatedPersonId === subject); // No row is no grant at all; a row without a tier keeps the forms rule's fail-open contract. if (!row || !mayCompleteFormsAsProxy(row.careTier)) { return { kind: 'refused', message: 'Your access no longer lets you change this document.' }; } return { kind: 'go' }; }; // One automatic read at a time, bounded and aborted at its deadline; a request while one runs is held as a // single follow-up. Nothing is read while the page is hidden. const autoRead = useRef<{ controller?: AbortController; again: boolean }>({ again: false }); const stopAutoRead = useCallback(() => { autoRead.current.controller?.abort(); autoRead.current = { again: false }; }, []); const runAutoReadRef = useRef<() => void>(() => undefined); runAutoReadRef.current = () => { if (!actingForSomebody || document.visibilityState === 'hidden') return; const state = autoRead.current; if (state.controller) { state.again = true; return; } const controller = new AbortController(); state.controller = controller; const deadline = window.setTimeout(() => controller.abort(), PROXY_READ_DEADLINE_MS); void refetchProxies(controller.signal, PROXY_READ_DEADLINE_MS).finally(() => { window.clearTimeout(deadline); if (autoRead.current.controller !== controller) return; const again = autoRead.current.again; autoRead.current = { again: false }; if (again) runAutoReadRef.current(); }); }; useEffect(() => { if (!actingForSomebody) return undefined; const onReturn = () => { if (document.visibilityState === 'hidden') stopAutoRead(); else runAutoReadRef.current(); }; document.addEventListener('visibilitychange', onReturn); window.addEventListener('focus', onReturn); return () => { document.removeEventListener('visibilitychange', onReturn); window.removeEventListener('focus', onReturn); stopAutoRead(); }; }, [actingForSomebody, scopeKey, stopAutoRead]); const documentDialogOpen = showDisplayModal || editingDocument !== null || archive.isOpen; useEffect(() => { if (!actingForSomebody || !documentDialogOpen) return undefined; runAutoReadRef.current(); const timer = window.setInterval(() => runAutoReadRef.current(), PROXY_RECHECK_MS); return () => { window.clearInterval(timer); stopAutoRead(); }; }, [actingForSomebody, documentDialogOpen, scopeKey, stopAutoRead]); useEffect(() => { archiveOpeningRef.current += 1; archiveSentRef.current = false; }, [archive.isOpen, documentPendingArchive]); const addOpeningRef = useRef(0); // Staff file through one filing per Add opening: the practice's own document in the drawer, else the patient's. const [addFilingOpening, setAddFilingOpening] = useState(0); const addFilingTarget = useMemo((): FilingTarget | undefined => (readerIsPatientSide ? undefined : isRoot ? { kind: 'create', purpose: 'practice-document' } : patientState?.id ? { kind: 'create', purpose: 'document', patientId: patientState.id } : undefined), [readerIsPatientSide, isRoot, patientState?.id]); const addFiling = useDocumentFiling(addFilingTarget, openCreateSheet ? addFilingOpening : -1, filings, filingOwnerOf({ currentUser, wpOrganization })); const addOwner = filingOwnerOf({ currentUser, wpOrganization }); const addSaves = useSaveScope(`${addOwner.viewer}|${addOwner.practice}|${scopeKey}|${openCreateSheet}|${addFilingOpening}`); const createSheetOpenRef = useRef(openCreateSheet); createSheetOpenRef.current = openCreateSheet; // The last Save's outcome in this opening, until the next try or close; set only through `sameAdd()`. const [addNotice, setAddNotice] = useState(null); useEffect(() => { addOpeningRef.current += 1; setAddNotice(null); }, [openCreateSheet]); // Losing the tier (or the context settling elsewhere) while a dialog or the Add sheet is open closes it, so it // never offers a write the server refuses. const closeArchive = archive.close; const archiveOpenRef = useRef(archive.isOpen); archiveOpenRef.current = archive.isOpen; // A change of viewer, practice or acted-as link (an exit included) retires everything opened in the old one: // the document on screen, its edit, its archive confirmation and submit, and the Add sheet. Nothing of the // former person's survives to be offered again once the new context's own record is on screen. const openedScopeGen = useRef(scope.current.gen); useEffect(() => { if (openedScopeGen.current === scope.current.gen) return; openedScopeGen.current = scope.current.gen; editSessionRef.current = null; setEditingDocument(null); setShowDisplayModal(false); setSelectedDocument(null); setOpenCreateSheet(false); setDocumentPendingArchive(null); setWithdrawnEditId(null); setArchiveNotice(null); closeArchive(); }, [scopeKey, closeArchive]); useEffect(() => { if (!proxyCannotAdd) return; if (demoted) { if (editSessionRef.current) setWithdrawnEditId(editSessionRef.current.id); setOpenCreateSheet(false); // An archive already sent is left to answer; its outcome is not known here, so nothing is claimed about it. if (archiveOpenRef.current && archiveSentRef.current) { setArchiveNotice(ARCHIVE_UNCONFIRMED); return; } if (archiveOpenRef.current) setArchiveNotice(ARCHIVE_WITHDRAWN); setDocumentPendingArchive(null); closeArchive(); return; } editSessionRef.current = null; setWithdrawnEditId(null); setArchiveNotice(null); setOpenCreateSheet(false); setEditingDocument(null); setDocumentPendingArchive(null); closeArchive(); }, [proxyCannotAdd, demoted, closeArchive]); useFhirDocumentReferenceBroadcast({ isRoot }); useFhirDocumentReferenceTargetedBroadcast({ documentReference: selectedDocument || undefined, isOpen: showDisplayModal, onUpdate: (updated) => setSelectedDocument(updated), onDelete: () => { setShowDisplayModal(false); setSelectedDocument(null); }, }); const viewDocument = (doc: DocumentReference) => { setSelectedDocument(doc); setShowDisplayModal(true); }; // The sheet keeps one form for the page, so every Add starts from an empty one however the last closed. const addDocument = () => { if (noStandingAdd) return; formMethods.reset({}); setAddFilingOpening((opening) => opening + 1); setOpenCreateSheet(true); }; // Helper functions const getStatusDisplay = (doc: DocumentReference): { text: string; color?: string } => { if (!doc.status) return { text: "Unknown", color: "#757575" }; switch (doc.status) { case "current": return { text: "Current", color: "#00a32a" }; case "superseded": return { text: "Superseded", color: "#e39700" }; case "entered-in-error": return { text: "Error", color: "#cc1818" }; default: return { text: doc.status, color: "#757575" }; } }; const getDocumentCategory = (doc: DocumentReference): string => { if (!doc.category?.length) return ""; return formatFhirCodeableConcept(doc.category); }; const getAuthorInfo = (doc: DocumentReference): string => { if (!doc.author?.length) return ""; const names = doc.author .map((a) => a.display || a.reference || "") .filter(Boolean) .slice(0, 2); if (names.length === 0) return ""; const result = names.join(", "); return doc.author.length > 2 ? `${result} +${doc.author.length - 2} more` : result; }; const getContentCount = (doc: DocumentReference): number => doc.content?.length || 0; const hasSecurityLabels = (doc: DocumentReference): boolean => !!doc.securityLabel?.length; return (
{/* Header */}

Documents

{/* Card 266 (B32) — mid-sentence capital. The same string renders on Admin and on the patient portal, so one correction covers both. */}

{copy.subtitle}

{!refused && ( <> {!cannotAdd && ( // The practice's drawer offers Add as the deck's quiet outlined action; the chart keeps the filled one. )} )}
{!refused && addFilingTarget && { void reloadAfterWrite(); }} />} {archiveNotice && ( setArchiveNotice(null)}> {archiveNotice} )} {proxyRefused && ( {describeProxyCannotAddToRecord(actingAs?.subjectName)} )} {/* Create Document Sheet */} Document Details
{addNotice && ( {addNotice.message} )}
{ if (cannotAdd) return; const opening = addOpeningRef.current; const gen = scope.current.gen; const alive = lifetime(); const operation = addSaves.start(); // One guard from the press to the last thing this save does: a reply for an earlier opening or // context closes, resets and says nothing in the one now on screen. const sameAdd = () => alive() && operation.belongs() && addOpeningRef.current === opening && createSheetOpenRef.current && scope.current.gen === gen; setAddNotice(null); const admission = await admitWriteRef.current(() => addOpeningRef.current === opening && createSheetOpenRef.current); if (admission.kind === 'stale') return; if (admission.kind === 'refused') { toast.error(admission.message); if (sameAdd()) setAddNotice({ kind: 'refused', message: admission.message }); return; } if (proxyCannotAddRef.current) return; const missingFile = missingDocumentFile(data); if (missingFile) { toast.error(missingFile); return; } try { // The practice drawer never files against a patient, even one selected earlier, and its // plain form has no status or type: a practice document is current and named by its title. const transformed = transformToFhirDocumentReference(data, isRoot ? undefined : patientState, wpOrganization.current as Organization); const fhirDoc: DocumentReference = isRoot ? { ...transformed, status: "current", type: { text: data.title }, author: practiceDocumentAuthors(currentUser?.fhir as Practitioner | undefined, wpOrganization.current as Organization) } : transformed; const created = await operation.step(() => createDocumentReference(fhirDoc, { filingId: addFiling.filingId(), transport: operation.transport }), true); if (!sameAdd()) return; formMethods.reset(); setOpenCreateSheet(false); if (!created) { toast.success(SAVED_WITHHELD_MESSAGE); return; } // The list loaded before the save would answer from cache; wait for this document instead. await reloadAfterWrite(created?.id); } catch (err) { // A refusal is relayed in the server's words, so the sheet never stays open saying // nothing; only a gateway's own wording is replaced. console.error('Error creating document reference', err); if (!sameAdd()) return; toast.error(describeWriteFailure(err, 'Unable to save the document')); setAddNotice(addNoticeFor(err)); } })}>
{/* Main Content */} {error ? (
{/* A refusal names itself and offers no retry. */}

{refused ? 'Documents are not available to you' : 'Unable to load Documents'}

{error}

{!refused && ( )}
) : !loading && documentReferencesList.length === 0 ? ( /* THE SHARED EmptyState, not a hand-rolled one. Its opening line is the rule this now follows: an empty screen is an INVITATION TO ACT, not a status report. "No documents found" was the sentence a failed SEARCH returns, and "there are no documents currently available to display" describes our renderer rather than the patient's situation. Neither is what somebody who simply has not uploaded anything yet needs to read. */ Add a document ) } data-testid="documents-empty" /> ) : loading ? ( ) : (
{practiceView.shown.map((doc: DocumentReference) => { getStatusDisplay(doc); const docCategory = getDocumentCategory(doc); const authorInfo = getAuthorInfo(doc); const contentCount = getContentCount(doc); const hasSecurity = hasSecurityLabels(doc); const displayTitle = isRoot ? doc.type?.text || doc.description || "Untitled" : doc.description || doc.type?.text || "Untitled"; const dateLabel = formatDateForDisplay(doc.date || doc.meta?.lastUpdated || ""); // The practice's row, as the deck draws it: the title over one line saying what the file // is and who added it, with Open beside it. No status pill: a practice document is current. if (isRoot) { const file = doc.content?.[0]?.attachment; const fileKind = file?.contentType === "application/pdf" ? "PDF" : file?.contentType?.split("/")[1]?.toUpperCase(); const fileSize = file?.size ? file.size >= 1024 * 1024 ? `${(file.size / (1024 * 1024)).toFixed(1)} MB` : `${Math.max(1, Math.round(file.size / 1024))} KB` : undefined; const addedAt = doc.date || doc.meta?.lastUpdated; const addedOn = addedAt ? new Date(addedAt).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }) : ""; const addedBy = practiceDocumentAddedBy(doc); const meta = [fileKind, fileSize, `added ${addedOn}${addedBy ? ` by ${addedBy}` : ""}`].filter(Boolean).join(" · "); // The deck's phone row drops "added by" and the Open button: the chevron is the action there. const addedShort = addedAt ? new Date(addedAt).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : ""; const phoneMeta = [fileKind, fileSize, addedShort].filter(Boolean).join(" · "); return ( viewDocument(doc)} data-testid="practice-document-row" >
); } return ( viewDocument(doc)} > {/* Inner row: content + chevron */}
{/* Left: all content rows */}
{/* Row 1: icon tile + title */}
{displayTitle} {doc.status && (() => { const s = doc.status; const cls = s === "current" ? "bg-emerald-100 text-emerald-700" : s === "superseded" ? "bg-yellow-100 text-yellow-700" : s === "entered-in-error" ? "bg-red-100 text-red-700" : "bg-gray-100 text-gray-600"; const label = s === "current" ? "Current" : s === "superseded" ? "Superseded" : s === "entered-in-error" ? "Error" : s; return ( {label} ); })()} {hasSecurity && }
{/* Row 2: meta */}
{dateLabel && ( {dateLabel} )} {docCategory && ( <>|{docCategory} )} {authorInfo && ( <>|By: {authorInfo} )}
{/* Row 3: attachment badge */} {contentCount > 0 && (
{contentCount} Attachment{contentCount !== 1 ? "s" : ""}
)}
{/* Right: chevron */}
); })} {isRoot && (

A document here is the practice's. It is never a patient's record, it is not shared with patients, and it is not an agreement — the BAA and the three patient agreements live on the Organization page.

)}
)}
{/* Document Details Modal */} {selectedDocument && (() => { const reviewed = selectedDocument.docStatus === 'final' || selectedDocument.docStatus === 'amended'; const reviewedForPatient = reviewed && !isStaffExperience(); return ( { setShowDisplayModal(false); setSelectedDocument(null); }} /* READ, THEN PLACE IT. A document uploaded by a patient arrives belonging to nothing, and until now there was no way to say what it was about: the reading dialog was read-only and the form that sets a context was reachable only while CREATING a document. This is the same form, on the document that is already here. */ onEdit={(!reviewedForPatient && !proxyCannotAdd && !isArchived(selectedDocument)) ? () => { const session = { id: editSessionId + 1, alive: lifetime() }; editSessionRef.current = session; setEditSessionId(session.id); setEditingDocument(selectedDocument); setShowDisplayModal(false); } : undefined} onDelete={(!reviewedForPatient && !proxyCannotAdd && !isArchived(selectedDocument)) ? () => { setDocumentPendingArchive(selectedDocument); archive.open(); } : undefined} reviewedForPatient={reviewedForPatient} forPatient={!isStaffExperience()} onReview={(isStaffExperience() && !reviewed && !isArchived(selectedDocument)) ? async () => { const alive = lifetime(); const gen = scope.current.gen; const updated = await updateDocumentReference({ ...selectedDocument, docStatus: 'final' }); if (!alive() || scope.current.gen !== gen) return; setSelectedDocument(updated ?? null); if (!updated) { setShowDisplayModal(false); toast.success(SAVED_WITHHELD_MESSAGE); return; } await reloadAfterWrite(updated?.id); } : undefined} /> ); })()} { if (!o) { archive.close(); setDocumentPendingArchive(null); } }} resourceLabel="Document" submitting={archive.submitting} error={archive.error} onConfirm={async (reason, note) => { /* GATED ON THE ARCHIVE HAVING SUCCEEDED, which is the hook's own instruction: closing the dialog on a failed archive would read exactly like a successful one and the error it sets could never render. */ if (proxyCannotAdd) return; const alive = lifetime(); const gen = scope.current.gen; const archived = await archive.submit(reason, note); if (!alive() || scope.current.gen !== gen) return; if (archived) { setArchiveNotice(null); setShowDisplayModal(false); setSelectedDocument(null); setDocumentPendingArchive(null); await reloadAfterWrite(); } else if (proxyCannotAddRef.current) { // Answered after access was lowered: the confirmation offers nothing more, and the notice stays neutral. archive.close(); setDocumentPendingArchive(null); } }} /> {editingDocument && (!proxyCannotAdd || demoted) && (() => { const session = editSessionRef.current; const admitted = () => session !== null && editSessionRef.current === session && session.alive(); return ( !proxyCannotAddRef.current} withdrawn={demoted || withdrawnEditId === editSessionId ? EDIT_WITHDRAWN : undefined} confirmWrite={async () => { const admission = await admitWriteRef.current(admitted); if (admission.kind === 'go') return undefined; return admission.kind === 'refused' ? admission.message : 'This view changed, so nothing was saved.'; }} title="Document Details" onClose={() => { if (!admitted()) return; editSessionRef.current = null; setWithdrawnEditId(null); setEditingDocument(null); setSelectedDocument(null); }} onSuccess={async () => { if (!admitted()) return; editSessionRef.current = null; setEditingDocument(null); setSelectedDocument(null); await reloadAfterWrite(); }} /> ); })()}
); }; export default DocumentReferences;