import React, { useEffect, useRef, useState } from 'react'; import { Attachment, DocumentReference, Reference } from 'fhir/r4'; import { usePhotoUploadService } from '../../services/PhotoUploadService'; import { HttpError } from '../../services/HttpService'; import { FhirCodeableConceptDisplay } from '../../utils/fhirCodeableConcept'; import { FhirCodeDisplay } from '../../utils/fhirCode'; import { FhirPeriodDisplay } from '../../utils/fhirPeriod'; import { FhirReferenceDisplay } from '../../utils/fhirReference'; import { formatDate } from '../../utils/dateUtils'; import { renderField } from '../../utils/renderField'; import { formatFileSize } from '../../utils/formatFileSize'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog'; import { DismissibleAlert } from '../ui/dismissible-alert'; import './FhirDocumentReferenceDisplay.css'; import { Card, CardHeader, CardContent } from '../ui/card'; import { Button } from '../ui/button'; import { FileText, ExternalLink } from 'lucide-react'; import { FieldHistoryProvider, FieldHistoryScope } from '../FieldHistory/FieldHistoryProvider'; import { ShowFieldHistoryToggle } from '../FieldHistory/ShowFieldHistoryToggle'; import { RecordEditedMarker } from '../FieldHistory/RecordEditedMarker'; import { FieldGroup, Section } from '../FieldHistory/FieldLayout'; import { patientConceptWords, patientFileType, patientRecordStatus, patientReferenceWords, patientReviewStatus } from './patientDocumentWords'; import { ArchivedNotice } from '../ArchivedNotice/ArchivedNotice'; import { isArchived } from '../../utils/archivedResource'; import { PRE_SCAN_FILE_WORDS, SCAN_HOLD_WORDS, scanHoldOf, type ScanHold } from '../../utils/uploadScanHold'; import { isSaveRetired, useSaveScope } from '../../utils/saveOperation'; import { viewerEpoch, writeThroughViewer } from '../../utils/writeThroughHold'; /** * AN ATTACHMENT HELD AS AN OBJECT KEY IS NOT A LINK, and this used to render it as one. * * MEASURED 2026-09-07 on a patient's insurance card photograph: the dialog printed * `users//personal/insurance_cards/.png` as an anchor, to a path that does not exist on * this host. There was no Open and no Download, because both of those are drawn only when the * attachment carries INLINE base64 - and a card photograph deliberately does not, so that every reader * gets the full-resolution original rather than a 500px thumbnail. So the one document in the record * that exists to be READ was the one nobody could open. * * An absolute URL is left exactly as it was: that is somebody else's document somewhere else, and * presigning a key we do not own is not a thing to attempt. A failure to presign says so rather than * offering a control that cannot work. */ // `carrier` names the record holding the file, which the server then reads by its id rather than waiting for a search. // Each link or notice belongs to the viewer, key and carrier it was answered for, in this mounted opening: once any of // them moves on, the old request is cancelled and nothing it answers is shown or opened. The viewer epoch is part of it, // so a viewer who leaves and comes back is a new owner, never the old one again. export const StoredAttachmentActions: React.FC<{ attachment: Attachment; carrier?: string }> = ({ attachment, carrier }) => { const { getPresignedDownloadUrl } = usePhotoUploadService(); const key = attachment.url; const ownerNow = () => [viewerEpoch(), writeThroughViewer() ?? '', key ?? '', carrier ?? ''].join('|'); const owner = ownerNow(); const onScreen = useRef(owner); onScreen.current = owner; const [answer, setAnswer] = useState<{ owner: string; href?: string; failed?: 'refused' | 'failed' | ScanHold }>(); // Drawn again on every retirement, so a viewer who changed with nothing yet answered is read afresh. const [, redraw] = useState(0); const lives = useSaveScope(owner, () => { setAnswer(undefined); redraw((n) => n + 1); }); const href = answer?.owner === owner ? answer.href : undefined; const failed = answer?.owner === owner ? answer.failed : undefined; useEffect(() => { if (!key) return; const life = lives.start(); getPresignedDownloadUrl(key, life.transport, carrier) .then((url) => { life.admit(); setAnswer({ owner, href: url }); }) // A REFUSAL IS NOT A FAILURE. "Try again later" on a 403 sends a reader back to the same // button for the rest of the afternoon; the file is not theirs to open and no amount of // waiting changes that. Measured reading a patient's insurance card from the staff portal. .catch((error: unknown) => { if (isSaveRetired(error) || !life.belongs()) return; const status = error instanceof HttpError ? error.status : undefined; setAnswer({ owner, failed: scanHoldOf(error) ?? (status === 403 || status === 404 ? 'refused' : 'failed') }); }); return () => life.retire(); // eslint-disable-next-line react-hooks/exhaustive-deps -- a new owner is a new read; the scope retires the old one }, [owner]); if (failed) { return (

{failed === 'refused' ? 'This file is not yours to open. Ask the practice that holds it.' : failed === 'failed' ? 'This file could not be opened. Try again later.' : SCAN_HOLD_WORDS[failed]}

); } // A link opens only while its owner is still the one on screen, checked again at the click itself. const open = () => { if (href && onScreen.current === owner && ownerNow() === owner) window.open(href, '_blank'); }; return ( ); }; interface FhirDocumentReferenceDisplayProps { documentReference: DocumentReference; isOpen: boolean; onClose: () => void; title?: string; /** * Offered as an Edit control beside the history toggle. OPTIONAL, so a surface that only reads * documents stays exactly as it was - and a caller that has nowhere to send an edit does not draw * a control that goes nowhere. */ onEdit?: () => void; /** * Offered as a Delete control beside Edit. OPTIONAL for the same reason `onEdit` is, and the * caller owns the confirm: this component reads a document, it does not decide what removing * one means. */ onDelete?: () => void; onReview?: () => void; /** The patient is reading a document their care team reviewed, which is why Edit and Delete are absent. */ reviewedForPatient?: boolean; /** A patient or their helper is reading, so coded fields are named in plain words. */ forPatient?: boolean; } // The records a document links to, as a patient reads them: one line each, in plain words, never an id. const PatientReferences = ({ references }: { references?: Reference[] }) => (
{(references ?? []).map((reference, index) => {patientReferenceWords(reference)})}
); export const FhirDocumentReferenceDisplay: React.FC = ({ documentReference, isOpen, onClose, title, onEdit, onDelete, onReview, reviewedForPatient, forPatient = false }) => { // An archived document is read-only: no Edit or Delete, here as on its edit page; the server refuses both. const archived = isArchived(documentReference); const renderDescriptionValue = () => { if (!documentReference.description) { return '-'; } return (
{documentReference.description}
); }; const renderContent = () => { if (!documentReference.content || documentReference.content.length === 0) { return (

No content recorded

); } return (
{documentReference.content.map((content, index) => ( {/* A patient reads "File"; staff keep the record's own structure. */}
{forPatient ? (documentReference.content.length > 1 ? `File ${index + 1}` : 'File') : `Content ${index + 1}`}
{!forPatient &&
Document Attachment
} {content.attachment ? ( {/* The name keeps a readable width; when the actions do not fit beside it they wrap below it. */}

{content.attachment.title || 'Untitled Document'}

{forPatient ? patientFileType(content.attachment.contentType) : content.attachment.contentType || 'Unknown type'} {content.attachment.size ? ` • ${formatFileSize(content.attachment.size)}` : null}

{content.attachment.creation && (

Created: {formatDate(content.attachment.creation)}

)}
{/* Only an uploaded file's key opens, through the scan-checked link. Bytes put inside the record, or a link to a file elsewhere, were never scanned and stay closed. */} {content.attachment.url && !/^[a-z][a-z0-9+.-]*:/i.test(content.attachment.url) ? (
) : (content.attachment.data || content.attachment.url) ? (

{PRE_SCAN_FILE_WORDS}

) : null}
) : (

No attachment

)}
Format
{forPatient // `format` is a Coding; a patient reads what was recorded, or that nothing was. ? renderField('Format', content.format?.display || content.format?.code || 'Format not specified') : renderField('Format', )}
))}
); }; const renderRelatesTo = () => { if (!documentReference.relatesTo || documentReference.relatesTo.length === 0) { return (

No related documents recorded

); } return (
{documentReference.relatesTo.map((relatesTo, index) => (
{forPatient ? (documentReference.relatesTo!.length > 1 ? `Related document ${index + 1}` : 'Related document') : `Related Document ${index + 1}`}
{renderField(forPatient ? 'How it relates' : 'Relationship Type', )} {forPatient ? renderField('Document', ) : renderField('Target of the relationship', )}
))}
); }; // No reader is shown the credentials placeholder for a coded field nobody filled in. const notRecorded = forPatient ? 'Not recorded' : '-'; // Callers keep this display mounted closed, often with no document selected yet. if (!isOpen) return null; // What a patient or helper reads about where a document came from: only what it records, in plain words. An // empty field is left out rather than named, since its technical name ("Main clinical acts documented", // "Kind of facility where patient was seen") is all it would say. Staff keep every field and its technical label. const context = documentReference.context; const hasRefs = (references?: unknown[]) => (references?.length ?? 0) > 0; const eventWords = patientConceptWords(context?.event); const facilityWords = patientConceptWords(context?.facilityType ? [context.facilityType] : undefined); const specialtyWords = patientConceptWords(context?.practiceSetting ? [context.practiceSetting] : undefined); const hasPeriod = Boolean(context?.period?.start || context?.period?.end); const hasAbout = hasRefs(documentReference.author) || hasRefs(context?.encounter) || hasRefs(context?.related) || Boolean(eventWords || facilityWords || specialtyWords) || hasPeriod; return ( { if (!next) onClose(); }} > {/* pr-12: the dialog's close control sits top-4 right-4 and is 44px square on the patient door, so a header running to the content edge put Delete under it and pressing Delete closed the dialog. flex-wrap lets the controls drop a line rather than push back into that space. */}
{title ?? (forPatient ? 'Document details' : 'Document Reference Details')}
{/* At phone width four controls outrun the dialog, so they wrap rather than run under its edge. */}
{/* WHERE A DOCUMENT GETS ITS CONTEXT. Everything this dialog shows was readable and nothing was changeable, so "Encounters: No references recorded" was a statement with no way to answer it - and the picker that sets one existed only on the dialog that CREATES a document, which is no use at all for a document a patient uploaded. */} {onEdit && !archived && ( )} {/* THE OTHER HALF OF "EDIT DETAILS OR DELETE". The route and the client method for removing a document have both existed for months and nothing anywhere called them, so a document uploaded to the wrong patient could be corrected in every particular except its existence. THE THREE REMOVE CONTROLS ON THE EDIT FORM ARE NOT THIS. They take away the file, a content block or the encounter link, and leave the document in the record - so a reader who found them and stopped looking emptied a document instead of removing it. Optional for the same reason `onEdit` is: a surface that only reads documents draws no control, rather than one that goes nowhere. */} {onDelete && !archived && ( // It opens the archive confirmation; nothing is deleted. )} {onReview && ( )}
{reviewedForPatient && ( Your care team has reviewed this document, so it can no longer be changed or removed. )} {forPatient ? ( <> {renderField('Record status', patientRecordStatus(documentReference.status), 'status')} {renderField('Review', patientReviewStatus(documentReference.docStatus), 'docStatus')} ) : ( <> {renderField('Status', , 'status')} {renderField('Document Status', , 'docStatus')} )} {forPatient ? renderField('Type', , 'type.coding[0].display') : renderField('Document Type', , 'type.coding[0].display')} {forPatient ? renderField('Category', patientConceptWords(documentReference.category)) : renderField('Document Category', )} {renderField('Document Date', formatDate(documentReference.date), 'date')} {renderField('Description', renderDescriptionValue(), 'description')} {forPatient ? renderField('Security labels', patientConceptWords(documentReference.securityLabel)) : renderField('Document security-tags', )}
{renderContent()}
{forPatient ? ( <> {hasAbout && (
{hasRefs(documentReference.author) && renderField('Created by', )} {hasRefs(context?.encounter) && renderField('Visits', )} {hasRefs(context?.related) && renderField('Related to', )} {eventWords && renderField('What it records', eventWords)} {facilityWords && renderField('Where it was created', facilityWords, 'context.facilityType.coding[0].display')} {specialtyWords && renderField('Specialty', specialtyWords, 'context.practiceSetting.coding[0].display')} {hasPeriod && renderField('Dates of care', )}
)} {hasRefs(documentReference.relatesTo) && (
{renderRelatesTo()}
)} ) : ( <>
{renderField('Who and/or what authored the document', )}
{renderRelatesTo()}
{renderField('Encounters', )} {/* WHAT THE DOCUMENT IS ABOUT, when that is a resource rather than a visit - the plan a photograph of an insurance card was filed beside. Without it the card reads as a loose file in a list, which is the thing filing it beside the Coverage was meant to prevent. */} {renderField('Related to', )} {renderField('Main clinical acts documented', )} {renderField('Kind of facility where patient was seen', , 'context.facilityType.coding[0].display')} {renderField('Additional details about where the content was created (e.g. clinical specialty)', , 'context.practiceSetting.coding[0].display')} {renderField('Time of service that is being documented', )}
)}
); }; export default FhirDocumentReferenceDisplay;