import { useProxyCannotAddToRecord } from '../../../../hooks/useProxyCannotAddToRecord'; import { useState, useEffect, useMemo, useRef } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { Observation } from "fhir/r4"; import { useForm, FormProvider, Controller, useFieldArray, } from "react-hook-form"; import { AppSpinner, FhirObservationDisplay, LoincCodeSearch } from "../../../../components"; import { formatFhirCodeableConcept, getCodeableConceptDisplay, } from "../../../../utils/fhirCodeableConcept.tsx"; import { formatDate, toLocalDateTimeInput } from "../../../../utils/dateUtils.ts"; import { useFhirObservationBroadcast, useFhirObservations, useFhirObservationTargetedBroadcast, useFhirEncounters, useFhirPractitioners, useFhirDevices, } from "../../../../hooks"; import { useObservationServiceContext, usePatientServiceContext, IValueSet, useValueSetServiceContext, useAuthServiceContext, } from "../../../../services"; import type { SelfReport } from "../../../../utils/selfReport"; import { Calendar, Clock, Edit, FileText, Heart, Plus, Stethoscope, Trash2, AlertCircle, User, } from "lucide-react"; import { Button } from "../../../../components/ui/button.tsx"; import { Card } from "../../../../components/ui/card.tsx"; import { formatDateForDisplay, formatTimeForDisplay, } from "../../../../lib/utils.ts"; import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from "../../../../components/ui/sheet.tsx"; import { Checkbox } from "../../../../components/ui/checkbox.tsx"; import { Label } from "../../../../components/ui/label.tsx"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../../../../components/ui/select.tsx"; import { Input } from "../../../../components/ui/input.tsx"; import { Textarea } from "../../../../components/ui/textarea.tsx"; import { TabsContent, TabsList, TabsTrigger, Tabs, } from "../../../../components/ui/tabs.tsx"; import { Alert, AlertDescription } from "../../../../components/ui/alert.tsx"; import { formatFhirHumanName } from "../../../../utils/fhirHumanName.tsx"; import SimpleReadingForm from "./SimpleReadingForm.tsx"; import { PATIENT_MEASUREMENTS, type PatientReadingInput, } from "./patientObservationCatalog.ts"; import { relayServerFailure } from '../../../../utils/fetchFailureMessage'; import { isSaveRetired, useSaveScope, type SaveOperation } from '../../../../utils/saveOperation'; import type { StaleReason } from '../../../../hooks/useEditBaseline'; import { StaleRecordNotice } from '../../../../components/StaleRecordNotice/StaleRecordNotice'; import { EarlierSaveNotice } from '../../../../components/EarlierSaveNotice/EarlierSaveNotice'; import { operationPartiesOf, useObservationOperation, type EarlierOutcome } from '../../../../hooks/useObservationOperation'; import { OPERATION_NOT_SAVED_MESSAGE, OPERATION_REUSED_MESSAGE, OPERATION_UNAVAILABLE_MESSAGE, operationNotYetAdmitted, operationReused, operationUnconfirmed } from '../../../../utils/observationOperation'; import { RecoveryFull, RecoveryUnavailable } from '../../../../utils/messaging/sendRecovery'; import { readingName, readingsSavedSoFar } from "../../../../utils/readingSave"; import { staleReasonOf } from "../../../../utils/saveOverBaseline"; import { olderThan } from "../../../../utils/versionOrder"; import { describeEncounterForPicker } from '../../../../utils/encounterPickerLabel'; import { keepStoredCode, keepStoredInstant, keepStoredQuantity, readingTakenAt, unitChoice } from '../../../../utils/observationEdit'; import { bloodPressureFormStart, bloodPressureReading, clearsStoredValue, componentLabel, componentReadings, componentsForSave, componentsWithoutUnit, enteredNumber, hasComponentValue, isBloodPressurePanel, MEASUREMENT_CHANGE_REFUSED, numberOverOtherValue, patientPairEdit, } from '../../../../utils/observationPanelValue'; import { SourcePractice } from '../../../../components/ui/source-practice'; import { RecordPracticeNote, recordPracticeId, useRecordPracticeView, useRetireOnPracticeChange } from '../../../../contexts/RecordPracticeContext'; import { SELF_REPORTED_SOURCE_WORDS, selfReportedSourceOf } from '../../../../utils/selfReportedSource'; import { laterThan } from '../../../../utils/writeThroughHold'; import { GENERIC_ENTRY_HELD_NOTE, editDropsSourceNote, releasableOriginOf } from '../../../../utils/releaseOrigin'; import { VitalsForm } from '../../ClinicianOverviewPage/VitalsForm'; import { observationPractice } from '../../../../utils/observationPractice'; // Observation profile options const OBSERVATION_PROFILES = [ { value: "none", label: "No profile selected" }, { value: "us-core-average-blood-pressure", label: "Average Blood Pressure" }, { value: "us-core-care-experience-preference", label: "Care Experience Preference", }, { value: "us-core-observation-adi-documentation", label: "Observation ADI Documentation", }, { value: "us-core-observation-pregnancyintent", label: "Observation Pregnancy Intent", }, { value: "us-core-observation-pregnancystatus", label: "Observation Pregnancy Status", }, { value: "us-core-observation-screening-assessment", label: "Observation Screening Assessment", }, { value: "us-core-observation-sexual-orientation", label: "Observation Sexual Orientation", }, { value: "us-core-smokingstatus", label: "Smoking Status Observation" }, { value: "us-core-treatment-intervention-preference", label: "Treatment Intervention Preference", }, { value: "pediatric-bmi-for-age", label: "Pediatric BMI for Age Observation", }, { value: "pediatric-weight-for-height", label: "Pediatric Weight for Height Observation", }, { value: "us-core-blood-pressure", label: "Blood Pressure" }, { value: "us-core-bmi", label: "BMI" }, { value: "us-core-body-height", label: "Body Height" }, { value: "us-core-body-temperature", label: "Body Temperature" }, { value: "us-core-body-weight", label: "Body Weight" }, { value: "us-core-head-circumference", label: "Head Circumference" }, { value: "us-core-heart-rate", label: "Heart Rate" }, { value: "us-core-respiratory-rate", label: "Respiratory Rate" }, { value: "head-occipital-frontal-circumference-percentile", label: "Pediatric Head Occipital Frontal Circumference Percentile", }, ]; // Hide the edit route until the role resolves; writes remain server-authorized. export const mayOpenObservationRecord = ( userRole: string | undefined, roles: { PATIENT: string; RELATED_PERSON: string }, ): boolean => { if (!userRole) return false; return userRole !== roles.PATIENT && userRole !== roles.RELATED_PERSON; }; const Observations = () => { const navigate = useNavigate(); /** * THE OBSERVATION BEING EDITED, and it was reading the PATIENT. * * `App.tsx` mounts this at `observations/:observationId` beneath `/dashboard/patients/:id`, so * `useParams().id` here is the patient - always present on a chart, never an observation. The comment * that used to sit on this line said "For edit mode", which is what everybody read instead of the * route. * * It broke two things that looked unrelated: * - edit-by-URL never seeded, because `observationsList.find(obs => obs.id === id)` compared an * observation id to a patient id and could not match; * - every save from a chart ran `if (id) navigate(...)`, since the patient id is always truthy. */ const { observationId } = useParams<{ observationId: string }>(); const { loading, error, refused, observationsList: listedObservations, refreshObservations } = useFhirObservations(); const { patientState } = usePatientServiceContext(); // An own report this patient just saved, as the server admitted it, stands in for its row until the list holds that // version or a later one. The list's own read again is quiet and no broadcast reaches a patient with no practice, so // without this the old row stays open to edit at the old value and version. Kept for this patient only. const [savedReports, setSavedReports] = useState<{ patientId?: string; byId: Record }>({ byId: {} }); const observationsList = useMemo(() => { if (!savedReports.patientId || savedReports.patientId !== patientState?.id) return listedObservations; return listedObservations.map((listed) => { const saved = listed.id ? savedReports.byId[listed.id] : undefined; return saved && laterThan(saved, listed) ? saved : listed; }); }, [listedObservations, savedReports, patientState?.id]); const { createObservationOnce, updateObservation, fetchObservation, recordSelfReport, editSelfReport } = useObservationServiceContext(); const { fetchValueSet, valueSetUrls } = useValueSetServiceContext(); const { userRole, USER_ROLES, currentUser, currentOrganization, wpOrganization, actingAs } = useAuthServiceContext(); // Only the patient adding a reading about themself may make it their own report; a caregiver or a helper acting // for them, and every edit, keep the ordinary path. const patientReportsForSelf = userRole === USER_ROLES.PATIENT && !actingAs; // With no practice the patient's ordinary update is refused, so a patient there is offered an edit only on a reading // they reported themself, which has its own typed edit. At a practice the ordinary edit stands. const patientHasPractice = Boolean(currentOrganization?.id ?? wpOrganization?.current?.id); const patientMayEdit = (observation: Observation) => !patientReportsForSelf || patientHasPractice || Boolean(selfReportedSourceOf(observation)); const proxyCannotAdd = useProxyCannotAddToRecord(); // A form opened before the helper's tier was known closes once it is known to be below full access. useEffect(() => { if (proxyCannotAdd) setOpenSheet(false); }, [proxyCannotAdd]); // Presentation only - the server still authorizes every write. const isSimple = userRole === USER_ROLES.PATIENT || userRole === USER_ROLES.RELATED_PERSON; // The practice a staff reader acts in: another practice's reading is read here, not changed. const readerOrganizationId = isSimple ? undefined : currentOrganization?.id ?? wpOrganization?.current?.id; // Who is recording the reading: the acting logged-in user (the patient // themselves, or a proxy/RelatedPerson acting for them). Used as the // Observation.performer so proxy entries are attributed correctly. Falls back // to the patient subject when the acting resource isn't resolved yet. const actingPerformerReference = currentUser?.fhir?.resourceType && currentUser?.fhir?.id ? `${currentUser.fhir.resourceType}/${currentUser.fhir.id}` : undefined; const [selectedObservation, setSelectedObservation] = useState(null); const [showDisplayModal, setShowDisplayModal] = useState(false); // The record's practice context: its own readings under a named practice; the empty state speaks for the whole record. const practiceView = useRecordPracticeView(observationsList, recordPracticeId); useRetireOnPracticeChange(() => { setShowDisplayModal(false); setSelectedObservation(null); }); const [openSheet, setOpenSheet] = useState(false); // Staff recording vital signs they took: the typed native route that asks where they came from. const [openVitals, setOpenVitals] = useState(false); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); // An opening of the sheet that can save nothing more: refused, out of date, unconfirmed, or stopped part way. Only // Reload record (an edit) or closing the sheet moves on. const [sheetEnded, setSheetEnded] = useState<{ reason: StaleReason; message?: string }>(); const sheetEndedRef = useRef(sheetEnded); sheetEndedRef.current = sheetEnded; const [reloading, setReloading] = useState(false); // Each opening of the sheet owns its saves: closing it, opening another, or another patient or person retires them, // so a late answer neither closes, navigates nor clears the opening that came after. const sheetScope = useSaveScope(patientState?.id, () => { setSheetEnded(undefined); setIsSaving(false); setReloading(false); setOpenSheet(false); }); // A new reading from this sheet is one save under one operation id, checked at the next opening if never confirmed. const createOperation = useObservationOperation("observation-sheet", operationPartiesOf(useAuthServiceContext(), patientState?.id), useObservationServiceContext()); const newOpening = () => { sheetScope.renew(); createOperation.retire(); setSheetEnded(undefined); setSaveError(null); setIsSaving(false); setReloading(false); }; const setSheetOpen = (open: boolean) => { if (!open) newOpening(); setOpenSheet(open); }; const [loadingValueSets, setLoadingValueSets] = useState(false); const [isEditMode, setIsEditMode] = useState(false); const [editingObservationId, setEditingObservationId] = useState< string | null >(null); // Full Observation being edited - only needed to prefill the patient/proxy // SimpleReadingForm; the full staff form re-derives its fields via react-hook-form's // `reset(formValues)` in loadObservationForEdit and doesn't need this. const [editingObservation, setEditingObservation] = useState(null); // The patient's own report being changed, and where they said it came from; only that reading has the typed edit. const editingOwnReport = isEditMode && patientReportsForSelf ? selfReportedSourceOf(editingObservation ?? undefined) : undefined; const [observationStatus, setObservationStatus] = useState([]); const [observationCategory, setObservationCategory] = useState( [], ); const [observationInterpretation, setObservationInterpretation] = useState< IValueSet[] >([]); const methods = useForm({ defaultValues: { observationProfile: "none", status: "final", category: [], code: { coding: [{ system: "http://loinc.org", code: "", display: "" }], text: "", }, effectiveType: "dateTime", effectiveDateTime: toLocalDateTimeInput(new Date()), effectivePeriod: { start: "", end: "" }, effectiveTiming: null, issued: toLocalDateTimeInput(new Date()), encounter: null, valueType: "quantity", valueQuantity: { value: "", unit: "", system: "http://unitsofmeasure.org", code: "", }, valueCodeableConcept: null, valueString: "", valueBoolean: false, valueInteger: 0, valueRange: null, valueRatio: null, valueTime: "", valueDateTime: "", valuePeriod: null, dataAbsentReason: null, interpretation: [], note: [], bodySite: null, method: null, device: null, component: [], performer: [], basedOn: [], partOf: [], }, }); const { control, handleSubmit, watch, reset, setValue, getValues, clearErrors, formState: { errors }, } = methods; const componentFieldArray = useFieldArray({ control, name: "component" }); const performerFieldArray = useFieldArray({ control, name: "performer" }); const basedOnFieldArray = useFieldArray({ control, name: "basedOn" }); const partOfFieldArray = useFieldArray({ control, name: "partOf" }); const noteFieldArray = useFieldArray({ control, name: "note" }); const effectiveType = watch("effectiveType"); const valueType = watch("valueType"); const observationProfile = watch("observationProfile"); // A new Blood Pressure reading starts as the panel and its two halves; anything already entered stays. useEffect(() => { if (isEditMode || observationProfile !== "us-core-blood-pressure") return; const start = bloodPressureFormStart(); if (!getValues("code.coding.0.code")) setValue("code", start.code, { shouldDirty: true }); if ((getValues("component") ?? []).length === 0) { setValue("valueQuantity.value", ""); start.halves.forEach((half) => componentFieldArray.append({ id: crypto.randomUUID(), ...half, interpretation: [], isMissing: false })); } // componentFieldArray is a new object each render; the profile is what this answers. // eslint-disable-next-line react-hooks/exhaustive-deps }, [observationProfile, isEditMode]); useEffect(() => { const loadValueSets = async () => { setLoadingValueSets(true); try { const [ observationStatusValueSet, observationCategoryValueSet, observationInterpretationValueSet, ] = await Promise.all([ fetchValueSet(valueSetUrls.observationStatus), fetchValueSet(valueSetUrls.observationCategory), fetchValueSet(valueSetUrls.observationInterpretation), ]); setObservationStatus(observationStatusValueSet); setObservationCategory(observationCategoryValueSet); setObservationInterpretation(observationInterpretationValueSet); } catch (err) { console.error("Error loading value sets:", err); } finally { setLoadingValueSets(false); } }; void loadValueSets(); }, [fetchValueSet, valueSetUrls]); const { encountersList } = useFhirEncounters(); const { practitionersList } = useFhirPractitioners(); const { devicesList } = useFhirDevices(); // Load observation for editing. A newer copy of the one already open refills an untouched form only: the // typing stays, and Save, made on the version opened, is refused if it changed. const editFormDirty = useRef(false); editFormDirty.current = methods.formState.isDirty; useEffect(() => { if (proxyCannotAdd) return; if (observationId && observationsList.length > 0) { const observation = observationsList.find((obs) => obs.id === observationId); // An ended opening is not refilled either: Reload record is the way on. const sameOpening = editingObservationId === observation?.id; if (observation && !(sameOpening && (editFormDirty.current || sheetEndedRef.current))) { loadObservationForEdit(observation); } } }, [observationId, observationsList, proxyCannotAdd]); const loadObservationForEdit = (observation: Observation) => { setIsEditMode(true); setEditingObservationId(observation.id!); setEditingObservation(observation); // Map FHIR Observation to form values const formValues: any = { observationProfile: observation.meta?.profile?.[0]?.split("/").pop() || "none", status: observation.status || "final", category: observation.category || [], code: observation.code || { coding: [{ system: "http://loinc.org", code: "", display: "" }], text: "", }, effectiveType: observation.effectiveDateTime ? "dateTime" : observation.effectivePeriod ? "period" : observation.effectiveTiming ? "timing" : "dateTime", effectiveDateTime: observation.effectiveDateTime ? toLocalDateTimeInput(new Date(observation.effectiveDateTime)) : toLocalDateTimeInput(new Date()), effectivePeriod: observation.effectivePeriod ? { start: observation.effectivePeriod.start ? toLocalDateTimeInput(new Date(observation.effectivePeriod.start)) : "", end: observation.effectivePeriod.end ? toLocalDateTimeInput(new Date(observation.effectivePeriod.end)) : "", } : { start: "", end: "" }, effectiveTiming: observation.effectiveTiming || null, issued: observation.issued ? toLocalDateTimeInput(new Date(observation.issued)) : "", encounter: observation.encounter || null, valueType: observation.valueQuantity ? "quantity" : observation.valueCodeableConcept ? "codeableConcept" : observation.valueString ? "string" : observation.valueBoolean !== undefined ? "boolean" : observation.valueInteger !== undefined ? "integer" : observation.valueRange ? "range" : observation.valueRatio ? "ratio" : observation.valueTime ? "time" : observation.valueDateTime ? "dateTime" : observation.valuePeriod ? "period" : "quantity", valueQuantity: observation.valueQuantity ? { ...observation.valueQuantity, unit: unitChoice(observation.valueQuantity) } : { value: "", unit: "", system: "http://unitsofmeasure.org", code: "", }, valueCodeableConcept: observation.valueCodeableConcept || null, valueString: observation.valueString || "", valueBoolean: observation.valueBoolean || false, valueInteger: observation.valueInteger || 0, valueRange: observation.valueRange || null, valueRatio: observation.valueRatio || null, valueTime: observation.valueTime || "", valueDateTime: observation.valueDateTime ? observation.valueDateTime.slice(0, 16) : "", valuePeriod: observation.valuePeriod || null, dataAbsentReason: observation.dataAbsentReason || null, interpretation: observation.interpretation || [], bodySite: observation.bodySite || null, method: observation.method || null, device: observation.device || null, note: observation.note || [], }; // Handle components if (observation.component && observation.component.length > 0) { formValues.component = observation.component.map((comp, storedIndex) => ({ id: crypto.randomUUID(), storedIndex, code: comp.code || { coding: [{ system: "http://loinc.org", code: "", display: "" }], text: "", }, valueType: comp.valueQuantity ? "quantity" : comp.valueCodeableConcept ? "codeableConcept" : "quantity", valueQuantity: comp.valueQuantity ? { ...comp.valueQuantity, unit: unitChoice(comp.valueQuantity) } : { value: "", unit: "", system: "http://unitsofmeasure.org", code: "", }, valueCodeableConcept: comp.valueCodeableConcept || null, valueString: comp.valueString || "", valueBoolean: comp.valueBoolean || false, valueInteger: comp.valueInteger || 0, valueRange: comp.valueRange || null, valueRatio: comp.valueRatio || null, valueTime: comp.valueTime || "", valueDateTime: comp.valueDateTime || "", valuePeriod: comp.valuePeriod || null, dataAbsentReason: comp.dataAbsentReason || null, interpretation: comp.interpretation || [], isMissing: !comp.valueQuantity && !comp.valueCodeableConcept && !comp.valueString && comp.valueBoolean === undefined && comp.valueInteger === undefined, })); } // Handle performers if (observation.performer && observation.performer.length > 0) { formValues.performer = observation.performer.map((performer) => ({ reference: { reference: performer.reference, display: performer.display || "", }, referenceType: "Practitioner", })); } // Handle basedOn if (observation.basedOn && observation.basedOn.length > 0) { formValues.basedOn = observation.basedOn.map((basedOn) => ({ reference: { reference: basedOn.reference, display: basedOn.display || "", }, referenceType: basedOn.type || "CarePlan", })); } // Handle partOf if (observation.partOf && observation.partOf.length > 0) { formValues.partOf = observation.partOf.map((partOf) => ({ reference: { reference: partOf.reference, display: partOf.display || "", }, referenceType: partOf.type || "Procedure", })); } reset(formValues); setOpenSheet(true); }; // Empties a reading's unit without validating it, so a changed concept asks for its own unit and shows no error yet; // Save still refuses a number with no unit. const clearUnit = (quantity: "valueQuantity" | `component.${number}.valueQuantity`) => { setValue(`${quantity}.unit`, "", { shouldValidate: false }); setValue(`${quantity}.code`, "", { shouldValidate: false }); clearErrors(`${quantity}.unit`); }; const handleAddComponent = () => { componentFieldArray.append({ id: crypto.randomUUID(), code: { coding: [{ system: "http://loinc.org", code: "", display: "" }], text: "", }, valueType: "quantity", valueQuantity: { value: "", unit: "", system: "http://unitsofmeasure.org", code: "", }, valueCodeableConcept: null, valueString: "", valueBoolean: false, valueInteger: 0, valueRange: null, valueRatio: null, valueTime: "", valueDateTime: "", valuePeriod: null, dataAbsentReason: null, interpretation: [], isMissing: false, }); }; const handleRemoveComponent = (index: number) => componentFieldArray.remove(index); const handleAddPerformer = () => performerFieldArray.append({ reference: { reference: "", display: "" }, referenceType: "Practitioner", }); const handleRemovePerformer = (index: number) => performerFieldArray.remove(index); const handleAddBasedOn = () => basedOnFieldArray.append({ reference: { reference: "", display: "" }, referenceType: "CarePlan", }); const handleRemoveBasedOn = (index: number) => basedOnFieldArray.remove(index); const handleAddPartOf = () => partOfFieldArray.append({ reference: { reference: "", display: "" }, referenceType: "Procedure", }); const handleRemovePartOf = (index: number) => partOfFieldArray.remove(index); const handleAddNote = () => noteFieldArray.append({ text: "", time: new Date().toISOString() }); const handleRemoveNote = (index: number) => noteFieldArray.remove(index); const formatDateForFHIR = (dateString: string): string => { if (!dateString) return new Date().toISOString(); try { if (dateString.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(dateString)) { return dateString; } let normalized = dateString; if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(normalized)) { normalized = `${normalized}:00`; } const date = new Date(normalized); if (isNaN(date.getTime())) { console.warn("Invalid date:", dateString); return new Date().toISOString(); } return date.toISOString(); } catch (error) { console.error("Error formatting date:", error); return new Date().toISOString(); } }; // Helper: build a safe interpretation array - never sends empty coding[] const buildInterpretation = (interpArray: any[]): any[] | undefined => { if (!interpArray || interpArray.length === 0) return undefined; const results = interpArray .map((interp: any) => { const text = interp?.text || interp?.coding?.[0]?.display || ""; if (!text) return null; const entry: any = { text }; const coding = interp?.coding; if (Array.isArray(coding) && coding.length > 0 && coding[0]?.code) { entry.coding = coding; } return entry; }) .filter(Boolean); return results.length > 0 ? results : undefined; }; const onSubmit = async (data: any) => { if (sheetEndedRef.current) return; // This opening's save only: an answer after the sheet closed or moved on changes nothing here. const operation: SaveOperation = sheetScope.start(); // The operation this press sends under, so its answer settles that operation and no other. let createdUnder: string | undefined; setIsSaving(true); setSaveError(null); try { if (!patientState?.id) { throw new Error( "Patient ID is not available. Please ensure a patient is selected.", ); } const observationData: any = { resourceType: "Observation", status: data.status || "final", ...(isEditMode && editingObservationId ? { id: editingObservationId } : {}), code: keepStoredCode(isEditMode ? editingObservation?.code : undefined, { coding: [ { system: "http://loinc.org", code: data.code?.coding?.[0]?.code || "", display: data.code?.coding?.[0]?.display || "", }, ], text: data.code?.coding?.[0]?.display || data.code?.text || "", }), subject: { reference: `Patient/${patientState.id}` }, }; // Add profile if selected and not "none" if (data.observationProfile && data.observationProfile !== "none") { observationData.meta = { profile: [ `http://hl7.org/fhir/us/core/StructureDefinition/${data.observationProfile}`, ], }; } const stored = isEditMode ? editingObservation : undefined; observationData.effectiveDateTime = keepStoredInstant(stored?.effectiveDateTime, data.effectiveDateTime) ?? (data.effectiveDateTime ? formatDateForFHIR(data.effectiveDateTime) : new Date().toISOString()); // An edited reading that never had an issued time is not given one. const issued = keepStoredInstant(stored?.issued, data.issued) ?? (data.issued ? formatDateForFHIR(data.issued) : stored ? undefined : new Date().toISOString()); if (issued) observationData.issued = issued; // Category if ( data.category && data.category.length > 0 && data.category[0]?.coding?.[0]?.code ) { const categoryCode = data.category[0].coding[0].code; const selectedCategory = observationCategory.find( (cat) => cat.code === categoryCode, ); if (selectedCategory) { observationData.category = [ { coding: [ { system: selectedCategory.system || "http://terminology.hl7.org/CodeSystem/observation-category", code: selectedCategory.code, display: selectedCategory.display || "", }, ], text: selectedCategory.display || "", }, ]; } } // Interpretation (top-level) const topLevelInterp = buildInterpretation(data.interpretation); if (topLevelInterp) { observationData.interpretation = topLevelInterp; } // Value: only a number that was entered. A panel read through its components has none of its own. const enteredValue = enteredNumber(data.valueQuantity?.value); if (data.valueType === "quantity" && enteredValue !== undefined) { observationData.valueQuantity = keepStoredQuantity( isEditMode ? editingObservation?.valueQuantity : undefined, enteredValue, data.valueQuantity.unit || "", ); } else if (data.valueType === "string" && data.valueString) { observationData.valueString = data.valueString; } else if (data.valueType === "boolean") { observationData.valueBoolean = data.valueBoolean || false; } else if (data.valueType === "integer" && data.valueInteger) { observationData.valueInteger = Number(data.valueInteger) || 0; } else if ( data.valueType === "codeableConcept" && data.valueCodeableConcept?.text ) { observationData.valueCodeableConcept = { text: data.valueCodeableConcept.text, }; } // Encounter if (data.encounter?.reference) { observationData.encounter = { reference: data.encounter.reference }; } // Performers if (data.performer && data.performer.length > 0) { const validPerformers = data.performer .filter((p: any) => p?.reference?.reference) .map((p: any) => { const practitionerId = p.reference.reference.split("/")[1]; const selectedPractitioner = practitionersList.find( (prac) => prac.practitioner?.id === practitionerId, ); return { reference: p.reference.reference, display: formatFhirHumanName(selectedPractitioner?.practitioner?.name) || "Practitioner", }; }); if (validPerformers.length > 0) observationData.performer = validPerformers; } // Components // Each component the edit opened on keeps what is stored and changes only what the form changed. const components = componentsForSave( data.component, isEditMode ? editingObservation?.component : undefined, buildInterpretation, ); if (components.length > 0) observationData.component = components; // BasedOn if (data.basedOn && data.basedOn.length > 0) { const validBasedOn = data.basedOn.filter( (item: any) => item?.reference?.reference || item?.reference?.display, ); if (validBasedOn.length > 0) { observationData.basedOn = validBasedOn.map((item: any) => ({ reference: item.reference?.reference || "", display: item.reference?.display || "", type: item.referenceType || "CarePlan", })); } } // PartOf if (data.partOf && data.partOf.length > 0) { const validPartOf = data.partOf.filter( (item: any) => item?.reference?.reference || item?.reference?.display, ); if (validPartOf.length > 0) { observationData.partOf = validPartOf.map((item: any) => ({ reference: item.reference?.reference || "", display: item.reference?.display || "", type: item.referenceType || "Procedure", })); } } // Notes if (data.note && data.note.length > 0) { const validNotes = data.note.filter( (note: any) => note.text && note.text.trim() !== "", ); if (validNotes.length > 0) { observationData.note = validNotes.map((note: any) => ({ text: note.text, time: note.time || new Date().toISOString(), })); } } // Body site if (data.bodySite?.text && data.bodySite.text.trim() !== "") { observationData.bodySite = { text: data.bodySite.text }; } // Method if (data.method?.text && data.method.text.trim() !== "") { observationData.method = { text: data.method.text }; } // Device if (data.device?.reference) { const deviceId = data.device.reference.split("/")[1]; const selectedDevice = devicesList.find((dev) => dev.id === deviceId); observationData.device = { reference: data.device.reference, display: selectedDevice?.deviceName?.[0]?.name || "Device", }; } // Clean up undefined/null values const cleanObservation = JSON.parse(JSON.stringify(observationData)); Object.keys(cleanObservation).forEach((key) => { if ( cleanObservation[key] === undefined || cleanObservation[key] === null || (Array.isArray(cleanObservation[key]) && cleanObservation[key].length === 0) ) { delete cleanObservation[key]; } }); if (isEditMode && editingObservationId) { // Make sure the ID is in the object if (!observationData.id) { observationData.id = editingObservationId; } // Written only over the version this form was opened on. await operation.step(() => updateObservation(observationData, editingObservation?.meta?.versionId, operation.transport), true); } else { let operationId: string; try { operationId = await createOperation.begin(); } catch (error) { if (error instanceof RecoveryUnavailable || error instanceof RecoveryFull) { setSaveError(OPERATION_UNAVAILABLE_MESSAGE); return; } throw error; } createdUnder = operationId; // A confirmed answer settles its own operation, even one that arrives after the sheet moved on. await operation.step(async () => { const created = await createObservationOnce(observationData, operationId, operation.transport); createOperation.settled(operationId); return created; }, true); } reset(); setOpenSheet(false); setIsEditMode(false); setEditingObservationId(null); // Back to the list, but ONLY when this form was opened as its own URL. Saving from the list's // own sheet is already back on the list once the sheet closes. // // RELATIVE, NEVER "/observations". The route is `observations` nested under this patient, so the // absolute path matches nothing and the router falls through - which put a clinician on the // overview, off the chart they were working in, every time they saved. if (observationId) { navigate("..", { relative: "path" }); } } catch (err: unknown) { if (isSaveRetired(err) || !operation.belongs()) return; console.error("Error saving observation:", err); if (!isEditMode) { // A new reading keeps its operation until its answer is known: checked, finished or deliberately replaced. if (operationUnconfirmed(err) || staleReasonOf(err) === "unconfirmed") { createOperation.unconfirmed(createdUnder); return; } if (operationReused(err)) { setSaveError(OPERATION_REUSED_MESSAGE); createOperation.unconfirmed(createdUnder); return; } // Fenced unsaved by the server: the same operation is checked, then finished, never sent again blind. if (operationNotYetAdmitted(err)) { setSaveError(OPERATION_NOT_SAVED_MESSAGE); createOperation.unconfirmed(createdUnder); return; } if (createdUnder) createOperation.settled(createdUnder); } // Refused, out of date, or sent with no answer of ours: this opening saves nothing more. const ended = staleReasonOf(err); if (ended) { setSheetEnded({ reason: ended }); return; } setSaveError( relayServerFailure(err, "Failed to save observation. Please try again."), ); } finally { if (operation.belongs()) setIsSaving(false); } }; const addObservation = () => { if (!patientState?.id) { setSaveError("Cannot create observation: No patient selected"); return; } newOpening(); void createOperation.opening(); setIsEditMode(false); setEditingObservationId(null); setEditingObservation(null); reset({ observationProfile: "none", status: "final", category: [], code: { coding: [{ system: "http://loinc.org", code: "", display: "" }], text: "", }, effectiveType: "dateTime", effectiveDateTime: toLocalDateTimeInput(new Date()), issued: toLocalDateTimeInput(new Date()), valueType: "quantity", valueQuantity: { value: "", unit: "", system: "http://unitsofmeasure.org", code: "", }, component: [], performer: [], basedOn: [], partOf: [], note: [], }); setSaveError(null); setOpenSheet(true); }; const editObservation = ( event: React.MouseEvent, observation: Observation, ) => { event.stopPropagation(); if (observationPractice(observation, readerOrganizationId).fromAnotherPractice) return; newOpening(); setIsEditMode(true); setEditingObservationId(observation.id!); loadObservationForEdit(observation); }; useFhirObservationBroadcast(); useFhirObservationTargetedBroadcast({ observation: selectedObservation || undefined, isOpen: showDisplayModal, onUpdate: (updatedObservation) => setSelectedObservation(updatedObservation), onDelete: () => { setShowDisplayModal(false); setSelectedObservation(null); }, }); const viewObservation = (observation: Observation) => { setSelectedObservation(observation); setShowDisplayModal(true); }; const getStatusDisplay = ( observation: Observation, ): { text: string; color?: string } => { if (!observation) return { text: "Unknown" }; switch (observation.status) { case "final": return { text: "Final", color: "#00a32a" }; case "preliminary": return { text: "Preliminary", color: "#e39700" }; case "amended": return { text: "Amended", color: "#4f94d4" }; case "corrected": return { text: "Corrected", color: "#4f94d4" }; case "cancelled": return { text: "Cancelled", color: "#cc1818" }; case "entered-in-error": return { text: "Error", color: "#cc1818" }; case "registered": return { text: "Registered", color: "#757575" }; default: return { text: observation.status.charAt(0).toUpperCase() + observation.status.slice(1).replace(/-/g, " "), }; } }; const getObservationValue = (observation: Observation): string => { // A blood pressure is its two halves, whatever else the stored panel carries. const pressure = bloodPressureReading(observation); if (pressure) return pressure; if (observation.valueQuantity) { return `${observation.valueQuantity.value} ${ observation.valueQuantity.unit || "" }`; } else if (observation.valueCodeableConcept) { return formatFhirCodeableConcept(observation.valueCodeableConcept); } else if (observation.valueString) { return observation.valueString; } else if (observation.valueBoolean !== undefined) { return observation.valueBoolean ? "Yes" : "No"; } else if (observation.valueInteger !== undefined) { return observation.valueInteger.toString(); } else if (observation.valueRange) { const low = observation.valueRange.low?.value; const high = observation.valueRange.high?.value; const unit = observation.valueRange.low?.unit || observation.valueRange.high?.unit || ""; return `${low || ""} - ${high || ""} ${unit}`; } else if (observation.valueRatio) { return `${observation.valueRatio.numerator?.value || ""}/${ observation.valueRatio.denominator?.value || "" }`; } else if (observation.valueTime) { return observation.valueTime; } else if (observation.valueDateTime) { return formatDate(observation.valueDateTime); } else if (observation.component && observation.component.length > 0) { return componentReadings(observation) ?? `${observation.component.length} components`; } return ""; }; const getCategoryDisplay = (observation: Observation): string => { if (!observation.category || observation.category.length === 0) return ""; // Prefer CodeableConcept.text, but fall back to the coding display so // observations that only carry coding[].display (e.g. patient self-entered // readings) still show a category heading like "Vital Signs" / "Laboratory". return observation.category .map((cat) => formatFhirCodeableConcept(cat) || getCodeableConceptDisplay(cat)) .filter(Boolean) .join(", "); }; // Who recorded this reading, derived from Observation.performer - a patient/proxy // self-entry vs. a clinician. Returns "" when no performer is recorded (older data). const getRecordedBy = (observation: Observation): string => { const performer = observation.performer?.[0]; const reference = performer?.reference ?? ""; if (reference.startsWith("Patient/")) return "Patient-entered"; if (reference.startsWith("RelatedPerson/")) return "Caregiver-entered"; if (reference.startsWith("Practitioner")) { return performer?.display ? `Entered by ${performer.display}` : "Clinician-entered"; } return ""; }; // Most recent height reading (LOINC 8302-2), used to auto-derive BMI when a patient // logs a weight. observationsList is NOT guaranteed newest-first - new/updated entries // arrive via the WebSocket broadcast handler and are appended to the end of the array // (see useFhirObservationBroadcast) - so sort by effectiveDateTime explicitly. const recentHeight = (() => { const heights = (observationsList ?? []) .filter( (o) => o.code?.coding?.some( (c) => c.system === "http://loinc.org" && c.code === "8302-2", ) && o.valueQuantity?.value != null, ) .slice() .sort((a, b) => { const at = a.effectiveDateTime ? new Date(a.effectiveDateTime).getTime() : 0; const bt = b.effectiveDateTime ? new Date(b.effectiveDateTime).getTime() : 0; return bt - at; }); const latest = heights[0]; return latest?.valueQuantity ? { value: latest.valueQuantity.value as number, unitCode: (latest.valueQuantity.code as string) ?? "[in_i]", } : undefined; })(); // Map an existing curated Observation back to the simple form's input shape for edit // prefill, matching on LOINC code against PATIENT_MEASUREMENTS. Returns undefined // (fresh picker) if it isn't a curated reading - shouldn't happen for patient-created // data, but staff-entered data may use codes outside the curated set. const toSimpleInitial = ( obs: Observation | null, ): PatientReadingInput | undefined => { if (!obs) return undefined; const measurement = isBloodPressurePanel(obs) ? PATIENT_MEASUREMENTS.find((x) => x.inputType === "bp") : PATIENT_MEASUREMENTS.find((x) => obs.code?.coding?.some((c) => c.system === "http://loinc.org" && c.code === x.loinc)); if (!measurement) return undefined; // An edit keeps what was recorded; with no recorded time the form asks for one rather than taking now. const eff = obs.effectiveDateTime ?? ''; if (measurement.inputType === "bp") { const sys = obs.component?.find((c) => c.code?.coding?.some((cc) => cc.code === "8480-6") && !c.code?.coding?.some((cc) => cc.code === "8462-4"), )?.valueQuantity?.value; const dia = obs.component?.find((c) => c.code?.coding?.some((cc) => cc.code === "8462-4") && !c.code?.coding?.some((cc) => cc.code === "8480-6"), )?.valueQuantity?.value; return { measurementKey: measurement.key, effectiveDateTime: eff, systolic: sys as number, diastolic: dia as number, note: obs.note?.[0]?.text, }; } return { measurementKey: measurement.key, effectiveDateTime: eff, value: obs.valueQuantity?.value as number, unitCode: obs.valueQuantity?.code as string, note: obs.note?.[0]?.text, }; }; const simpleInitial = isSimple ? toSimpleInitial(editingObservation) : undefined; // Mirrors onSubmit's try/catch/finally shape and reuses the same create/update calls. // No separate list-refresh call is needed: useFhirObservationBroadcast() (wired below, // active for the whole component regardless of which form submits) subscribes to the // server's observation.created/updated WebSocket events and updates observationsList // itself once the server confirms the write - the same mechanism the full onSubmit // already relies on. // Reload record: an edit reads what is saved now and opens it, discarding the draft (the notice says so first), // never older than the list's copy. A new reading has no record to reload: the sheet closes on the list. const reloadSheet = async () => { const id = editingObservationId; if (!isEditMode || !id) { setSheetOpen(false); return; } const operation = sheetScope.start(); setReloading(true); setSaveError(null); try { const fresh = await operation.step(() => fetchObservation(id, true, operation.transport)); const listed = observationsList.find((obs) => obs.id === id); newOpening(); loadObservationForEdit(listed && olderThan(fresh, listed) ? listed : fresh); } catch (reloadError) { if (isSaveRetired(reloadError) || !operation.belongs()) return; setSaveError("The record could not be read. Try Reload record again."); } finally { if (operation.belongs()) setReloading(false); } }; const handleSimpleSave = async (observations: Observation[], report?: SelfReport) => { if (!observations.length || sheetEndedRef.current) return; // A patient's own report goes to its route for the reading itself, and a change to it to that report's edit; a // derived BMI stays an ordinary record. const selfReport = report && patientReportsForSelf && (!isEditMode || editingOwnReport) ? report : undefined; // This opening's save only: an answer after the sheet closed or moved on changes nothing here. const operation: SaveOperation = sheetScope.start(); setIsSaving(true); setSaveError(null); // An edited blood pressure is written over its stored record, keeping what this form cannot show, or refused. const pairEdit = isEditMode && editingObservation && isBloodPressurePanel(editingObservation) ? observations.map((obs) => (isBloodPressurePanel(obs) ? patientPairEdit(editingObservation, obs) : { refused: MEASUREMENT_CHANGE_REFUSED })) : undefined; const refusedPair = pairEdit?.find((edit): edit is { refused: string } => "refused" in edit); if (refusedPair) { setSaveError(refusedPair.refused); setIsSaving(false); return; } if (pairEdit) observations = pairEdit.map((edit) => (edit as { observation: Observation }).observation); // The readings confirmed so far, by name: a later one failing never reads as nothing saved. const saved: string[] = []; let current: Observation | undefined; try { for (const obs of observations) { current = obs; if ( isEditMode && editingObservationId && selfReport && obs.code?.coding?.[0]?.code !== "39156-5" ) { const savedReport = await operation.step(() => editSelfReport(editingObservationId, selfReport, editingObservation?.meta?.versionId, operation.transport), true); // No broadcast reaches a patient with no practice: the saved version is shown at once, and the list reads again. if (operation.belongs()) { const forPatient = patientState?.id; if (forPatient && savedReport.resourceType === "Observation" && savedReport.id === editingObservationId) { setSavedReports((prev) => ({ patientId: forPatient, byId: { ...(prev.patientId === forPatient ? prev.byId : {}), [editingObservationId]: savedReport as Observation }, })); } void refreshObservations(); } } else if ( isEditMode && editingObservationId && obs.code?.coding?.[0]?.code !== "39156-5" ) { await operation.step(() => updateObservation({ ...obs, id: editingObservationId }, editingObservation?.meta?.versionId, operation.transport), true); } else if (selfReport && obs.code?.coding?.[0]?.code !== "39156-5") { await operation.step(() => recordSelfReport(patientState?.id as string, selfReport, operation.transport), true); // As for an edit: no broadcast reaches a patient with no practice, so the list reads again and shows the held record. if (operation.belongs()) void refreshObservations(); } else { // Each new reading is its own save under its own operation id, held in the tab until it is answered. let operationId: string; try { operationId = await createOperation.begin(); } catch (holdError) { if (holdError instanceof RecoveryUnavailable || holdError instanceof RecoveryFull) { throw Object.assign(new Error(OPERATION_UNAVAILABLE_MESSAGE), { unheld: true }); } throw holdError; } try { // A confirmed answer settles its own operation, even one that arrives after the sheet moved on. await operation.step(async () => { const created = await createObservationOnce(obs, operationId, operation.transport); createOperation.settled(operationId); return created; }, true); } catch (createError) { // Refused with nothing written: no longer held. Unconfirmed, not yet admitted or a reused id: held, and checked next. const held = operationNotYetAdmitted(createError) || operationReused(createError); if (!isSaveRetired(createError) && !operationUnconfirmed(createError) && staleReasonOf(createError) !== "unconfirmed" && !held) createOperation.settled(operationId); if (!isSaveRetired(createError) && held) createOperation.unconfirmed(operationId); throw createError; } } saved.push(readingName(obs)); } setSheetOpen(false); setIsEditMode(false); setEditingObservationId(null); setEditingObservation(null); // Same rule as the full form above: relative, and only when this was its own URL. if (observationId) { navigate("..", { relative: "path" }); } } catch (err: unknown) { if (isSaveRetired(err) || !operation.belongs()) return; console.error("Error saving observation:", err); if ((err as { unheld?: boolean } | null)?.unheld && !saved.length) { setSaveError(OPERATION_UNAVAILABLE_MESSAGE); return; } const ended = staleReasonOf(err) ?? (operationUnconfirmed(err) ? "unconfirmed" : undefined); // Something is on record already: name it and what did not finish, and send nothing more from this opening. if (saved.length) { setSheetEnded({ reason: ended ?? 'unconfirmed', message: readingsSavedSoFar(saved, readingName(current as Observation), ended === 'unconfirmed' || operationReused(err)) }); return; } if (ended) { setSheetEnded({ reason: ended }); return; } if (operationNotYetAdmitted(err)) { setSaveError(OPERATION_NOT_SAVED_MESSAGE); return; } if (operationReused(err)) { setSaveError(OPERATION_REUSED_MESSAGE); return; } setSaveError( relayServerFailure(err, "Could not save your reading. Please try again."), ); } finally { if (operation.belongs()) setIsSaving(false); } }; // An earlier new reading, checked or finished: saved closes the sheet as a save does; refused leaves it open to enter. const settleEarlier = async (outcome: Promise) => { const result = await outcome; if (result?.saved) { reset(); setOpenSheet(false); if (observationId) navigate("..", { relative: "path" }); } else if (result) { setSaveError("The earlier save was refused, so nothing was saved from it."); } }; // The one notice for an opening that saves nothing more; a new reading has no record to reload, so it closes instead. const sheetNotice = ( void reloadSheet()} reloading={reloading} /> ); return ( <>

Readings & measurements

Readings and measurements in your record, whoever recorded them

{!refused && !proxyCannotAdd && (
{!isSimple && patientState?.id && ( )}
)} {!isSimple && ( Record vitals Readings taken here are recorded as this practice's own when you answer "Measured here". A reading copied from another record is kept, but a records release leaves it out.
)} {isEditMode ? "Edit Observation" : "New Observation"} {isSimple ? (
{sheetNotice} {!isEditMode && createOperation.earlier && ( void settleEarlier(createOperation.check())} onFinish={() => void settleEarlier(createOperation.finish())} onStartNew={() => { setSaveError(null); createOperation.startNew(); }} /> )} setSheetOpen(false)} />
) : (
{/* The record-changed notice is pinned with the actions, under them: pinned on its own it covered Cancel and Update. */}
{sheetEnded &&
{sheetNotice}
} {!isEditMode && createOperation.earlier && (
void settleEarlier(createOperation.check())} onFinish={() => void settleEarlier(createOperation.finish())} onStartNew={() => { setSaveError(null); createOperation.startNew(); }} />
)}
{saveError && ( {saveError} )} {(() => { const origin = isEditMode ? releasableOriginOf(editingObservation) : undefined; const note = !isEditMode ? GENERIC_ENTRY_HELD_NOTE : origin ? editDropsSourceNote(origin) : undefined; return note &&

{note}

; })()} {/* Observation Profile Selection */}
( )} /> {(!observationProfile || observationProfile === "none") && (

No observation profile selected. Some features may be limited.

)}
( )} /> {errors.status && (

{errors.status.message as string}

)}
( )} />
{ // A unit belongs to the concept it was chosen for: another concept starts unchosen. if (coding.code !== getValues("code.coding.0.code")) clearUnit("valueQuantity"); setValue("code.coding.0.system", coding.system, { shouldDirty: true, }); setValue("code.coding.0.code", coding.code, { shouldValidate: true, shouldDirty: true, }); setValue("code.coding.0.display", coding.display, { shouldValidate: true, shouldDirty: true, }); }} />
( )} /> ( )} />
( )} /> {(() => { const codeErrors = errors.code as | { coding?: Array<{ code?: { message?: string }; display?: { message?: string }; }>; } | undefined; const codeMessage = codeErrors?.coding?.[0]?.code?.message || codeErrors?.coding?.[0]?.display?.message; return codeMessage ? (

{codeMessage}

) : null; })()}

Enter a LOINC code and display name for the observation type

( )} />
{effectiveType === "dateTime" && (
( )} />
)} {effectiveType === "period" && ( <>
( )} />
( )} />
)}
( )} />
{encountersList.length > 0 && (

The visit this reading was taken at. Leave it empty if it was not taken at one.

( )} />
)}
( )} />
Value & Components Details Performers Based On Part Of Notes

Value

Enter the observation value

( )} />
{valueType === "quantity" && (
enteredNumber(value) !== undefined || hasComponentValue(getValues("component")) || "Enter a value, or a value for a component", }} render={({ field }) => ( field.onChange( e.target.value, ) } /> )} /> {(() => { const valueErrors = errors.valueQuantity as | { value?: { message?: string }; } | undefined; const message = valueErrors?.value?.message; return message ? (

{message}

) : null; })()}
!!unit || enteredNumber(getValues("valueQuantity.value")) === undefined || "Unit is required", }} render={({ field }) => ( )} /> {(errors.valueQuantity as { unit?: { message?: string } } | undefined)?.unit?.message && (

{(errors.valueQuantity as { unit?: { message?: string } }).unit?.message}

)}
)} {valueType === "codeableConcept" && (
( field.onChange({ text: e.target.value }) } /> )} />
)} {valueType === "string" && (
(