import React, { useState, useEffect, useLayoutEffect, useRef, useCallback } from "react"; import { TextareaControl } from "../ui/textarea-control"; import { CustomSelectControl } from "../ui/custom-select-control"; import { useFormContext, useFieldArray, Controller, useWatch } from "react-hook-form"; import { useAuthServiceContext, usePatientServiceContext, useValueSetServiceContext, IValueSet, } from "../../services"; import { usePhotoUploadService } from "../../services/PhotoUploadService"; import type { SaveTransport } from '../../utils/saveOperation'; import { useFhirDevices, useFhirDocumentReferences, useFhirEncounters, useFhirEpisodesOfCare, useFhirPractitioners, useFhirRelatedPersons, } from "../../hooks"; import { Button } from "../ui/button"; import { DismissibleAlert } from "../ui/dismissible-alert"; import { InputControl } from '../ui/input-control'; import { DOCUMENT_NEEDS_FILE } from '../../utils/documentNeedsFile'; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; import { Plus, Trash2, FileText, Edit } from "lucide-react"; import { FILED_ORIGINAL_FILE_NOTE } from "../../utils/records/filedOriginal"; import { formatFileSize } from "../../utils/formatFileSize"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs"; import { FhirCode, FhirCodeableConcept, FhirPeriod, FhirReference, FhirReferenceCollection, FhirValueSetSearch, } from ".."; import { formatFhirHumanName } from "../../utils/fhirHumanName.tsx"; interface FhirDocumentReferenceProps { elevated?: boolean; disabled?: boolean; isRoot: boolean; enableInlineActions?: boolean; // The practice's own drawer: a title, a description and the files, with nothing clinical. practice?: boolean; // The container already says "Document Details" (a sheet or modal title), so the form doesn't again. titled?: boolean; // An outside original filed from Records: its file is kept as received, so nothing here replaces or removes it. fileLocked?: boolean; // Staff filing: each pick is uploaded under the opening's filing, in its own slot (services/DocumentFilingService.tsx). filing?: { upload: (file: File, slotId: string, transport?: Partial) => Promise<{ key: string }> }; } export const FhirDocumentReference: React.FC = ({ elevated = true, disabled = false, isRoot = false, enableInlineActions = true, practice = false, titled = false, fileLocked = false, filing, }) => { const { control, setValue, getValues, formState: { errors } } = useFormContext(); const { userRole, USER_ROLES } = useAuthServiceContext(); const { fetchValueSet, valueSetUrls } = useValueSetServiceContext(); const { patientState } = usePatientServiceContext(); const { uploadDocument } = usePhotoUploadService(); const [loading, setLoading] = useState(false); const [documentReferenceStatus, setDocumentReferenceStatus] = useState([]); const [compositionStatus, setCompositionStatus] = useState([]); const [documentClasscodes, setDocumentClasscodes] = useState([]); const [documentRelationshipType, setDocumentRelationshipType] = useState([]); const [c80Facilitycodes, setC80Facilitycodes] = useState([]); const [c80PracticeCodes, setC80PracticeCodes] = useState([]); const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes(userRole as string); const { documentReferencesList } = useFhirDocumentReferences({ isRoot }); const { encountersList } = useFhirEncounters(); const { episodesOfCareList } = useFhirEpisodesOfCare(); const { practitionersList } = useFhirPractitioners(); const { devicesList: _devicesList } = useFhirDevices(); const { relatedPersonsList: _relatedPersonsList } = useFhirRelatedPersons(); const authorFieldArray = useFieldArray({ control, name: "author" }); const relatesToFieldArray = useFieldArray({ control, name: "relatesTo" }); // A slot rule has nothing to check once every slot is removed, so the list itself must hold one. const contentFieldArray = useFieldArray({ control, name: "content", rules: { validate: (content) => (Array.isArray(content) && content.length > 0) || DOCUMENT_NEEDS_FILE }, }); const noFileSlot = (errors.content as { root?: { message?: string } } | undefined)?.root?.message; const contextEncounterFieldArray = useFieldArray({ control, name: "context.encounter" }); const fileInputsRef = useRef<(HTMLInputElement | null)[]>([]); // A file read outlives the pick that started it. It may land only in the editable opening it was picked in, which // ends at the commit that disables or unmounts the form, and only in the same content slot: a reset to another // version, or a removed row, replaces that slot in the form at once, before anything renders. const openingRef = useRef(null); useLayoutEffect(() => { if (!disabled) openingRef.current = {}; return () => { openingRef.current = null; }; }, [disabled]); // The newest pick or Remove for a slot is the one that counts, so an older read finishing later never lands over it. const slotIntentRef = useRef(new WeakMap()); const nextIntent = (slot: object) => { const intent = (slotIntentRef.current.get(slot) ?? 0) + 1; slotIntentRef.current.set(slot, intent); return intent; }; const contentValues = useWatch({ control, name: "content" }) as any[] | undefined; // A content row keeps its slot while it stays in the form; removing it drops the slot, reordering keeps it. const slotIdsRef = useRef(new WeakMap()); const slotIdOf = (slot: object) => { const known = slotIdsRef.current.get(slot); if (known) return known; const minted = crypto.randomUUID(); slotIdsRef.current.set(slot, minted); return minted; }; // A document's file is uploaded and scanned like any other, then named by key: for the patient the document is // about, or for the practice when it is about nobody. Some browsers leave a Word file's type blank. const uploadDocumentFile = useCallback(async (file: File, slotId: string, transport: Partial): Promise<{ key: string; contentType: string }> => { const name = file.name.toLowerCase(); const contentType = file.type || (name.endsWith('.docx') ? 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : name.endsWith('.doc') ? 'application/msword' : name.endsWith('.pdf') ? 'application/pdf' : ''); const typed = contentType === file.type ? file : new File([file], file.name, { type: contentType }); const { key } = filing ? await filing.upload(typed, slotId, transport) : await uploadDocument(typed, isRoot ? undefined : patientState?.id, { transport }); return { key, contentType }; }, [filing, isRoot, patientState?.id, uploadDocument]); useEffect(() => { const loadValueSets = async () => { setLoading(true); try { const [ documentReferenceStatusValueSet, compositionStatusValueSet, documentClasscodesValueSet, documentRelationshipTypeValueSet, c80FacilitycodesValueSet, c80PracticeCodesValueSet, ] = await Promise.all([ fetchValueSet(valueSetUrls.documentReferenceStatus), fetchValueSet(valueSetUrls.compositionStatus), fetchValueSet(valueSetUrls.documentClasscodes), fetchValueSet(valueSetUrls.documentRelationshipType), fetchValueSet(valueSetUrls.c80Facilitycodes), fetchValueSet(valueSetUrls.c80PracticeCodes), ]); setDocumentReferenceStatus(documentReferenceStatusValueSet); setCompositionStatus(compositionStatusValueSet); setDocumentClasscodes(documentClasscodesValueSet); setDocumentRelationshipType(documentRelationshipTypeValueSet); setC80Facilitycodes(c80FacilitycodesValueSet); setC80PracticeCodes(c80PracticeCodesValueSet); } catch (err) { console.error("Error loading value sets:", err); } finally { setLoading(false); } }; loadValueSets(); }, [fetchValueSet, valueSetUrls]); const addRelatesTo = () => { relatesToFieldArray.append({ code: "", target: { reference: "", identifier: null } }); }; const removeRelatesTo = (index: number) => relatesToFieldArray.remove(index); const addContent = () => { contentFieldArray.append({ attachment: undefined, format: null }); }; const removeContent = (index: number) => contentFieldArray.remove(index); return (
{/* Scoped style: reorder chips below the token input for the Document Type field only */} {!titled && (

Document Details

)}
{practice ? ( (
{fieldState.error &&

{fieldState.error.message}

}
)} /> ) : ( <> {/* Row 1: Status & Document Status */}
{/* Reviewed is the practice's mark; the server refuses a patient-side document that claims it. */} {!isPatient && ( )}
{/* Row 2: Document Type & Date/Time Available*/}
( )} />
{/* Row 3: Categorization of Document */} )} {/* Row 4: Description */} ( )} /> Content {!practice && ( Authors )}
{fileLocked && (

{FILED_ORIGINAL_FILE_NOTE}

)} {contentFieldArray.fields.map((field: any, index) => ( Document {index + 1} {!fileLocked && ( )} {/* * THE DOCUMENT IS THE RECORD. FHIR marks DocumentReference.content 1..1 and the modal * seeds `content: [{}]`, so a reference to a document used to save with no document * attached to it. The dropzone sets the value through `setValue` rather than a * control, so it was never part of the validation graph either - this registers the * attachment itself so the save refuses, and says which document is missing. */} ( <>

Document file *

{fieldState.error && ( A file is required for Document {index + 1} )} )}/> {/* Hidden file input */} { fileInputsRef.current[index] = el; }} onChange={async (e) => { const file = e.target.files?.[0]; if (e.target.value) e.target.value = ''; if (!file || disabled || fileLocked) return; const opening = openingRef.current; const slot = getValues(`content.${index}`); if (!opening || !slot || typeof slot !== 'object') return; const intent = nextIntent(slot); const live = () => openingRef.current === opening && getValues(`content.${index}`) === slot && slotIntentRef.current.get(slot) === intent; // A pick no longer live sends nothing more: not its filing, its upload link or its bytes. const transport = { admit: () => { if (!live()) throw new Error('This file was replaced or its form closed.'); } }; try { const { key, contentType } = await uploadDocumentFile(file, slotIdOf(slot), transport); if (!live()) return; setValue(`content.${index}.attachment`, { contentType, url: key, title: file.name, size: file.size, creation: new Date().toISOString(), }, { shouldDirty: true, shouldValidate: true }); } catch (err) { if (!live()) return; console.error('Error processing file:', err); } }} /> {!contentValues?.[index]?.attachment && fileLocked ? ( // A filed original with no file on record: nothing here may supply one.

No file is on record for this original.

) : !contentValues?.[index]?.attachment ? ( /* Empty state - choose file */ ) : ( /* File selected - preview card */

{contentValues[index].attachment.title}

{contentValues[index].attachment.size ? formatFileSize(contentValues[index].attachment.size) : contentValues[index].attachment.contentType}

{!fileLocked && (
)}
)}
))} {noFileSlot && ( {noFileSlot} )} {!fileLocked && ( )}
{authorFieldArray.fields.map((field, index) => ( Reference {index + 1} {/* Both of these carried an asterisk and enforced nothing, which is worse than no asterisk: it promises a refusal that never comes. The type is now genuinely required, because an author row that does not say what kind of author it is cannot be resolved. The practitioner below keeps no asterisk - that select renders whatever type is chosen, so it cannot be demanded of an Organization or Device author. */} (
field.onChange(newValue.selectedItem.key)} disabled={disabled} />
)} /> (
{ const pr = p?.practitioner ?? p; const nameEntry = pr?.name?.[0]; const displayName = (nameEntry && formatFhirHumanName(nameEntry, { emptyText: '' })) || pr?.id || "Unknown"; return { key: pr?.id, name: displayName }; })] } value={(() => { const sel = practitionersList.find((p:any) => (p?.practitioner ?? p)?.id === field.value); if (!sel) return undefined; const pr = sel?.practitioner ?? sel; const nameEntry = pr?.name?.[0]; const displayName = (nameEntry && formatFhirHumanName(nameEntry, { emptyText: '' })) || pr?.id || "Unknown"; return { key: pr?.id, name: displayName }; })()} onChange={(newValue: any) => field.onChange(newValue.selectedItem.key)} disabled={disabled} />
)} />
))}
{/* Advanced Sections for non-patients */} {!isPatient && !practice && ( <> {/* Related Documents */}

Related Documents

{relatesToFieldArray.fields.map((field, index) => ( Related Document {index + 1} ))}
{/* Context */}

Context

{ switch (type) { case "Encounter": return encountersList; case "EpisodeOfCare": return episodesOfCareList; default: return []; } }} required={false} multiple disabled={disabled} enableInlineActions={enableInlineActions} />
)}
); }; export default FhirDocumentReference; // import React, { useState, useEffect } from 'react'; // import { useFormContext, Controller, useFieldArray } from 'react-hook-form'; // import { // IValueSet, // useAuthServiceContext, // usePatientServiceContext, // useValueSetServiceContext // } from '../../services'; // import { // FhirAttachment, // FhirCode, // FhirCodeableConcept, // FhirPeriod, // FhirReference, // FhirReferenceCollection, // FhirValueSetSearch // } from '../../components'; // import { // useFhirDevices, // useFhirDocumentReferenceBroadcast, // useFhirDocumentReferences, // useFhirEncounters, // useFhirEpisodesOfCare, // useFhirPractitioners, // useFhirRelatedPersons // } from '../../hooks'; // import { FhirResource } from 'fhir/r4'; // interface FhirDocumentReferenceProps { // elevated?: boolean; // disabled?: boolean; // isRoot: boolean; // enableInlineActions?: boolean; // } // export const FhirDocumentReference: React.FC = ({ elevated = true, disabled = false, isRoot = false, enableInlineActions = true }) => { // const { control } = useFormContext(); // const { userRole, USER_ROLES } = useAuthServiceContext(); // const { patientState } = usePatientServiceContext(); // const { fetchValueSet, valueSetUrls } = useValueSetServiceContext(); // const [loading, setLoading] = useState(false); // // value sets // const [documentReferenceStatus, setDocumentReferenceStatus] = useState([]); // const [compositionStatus, setCompositionStatus] = useState([]); // const [documentClasscodes, setDocumentClasscodes] = useState([]); // const [documentRelationshipType, setDocumentRelationshipType] = useState([]); // const [securityLabels, setSecurityLabels] = useState([]); // // const [formatcodes, setFormatcodes] = useState([]); // const [c80Facilitycodes, setC80Facilitycodes] = useState([]); // const [c80PracticeCodes, setC80PracticeCodes] = useState([]); // const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes(userRole as string); // const spacerMargin = '24px'; // useFhirDocumentReferenceBroadcast({ isRoot: isRoot }); // const { documentReferencesList } = useFhirDocumentReferences({ isRoot: isRoot }); // const { encountersList } = useFhirEncounters(); // const { episodesOfCareList } = useFhirEpisodesOfCare(); // const { practitionersList } = useFhirPractitioners(); // const { devicesList } = useFhirDevices(); // const { relatedPersonsList } = useFhirRelatedPersons(); // const authorFieldArray = useFieldArray({ control, name: 'author' }); // const relatesToFieldArray = useFieldArray({ control, name: 'relatesTo' }); // const contentFieldArray = useFieldArray({ control, name: 'content' }); // const contextEncounterFieldArray = useFieldArray({ control, name: 'context.encounter' }); // useEffect(() => { // const loadValueSets = async () => { // setLoading(true); // try { // const [ // documentReferenceStatusValueSet, // compositionStatusValueSet, // documentClasscodesValueSet, // documentRelationshipTypeValueSet, // securityLabelsValueSet, // // formatcodesValueSet, // c80FacilitycodesValueSet, // c80PracticeCodesValueSet // ] = await Promise.all([ // fetchValueSet(valueSetUrls.documentReferenceStatus), // fetchValueSet(valueSetUrls.compositionStatus), // fetchValueSet(valueSetUrls.documentClasscodes), // fetchValueSet(valueSetUrls.documentRelationshipType), // fetchValueSet(valueSetUrls.securityLabels), // // fetchValueSet(valueSetUrls.formatcodes), // fetchValueSet(valueSetUrls.c80Facilitycodes), // fetchValueSet(valueSetUrls.c80PracticeCodes) // ]); // setDocumentReferenceStatus(documentReferenceStatusValueSet); // setCompositionStatus(compositionStatusValueSet); // setDocumentClasscodes(documentClasscodesValueSet); // setDocumentRelationshipType(documentRelationshipTypeValueSet); // setSecurityLabels(securityLabelsValueSet); // // setFormatcodes(formatcodesValueSet); // setC80Facilitycodes(c80FacilitycodesValueSet); // setC80PracticeCodes(c80PracticeCodesValueSet); // } catch (err) { // console.error('Error loading value sets:', err); // } finally { // setLoading(false); // } // }; // loadValueSets(); // }, [fetchValueSet, valueSetUrls]); // const addRelatesTo = () => { // relatesToFieldArray.append({ // code: '', // target: { // reference: '', // identifier: null // } // }); // }; // const removeRelatesTo = (index: number) => { // relatesToFieldArray.remove(index); // }; // const addContent = () => { // contentFieldArray.append({ // attachment: null, // format: null // }); // }; // const removeContent = (index: number) => { // contentFieldArray.remove(index); // }; // return ( // // {elevated && } // // Document Details // // // // {/* status */} // // // {/* docStatus */} // // // {/* type */} // // // {/* category */} // // // {/* date */} // ( // // )}/> // // {/* description */} // ( // // )}/> // // {!isPatient && ( // <> // {/* securityLabel */} // // // // )} // // // // // {(tab) => { // // content // if (tab.name === 'content') { // return ( //
// // {contentFieldArray.fields.map((content, index) => ( // // // // //
Content {index + 1}
//
// // //
// ); // } // // authors // if (tab.name === 'authors') { // return ( //
// // {/* Authors */} // { // switch (referenceType) { // case 'Practitioner': // return practitionersList.map(p => p.practitioner); // case 'Device': // return devicesList; // case 'RelatedPerson': // return relatedPersonsList; // default: // return []; // } // }} // required={false} // multiple={true} // disabled={disabled} // enableInlineActions={enableInlineActions}/> // // {/* authenticator */} // {/* {}, remove: () => {} }}*/} // {/* context="authenticator"*/} // {/* referenceTypes={[*/} // {/* { name: 'Practitioner', key: 'Practitioner' },*/} // {/* { name: 'Organization', key: 'Organization' }*/} // {/* ]}*/} // {/* getReferenceList={(referenceType: string): FhirResource[] => {*/} // {/* switch (referenceType) {*/} // {/* case 'Practitioner':*/} // {/* return practitionersList;*/} // {/* case 'Organization':*/} // {/* return organizationsList;*/} // {/* default:*/} // {/* return [];*/} // {/* }*/} // {/* }}*/} // {/* required={false}*/} // {/* multiple={false}*/} // {/* disabled={submitting}*/} // {/* enableInlineCreation={false}*/} // {/* onInlineCreate={(referenceType) => {*/} // {/* console.log(`Create new ${referenceType}`);*/} // {/* }}/>*/} // {/**/} // {/* custodian */} // {/* {}, remove: () => {} }}*/} // {/* context="custodian"*/} // {/* referenceTypes={[*/} // {/* { name: 'Organization', key: 'Organization' }*/} // {/* ]}*/} // {/* getReferenceList={(referenceType: string): FhirResource[] => {*/} // {/* switch (referenceType) {*/} // {/* case 'Organization':*/} // {/* return organizationsList;*/} // {/* default:*/} // {/* return [];*/} // {/* }*/} // {/* }}*/} // {/* required={false}*/} // {/* multiple={false}*/} // {/* disabled={submitting}*/} // {/* enableInlineCreation={false}*/} // {/* onInlineCreate={(referenceType) => {*/} // {/* console.log(`Create new ${referenceType}`);*/} // {/* }}/>*/} //
// ); // } // // relatesTo // if (tab.name === 'relatesTo') { // return ( // //
// // {relatesToFieldArray.fields.map((relatesTo, index) => ( // // // // //
Related Document {index + 1}
//
// // {!isPatient && ( // // )} //
//
// ); // } // // context // if (tab.name === 'context') { // return ( // //
// // {/* context.encounter */} // { // switch (referenceType) { // case 'Encounter': // return encountersList; // case 'EpisodeOfCare': // return episodesOfCareList; // default: // return []; // } // }} // required={false} // multiple={true} // disabled={disabled} // readonly={isPatient} // enableInlineActions={enableInlineActions}/> // // {/* context.event */} // // // {/* context.facilityType */} // // // {/* context.practiceSetting */} // // // {/* context.period */} // // //
//
// ); // } // }} //
//
//
// ); // }; // export default FhirDocumentReference;