import React, { useState, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { Card, CardBody, CardDivider, CardHeader } from '../../../../../components/ui/wp-card'; import { InputControl } from '../../../../../components/ui/input-control'; import { Divider } from '../../../../../components/ui/divider'; import { DismissibleAlert } from '../../../../../components/ui/dismissible-alert'; import { Heading } from '../../../../../components/ui/heading'; import { Flex, FlexItem } from '../../../../../components/ui/flex'; import { Text } from '../../../../../components/ui/text'; import { Spacer } from '../../../../../components/ui/spacer'; import { TabPanel } from '../../../../../components/ui/tab-panel'; import { Modal } from '../../../../../components/ui/wp-modal-shim'; import { useForm, FormProvider, useFieldArray, Controller } from 'react-hook-form'; import { Media } from 'fhir/r4'; import { Check, PlusCircle, Trash2 } from 'lucide-react'; import { IValueSet, useAuthServiceContext, useMediaServiceContext, usePatientServiceContext, useValueSetServiceContext } from '../../../../../services'; import { usePhotoUploadService } from '../../../../../services/PhotoUploadService'; import { mediaChangeableHere, mediaRefusalCopy } from '../../../../../services/MediaService'; import { AppHeader, AppSpinner, FhirCode, FhirCodeableConcept, FhirValueSetSearch, ProfileLayoutContainer } from '../../../../../components'; import FhirAnnotation from '../../../../../components/FhirAnnotation/FhirAnnotation.tsx'; const EditMediaPage: React.FC = () => { const navigate = useNavigate(); const { mediaId } = useParams<{ mediaId: string }>(); const { wpOrganization } = useAuthServiceContext(); const { patientState } = usePatientServiceContext(); const { uploadDocument, getPresignedDownloadUrl } = usePhotoUploadService(); const { fetchMedia, transformToFhirMedia, transformFromFhirMedia, createMedia, updateMedia, deleteMedia, setMediaState, setShouldReload, setLastUpserted, setUnsaved } = useMediaServiceContext(); const { fetchValueSet, valueSetUrls } = useValueSetServiceContext(); const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [media, setMedia] = useState(null); const [showDeleteModal, setShowDeleteModal] = useState(false); const [previewUrl, setPreviewUrl] = useState(null); const [statusOptions, setStatusOptions] = useState([]); const [typeOptions, setTypeOptions] = useState([]); const [modalityOptions, setModalityOptions] = useState([]); const [viewOptions, setViewOptions] = useState([]); const [bodySiteOptions, setBodySiteOptions] = useState([]); const isNew = mediaId === 'new'; // Another practice's Media, or one whose creator or version cannot be proved, is viewable but not changeable here. const readOnly = !isNew && !!media && !mediaChangeableHere(media, wpOrganization?.current?.id); const spacerMargin = '24px'; useEffect(() => { const loadValueSets = async () => { const [ statusValueSet, typeValueSet, modalityValueSet, viewValueSet, bodySiteValueSet ] = await Promise.all([ fetchValueSet(valueSetUrls.mediaStatus), fetchValueSet(valueSetUrls.mediaType), fetchValueSet(valueSetUrls.mediaModality), fetchValueSet(valueSetUrls.mediaView), fetchValueSet(valueSetUrls.bodySite) ]); setStatusOptions(statusValueSet); setTypeOptions(typeValueSet); setModalityOptions(modalityValueSet); setViewOptions(viewValueSet); setBodySiteOptions(bodySiteValueSet); }; loadValueSets(); }, [fetchValueSet, valueSetUrls]); const methods = useForm({ defaultValues: { identifier: [], status: 'preparation', type: null, modality: null, view: null, encounter: null, createdDateTime: '', issued: '', operator: null, reasonCode: [], bodySite: null, deviceName: '', device: null, height: 0, width: 0, frames: 0, duration: 0, content: { contentType: '', url: '', size: 0, title: '' }, note: [], basedOn: [], partOf: [] } }); const { control, handleSubmit, getValues, setValue, reset, watch } = methods; const identifierFieldArray = useFieldArray({ control, name: 'identifier' }); const reasonCodeFieldArray = useFieldArray({ control, name: 'reasonCode' }); const noteFieldArray = useFieldArray({ control, name: 'note' }); const basedOnFieldArray = useFieldArray({ control, name: 'basedOn' }); const partOfFieldArray = useFieldArray({ control, name: 'partOf' }); const contentUrl = watch('content.url'); const contentType = watch('content.contentType'); // An image previews only through the link the server signs once its scan is clean, for whoever may read this saved // item; a file not saved on it yet, or a link to an image elsewhere, is never loaded. useEffect(() => { setPreviewUrl(null); if (isNew || !mediaId || !contentUrl || !contentType?.startsWith('image/') || /^[a-z][a-z0-9+.-]*:/i.test(contentUrl)) return; let current = true; getPresignedDownloadUrl(contentUrl, undefined, `Media/${mediaId}`) .then((url) => { if (current) setPreviewUrl(url); }) .catch(() => { if (current) setPreviewUrl(null); }); return () => { current = false; }; }, [contentUrl, contentType, isNew, mediaId]); useEffect(() => { const loadMedia = async () => { if (isNew) return; setLoading(true); try { const fetchedMedia = await fetchMedia(mediaId as string); setMedia(fetchedMedia); setMediaState(fetchedMedia); const formData = transformFromFhirMedia(fetchedMedia); reset(formData); } catch (err) { console.error('Error loading media:', err); setError('Could not load media. Try again.'); } finally { setLoading(false); } }; loadMedia(); }, [mediaId, isNew, fetchMedia, transformFromFhirMedia, reset, setMediaState]); const onSubmit = async (data: any) => { if (!patientState) { setError('Patient information is missing. Please try again.'); return; } setSubmitting(true); setError(null); try { let result; if (isNew) { result = await createMedia(transformToFhirMedia(data, patientState)); } else { // The edition the user opened, so a change made since is refused rather than overwritten. result = await updateMedia({ id: media?.id, ...transformToFhirMedia(data, patientState), meta: { versionId: media?.meta?.versionId }, }); } setMediaState(result); setLastUpserted(result.id); navigate(-1); } catch (err: any) { console.error(`Error ${isNew ? 'creating' : 'updating'} media:`, err); setError(mediaRefusalCopy(err) ?? `Failed to ${isNew ? 'create' : 'update'} media. Please try again.`); } finally { setSubmitting(false); setShouldReload(true); setUnsaved(false); } }; const addIdentifier = () => { identifierFieldArray.append({ system: '', value: '' }); }; const removeIdentifier = (index: number) => { identifierFieldArray.remove(index); }; const addReasonCode = () => { reasonCodeFieldArray.append(null); }; const removeReasonCode = (index: number) => { reasonCodeFieldArray.remove(index); }; const addNote = () => { noteFieldArray.append({ text: '' }); }; const removeNote = (index: number) => { noteFieldArray.remove(index); }; const addBasedOn = () => { basedOnFieldArray.append(null); }; const removeBasedOn = (index: number) => { basedOnFieldArray.remove(index); }; const addPartOf = () => { partOfFieldArray.append(null); }; const removePartOf = (index: number) => { partOfFieldArray.remove(index); }; // The file is uploaded and scanned like any other, then named by key for this patient; its bytes never ride // inside the record. const handleFileUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; try { const { key } = await uploadDocument(file, patientState?.id); setValue('content.contentType', file.type); setValue('content.url', key); setValue('content.data', undefined); setValue('content.size', file.size); setValue('content.title', file.name); } catch (err) { console.error('Error uploading media:', err); setError('Could not upload the file. A PDF, Word document or image can be added here.'); } }; const handleDeleteMedia = async () => { if (!media) return; // A refused or unanswered removal stays on this page and says so; only a removal the server confirmed leaves it. try { await deleteMedia(media); } catch (deleteError) { console.error('Failed to delete media:', deleteError); setShowDeleteModal(false); setError(mediaRefusalCopy(deleteError) ?? 'The media could not be removed. Reload it to see whether it is still there.'); return; } setShouldReload(true); setShowDeleteModal(false); navigate(-1); }; if (loading) { return ( ); } return (
{!readOnly && ( )} {!isNew && !readOnly && ( )} {error && ( <> setError(null)}> {error} )} {readOnly && ( <>

Added by another practice, or its practice or version cannot be confirmed. You can view it; only the practice that added it can change or remove it.

)}
{/* status */} {/* type */} {/* modality */} {/* view */} {/* bodySite */} {(tab) => { // content if (tab.name === 'content') { return (
Media Content {/* File Upload */} {/* Content URL */} ( )}/> {/* Content Type */} ( )}/> {/* Content Title */} ( )}/> {/* Preview */} {previewUrl && ( <> Preview
Media preview
)} {/* Dimensions */} Dimensions ( )}/> ( )}/> ( )}/> ( )}/>
); } // details if (tab.name === 'details') { return (
{/* createdDateTime */} ( )}/> {/* issued */} ( )}/> {/* operator */} {/* encounter */} {/* reasonCode */} Reason Codes {reasonCodeFieldArray.fields.length === 0 && ( <> No reason codes added yet. )} {reasonCodeFieldArray.fields.map((field, index) => ( ))} {reasonCodeFieldArray.fields.length > 0 && ( )}
); } // metadata if (tab.name === 'metadata') { return (
{/* deviceName */} ( )}/> {/* device */} {/* identifiers */} Identifiers {identifierFieldArray.fields.length === 0 && ( <> No identifiers added yet. )} {identifierFieldArray.fields.map((field, index) => (

Identifier {index + 1}

( )}/> ( )}/>
))} {identifierFieldArray.fields.length > 0 && ( )} {/* basedOn */} Based On {basedOnFieldArray.fields.length === 0 && ( <> No based on references added yet. )} {basedOnFieldArray.fields.map((field, index) => ( ))} {basedOnFieldArray.fields.length > 0 && ( )} {/* partOf */} Part Of {partOfFieldArray.fields.length === 0 && ( <> No part of references added yet. )} {partOfFieldArray.fields.map((field, index) => ( ))} {partOfFieldArray.fields.length > 0 && ( )}
); } // notes if (tab.name === 'notes') { return (
{/* note */}
); } return null; }}
{/* Delete Confirmation Modal */} {showDeleteModal && ( setShowDeleteModal(false)}> Are you sure you want to delete this media item? )}
); }; export default EditMediaPage;