import React, { useState, useEffect } from 'react'; import { Card, CardHeader, CardBody, CardDivider } from '../ui/wp-card'; import { Elevation } from '../ui/elevation'; import { InputControl } from '../ui/input-control'; import { Divider } from '../ui/divider'; import { DismissibleAlert } from '../ui/dismissible-alert'; import { Heading } from '../ui/heading'; import { Flex, FlexItem } from '../ui/flex'; import { Text } from '../ui/text'; import { Spacer } from '../ui/spacer'; import { TabPanel } from '../ui/tab-panel'; import { Disabled } from '../ui/disabled'; import { useFormContext, Controller, useFieldArray } from 'react-hook-form'; import { IValueSet, useAuthServiceContext, usePatientServiceContext, useValueSetServiceContext } from '../../services'; import { FhirCode, FhirCodeableConcept, FhirPeriod, FhirReference, FhirReferenceCollection, FhirValueSetSearch } from '../../components'; import { useFhirAppointments, useFhirConditions, useFhirEncounterBroadcast, useFhirEncounters, useFhirEpisodesOfCare, useFhirImmunizationRecommendations, useFhirLocations, useFhirObservations, useFhirPractitioners, useFhirProcedures, useFhirRelatedPersons } from '../../hooks'; import { Encounter, FhirResource } from 'fhir/r4'; import { ExternalLink, PlusCircle, Trash2 } from 'lucide-react'; import { useNavigate, useParams } from 'react-router-dom'; interface FhirEncounterProps { encounter?: Encounter | undefined; isNew?: boolean; elevated?: boolean; disabled?: boolean; enableInlineActions?: boolean; } const DiagnosisCondition: React.FC<{ index: number; disabled: boolean; readonly: boolean; enableInlineActions: boolean; }> = ({ index, disabled, readonly = false, enableInlineActions = true }) => { const { control } = useFormContext(); const itemReferenceFieldArray = useFieldArray({ control, name: `diagnosis.${index}.condition` }); const { conditionsList } = useFhirConditions(); const { proceduresList } = useFhirProcedures(); return ( { switch (referenceType) { case 'Condition': return conditionsList; case 'Procedure': return proceduresList; default: return []; } }} required={true} disabled={disabled} readonly={readonly} enableInlineCreation={false} multiple={false} onInlineCreate={(referenceType) => { console.log(`Create new ${referenceType}`); }}/> ); }; const ParticipantIndividual: React.FC<{ index: number; disabled: boolean; readonly: boolean; enableInlineActions: boolean; }> = ({ index, disabled, readonly = false, enableInlineActions = true }) => { const { control } = useFormContext(); const itemReferenceFieldArray = useFieldArray({ control, name: `participant.${index}.individual` }); const { practitionersList } = useFhirPractitioners(); const { relatedPersonsList } = useFhirRelatedPersons(); return ( { switch (referenceType) { case 'Practitioner': return practitionersList.map(p => p.practitioner); case 'RelatedPerson': return relatedPersonsList; default: return []; } }} required={false} disabled={disabled} readonly={readonly} enableInlineCreation={false} multiple={false} onInlineCreate={(referenceType) => { console.log(`Create new ${referenceType}`); }}/> ); }; export const FhirEncounter: React.FC = ({ encounter, isNew, elevated = true, disabled = false, enableInlineActions = true }) => { const { id } = useParams<{ id: string; encounterId: string; appointmentId: string; }>(); const navigate = useNavigate(); const { control } = useFormContext(); const { userRole, USER_ROLES } = useAuthServiceContext(); const { encounterState, setEncounterState } = usePatientServiceContext(); const { fetchValueSet, valueSetUrls } = useValueSetServiceContext(); const [loading, setLoading] = useState(false); // value sets const [encounterStatus, setEncounterStatus] = useState([]); const [actEncounterCode, setActEncounterCode] = useState([]); const [usCoreEncounterType, setUsCoreEncounterType] = useState([]); const [serviceType, setServiceType] = useState([]); const [actPriority, setActPriority] = useState([]); const [encounterParticipantType, setEncounterParticipantType] = useState([]); const [diagnosisRole, setDiagnosisRole] = useState([]); const [encounterAdmitSource, setEncounterAdmitSource] = useState([]); const [reAdmissionIndicator, setReAdmissionIndicator] = useState([]); const [encounterDiet, setEncounterDiet] = useState([]); const [encounterSpecialCourtesy, setEncounterSpecialCourtesy] = useState([]); const [encounterSpecialArrangements, setEncounterSpecialArrangements] = useState([]); const [encounterDischargeDisposition, setEncounterDischargeDisposition] = useState([]); const [encounterLocationStatus, setEncounterLocationStatus] = useState([]); const [locationPhysicalType, setLocationPhysicalType] = useState([]); const [interpreterRequired, setInterpreterRequired] = useState([]); const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes(userRole as string); const spacerMargin = '24px'; useFhirEncounterBroadcast(); const { encountersList } = useFhirEncounters(); const { episodesOfCareList } = useFhirEpisodesOfCare(); const { conditionsList } = useFhirConditions(); const { proceduresList } = useFhirProcedures(); const { observationsList } = useFhirObservations(); const { immunizationRecommendationsList } = useFhirImmunizationRecommendations(); const { locationsList } = useFhirLocations(); const { appointmentsList } = useFhirAppointments(); const statusHistoryFieldArray = useFieldArray({ control, name: 'statusHistory' }); const classHistoryFieldArray = useFieldArray({ control, name: 'classHistory' }); const participantFieldArray = useFieldArray({ control, name: 'participant' }); const reasonReferenceFieldArray = useFieldArray({ control, name: 'reasonReference' }); const diagnosisFieldArray = useFieldArray({ control, name: 'diagnosis' }); const locationFieldArray = useFieldArray({ control, name: 'location' }); useEffect(() => { const loadValueSets = async () => { setLoading(true); try { const [ encounterStatusValueSet, actEncounterCodeValueSet, usCoreEncounterTypeValueSet, serviceTypeValueSet, actPriorityValueSet, encounterParticipantTypeValueSet, diagnosisRoleValueSet, encounterAdmitSourceValueSet, reAdmissionIndicatorValueSet, encounterDietValueSet, encounterSpecialCourtesyValueSet, encounterSpecialArrangementsValueSet, encounterDischargeDispositionValueSet, encounterLocationStatusValueSet, locationPhysicalTypeValueSet, interpreterRequiredValueSet ] = await Promise.all([ fetchValueSet(valueSetUrls.encounterStatus), fetchValueSet(valueSetUrls.actEncounterCode), fetchValueSet(valueSetUrls.usCoreEncounterType), fetchValueSet(valueSetUrls.serviceType), fetchValueSet(valueSetUrls.actPriority), fetchValueSet(valueSetUrls.encounterParticipantType), fetchValueSet(valueSetUrls.diagnosisRole), fetchValueSet(valueSetUrls.encounterAdmitSource), fetchValueSet(valueSetUrls.reAdmissionIndicator), fetchValueSet(valueSetUrls.encounterDiet), fetchValueSet(valueSetUrls.encounterSpecialCourtesy), fetchValueSet(valueSetUrls.encounterSpecialArrangements), fetchValueSet(valueSetUrls.encounterDischargeDisposition), fetchValueSet(valueSetUrls.encounterLocationStatus), fetchValueSet(valueSetUrls.locationPhysicalType), fetchValueSet(valueSetUrls.interpreterRequired) ]); setEncounterStatus(encounterStatusValueSet); setActEncounterCode(actEncounterCodeValueSet); setUsCoreEncounterType(usCoreEncounterTypeValueSet); setServiceType(serviceTypeValueSet); setActPriority(actPriorityValueSet); setEncounterParticipantType(encounterParticipantTypeValueSet); setDiagnosisRole(diagnosisRoleValueSet); setEncounterAdmitSource(encounterAdmitSourceValueSet); setReAdmissionIndicator(reAdmissionIndicatorValueSet); setEncounterDiet(encounterDietValueSet); setEncounterSpecialCourtesy(encounterSpecialCourtesyValueSet); setEncounterSpecialArrangements(encounterSpecialArrangementsValueSet); setEncounterDischargeDisposition(encounterDischargeDispositionValueSet); setEncounterLocationStatus(encounterLocationStatusValueSet); setLocationPhysicalType(locationPhysicalTypeValueSet); setInterpreterRequired(interpreterRequiredValueSet); } catch (err) { console.error('Error loading value sets:', err); } finally { setLoading(false); } }; loadValueSets(); }, [fetchValueSet, valueSetUrls]); const addStatusHistory = () => { statusHistoryFieldArray.append({ status: '', }); }; const removeStatusHistory = (index: number) => { statusHistoryFieldArray.remove(index); }; const addClassHistory = () => { classHistoryFieldArray.append({ class: null }); }; const removeClassHistory = (index: number) => { classHistoryFieldArray.remove(index); }; const addParticipant = () => { participantFieldArray.append({ type: [], individual: null }); }; const removeParticipant = (index: number) => { participantFieldArray.remove(index); }; const addDiagnosis = () => { diagnosisFieldArray.append({ condition: null, use: null, rank: null }); }; const removeDiagnosis = (index: number) => { diagnosisFieldArray.remove(index); }; const addLocation = () => { locationFieldArray.append({ location: null, status: '', physicalType: null }); }; const removeLocation = (index: number) => { locationFieldArray.remove(index); }; // (design-pass finding B37) - THIS BUTTON NEVER CONSULTED STATUS. // // The render condition below is `!isNew && encounter && !isPatient && id && !encounterState` - status // is absent from it, so the button appeared identically on `planned`, `in-progress` and `finished` // encounters. On a finished one it invited appending clinical data to a CLOSED encounter, presented as // routine, under the copy "Clinical data added will be linked to this encounter". // // SPENCER RULED (b), 2026-08-12: amending a closed encounter STAYS - late addenda are real clinical // practice - but it must NAME what it does and CONFIRM first. It is not hidden, and it is not silent. // // Deliberately NOT a status write: this is pure client-side navigation (it was traced as such on the // card). Nothing here reopens the encounter; `reopen` in useEncounterActions is the separate, // deliberate action that does, and it rebuilds `period` precisely because this one must not. const isFinishedEncounter = encounter?.status === 'finished'; const startEncounter = () => { if (!encounter) return; if (isFinishedEncounter) { const proceed = window.confirm( 'This visit is already finished.\n\n' + 'Anything you add will be recorded against a closed encounter, and that is visible in the ' + 'record as a later addition.\n\nAdd to this finished visit?', ); if (!proceed) return; } setEncounterState(encounter); navigate(`/dashboard/encounters/${encounter.id}/patients/${id}/management/encounters/${encounter.id}`); }; return ( {elevated && } Details {!isNew && encounter && !isPatient && id && !encounterState && ( // {isFinishedEncounter ? 'This visit is finished — anything you add is recorded as a later addition' : 'Clinical data added will be linked to this encounter'} )} {/* status */} {/* class */} {/* type */} {/* serviceType */} {/* priority */} {/* interpreterRequired */} {/* appointment */} {/* partOf */} {/* episodeOfCare */} {/* period */} {(tab) => { // reason if (tab.name === 'reason') { return (
{/* reasonCode */} {/* reasonReference */} { switch (referenceType) { case 'Condition': return conditionsList; case 'Procedure': return proceduresList; case 'Observation': return observationsList; case 'ImmunizationRecommendation': return immunizationRecommendationsList; default: return []; } }} multiple={true} required={false} readonly={isPatient} disabled={disabled} enableInlineActions={enableInlineActions}/>
); } // participants if (tab.name === 'participants') { return (
{participantFieldArray.fields.length === 0 && ( <> Participants No participants added yet. )} {participantFieldArray.fields.map((field, index) => (

Participant {index + 1}

{!isPatient && ( )}
{/* individual */} {/* type */} {/* period */}
))} {!isPatient && participantFieldArray.fields.length > 0 && ( )}
); } // diagnosis if (tab.name === 'diagnosis') { return (
{diagnosisFieldArray.fields.length === 0 && ( <> The list of diagnosis relevant to this encounter {!isPatient && ( )} No diagnoses added yet. )} {diagnosisFieldArray.fields.map((field, index) => (

Diagnosis {index + 1}

{!isPatient && ( )}
{/* diagnosis.condition */} {/* diagnosis.use */} {/* diagnosis.rank */} ( )}/>
))} {!isPatient && diagnosisFieldArray.fields.length > 0 && ( )}
); } // locations if (tab.name === 'locations') { return (
{locationFieldArray.fields.length === 0 && ( <> List of locations where the patient has been {!isPatient && ( )} No locations added yet. )} {locationFieldArray.fields.map((field, index) => (

Location {index + 1}

{!isPatient && ( )}
{/* location */} {/* status */} {/* physicalType */} {/* period */}
))} {!isPatient && locationFieldArray.fields.length > 0 && ( )}
); } // hospitalization if (tab.name === 'hospitalization') { return (
{/* origin */} {/* admitSource */} {/* reAdmission */} {/* dietPreference */} {/* specialCourtesy */} {/* specialArrangement */} {/* destination */} {/* dischargeDisposition */}
); } // statusHistory if (tab.name === 'statusHistory') { return (
{statusHistoryFieldArray.fields.length === 0 && ( <> List of past encounter statuses {!isPatient && ( )} No status history added yet. )} {statusHistoryFieldArray.fields.map((field, index) => (

Status History {index + 1}

{!isPatient && ( )}
{/* status */} {/* period */}
))} {!isPatient && statusHistoryFieldArray.fields.length > 0 && ( )}
); } // classHistory if (tab.name === 'classHistory') { return (
{classHistoryFieldArray.fields.length === 0 && ( <> List of past encounter classes {!isPatient && ( )} No class history added yet. )} {classHistoryFieldArray.fields.map((field, index) => (

Class History {index + 1}

{!isPatient && ( )}
{/* class */} {/* period */}
))} {!isPatient && classHistoryFieldArray.fields.length > 0 && ( )}
); } }}
); }; export default FhirEncounter;