From 26890352f20597c8e39cf0ba188b7b74a8644eb5 Mon Sep 17 00:00:00 2001 From: bxr1nG Date: Fri, 17 Apr 2026 18:53:49 +0300 Subject: [PATCH] Revert "Merge pull request #6785 from masslight/feature/followup-visit-tracking-board" This reverts commit e14c56e24c0ed07da001560f64b4aae98216ce21, reversing changes made to 8eb1f2c2ce05272d0da5f561d97d7b0839e700cb. --- .../src/components/AppointmentTableRow.tsx | 36 +- .../components/AppointmentTableRowMobile.tsx | 3 +- .../src/components/PatientEncountersGrid.tsx | 24 +- .../components/labs-orders/LabBreadcrumbs.tsx | 8 +- .../pages/CreateExternalLabOrder.tsx | 17 +- .../components/InHouseLabsBreadcrumbs.tsx | 9 +- .../pages/InHouseLabOrderCreatePage.tsx | 9 +- .../in-house-labs/pages/InHouseLabsPage.tsx | 7 +- .../nursing-orders/components/BreadCrumbs.tsx | 6 +- .../pages/NursingOrdersPage.tsx | 7 +- .../components/RadiologyBreadcrumbs.tsx | 9 +- .../radiology/pages/CreateRadiologyOrder.tsx | 9 +- .../components/EncounterSwitcher.tsx | 49 +- .../visits/in-person/components/Header.tsx | 17 +- .../progress-note/VisitDetailsContainer.tsx | 5 +- .../context/InPersonNavigationContext.tsx | 4 +- .../visits/in-person/routing/helpers.ts | 22 - .../shared/components/OrdersIconsToolTip.tsx | 40 +- .../shared/components/VitalsIconTooltip.tsx | 2 +- .../components/patient/AddPatientFollowup.tsx | 33 +- .../patient/PatientFollowupForm.tsx | 477 +++++++++--------- .../ScheduledFollowupParentSelector.tsx | 111 ---- .../components/patient/useParentEncounters.ts | 102 ---- .../components/review-tab/MissingCard.tsx | 12 +- .../stores/appointment/appointment.queries.ts | 2 +- .../stores/appointment/appointment.store.tsx | 138 +---- .../appointment/parser/extractors.test.ts | 114 ----- .../stores/appointment/parser/extractors.ts | 33 +- .../utils/appointment-accessibility.helper.ts | 3 +- .../visits/telemed/utils/appointments.test.ts | 85 ---- .../visits/telemed/utils/appointments.ts | 30 +- apps/ehr/src/pages/AddPatient.tsx | 65 +-- .../component/AddPatientFollowup.test.tsx | 123 ----- .../in-person/scheduled-followup.spec.ts | 156 ------ packages/utils/lib/fhir/encounter.test.ts | 143 ------ packages/utils/lib/fhir/encounter.ts | 104 +--- .../utils/lib/types/api/encounter.types.ts | 3 +- .../types/api/patient-visit-history.types.ts | 3 - .../prebook-create-appointment.types.ts | 1 - .../data/appointments/appointments.types.ts | 3 - packages/utils/lib/utils/scheduleUtils.ts | 4 +- .../zambdas/src/ehr/get-appointments/index.ts | 11 +- .../ehr/patient-visit-history/get/index.ts | 45 +- .../src/ehr/practice-kpis-report/index.ts | 5 +- .../ehr/save-followup-encounter/helpers.ts | 12 +- .../visit-details/get-visit-details/index.ts | 4 +- .../src/ehr/visits-overview-report/index.ts | 4 +- .../appointment/create-appointment/index.ts | 39 -- .../validateRequestParameters.ts | 6 +- .../appointment/get-past-visits/helpers.ts | 4 +- .../appointment/get-visit-details/index.ts | 3 +- .../zambdas/src/patient/check-in/index.ts | 4 +- .../update-paperwork-in-progress/index.ts | 4 +- 53 files changed, 437 insertions(+), 1732 deletions(-) delete mode 100644 apps/ehr/src/features/visits/shared/components/patient/ScheduledFollowupParentSelector.tsx delete mode 100644 apps/ehr/src/features/visits/shared/components/patient/useParentEncounters.ts delete mode 100644 apps/ehr/src/features/visits/shared/stores/appointment/parser/extractors.test.ts delete mode 100644 apps/ehr/src/features/visits/telemed/utils/appointments.test.ts delete mode 100644 apps/ehr/tests/component/AddPatientFollowup.test.tsx delete mode 100644 apps/ehr/tests/e2e/specs/in-person/scheduled-followup.spec.ts delete mode 100644 packages/utils/lib/fhir/encounter.test.ts diff --git a/apps/ehr/src/components/AppointmentTableRow.tsx b/apps/ehr/src/components/AppointmentTableRow.tsx index 16776f4c3d..ac1763dfc2 100644 --- a/apps/ehr/src/components/AppointmentTableRow.tsx +++ b/apps/ehr/src/components/AppointmentTableRow.tsx @@ -1,5 +1,4 @@ import { progressNoteIcon, startIntakeIcon } from '@ehrTheme/icons'; -import CallSplitIcon from '@mui/icons-material/CallSplit'; import ChatOutlineIcon from '@mui/icons-material/ChatOutlined'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; @@ -31,10 +30,6 @@ import { enqueueSnackbar } from 'notistack'; import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { FEATURE_FLAGS } from 'src/constants/feature-flags'; -import { - getAppointmentVisitDetailsUrl, - getInPersonUrlByAppointmentType, -} from 'src/features/visits/in-person/routing/helpers'; import { ROUTER_PATH } from 'src/features/visits/in-person/routing/routesInPerson'; import { VitalsIconTooltip } from 'src/features/visits/shared/components/VitalsIconTooltip'; import { getTelemedQuickTexts } from 'src/features/visits/telemed/utils/appointments'; @@ -580,7 +575,7 @@ export default function AppointmentTableRow({ }, oystehrZambda ); - navigate(getInPersonUrlByAppointmentType(appointment, 'patient-info')); + navigate(`/in-person/${appointment.id}/patient-info`); } catch (error) { console.error(error); enqueueSnackbar('An error occurred. Please try again.', { variant: 'error' }); @@ -607,7 +602,7 @@ export default function AppointmentTableRow({ const handleProgressNoteButton = async (): Promise => { setProgressNoteButtonLoading(true); try { - navigate(getInPersonUrlByAppointmentType(appointment, ROUTER_PATH.REVIEW_AND_SIGN)); + navigate(`/in-person/${appointment.id}/${ROUTER_PATH.REVIEW_AND_SIGN}`); } catch (error) { console.error(error); enqueueSnackbar('An error occurred. Please try again.', { variant: 'error' }); @@ -868,22 +863,15 @@ export default function AppointmentTableRow({ - - - - {patientName} - - - {appointment.isFollowUp && ( - - - - )} - + + + {patientName} + + {appointment.needsDOBConfirmation ? ( @@ -1032,7 +1020,7 @@ export default function AppointmentTableRow({ navigate(getAppointmentVisitDetailsUrl(appointment))} + onClick={() => navigate(`/visit/${appointment.id}`)} dataTestId={dataTestIds.dashboard.visitDetailsButton} > diff --git a/apps/ehr/src/components/AppointmentTableRowMobile.tsx b/apps/ehr/src/components/AppointmentTableRowMobile.tsx index b15eda7fd3..b890dc5f39 100644 --- a/apps/ehr/src/components/AppointmentTableRowMobile.tsx +++ b/apps/ehr/src/components/AppointmentTableRowMobile.tsx @@ -3,7 +3,6 @@ import { Box, capitalize, Grid, Modal, TableCell, TableRow, Typography } from '@ import { CSSProperties, ReactElement, useState } from 'react'; import { Link } from 'react-router-dom'; import { RecordAudioContainer } from 'src/features/visits/in-person/components/progress-note/RecordAudioContainer'; -import { getAppointmentVisitDetailsUrl } from 'src/features/visits/in-person/routing/helpers'; import { InPersonAppointmentInformation } from 'utils'; import { MOBILE_MODAL_STYLE } from '../constants'; import { ApptTab } from './AppointmentTabs'; @@ -51,7 +50,7 @@ export default function AppointmentTableRowMobile({ }} > - + = (props) => return encounter.office ? encounter.office : '-'; case 'status': { if (!encounter.status) return null; - // Scheduled follow-ups use encounter status directly (planned, arrived, etc.) - // Annotation follow-ups use OPEN/RESOLVED - if (encounter.followupSubtype === 'scheduled') { - return {encounter.status}; - } - return getFollowupStatusChip(getAnnotationFollowupStatusLabel(encounter.status)); + const statusVal = encounter.status === 'in-progress' ? 'OPEN' : 'RESOLVED'; + return getFollowupStatusChip(statusVal); } case 'note': { - const { encounterId, originalAppointmentId, followupSubtype } = encounter; - const pathSegment = getFollowUpProgressNotePathSegment(followupSubtype, encounter.status); - if (!pathSegment || !originalAppointmentId) return '-'; - const to = getInPersonUrlByAppointmentType( - { id: originalAppointmentId, encounterId, isFollowUp: true }, - pathSegment - ); + const { encounterId, originalAppointmentId } = encounter; + if (!originalAppointmentId) return '-'; + const to = `/in-person/${originalAppointmentId}/follow-up-note${ + encounterId ? `?encounterId=${encounterId}` : '' + }`; + return Progress Note; } default: diff --git a/apps/ehr/src/features/external-labs/components/labs-orders/LabBreadcrumbs.tsx b/apps/ehr/src/features/external-labs/components/labs-orders/LabBreadcrumbs.tsx index 89a2a718b6..decdfad406 100644 --- a/apps/ehr/src/features/external-labs/components/labs-orders/LabBreadcrumbs.tsx +++ b/apps/ehr/src/features/external-labs/components/labs-orders/LabBreadcrumbs.tsx @@ -1,6 +1,6 @@ import { FC, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; import { BaseBreadcrumbs } from 'src/components/BaseBreadcrumbs'; +import { useAppointmentData } from 'src/features/visits/shared/stores/appointment/appointment.store'; interface LabBreadcrumbsProps { sectionName: string; @@ -8,14 +8,14 @@ interface LabBreadcrumbsProps { } export const LabBreadcrumbs: FC = ({ sectionName, children }) => { - const { id: appointmentIdFromUrl } = useParams(); + const { appointment } = useAppointmentData(); const baseCrumb = useMemo( () => ({ label: 'External Labs', - path: appointmentIdFromUrl ? `/in-person/${appointmentIdFromUrl}/external-lab-orders` : null, + path: appointment?.id ? `/in-person/${appointment.id}/external-lab-orders` : null, }), - [appointmentIdFromUrl] + [appointment?.id] ); return ( diff --git a/apps/ehr/src/features/external-labs/pages/CreateExternalLabOrder.tsx b/apps/ehr/src/features/external-labs/pages/CreateExternalLabOrder.tsx index 351efe90da..0d164afe96 100644 --- a/apps/ehr/src/features/external-labs/pages/CreateExternalLabOrder.tsx +++ b/apps/ehr/src/features/external-labs/pages/CreateExternalLabOrder.tsx @@ -18,7 +18,7 @@ import { import Oystehr from '@oystehr/sdk'; import { enqueueSnackbar } from 'notistack'; import React, { ReactElement, useEffect, useMemo, useState } from 'react'; -import { Link, useNavigate, useParams } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { ActionsList } from 'src/components/ActionsList'; import { DeleteIconButton } from 'src/components/DeleteIconButton'; import DetailPageContainer from 'src/features/common/DetailPageContainer'; @@ -65,11 +65,16 @@ export const CreateExternalLabOrder: React.FC = () const theme = useTheme(); const { oystehrZambda } = useApiClients(); const navigate = useNavigate(); - const { id: appointmentIdFromUrl } = useParams(); const [error, setError] = useState<(string | ReactElement)[] | undefined>(undefined); const [submitting, setSubmitting] = useState(false); const apiClient = useOystehrAPIClient(); - const { encounter, patient, location: apptLocation, followUpOriginEncounter: mainEncounter } = useAppointmentData(); + const { + encounter, + appointment, + patient, + location: apptLocation, + followUpOriginEncounter: mainEncounter, + } = useAppointmentData(); const { chartData, setPartialChartData } = useChartData(); const { mutate: saveCPTChartData } = useSaveChartData(); const { visitType } = useGetAppointmentAccessibility(); @@ -207,7 +212,7 @@ export const CreateExternalLabOrder: React.FC = () selectedPaymentMethod: selectedPaymentMethod, clinicalInfoNoteByUser: clinicalInfoNotes, }); - navigate(`/in-person/${appointmentIdFromUrl}/external-lab-orders`); + navigate(`/in-person/${appointment?.id}/external-lab-orders`); } catch (e) { const sdkError = e as Oystehr.OystehrSdkError; console.log('error creating external lab order', sdkError.code, sdkError.message); @@ -216,7 +221,7 @@ export const CreateExternalLabOrder: React.FC = () setError([ <> Information necessary to submit labs is missing. Please navigate to{' '} - visit details and complete all Worker's Compensation + visit details and complete all Worker's Compensation Information. , ]); @@ -606,7 +611,7 @@ export const CreateExternalLabOrder: React.FC = () variant="outlined" sx={{ borderRadius: '50px', textTransform: 'none', fontWeight: 600 }} onClick={() => { - navigate(`/in-person/${appointmentIdFromUrl}/external-lab-orders`); + navigate(`/in-person/${appointment?.id}/external-lab-orders`); }} > Cancel diff --git a/apps/ehr/src/features/in-house-labs/components/InHouseLabsBreadcrumbs.tsx b/apps/ehr/src/features/in-house-labs/components/InHouseLabsBreadcrumbs.tsx index 5c3d3e4275..6a6e99eaef 100644 --- a/apps/ehr/src/features/in-house-labs/components/InHouseLabsBreadcrumbs.tsx +++ b/apps/ehr/src/features/in-house-labs/components/InHouseLabsBreadcrumbs.tsx @@ -1,20 +1,21 @@ import { useMemo } from 'react'; -import { useParams } from 'react-router-dom'; import { BaseBreadcrumbs } from 'src/components/BaseBreadcrumbs'; import { getInHouseLabsUrl } from 'src/features/visits/in-person/routing/helpers'; +import { useAppointmentData } from 'src/features/visits/shared/stores/appointment/appointment.store'; export const InHouseLabsBreadcrumbs: React.FC<{ children: React.ReactNode; pageName: string }> = ({ children, pageName, }) => { - const { id: appointmentIdFromUrl } = useParams(); + const { appointment } = useAppointmentData(); + const appointmentId = appointment?.id; const baseCrumb = useMemo(() => { return { label: 'In-House Labs', - path: appointmentIdFromUrl ? getInHouseLabsUrl(appointmentIdFromUrl) : null, + path: appointmentId ? getInHouseLabsUrl(appointmentId) : null, }; - }, [appointmentIdFromUrl]); + }, [appointmentId]); return ( diff --git a/apps/ehr/src/features/in-house-labs/pages/InHouseLabOrderCreatePage.tsx b/apps/ehr/src/features/in-house-labs/pages/InHouseLabOrderCreatePage.tsx index f5066a2909..0cdc8d95a7 100644 --- a/apps/ehr/src/features/in-house-labs/pages/InHouseLabOrderCreatePage.tsx +++ b/apps/ehr/src/features/in-house-labs/pages/InHouseLabOrderCreatePage.tsx @@ -25,7 +25,7 @@ import { import Oystehr from '@oystehr/sdk'; import { enqueueSnackbar } from 'notistack'; import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { ActionsList } from 'src/components/ActionsList'; import { DeleteIconButton } from 'src/components/DeleteIconButton'; import { dataTestIds } from 'src/constants/data-test-ids'; @@ -59,7 +59,6 @@ export const InHouseLabOrderCreatePage: React.FC = () => { const theme = useTheme(); const { oystehrZambda } = useApiClients(); const navigate = useNavigate(); - const { id: appointmentIdFromUrl } = useParams(); const location = useLocation(); const [loading, setLoading] = useState(false); const [selectedTests, setSelectedTests] = useState([]); @@ -74,7 +73,7 @@ export const InHouseLabOrderCreatePage: React.FC = () => { type?: 'repeat' | 'reflex'; }; - const { encounter } = useAppointmentData(); + const { encounter, appointment } = useAppointmentData(); const { chartData, setPartialChartData } = useChartData(); const didPrimaryDiagnosisInit = useRef(false); const didPrefillInit = useRef(false); @@ -221,9 +220,9 @@ export const InHouseLabOrderCreatePage: React.FC = () => { if (res.serviceRequestIds.length === 1) { // we will only nav forward if one test was created, else we will direct the user back to the table - navigate(`/in-person/${appointmentIdFromUrl}/in-house-lab-orders/${res.serviceRequestIds[0]}/order-details`); + navigate(`/in-person/${appointment?.id}/in-house-lab-orders/${res.serviceRequestIds[0]}/order-details`); } else { - navigate(`/in-person/${appointmentIdFromUrl}/in-house-lab-orders`); + navigate(`/in-person/${appointment?.id}/in-house-lab-orders`); } } catch (e) { const sdkError = e as Oystehr.OystehrSdkError; diff --git a/apps/ehr/src/features/in-house-labs/pages/InHouseLabsPage.tsx b/apps/ehr/src/features/in-house-labs/pages/InHouseLabsPage.tsx index feda170a1b..9eb0a4c804 100644 --- a/apps/ehr/src/features/in-house-labs/pages/InHouseLabsPage.tsx +++ b/apps/ehr/src/features/in-house-labs/pages/InHouseLabsPage.tsx @@ -1,6 +1,6 @@ import { Box, Stack } from '@mui/material'; import React, { useCallback } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import ListViewContainer from 'src/features/common/ListViewContainer'; import { getInHouseLabOrderCreateUrl } from 'src/features/visits/in-person/routing/helpers'; import { useGetAppointmentAccessibility } from 'src/features/visits/shared/hooks/useGetAppointmentAccessibility'; @@ -14,9 +14,8 @@ const inHouseLabsColumns: InHouseLabsTableColumn[] = ['testType', 'orderAdded', export const InHouseLabsPage: React.FC = () => { const navigate = useNavigate(); - const { id: appointmentIdFromUrl } = useParams(); - const { encounter } = useAppointmentData(); - const appointmentId = appointmentIdFromUrl; + const { appointment, encounter } = useAppointmentData(); + const appointmentId = appointment?.id; const encounterId = encounter?.id; const { isAppointmentReadOnly: isReadOnly } = useGetAppointmentAccessibility(); diff --git a/apps/ehr/src/features/nursing-orders/components/BreadCrumbs.tsx b/apps/ehr/src/features/nursing-orders/components/BreadCrumbs.tsx index 72e6f7200f..002d02ea82 100644 --- a/apps/ehr/src/features/nursing-orders/components/BreadCrumbs.tsx +++ b/apps/ehr/src/features/nursing-orders/components/BreadCrumbs.tsx @@ -1,14 +1,14 @@ import { FC } from 'react'; -import { useParams } from 'react-router-dom'; import CustomBreadcrumbs from 'src/components/CustomBreadcrumbs'; +import { useAppointmentData } from 'src/features/visits/shared/stores/appointment/appointment.store'; export const BreadCrumbs: FC = () => { - const { id: appointmentIdFromUrl } = useParams(); + const { appointment } = useAppointmentData(); return ( diff --git a/apps/ehr/src/features/nursing-orders/pages/NursingOrdersPage.tsx b/apps/ehr/src/features/nursing-orders/pages/NursingOrdersPage.tsx index 3aca04b5cf..5675d64c93 100644 --- a/apps/ehr/src/features/nursing-orders/pages/NursingOrdersPage.tsx +++ b/apps/ehr/src/features/nursing-orders/pages/NursingOrdersPage.tsx @@ -1,6 +1,6 @@ import { Box, Stack } from '@mui/material'; import React, { useCallback } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { dataTestIds } from 'src/constants/data-test-ids'; import { getNursingOrderCreateUrl } from 'src/features/visits/in-person/routing/helpers'; import { useGetAppointmentAccessibility } from 'src/features/visits/shared/hooks/useGetAppointmentAccessibility'; @@ -13,9 +13,8 @@ const nursingOrdersColumns: NursingOrdersTableColumn[] = ['order', 'orderAdded', export const NursingOrdersPage: React.FC = () => { const navigate = useNavigate(); - const { id: appointmentIdFromUrl } = useParams(); - const { encounter } = useAppointmentData(); - const appointmentId = appointmentIdFromUrl; + const { appointment, encounter } = useAppointmentData(); + const appointmentId = appointment?.id; const encounterId = encounter?.id; const { isAppointmentReadOnly: isReadOnly } = useGetAppointmentAccessibility(); diff --git a/apps/ehr/src/features/radiology/components/RadiologyBreadcrumbs.tsx b/apps/ehr/src/features/radiology/components/RadiologyBreadcrumbs.tsx index 1af01d45a1..a637447a79 100644 --- a/apps/ehr/src/features/radiology/components/RadiologyBreadcrumbs.tsx +++ b/apps/ehr/src/features/radiology/components/RadiologyBreadcrumbs.tsx @@ -1,8 +1,9 @@ import { Box, Link as MuiLink, Typography } from '@mui/material'; import { styled } from '@mui/material/styles'; import { FC } from 'react'; -import { Link, useParams } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { getRadiologyUrl } from 'src/features/visits/in-person/routing/helpers'; +import { useAppointmentData } from 'src/features/visits/shared/stores/appointment/appointment.store'; interface RadiologyBreadcrumbsProps { sectionName: string; @@ -34,13 +35,13 @@ export const WithRadiologyBreadcrumbs: FC = ({ disableLabsLink = false, children, }) => { - const { id: appointmentIdFromUrl } = useParams(); + const { appointment } = useAppointmentData(); return ( - {!disableLabsLink && appointmentIdFromUrl ? ( - + {!disableLabsLink && appointment?.id ? ( + Radiology ) : ( diff --git a/apps/ehr/src/features/radiology/pages/CreateRadiologyOrder.tsx b/apps/ehr/src/features/radiology/pages/CreateRadiologyOrder.tsx index fa9a7b3df4..fcc07e3756 100644 --- a/apps/ehr/src/features/radiology/pages/CreateRadiologyOrder.tsx +++ b/apps/ehr/src/features/radiology/pages/CreateRadiologyOrder.tsx @@ -28,7 +28,7 @@ import { import { ClearIcon } from '@mui/x-date-pickers'; import { enqueueSnackbar } from 'notistack'; import React, { useState } from 'react'; -import { Link, useNavigate, useParams } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { dataTestIds } from 'src/constants/data-test-ids'; import DetailPageContainer from 'src/features/common/DetailPageContainer'; import { getRadiologyUrl } from 'src/features/visits/in-person/routing/helpers'; @@ -72,11 +72,10 @@ export const CreateRadiologyOrder: React.FC = () => const theme = useTheme(); const { oystehrZambda } = useApiClients(); const navigate = useNavigate(); - const { id: appointmentIdFromUrl } = useParams(); const [error, setError] = useState(undefined); const [submitting, setSubmitting] = useState(false); const { mutate: saveChartData } = useSaveChartData(); - const { encounter } = useAppointmentData(); + const { encounter, appointment } = useAppointmentData(); const { chartData, setPartialChartData } = useChartData(); const { diagnosis } = chartData || {}; const primaryDiagnosis = diagnosis?.find((d) => d.isPrimary); @@ -229,7 +228,7 @@ export const CreateRadiologyOrder: React.FC = () => }); } - navigate(getRadiologyUrl(appointmentIdFromUrl || '')); + navigate(getRadiologyUrl(appointment?.id || '')); } catch (e) { const error = e as any; console.log('error', JSON.stringify(error)); @@ -483,7 +482,7 @@ export const CreateRadiologyOrder: React.FC = () => variant="outlined" sx={{ borderRadius: '50px', textTransform: 'none', fontWeight: 600 }} onClick={() => { - navigate(`/in-person/${appointmentIdFromUrl}/radiology`); + navigate(`/in-person/${appointment?.id}/radiology`); }} > Cancel diff --git a/apps/ehr/src/features/visits/in-person/components/EncounterSwitcher.tsx b/apps/ehr/src/features/visits/in-person/components/EncounterSwitcher.tsx index 1c0c7ed03f..4f1d18d8c7 100644 --- a/apps/ehr/src/features/visits/in-person/components/EncounterSwitcher.tsx +++ b/apps/ehr/src/features/visits/in-person/components/EncounterSwitcher.tsx @@ -1,19 +1,10 @@ import AssignmentIcon from '@mui/icons-material/Assignment'; import { Box, Button, Collapse, List, ListItem, ListItemButton, ListItemText, Typography } from '@mui/material'; -import { useQueryClient } from '@tanstack/react-query'; import { Encounter } from 'fhir/r4b'; import { DateTime } from 'luxon'; -import { FC, useMemo, useState } from 'react'; -import { CHART_DATA_QUERY_KEY } from 'src/constants'; +import { FC, useState } from 'react'; import { formatISOStringToDateAndTime } from 'src/helpers/formatDateTime'; -import { - buildAppointmentStartMap, - getEncounterDateTime, - getEncounterDisplayName, - getInteractionModeForEncounter, -} from 'utils'; import { useAppointmentData } from '../../shared/stores/appointment/appointment.store'; -import { resetExamObservationsStore } from '../../shared/stores/appointment/reset-exam-observations'; import { useInPersonNavigationContext } from '../context/InPersonNavigationContext'; type EncounterSwitcherProps = { @@ -21,36 +12,36 @@ type EncounterSwitcherProps = { }; export const EncounterSwitcher: FC = ({ open }) => { - const { followUpOriginEncounter, followupEncounters, selectedEncounterId, setSelectedEncounter, rawResources } = + const { followUpOriginEncounter, followupEncounters, selectedEncounterId, setSelectedEncounter } = useAppointmentData(); const [isExpanded, setIsExpanded] = useState(true); const { setInteractionMode } = useInPersonNavigationContext(); - const queryClient = useQueryClient(); - - const appointmentStartMap = useMemo(() => buildAppointmentStartMap(rawResources ?? []), [rawResources]); const sortedFollowupEncounters = [...(followupEncounters || [])].filter(Boolean).sort((a, b) => { - return DateTime.fromISO(getEncounterDateTime(a, appointmentStartMap) ?? '').diff( - DateTime.fromISO(getEncounterDateTime(b, appointmentStartMap) ?? ''), - 'milliseconds' - ).milliseconds; + return DateTime.fromISO(a.period?.start ?? '').diff(DateTime.fromISO(b.period?.start ?? ''), 'milliseconds') + .milliseconds; }); const allEncounters = [followUpOriginEncounter, ...sortedFollowupEncounters].filter(Boolean) as Encounter[]; const handleEncounterSelect = (encounterId: string): void => { - // Reset exam observations and invalidate chart data cache so it refetches - // and repopulates the exam store for the new encounter - resetExamObservationsStore(); - void queryClient.invalidateQueries({ - queryKey: [CHART_DATA_QUERY_KEY, encounterId], - exact: false, - }); setSelectedEncounter(encounterId); - const selectedEnc = allEncounters.find((e) => e.id === encounterId); - if (selectedEnc) { - setInteractionMode(getInteractionModeForEncounter(selectedEnc, followUpOriginEncounter?.id), true); + if (encounterId === followUpOriginEncounter?.id) { + setInteractionMode('main', true); + } else { + setInteractionMode('follow-up', true); + } + }; + + const getEncounterDisplayName = (encounter: Encounter): string => { + if (!encounter.partOf) { + return 'Main Visit'; } + + const typeText = encounter.type?.[0]?.text || 'Follow-up'; + const date = encounter.period?.start ? formatISOStringToDateAndTime(encounter.period.start) : ''; + + return `${typeText}${date ? ` - ${date}` : ''}`; }; if (allEncounters.length <= 1) { @@ -110,7 +101,7 @@ export const EncounterSwitcher: FC = ({ open }) => { }} > { location, locations, encounter, - followUpOriginEncounter, appointmentRefetch, selectedEncounterId, } = useAppointmentData(); @@ -176,9 +172,11 @@ export const Header = (): JSX.Element => { let optionalVisitLabel = ''; if (isFollowup) { - const locationId = getEncounterLocationId(encounter); - if (locationId) { + const locationRef = encounter?.location?.[0]?.location?.reference; + if (locationRef) { + const locationId = locationRef.split('/')[1]; const matchedLocation = locations.find((location) => location?.id === locationId); + optionalVisitLabel = matchedLocation?.name ?? ''; } } else { @@ -375,7 +373,7 @@ export const Header = (): JSX.Element => { {isFollowup ? ( - getFollowupStatusChip(getAnnotationFollowupStatusLabel(encounter?.status)) + getFollowupStatusChip(encounter?.status === 'in-progress' ? 'OPEN' : 'RESOLVED') ) : ( { onClick={() => { setHeaderMenuAnchorEl(null); if (patient?.id) { - const initialEncounterId = getInitialEncounterIdForFollowUp(encounter, followUpOriginEncounter); - navigate(`/patient/${patient.id}/followup/add`, { - state: { initialEncounterId }, - }); + navigate(`/patient/${patient.id}/followup/add`); } }} disabled={!patient?.id} diff --git a/apps/ehr/src/features/visits/in-person/components/progress-note/VisitDetailsContainer.tsx b/apps/ehr/src/features/visits/in-person/components/progress-note/VisitDetailsContainer.tsx index 8cf561870f..3643799619 100644 --- a/apps/ehr/src/features/visits/in-person/components/progress-note/VisitDetailsContainer.tsx +++ b/apps/ehr/src/features/visits/in-person/components/progress-note/VisitDetailsContainer.tsx @@ -1,6 +1,6 @@ import { Box, Stack, Typography } from '@mui/material'; import { FC } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { ActionsList } from 'src/components/ActionsList'; import { VisitNoteItem } from 'src/features/visits/shared/components/VisitNoteItem'; import { useChartFields } from 'src/features/visits/shared/hooks/useChartFields'; @@ -16,7 +16,6 @@ import { ButtonRounded } from '../RoundedButton'; export const VisitDetailsContainer: FC = () => { const navigate = useNavigate(); - const { id: appointmentIdFromUrl } = useParams(); const { appointment, location, questionnaireResponse, encounter } = useAppointmentData(); const { data: chartFields } = useChartFields({ @@ -61,7 +60,7 @@ export const VisitDetailsContainer: FC = () => { Visit information navigate(`/visit/${appointmentIdFromUrl}`)} + onClick={() => navigate(`/visit/${appointment?.id}`)} variant="outlined" sx={{ whiteSpace: 'nowrap', diff --git a/apps/ehr/src/features/visits/in-person/context/InPersonNavigationContext.tsx b/apps/ehr/src/features/visits/in-person/context/InPersonNavigationContext.tsx index aba78543b6..ed5772cfc8 100644 --- a/apps/ehr/src/features/visits/in-person/context/InPersonNavigationContext.tsx +++ b/apps/ehr/src/features/visits/in-person/context/InPersonNavigationContext.tsx @@ -110,8 +110,7 @@ export const InPersonNavigationProvider: React.FC<{ children: ReactNode }> = ({ return; } - const isEncounterLoadedToStore = - appointmentIdReferenceFromEncounter === appointmentIdFromUrl || !!encounter?.partOf; + const isEncounterLoadedToStore = appointmentIdReferenceFromEncounter === appointmentIdFromUrl; if (!isEncounterLoadedToStore) { return; @@ -134,7 +133,6 @@ export const InPersonNavigationProvider: React.FC<{ children: ReactNode }> = ({ }, [ encounter?.id, encounter?.participant, - encounter?.partOf, setInteractionMode, interactionMode, isModeInitialized, diff --git a/apps/ehr/src/features/visits/in-person/routing/helpers.ts b/apps/ehr/src/features/visits/in-person/routing/helpers.ts index 8520ec225f..878ec6b4ab 100644 --- a/apps/ehr/src/features/visits/in-person/routing/helpers.ts +++ b/apps/ehr/src/features/visits/in-person/routing/helpers.ts @@ -1,5 +1,3 @@ -import { InPersonAppointmentInformation } from 'utils'; - export const getNewOrderUrl = (appointmentId: string): string => { return `/in-person/${appointmentId}/in-house-medication/order/new`; }; @@ -80,26 +78,6 @@ export const getInPersonVisitDetailsUrl = (appointmentId: string): string => { return `/visit/${appointmentId}`; }; -export const getAppointmentVisitDetailsUrl = ( - appointment: Pick -): string => { - const navAppointmentId = appointment.parentAppointmentId || appointment.id; - return `/visit/${navAppointmentId}`; -}; -export const getInPersonUrlByAppointmentType = ( - appointment: Pick, - targetUrl?: string -): string => { - let baseVisitUrl = `/in-person/${appointment.parentAppointmentId || appointment.id}`; - if (targetUrl) { - baseVisitUrl = `${baseVisitUrl}/${targetUrl}`; - } - if (appointment.isFollowUp && appointment.encounterId) { - baseVisitUrl = `${baseVisitUrl}?encounterId=${appointment.encounterId}`; - } - return baseVisitUrl; -}; - export const getChiefComplaintUrl = (appointmentId: string): string => { return `/in-person/${appointmentId}/cc-and-intake-notes`; }; diff --git a/apps/ehr/src/features/visits/shared/components/OrdersIconsToolTip.tsx b/apps/ehr/src/features/visits/shared/components/OrdersIconsToolTip.tsx index 18d8d5705c..16d5958862 100644 --- a/apps/ehr/src/features/visits/shared/components/OrdersIconsToolTip.tsx +++ b/apps/ehr/src/features/visits/shared/components/OrdersIconsToolTip.tsx @@ -55,8 +55,6 @@ export const OrdersIconsToolTip: React.FC = ({ appointm const ordersExistForAppointment = hasAtLeastOneOrder(orders); if (!ordersExistForAppointment) return null; - const navAppointmentId = appointment.parentAppointmentId || appointment.id; - const { externalLabOrders, inHouseLabOrders, @@ -76,14 +74,14 @@ export const OrdersIconsToolTip: React.FC = ({ appointm const externalLabOrderConfig: OrderToolTipConfig = { icon: sidebarMenuIcons['External Labs'], title: 'External Labs', - tableUrl: getExternalLabOrdersUrl(navAppointmentId), + tableUrl: getExternalLabOrdersUrl(appointment.id), unreadBadge: Boolean( externalLabOrders.find((ord) => EXTERNAL_LAB_ORDERS_PENDING_BADGE_STATUSES.includes(ord.orderStatus)) ), orders: externalLabOrders.map((order) => ({ fhirResourceId: order.serviceRequestId, itemDescription: order.testItem, - detailPageUrl: getExternalLabOrderEditUrl(navAppointmentId, order.serviceRequestId), + detailPageUrl: getExternalLabOrderEditUrl(appointment.id, order.serviceRequestId), statusChip: , unreadBadge: EXTERNAL_LAB_ORDERS_PENDING_BADGE_STATUSES.includes(order.orderStatus), })), @@ -95,14 +93,14 @@ export const OrdersIconsToolTip: React.FC = ({ appointm const inHouseLabOrderConfig: OrderToolTipConfig = { icon: sidebarMenuIcons['In-House Labs'], title: 'In-House Labs', - tableUrl: getInHouseLabsUrl(navAppointmentId), + tableUrl: getInHouseLabsUrl(appointment.id), unreadBadge: Boolean( inHouseLabOrders.find((ord) => IN_HOUSE_LAB_ORDERS_PENDING_BADGE_STATUSES.includes(ord.status)) ), orders: inHouseLabOrders.map((order) => ({ fhirResourceId: order.serviceRequestId, itemDescription: order.testItemName, - detailPageUrl: getInHouseLabOrderDetailsUrl(navAppointmentId, order.serviceRequestId), + detailPageUrl: getInHouseLabOrderDetailsUrl(appointment.id, order.serviceRequestId), statusChip: , unreadBadge: IN_HOUSE_LAB_ORDERS_PENDING_BADGE_STATUSES.includes(order.status), })), @@ -114,14 +112,14 @@ export const OrdersIconsToolTip: React.FC = ({ appointm const nursingOrdersConfig: OrderToolTipConfig = { icon: sidebarMenuIcons['Nursing Orders'], title: 'Nursing Orders', - tableUrl: getNursingOrdersUrl(navAppointmentId), + tableUrl: getNursingOrdersUrl(appointment.id), unreadBadge: Boolean(nursingOrders.find((ord) => NURSING_ORDERS_PENDING_BADGE_STATUSES.includes(ord.status))), orders: nursingOrders .filter((order) => order.status !== NursingOrdersStatus.cancelled) .map((order) => ({ fhirResourceId: order.serviceRequestId, itemDescription: order.note, - detailPageUrl: getNursingOrderDetailsUrl(navAppointmentId, order.serviceRequestId), + detailPageUrl: getNursingOrderDetailsUrl(appointment.id, order.serviceRequestId), statusChip: , unreadBadge: NURSING_ORDERS_PENDING_BADGE_STATUSES.includes(order.status), })), @@ -133,7 +131,7 @@ export const OrdersIconsToolTip: React.FC = ({ appointm const inHouseMedicationConfig: OrderToolTipConfig = { icon: sidebarMenuIcons['Med. Administration'], title: 'In-House Medications', - tableUrl: getInHouseMedicationMARUrl(navAppointmentId), + tableUrl: getInHouseMedicationMARUrl(appointment.id), unreadBadge: Boolean( filteredInHouseMedications.find((ord) => FILTERED_IN_HOUSE_MEDICATIONS_PENDING_BADGE_STATUSES.includes(ord.status as MedicationOrderStatuses) @@ -142,8 +140,8 @@ export const OrdersIconsToolTip: React.FC = ({ appointm orders: filteredInHouseMedications.map((med) => { const isPending = med.status === 'pending'; const targetUrl = isPending - ? `${getInHouseMedicationDetailsUrl(navAppointmentId)}?scrollTo=${med.id}` - : `${getInHouseMedicationMARUrl(navAppointmentId)}?scrollTo=${med.id}`; + ? `${getInHouseMedicationDetailsUrl(appointment.id)}?scrollTo=${med.id}` + : `${getInHouseMedicationMARUrl(appointment.id)}?scrollTo=${med.id}`; return { fhirResourceId: med.id, @@ -163,12 +161,12 @@ export const OrdersIconsToolTip: React.FC = ({ appointm const radiologyOrdersConfig: OrderToolTipConfig = { icon: sidebarMenuIcons['Radiology'], title: 'Radiology Orders', - tableUrl: getRadiologyUrl(navAppointmentId), + tableUrl: getRadiologyUrl(appointment.id), unreadBadge: Boolean(radiologyOrders.find((ord) => RADIOLOGY_ORDERS_PENDING_BADGE_STATUSES.includes(ord.status))), orders: radiologyOrders.map((order) => ({ fhirResourceId: order.serviceRequestId, itemDescription: order.studyType, - detailPageUrl: getRadiologyOrderEditUrl(navAppointmentId, order.serviceRequestId), + detailPageUrl: getRadiologyOrderEditUrl(appointment.id, order.serviceRequestId), statusChip: , unreadBadge: RADIOLOGY_ORDERS_PENDING_BADGE_STATUSES.includes(order.status), })), @@ -180,11 +178,11 @@ export const OrdersIconsToolTip: React.FC = ({ appointm const ordersConfig: OrderToolTipConfig = { icon: sidebarMenuIcons['eRX'], title: 'eRx', - tableUrl: getErxUrl(navAppointmentId), + tableUrl: getErxUrl(appointment.id), orders: erxOrders.map((order) => ({ fhirResourceId: order.resourceId ?? '', itemDescription: order.name ?? '', - detailPageUrl: getErxUrl(navAppointmentId), + detailPageUrl: getErxUrl(appointment.id), statusChip: , })), }; @@ -195,13 +193,13 @@ export const OrdersIconsToolTip: React.FC = ({ appointm const proceduresConfig: OrderToolTipConfig = { icon: sidebarMenuIcons['Procedures'], title: 'Procedures', - tableUrl: getProceduresUrl(navAppointmentId), + tableUrl: getProceduresUrl(appointment.id), orders: procedures.map((procedure) => ({ fhirResourceId: procedure.resourceId ?? '', itemDescription: procedure.procedureType ?? '', detailPageUrl: procedure.resourceId - ? getProcedureDetailsUrl(navAppointmentId, procedure.resourceId) - : getProceduresUrl(navAppointmentId), + ? getProcedureDetailsUrl(appointment.id, procedure.resourceId) + : getProceduresUrl(appointment.id), statusChip: <>, })), }; @@ -212,14 +210,14 @@ export const OrdersIconsToolTip: React.FC = ({ appointm const config: OrderToolTipConfig = { icon: sidebarMenuIcons['Immunization'], title: 'Immunization', - tableUrl: getImmunizationMARUrl(navAppointmentId), + tableUrl: getImmunizationMARUrl(appointment.id), orders: immunizationOrders .filter((order) => order.status !== 'cancelled') .map((order) => { const isPending = order.status === 'pending'; const targetUrl = isPending - ? `${getImmunizationVaccineDetailsUrl(navAppointmentId)}?scrollTo=${order.id}` - : `${getImmunizationMARUrl(navAppointmentId)}?scrollTo=${order.id}`; + ? `${getImmunizationVaccineDetailsUrl(appointment.id)}?scrollTo=${order.id}` + : `${getImmunizationMARUrl(appointment.id)}?scrollTo=${order.id}`; return { fhirResourceId: order.id ?? '', itemDescription: order.details.medication.name, diff --git a/apps/ehr/src/features/visits/shared/components/VitalsIconTooltip.tsx b/apps/ehr/src/features/visits/shared/components/VitalsIconTooltip.tsx index ca9d2d5084..d27d6d52a3 100644 --- a/apps/ehr/src/features/visits/shared/components/VitalsIconTooltip.tsx +++ b/apps/ehr/src/features/visits/shared/components/VitalsIconTooltip.tsx @@ -92,7 +92,7 @@ export const VitalsIconTooltip: React.FC = ({ appointmen > diff --git a/apps/ehr/src/features/visits/shared/components/patient/AddPatientFollowup.tsx b/apps/ehr/src/features/visits/shared/components/patient/AddPatientFollowup.tsx index e315ddf094..15896bd43c 100644 --- a/apps/ehr/src/features/visits/shared/components/patient/AddPatientFollowup.tsx +++ b/apps/ehr/src/features/visits/shared/components/patient/AddPatientFollowup.tsx @@ -1,20 +1,14 @@ -import { CircularProgress, FormControlLabel, Grid, Paper, Radio, RadioGroup, Typography } from '@mui/material'; -import { useState } from 'react'; -import { useLocation, useParams } from 'react-router-dom'; +import { CircularProgress, Grid, Typography } from '@mui/material'; +import { useParams } from 'react-router-dom'; import CustomBreadcrumbs from 'src/components/CustomBreadcrumbs'; import { useGetPatient } from 'src/hooks/useGetPatient'; import PageContainer from 'src/layout/PageContainer'; -import { FollowupSubtype, getFullName } from 'utils'; +import { getFullName } from 'utils'; import PatientFollowupForm from './PatientFollowupForm'; -import ScheduledFollowupParentSelector from './ScheduledFollowupParentSelector'; export default function AddPatientFollowup(): JSX.Element { const { id } = useParams(); const { patient } = useGetPatient(id); - const [followupSubtype, setFollowupSubtype] = useState('annotation'); - const location = useLocation(); - const routerState = location.state as { initialEncounterId?: string } | undefined; - const initialEncounterId = routerState?.initialEncounterId; const fullName = patient ? getFullName(patient) : ''; @@ -38,27 +32,10 @@ export default function AddPatientFollowup(): JSX.Element { }, ]} /> - + Add Follow-up Visit - - - setFollowupSubtype(e.target.value as FollowupSubtype)} - sx={{ mb: 2 }} - > - } label="Annotation" /> - } label="Scheduled Visit" /> - - - {followupSubtype === 'annotation' ? ( - - ) : ( - - )} - + )} diff --git a/apps/ehr/src/features/visits/shared/components/patient/PatientFollowupForm.tsx b/apps/ehr/src/features/visits/shared/components/patient/PatientFollowupForm.tsx index 38da87617c..38b53d43ad 100644 --- a/apps/ehr/src/features/visits/shared/components/patient/PatientFollowupForm.tsx +++ b/apps/ehr/src/features/visits/shared/components/patient/PatientFollowupForm.tsx @@ -1,27 +1,31 @@ import { LoadingButton } from '@mui/lab'; -import { Autocomplete, Box, Button, Grid, TextField } from '@mui/material'; +import { Autocomplete, Box, Button, Grid, Paper, TextField } from '@mui/material'; import { DatePicker, TimePicker } from '@mui/x-date-pickers'; import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; import { LocalizationProvider } from '@mui/x-date-pickers-pro'; import Oystehr from '@oystehr/sdk'; -import { Appointment, Encounter, Location, Patient } from 'fhir/r4b'; +import { Appointment, Encounter, Patient } from 'fhir/r4b'; import { DateTime } from 'luxon'; import { enqueueSnackbar } from 'notistack'; import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { getEmployees, saveFollowup } from 'src/api/api'; import LocationSelect from 'src/components/LocationSelect'; -import { getInPersonUrlByAppointmentType } from 'src/features/visits/in-person/routing/helpers'; import { formatISOStringToDateAndTime } from 'src/helpers/formatDateTime'; import { useApiClients } from 'src/hooks/useAppClients'; -import useEvolveUser from 'src/hooks/useEvolveUser'; import { LocationWithWalkinSchedule } from 'src/pages/AddPatient'; -import { FOLLOWUP_REASONS, FollowupReason, isFollowupEncounter, PatientFollowupDetails, ProviderDetails } from 'utils'; +import { + FOLLOWUP_REASONS, + FOLLOWUP_SYSTEMS, + FollowupReason, + PatientFollowupDetails, + PRACTITIONER_CODINGS, + ProviderDetails, +} from 'utils'; interface PatientFollowupFormProps { patient: Patient | undefined; followupDetails?: PatientFollowupDetails; - initialEncounterId?: string; } interface EncounterRow { @@ -30,14 +34,13 @@ interface EncounterRow { dateTime: string | undefined; appointment: Appointment; encounter: Encounter; - location?: Location; } interface FormData { provider: ProviderDetails | null; reason: FollowupReason | undefined; otherReason: string; - initialVisit: EncounterRow | null; + initialVisit: EncounterRow | undefined; followupDate: DateTime; followupTime: DateTime; location: LocationWithWalkinSchedule | undefined; @@ -53,14 +56,9 @@ interface FormErrors { location?: string; } -export default function PatientFollowupForm({ - patient, - followupDetails, - initialEncounterId, -}: PatientFollowupFormProps): JSX.Element { +export default function PatientFollowupForm({ patient, followupDetails }: PatientFollowupFormProps): JSX.Element { const navigate = useNavigate(); const { oystehrZambda } = useApiClients(); - const currentUser = useEvolveUser(); const patientId = patient?.id; @@ -68,13 +66,13 @@ export default function PatientFollowupForm({ const [providers, setProviders] = useState([]); const [previousEncounters, setPreviousEncounters] = useState([]); const [errors, setErrors] = useState({}); - const [_locations, setLocations] = useState([]); + const [locations, setLocations] = useState([]); const [formData, setFormData] = useState({ provider: followupDetails?.provider || null, reason: followupDetails?.reason || undefined, otherReason: followupDetails?.otherReason || '', - initialVisit: null, + initialVisit: undefined, followupDate: followupDetails?.start ? DateTime.fromISO(followupDetails.start) : DateTime.now(), followupTime: followupDetails?.start ? DateTime.fromISO(followupDetails.start) : DateTime.now(), location: followupDetails?.location as LocationWithWalkinSchedule | undefined, @@ -96,10 +94,10 @@ export default function PatientFollowupForm({ newErrors.initialVisit = 'Initial visit is required'; } if (!formData.followupDate) { - newErrors.followupDate = 'Annotation date is required'; + newErrors.followupDate = 'Follow-up date is required'; } if (!formData.followupTime) { - newErrors.followupTime = 'Annotation time is required'; + newErrors.followupTime = 'Follow-up time is required'; } if (!formData.location) { newErrors.location = 'Location is required'; @@ -153,10 +151,6 @@ export default function PatientFollowupForm({ name: '_include', value: 'Encounter:appointment', }, - { - name: '_include', - value: 'Encounter:location', - }, { name: '_sort', value: '-date', @@ -167,9 +161,16 @@ export default function PatientFollowupForm({ const encounters = resources.filter((resource) => resource.resourceType === 'Encounter') as Encounter[]; const appointments = resources.filter((resource) => resource.resourceType === 'Appointment') as Appointment[]; - const fhirLocations = resources.filter((resource) => resource.resourceType === 'Location') as Location[]; - const nonFollowupEncounters = encounters.filter((encounter) => !isFollowupEncounter(encounter)); + const nonFollowupEncounters = encounters.filter((encounter) => { + const isFollowup = encounter.type?.some( + (type) => + type.coding?.some( + (coding) => coding.system === FOLLOWUP_SYSTEMS.type.url && coding.code === FOLLOWUP_SYSTEMS.type.code + ) + ); + return !isFollowup; + }); const encounterRows: EncounterRow[] = nonFollowupEncounters .map((encounter) => { @@ -177,16 +178,12 @@ export default function PatientFollowupForm({ ? appointments.find((app) => `Appointment/${app.id}` === encounter.appointment?.[0]?.reference) : undefined; - const locationRef = encounter.location?.[0]?.location?.reference?.replace('Location/', ''); - const encounterLocation = locationRef ? fhirLocations.find((loc) => loc.id === locationRef) : undefined; - return { id: encounter.id, typeLabel: appointment?.appointmentType?.text || 'Visit', dateTime: appointment?.start, appointment: appointment!, encounter: encounter, - location: encounterLocation, }; }) .filter((row) => row.id) @@ -198,25 +195,12 @@ export default function PatientFollowupForm({ setPreviousEncounters(encounterRows); - const preselectedEncounterId = followupDetails?.initialEncounterID || initialEncounterId; - const preselectedVisit = preselectedEncounterId - ? encounterRows.find((row) => row.encounter?.id === preselectedEncounterId) - : undefined; - - // Pre-populate provider from the logged-in user - const userProfile = currentUser?.profileResource; - const userProvider = userProfile?.id - ? { - practitionerId: userProfile.id, - name: [userProfile.name?.[0]?.given?.[0], userProfile.name?.[0]?.family].filter(Boolean).join(' '), - } - : undefined; - - setFormData((prev) => ({ - ...prev, - ...(preselectedVisit ? { initialVisit: preselectedVisit } : {}), - ...(userProvider ? { provider: userProvider } : {}), - })); + if (followupDetails?.initialEncounterID && encounterRows.length > 0) { + const matchingVisit = encounterRows.find((row) => row.encounter?.id === followupDetails.initialEncounterID); + if (matchingVisit) { + setFormData((prev) => ({ ...prev, initialVisit: matchingVisit })); + } + } } catch (error) { console.error('Error fetching previous encounters:', error); } @@ -225,21 +209,49 @@ export default function PatientFollowupForm({ if (oystehrZambda && patientId) { void getPreviousEncounters(oystehrZambda); } - }, [oystehrZambda, patientId, followupDetails?.initialEncounterID, initialEncounterId, currentUser]); + }, [oystehrZambda, patientId, followupDetails?.initialEncounterID]); useEffect(() => { - if (previousEncounters.length > 0) { + if (previousEncounters.length > 0 && providers.length > 0) { const latestInitialVisit = previousEncounters[0]; - if (latestInitialVisit.location) { - setFormData((prev) => ({ ...prev, location: latestInitialVisit.location as LocationWithWalkinSchedule })); - } else { - const selectedLocation = localStorage.getItem('selectedLocation'); - if (selectedLocation) { - setFormData((prev) => ({ ...prev, location: JSON.parse(selectedLocation) })); - } + const provider = providers.find( + (provider) => + latestInitialVisit.encounter.participant?.find( + (participant) => + participant.individual?.reference?.split('/')[1] === provider.practitionerId && + participant.type?.find( + (type) => + type.coding?.some( + (coding) => + coding.system === PRACTITIONER_CODINGS.Attender[0].system && + coding.code === PRACTITIONER_CODINGS.Attender[0].code + ) + ) + ) + ); + if (provider) { + setFormData((prev) => ({ ...prev, provider: provider })); + } + } + }, [previousEncounters, providers]); + + useEffect(() => { + if (previousEncounters.length > 0 && locations.length > 0) { + const latestInitialVisit = previousEncounters[0]; + const location = locations.find( + (location) => + latestInitialVisit.encounter.location?.find( + (latestLocation) => location.id === latestLocation.location?.reference?.split('/')[1] + ) + ); + const selectedLocation = localStorage.getItem('selectedLocation'); + if (location) { + setFormData((prev) => ({ ...prev, location: location })); + } else if (selectedLocation) { + setFormData((prev) => ({ ...prev, location: JSON.parse(selectedLocation) })); } } - }, [previousEncounters]); + }, [previousEncounters, locations]); const handleFormSubmit = async (e: React.FormEvent): Promise => { e.preventDefault(); @@ -286,14 +298,9 @@ export default function PatientFollowupForm({ const followup = await saveFollowup(oystehrZambda, { encounterDetails }); navigate( - getInPersonUrlByAppointmentType( - { - id: formData.initialVisit?.appointment?.id || '', - encounterId: followup.encounterId, - isFollowUp: true, - }, - 'follow-up-note' - ) + `/in-person/${formData.initialVisit?.appointment?.id}/follow-up-note${ + followup.encounterId ? `?encounterId=${followup.encounterId}` : '' + }` ); } catch (error) { console.error(`Failed to add patient followup: ${error}`); @@ -337,189 +344,191 @@ export default function PatientFollowupForm({ }; return ( -
- - - `${option.name}`} - isOptionEqualToValue={(option, value) => option.practitionerId === value.practitionerId} - value={formData.provider} - onChange={(_, newVal) => updateFormData('provider', newVal || null)} - renderOption={(props, option) => { - return ( -
  • - {option.name} -
  • - ); - }} - renderInput={(params) => ( - - )} - /> -
    + + + + + `${option.name}`} + isOptionEqualToValue={(option, value) => option.practitionerId === value.practitionerId} + value={formData.provider} + onChange={(_, newVal) => updateFormData('provider', newVal || null)} + renderOption={(props, option) => { + return ( +
  • + {option.name} +
  • + ); + }} + renderInput={(params) => ( + + )} + /> +
    + + + { + updateFormData('reason', newVal || undefined); + + if (newVal !== 'Other') { + updateFormData('otherReason', ''); + } + }} + size="small" + fullWidth + value={formData.reason} + renderInput={(params) => ( + + )} + /> + - - { - updateFormData('reason', newVal || undefined); - - if (newVal !== 'Other') { - updateFormData('otherReason', ''); - } - }} - size="small" - fullWidth - value={formData.reason} - renderInput={(params) => ( + {formData.reason === 'Other' && ( + updateFormData('otherReason', e.target.value)} + placeholder="Please specify the reason" + error={!!errors.otherReason} + helperText={errors.otherReason} /> - )} - /> - + + )} - {formData.reason === 'Other' && ( - updateFormData('otherReason', e.target.value)} - placeholder="Please specify the reason" - error={!!errors.otherReason} - helperText={errors.otherReason} + getOptionLabel={(option) => { + const dateTime = option.dateTime ? formatISOStringToDateAndTime(option.dateTime) : 'Unknown date/time'; + const type = option.typeLabel || 'Visit'; + return `${dateTime} - ${type}`; + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={formData.initialVisit} + onChange={(_, newVal) => updateFormData('initialVisit', newVal || undefined)} + renderInput={(params) => ( + + )} /> - )} - - - { - const dateTime = option.dateTime ? formatISOStringToDateAndTime(option.dateTime) : 'Unknown date/time'; - const type = option.typeLabel || 'Visit'; - return `${dateTime} - ${type}`; - }} - isOptionEqualToValue={(option, value) => option.id === value.id} - value={formData.initialVisit} - onChange={(_, newVal) => updateFormData('initialVisit', newVal || null)} - renderInput={(params) => ( - + + val && handleDateChange(val)} + label="Follow-up date" + format="MM/dd/yyyy" + value={formData.followupDate} + minDate={DateTime.now().startOf('day')} + slotProps={{ + textField: { + id: 'followup-date', + label: 'Follow-up date *', + fullWidth: true, + size: 'small', + error: !!errors.followupDate, + helperText: errors.followupDate, + }, + }} /> - )} - /> - + +
    - - - val && handleDateChange(val)} - label="Annotation date" - format="MM/dd/yyyy" - value={formData.followupDate} - minDate={DateTime.now().startOf('day')} - slotProps={{ - textField: { - id: 'followup-date', - label: 'Annotation date *', - fullWidth: true, - size: 'small', - error: !!errors.followupDate, - helperText: errors.followupDate, - }, - }} - /> - - + + + val && updateFormData('followupTime', val)} + value={formData.followupTime} + label="Follow-up time *" + minTime={formData.followupDate.hasSame(DateTime.now(), 'day') ? DateTime.now() : undefined} + slotProps={{ + textField: { + fullWidth: true, + size: 'small', + error: !!errors.followupTime, + helperText: errors.followupTime, + }, + }} + /> + + - - - val && updateFormData('followupTime', val)} - value={formData.followupTime} - label="Annotation time *" - minTime={formData.followupDate.hasSame(DateTime.now(), 'day') ? DateTime.now() : undefined} - slotProps={{ - textField: { - fullWidth: true, - size: 'small', - error: !!errors.followupTime, - helperText: errors.followupTime, - }, - }} + + updateFormData('location', location)} + setLocations={setLocations} + updateURL={false} + renderInputProps={{ size: 'small' }} /> - - - - - updateFormData('location', location)} - setLocations={setLocations} - updateURL={false} - renderInputProps={{ size: 'small' }} - /> - {errors.location && {errors.location}} - + {errors.location && {errors.location}} +
    - - - - - - Create follow-up - - + + + + + + Create follow-up + + + - -
    + + ); } diff --git a/apps/ehr/src/features/visits/shared/components/patient/ScheduledFollowupParentSelector.tsx b/apps/ehr/src/features/visits/shared/components/patient/ScheduledFollowupParentSelector.tsx deleted file mode 100644 index 872cda0139..0000000000 --- a/apps/ehr/src/features/visits/shared/components/patient/ScheduledFollowupParentSelector.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { LoadingButton } from '@mui/lab'; -import { Autocomplete, Box, Button, Grid, TextField } from '@mui/material'; -import { Patient } from 'fhir/r4b'; -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { formatISOStringToDateAndTime } from 'src/helpers/formatDateTime'; -import { useParentEncounters } from './useParentEncounters'; - -interface ScheduledFollowupParentSelectorProps { - patient: Patient; - initialEncounterId?: string; -} - -export default function ScheduledFollowupParentSelector({ - patient, - initialEncounterId, -}: ScheduledFollowupParentSelectorProps): JSX.Element { - const navigate = useNavigate(); - const patientId = patient?.id; - - const { previousEncounters, selectedParentEncounter, setSelectedParentEncounter } = useParentEncounters( - patientId, - initialEncounterId - ); - const [error, setError] = useState(); - - const handleContinue = (): void => { - if (!selectedParentEncounter) { - setError('Please select a initial visit'); - return; - } - - // Navigate to the standard Add Visit page with parent encounter context - console.log('[ScheduledFollowup] Navigating with parentEncounterId:', selectedParentEncounter.encounter.id); - navigate('/visits/add', { - state: { - parentEncounterId: selectedParentEncounter.encounter.id, - parentLocation: selectedParentEncounter.location, - patientId: patientId, - patientInfo: { - id: patient.id, - newPatient: false, - firstName: patient.name?.[0]?.given?.[0] || '', - lastName: patient.name?.[0]?.family || '', - dateOfBirth: patient.birthDate || '', - sex: patient.gender, - phoneNumber: patient.telecom?.find((t) => t.system === 'phone')?.value?.replace('+1', '') || '', - }, - }, - }); - }; - - const handleCancel = (): void => { - if (patientId) { - navigate(`/patient/${patientId}`, { state: { defaultTab: 'encounters' } }); - } else { - navigate('/visits'); - } - }; - - return ( - - - { - const dateTime = option.dateTime ? formatISOStringToDateAndTime(option.dateTime) : 'Unknown date/time'; - const type = option.typeLabel || 'Visit'; - return `${dateTime} - ${type}`; - }} - isOptionEqualToValue={(option, value) => option.id === value.id} - value={selectedParentEncounter ?? null} - onChange={(_, newVal) => { - setSelectedParentEncounter(newVal || undefined); - setError(undefined); - }} - renderInput={(params) => ( - - )} - /> - - - - - - - Continue to Add Visit - - - - - ); -} diff --git a/apps/ehr/src/features/visits/shared/components/patient/useParentEncounters.ts b/apps/ehr/src/features/visits/shared/components/patient/useParentEncounters.ts deleted file mode 100644 index f5eec91aea..0000000000 --- a/apps/ehr/src/features/visits/shared/components/patient/useParentEncounters.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Appointment, Encounter, Location } from 'fhir/r4b'; -import { DateTime } from 'luxon'; -import { useEffect, useState } from 'react'; -import { useApiClients } from 'src/hooks/useAppClients'; -import { isFollowupEncounter } from 'utils'; - -export interface EncounterRow { - id: string | undefined; - typeLabel: string; - dateTime: string | undefined; - appointment: Appointment; - encounter: Encounter; - location?: Location; -} - -interface UseParentEncountersResult { - previousEncounters: EncounterRow[]; - selectedParentEncounter: EncounterRow | undefined; - setSelectedParentEncounter: (encounter: EncounterRow | undefined) => void; -} - -export function useParentEncounters( - patientId: string | undefined, - initialEncounterId?: string -): UseParentEncountersResult { - const { oystehrZambda } = useApiClients(); - const [previousEncounters, setPreviousEncounters] = useState([]); - const [selectedParentEncounter, setSelectedParentEncounter] = useState(undefined); - - useEffect(() => { - const getPreviousEncounters = async (): Promise => { - if (!oystehrZambda || !patientId) return; - try { - const resources = ( - await oystehrZambda.fhir.search({ - resourceType: 'Encounter', - params: [ - { name: 'patient', value: patientId }, - { name: '_include', value: 'Encounter:appointment' }, - { name: '_include', value: 'Encounter:location' }, - { name: '_sort', value: '-date' }, - ], - }) - ).unbundle(); - - const encounters = resources.filter((r) => r.resourceType === 'Encounter') as Encounter[]; - const appointments = resources.filter((r) => r.resourceType === 'Appointment') as Appointment[]; - const locations = resources.filter((r) => r.resourceType === 'Location') as Location[]; - - // Only show non-followup (top-level) encounters as parent options - const nonFollowupEncounters = encounters.filter((encounter) => !isFollowupEncounter(encounter)); - - const encounterRows: EncounterRow[] = nonFollowupEncounters - .map((encounter) => { - const appointment = encounter.appointment?.[0]?.reference - ? appointments.find((app) => `Appointment/${app.id}` === encounter.appointment?.[0]?.reference) - : undefined; - - const locationRef = encounter.location?.[0]?.location?.reference?.replace('Location/', ''); - const encounterLocation = locationRef ? locations.find((loc) => loc.id === locationRef) : undefined; - - return { - id: encounter.id, - typeLabel: appointment?.appointmentType?.text || 'Visit', - dateTime: appointment?.start, - appointment: appointment!, - encounter: encounter, - location: encounterLocation, - }; - }) - .filter((row) => row.id) - .sort((a, b) => { - const dateA = DateTime.fromISO(a.dateTime ?? ''); - const dateB = DateTime.fromISO(b.dateTime ?? ''); - return dateB.diff(dateA).milliseconds; - }); - - setPreviousEncounters(encounterRows); - - if (initialEncounterId && encounterRows.length > 0) { - console.log( - '[ScheduledSelector] Looking for initialEncounterId:', - initialEncounterId, - 'in', - encounterRows.map((r) => r.id) - ); - const matchingVisit = encounterRows.find((row) => row.encounter?.id === initialEncounterId); - console.log('[ScheduledSelector] Match:', matchingVisit?.id); - if (matchingVisit) { - setSelectedParentEncounter(matchingVisit); - } - } - } catch (err) { - console.error('Error fetching previous encounters:', err); - } - }; - - void getPreviousEncounters(); - }, [oystehrZambda, patientId, initialEncounterId]); - - return { previousEncounters, selectedParentEncounter, setSelectedParentEncounter }; -} diff --git a/apps/ehr/src/features/visits/shared/components/review-tab/MissingCard.tsx b/apps/ehr/src/features/visits/shared/components/review-tab/MissingCard.tsx index 98e4c09c11..30b9d6bcea 100644 --- a/apps/ehr/src/features/visits/shared/components/review-tab/MissingCard.tsx +++ b/apps/ehr/src/features/visits/shared/components/review-tab/MissingCard.tsx @@ -2,7 +2,7 @@ import { otherColors } from '@ehrTheme/colors'; import { WarningAmber } from '@mui/icons-material'; import { Avatar, Box, Link, Typography } from '@mui/material'; import { FC, useEffect, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { AccordionCard } from 'src/components/AccordionCard'; import { LoadingScreen } from 'src/components/LoadingScreen'; import { dataTestIds } from 'src/constants/data-test-ids'; @@ -10,11 +10,11 @@ import { getAssessmentUrl, getChiefComplaintUrl, getHPIUrl } from 'src/features/ import { TelemedAppointmentVisitTabs } from 'utils'; import { useChartFields } from '../../hooks/useChartFields'; import { useAiSuggestionNotes } from '../../stores/appointment/appointment.queries'; -import { useAppTelemedLocalStore, useChartData } from '../../stores/appointment/appointment.store'; +import { useAppointmentData, useAppTelemedLocalStore, useChartData } from '../../stores/appointment/appointment.store'; import { useAppFlags } from '../../stores/contexts/useAppFlags'; export const MissingCard: FC = () => { - const { id: appointmentIdFromUrl } = useParams(); + const { appointment } = useAppointmentData(); const { chartData } = useChartData(); const { data: chartFields, isFetching } = useChartFields({ @@ -68,9 +68,9 @@ export const MissingCard: FC = () => { const navigateTo = (target: 'chief-complaint' | 'hpi' | 'assessment'): void => { if (isInPerson) { const inPersonRoutes: Record<'chief-complaint' | 'hpi' | 'assessment', string> = { - 'chief-complaint': getChiefComplaintUrl(appointmentIdFromUrl || ''), - hpi: getHPIUrl(appointmentIdFromUrl || ''), - assessment: getAssessmentUrl(appointmentIdFromUrl || ''), + 'chief-complaint': getChiefComplaintUrl(appointment?.id || ''), + hpi: getHPIUrl(appointment?.id || ''), + assessment: getAssessmentUrl(appointment?.id || ''), }; requestAnimationFrame(() => { diff --git a/apps/ehr/src/features/visits/shared/stores/appointment/appointment.queries.ts b/apps/ehr/src/features/visits/shared/stores/appointment/appointment.queries.ts index 81e22061d7..c20b2cc662 100644 --- a/apps/ehr/src/features/visits/shared/stores/appointment/appointment.queries.ts +++ b/apps/ehr/src/features/visits/shared/stores/appointment/appointment.queries.ts @@ -97,7 +97,7 @@ export const useGetReviewAndSignData = ( if (!data || !onSuccess) { return; } - const reviewAndSignData = extractReviewAndSignAppointmentData(data, { appointmentId }); + const reviewAndSignData = extractReviewAndSignAppointmentData(data); onSuccess(reviewAndSignData); }); diff --git a/apps/ehr/src/features/visits/shared/stores/appointment/appointment.store.tsx b/apps/ehr/src/features/visits/shared/stores/appointment/appointment.store.tsx index 41997a599e..7c661d7abf 100644 --- a/apps/ehr/src/features/visits/shared/stores/appointment/appointment.store.tsx +++ b/apps/ehr/src/features/visits/shared/stores/appointment/appointment.store.tsx @@ -56,7 +56,7 @@ import { create } from 'zustand'; import { OystehrTelemedAPIClient } from '../../api/oystehrApi'; import { useGetAppointmentAccessibility } from '../../hooks/useGetAppointmentAccessibility'; import { useOystehrAPIClient } from '../../hooks/useOystehrAPIClient'; -import { getAppointmentValues, getEncounterValues } from './parser/extractors'; +import { getEncounterValues } from './parser/extractors'; import { parseBundle } from './parser/parser'; import { VisitMappedData, VisitResources } from './parser/types'; import { resetExamObservationsStore } from './reset-exam-observations'; @@ -287,50 +287,17 @@ export const useAppointmentData = ( const selectedEncounter = getSelectedEncounter(); const encounterToUse = selectedEncounter || state.followUpOriginEncounter; - let appointmentToUse = state.appointment; - if (encounterToUse?.appointment?.[0]?.reference) { - const encApptId = encounterToUse.appointment[0].reference.replace('Appointment/', ''); - if (encApptId !== appointmentToUse?.id) { - const matchingAppointment = state.rawResources?.find( - (r): r is Appointment => r.resourceType === 'Appointment' && r.id === encApptId - ); - if (matchingAppointment) { - appointmentToUse = matchingAppointment; - } - } - } - - let questionnaireResponseToUse = state.questionnaireResponse; - if (encounterToUse?.id) { - const matchingQR = state.rawResources?.find( - (r): r is QuestionnaireResponse => - r.resourceType === 'QuestionnaireResponse' && r.encounter?.reference === `Encounter/${encounterToUse.id}` - ); - if (matchingQR) { - questionnaireResponseToUse = matchingQR; - } - } - return { ...state, - appointment: appointmentToUse, encounter: encounterToUse, - questionnaireResponse: questionnaireResponseToUse, visitState: { ...state.visitState, - appointment: appointmentToUse, encounter: encounterToUse, - questionnaireResponse: questionnaireResponseToUse, }, resources: { ...state.resources, - appointment: getAppointmentValues(appointmentToUse), encounter: getEncounterValues(encounterToUse), }, - reviewAndSignData: extractReviewAndSignAppointmentData(state.rawResources || [], { - appointmentId: appointmentToUse?.id, - encounterId: encounterToUse?.id, - }), isAppointmentLoading: isLoading, appointmentRefetch: refetch, appointmentSetState: setState, @@ -370,27 +337,16 @@ export type AppointmentResources = const selectAppointmentData = ( data: AppointmentResources[] | undefined, - preserveSelectedEncounterId?: string, - appointmentId?: string + preserveSelectedEncounterId?: string ): (AppointmentTelemedState & InPersonAppointmentState & AppointmentRawResourcesState) | null => { if (!data) return null; + const questionnaireResponse = data?.find( + (resource: FhirResource) => resource.resourceType === 'QuestionnaireResponse' + ) as QuestionnaireResponse; + const parsed = parseBundle(data); - const appointments = data.filter( - (resource: FhirResource) => resource.resourceType === 'Appointment' - ) as Appointment[]; - const allEncountersForSelection = data.filter( - (resource: FhirResource) => resource.resourceType === 'Encounter' - ) as Encounter[]; - const appointment = ( - appointmentId - ? appointments.find((resource) => resource.id === appointmentId) - : appointments.find((apt) => - allEncountersForSelection.some( - (enc) => !enc.partOf && enc.appointment?.some((ref) => ref.reference === `Appointment/${apt.id}`) - ) - ) ?? appointments[0] - ) as Appointment; + const appointment = data?.find((resource: FhirResource) => resource.resourceType === 'Appointment') as Appointment; const patient = data?.find((resource: FhirResource) => resource.resourceType === 'Patient') as Patient; const appointmentLocationRef = appointment?.participant?.find((p) => p.actor?.reference?.startsWith('Location/')) @@ -401,22 +357,12 @@ const selectAppointmentData = ( resource.resourceType === 'Location' && resource.id === appointmentLocationId && !isLocationVirtual(resource) ); - const allEncountersRaw = data?.filter( - (resource: FhirResource) => resource.resourceType === 'Encounter' - ) as Encounter[]; - const appointmentRef = `Appointment/${appointment?.id}`; - let followUpOriginEncounter = allEncountersRaw?.find( - (e) => !e.partOf && e.appointment?.some((encAppointmentRef) => encAppointmentRef.reference === appointmentRef) + const followUpOriginEncounter = data?.find( + (resource: FhirResource) => resource.resourceType === 'Encounter' && !resource.partOf ) as Encounter; - const followupEncounters = allEncountersRaw?.filter((e) => e.partOf) as Encounter[] | undefined; - - // For scheduled follow-up appointments, the encounter has partOf set. - // If no non-partOf encounter exists, use the encounter that references this appointment. - if (!followUpOriginEncounter && allEncountersRaw?.length > 0) { - followUpOriginEncounter = allEncountersRaw.find( - (e) => e.appointment?.some((encAppointmentRef) => encAppointmentRef.reference === appointmentRef) - ) as Encounter; - } + const followupEncounters = data?.filter( + (resource: FhirResource) => resource.resourceType === 'Encounter' && resource.partOf + ) as Encounter[] | undefined; // Preserve the selected encounter ID if it exists and is valid, otherwise default to main encounter const allEncounters = [followUpOriginEncounter, ...(followupEncounters || [])].filter(Boolean); @@ -424,14 +370,6 @@ const selectAppointmentData = ( preserveSelectedEncounterId && allEncounters.some((enc) => enc.id === preserveSelectedEncounterId) ? preserveSelectedEncounterId : followUpOriginEncounter?.id; - const selectedEncounter = allEncounters.find((enc) => enc.id === validSelectedEncounterId) ?? followUpOriginEncounter; - const questionnaireResponse = data - ?.filter((resource: FhirResource) => resource.resourceType === 'QuestionnaireResponse') - .find( - (resource) => - (resource as QuestionnaireResponse).encounter?.reference === `Encounter/${selectedEncounter?.id}` || - (resource as QuestionnaireResponse).encounter?.reference === `Encounter/${followUpOriginEncounter?.id}` - ) as QuestionnaireResponse; return { rawResources: data, @@ -459,10 +397,7 @@ const selectAppointmentData = ( ) .flatMap((docRef: FhirResource) => (docRef as DocumentReference).content.map((cnt) => cnt.attachment.url)) .filter(Boolean) as string[]) || [], - reviewAndSignData: extractReviewAndSignAppointmentData(data, { - appointmentId: appointment?.id, - encounterId: selectedEncounter?.id, - }), + reviewAndSignData: extractReviewAndSignAppointmentData(data), resources: parsed.resources, mappedData: parsed.mappedData, @@ -540,10 +475,6 @@ const useGetAppointment = ( name: '_revinclude:iterate', value: 'Encounter:part-of', }, - { - name: '_include:iterate', - value: 'Encounter:appointment', - }, { name: '_revinclude:iterate', value: 'QuestionnaireResponse:encounter', @@ -560,48 +491,7 @@ const useGetAppointment = ( resource.questionnaire?.includes('https://ottehr.com/FHIR/Questionnaire/intake-paperwork-virtual') ); - // For scheduled follow-ups: the encounter has partOf referencing a parent encounter. - // We need to fetch the parent encounter's full tree to show all encounters in the sidebar. - const scheduledFollowupEncounter = data.find( - (r) => r.resourceType === 'Encounter' && (r as Encounter).partOf - ) as Encounter | undefined; - - if (scheduledFollowupEncounter?.partOf?.reference) { - const parentEncounterId = scheduledFollowupEncounter.partOf.reference.replace('Encounter/', ''); - // Fetch the parent encounter to find its appointment, then fetch the full tree - const parentEncounter = await oystehr.fhir.get({ - resourceType: 'Encounter', - id: parentEncounterId, - }); - const parentAppointmentId = parentEncounter.appointment?.[0]?.reference?.replace('Appointment/', ''); - - if (parentAppointmentId && parentAppointmentId !== appointmentId) { - const parentData = ( - await oystehr.fhir.search({ - resourceType: 'Appointment', - params: [ - { name: '_id', value: parentAppointmentId }, - { name: '_revinclude:iterate', value: 'Encounter:appointment' }, - { name: '_revinclude:iterate', value: 'Encounter:part-of' }, - { name: '_include:iterate', value: 'Encounter:appointment' }, - { name: '_revinclude:iterate', value: 'QuestionnaireResponse:encounter' }, - ], - }) - ).unbundle(); - - // Merge parent tree data, avoiding duplicates - const existingIds = new Set(data.map((r) => `${r.resourceType}/${r.id}`)); - for (const resource of parentData) { - const key = `${resource.resourceType}/${resource.id}`; - if (!existingIds.has(key)) { - data.push(resource); - existingIds.add(key); - } - } - } - } - - return selectAppointmentData(data, currentSelectedEncounterId || scheduledFollowupEncounter?.id, appointmentId); + return selectAppointmentData(data, currentSelectedEncounterId); } throw new Error('fhir client not defined or appointmentId not provided'); }, diff --git a/apps/ehr/src/features/visits/shared/stores/appointment/parser/extractors.test.ts b/apps/ehr/src/features/visits/shared/stores/appointment/parser/extractors.test.ts deleted file mode 100644 index 4d2945628c..0000000000 --- a/apps/ehr/src/features/visits/shared/stores/appointment/parser/extractors.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Appointment, Encounter, QuestionnaireResponse } from 'fhir/r4b'; -import { describe, expect, it } from 'vitest'; -import { getResources } from './extractors'; - -describe('getResources', () => { - it('selects appointment and questionnaire by explicit context IDs', () => { - const parentAppointment: Appointment = { - resourceType: 'Appointment', - id: 'parent-appointment', - status: 'booked', - participant: [], - }; - const followupAppointment: Appointment = { - resourceType: 'Appointment', - id: 'followup-appointment', - status: 'booked', - participant: [], - }; - const parentEncounter: Encounter = { - resourceType: 'Encounter', - id: 'parent-encounter', - status: 'in-progress', - class: { system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', code: 'VR', display: 'virtual' }, - appointment: [{ reference: 'Appointment/parent-appointment' }], - }; - const followupEncounter: Encounter = { - resourceType: 'Encounter', - id: 'followup-encounter', - status: 'planned', - class: { system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', code: 'VR', display: 'virtual' }, - partOf: { reference: 'Encounter/parent-encounter' }, - appointment: [{ reference: 'Appointment/followup-appointment' }], - }; - const parentQuestionnaire: QuestionnaireResponse = { - resourceType: 'QuestionnaireResponse', - id: 'parent-qr', - status: 'completed', - encounter: { reference: 'Encounter/parent-encounter' }, - }; - const followupQuestionnaire: QuestionnaireResponse = { - resourceType: 'QuestionnaireResponse', - id: 'followup-qr', - status: 'completed', - encounter: { reference: 'Encounter/followup-encounter' }, - }; - - const result = getResources( - [ - parentAppointment, - followupQuestionnaire, - parentQuestionnaire, - followupEncounter, - parentEncounter, - followupAppointment, - ], - { - appointmentId: 'followup-appointment', - encounterId: 'followup-encounter', - } - ); - - expect(result.appointment?.id).toBe('followup-appointment'); - expect(result.encounter?.id).toBe('followup-encounter'); - expect(result.questionnaireResponse?.id).toBe('followup-qr'); - }); - - it('prefers the main appointment when no context is provided and multiple appointments exist', () => { - const parentAppointment: Appointment = { - resourceType: 'Appointment', - id: 'parent-appointment', - status: 'booked', - participant: [], - }; - const followupAppointment: Appointment = { - resourceType: 'Appointment', - id: 'followup-appointment', - status: 'booked', - participant: [], - }; - const parentEncounter: Encounter = { - resourceType: 'Encounter', - id: 'parent-encounter', - status: 'in-progress', - class: { system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', code: 'VR', display: 'virtual' }, - appointment: [{ reference: 'Appointment/parent-appointment' }], - }; - const followupEncounter: Encounter = { - resourceType: 'Encounter', - id: 'followup-encounter', - status: 'planned', - class: { system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', code: 'VR', display: 'virtual' }, - partOf: { reference: 'Encounter/parent-encounter' }, - appointment: [{ reference: 'Appointment/followup-appointment' }], - }; - const parentQuestionnaire: QuestionnaireResponse = { - resourceType: 'QuestionnaireResponse', - id: 'parent-qr', - status: 'completed', - encounter: { reference: 'Encounter/parent-encounter' }, - }; - - const result = getResources([ - followupAppointment, - parentAppointment, - followupEncounter, - parentEncounter, - parentQuestionnaire, - ]); - - expect(result.appointment?.id).toBe('parent-appointment'); - expect(result.encounter?.id).toBe('parent-encounter'); - expect(result.questionnaireResponse?.id).toBe('parent-qr'); - }); -}); diff --git a/apps/ehr/src/features/visits/shared/stores/appointment/parser/extractors.ts b/apps/ehr/src/features/visits/shared/stores/appointment/parser/extractors.ts index 11c519ff91..7aaab0ff76 100644 --- a/apps/ehr/src/features/visits/shared/stores/appointment/parser/extractors.ts +++ b/apps/ehr/src/features/visits/shared/stores/appointment/parser/extractors.ts @@ -192,17 +192,8 @@ export const extractUrlsFromAppointmentData = (resourceBundle: FhirResource[], d ); }; -const findMainAppointment = (appointments: Appointment[], encounters: Encounter[]): Appointment | undefined => { - return ( - appointments.find((apt) => - encounters.some((enc) => !enc.partOf && enc.appointment?.some((ref) => ref.reference === `Appointment/${apt.id}`)) - ) ?? appointments[0] - ); -}; - export const getResources = ( - resourceBundle: FhirResource[] | null, - context?: { appointmentId?: string; encounterId?: string } + resourceBundle: FhirResource[] | null ): Partial<{ appointment: Appointment; patient: Patient; @@ -217,32 +208,16 @@ export const getResources = ( resourceBundle.filter((resource: FhirResource) => resource.resourceType === resourceType) as T[] | undefined; const locations = findResources('Location'); - const appointments = findResources('Appointment') ?? []; - const encounters = findResources('Encounter') ?? []; - const questionnaireResponses = findResources('QuestionnaireResponse') ?? []; const virtualLocation = locations?.find(isLocationVirtual); const physicalLocation = locations?.find((location) => !isLocationVirtual(location)); - const appointment = context?.appointmentId - ? appointments.find((resource) => resource.id === context.appointmentId) - : findMainAppointment(appointments, encounters); - const encounter = context?.encounterId - ? encounters.find((resource) => resource.id === context.encounterId) - : encounters.find( - (resource) => - !resource.partOf && - resource.appointment?.some((appointmentRef) => appointmentRef.reference === `Appointment/${appointment?.id}`) - ) ?? encounters.find((resource) => !resource.partOf); - const questionnaireResponse = - questionnaireResponses.find((resource) => resource.encounter?.reference === `Encounter/${encounter?.id}`) ?? - questionnaireResponses[0]; return { - appointment, + appointment: findResources('Appointment')?.[0], patient: findResources('Patient')?.[0], location: physicalLocation, locationVirtual: virtualLocation, - encounter, - questionnaireResponse, + encounter: findResources('Encounter')?.find((encounter) => !encounter.partOf), + questionnaireResponse: findResources('QuestionnaireResponse')?.[0], }; }; diff --git a/apps/ehr/src/features/visits/shared/utils/appointment-accessibility.helper.ts b/apps/ehr/src/features/visits/shared/utils/appointment-accessibility.helper.ts index d2b830f2bf..f6f55df805 100644 --- a/apps/ehr/src/features/visits/shared/utils/appointment-accessibility.helper.ts +++ b/apps/ehr/src/features/visits/shared/utils/appointment-accessibility.helper.ts @@ -65,11 +65,10 @@ export const getAppointmentAccessibilityData = ({ const isAppointmentLockedByMetaTag = appointment ? isAppointmentLocked(appointment) : false; const visitType = getEncounterVisitType(encounter); const isFollowup = visitType === 'follow-up'; - const isScheduledFollowup = visitType === 'scheduled-follow-up'; const isAppointmentReadOnly = (() => { if (appFlags.isInPerson) { - return isAppointmentLockedByMetaTag && !isFollowup && !isScheduledFollowup; + return isAppointmentLockedByMetaTag && !isFollowup; } return ( diff --git a/apps/ehr/src/features/visits/telemed/utils/appointments.test.ts b/apps/ehr/src/features/visits/telemed/utils/appointments.test.ts deleted file mode 100644 index f27a4da09d..0000000000 --- a/apps/ehr/src/features/visits/telemed/utils/appointments.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Appointment, Encounter } from 'fhir/r4b'; -import { describe, expect, it } from 'vitest'; -import { extractReviewAndSignAppointmentData } from './appointments'; - -describe('extractReviewAndSignAppointmentData', () => { - it('matches encounter by appointment reference instead of first bundle order', () => { - const targetAppointment: Appointment = { - resourceType: 'Appointment', - id: 'target-appointment', - status: 'fulfilled', - start: '2026-03-23T10:00:00.000Z', - participant: [], - }; - const parentAppointment: Appointment = { - resourceType: 'Appointment', - id: 'parent-appointment', - status: 'fulfilled', - start: '2026-03-20T10:00:00.000Z', - participant: [], - }; - const parentEncounter: Encounter = { - resourceType: 'Encounter', - id: 'parent-encounter', - status: 'finished', - appointment: [{ reference: 'Appointment/parent-appointment' }], - class: { system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', code: 'VR', display: 'virtual' }, - statusHistory: [{ status: 'finished', period: { end: '2026-03-20T10:45:00.000Z' } }], - }; - const targetEncounter: Encounter = { - resourceType: 'Encounter', - id: 'target-encounter', - status: 'finished', - appointment: [{ reference: 'Appointment/target-appointment' }], - class: { system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', code: 'VR', display: 'virtual' }, - statusHistory: [{ status: 'finished', period: { end: '2026-03-23T10:30:00.000Z' } }], - }; - - const result = extractReviewAndSignAppointmentData( - [parentAppointment, targetAppointment, parentEncounter, targetEncounter], - { appointmentId: 'target-appointment' } - ); - - expect(result).toEqual({ signedOnDate: '2026-03-23T10:30:00.000Z' }); - }); - - it('prefers main appointment when no context is provided and follow-up appointment appears first', () => { - const followupAppointment: Appointment = { - resourceType: 'Appointment', - id: 'followup-appointment', - status: 'booked', - participant: [], - }; - const mainAppointment: Appointment = { - resourceType: 'Appointment', - id: 'main-appointment', - status: 'fulfilled', - participant: [], - }; - const mainEncounter: Encounter = { - resourceType: 'Encounter', - id: 'main-encounter', - status: 'finished', - appointment: [{ reference: 'Appointment/main-appointment' }], - class: { system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', code: 'VR', display: 'virtual' }, - statusHistory: [{ status: 'finished', period: { end: '2026-03-20T12:00:00.000Z' } }], - }; - const followupEncounter: Encounter = { - resourceType: 'Encounter', - id: 'followup-encounter', - status: 'planned', - partOf: { reference: 'Encounter/main-encounter' }, - appointment: [{ reference: 'Appointment/followup-appointment' }], - class: { system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', code: 'VR', display: 'virtual' }, - }; - - const result = extractReviewAndSignAppointmentData([ - followupAppointment, - mainAppointment, - followupEncounter, - mainEncounter, - ]); - - expect(result).toEqual({ signedOnDate: '2026-03-20T12:00:00.000Z' }); - }); -}); diff --git a/apps/ehr/src/features/visits/telemed/utils/appointments.ts b/apps/ehr/src/features/visits/telemed/utils/appointments.ts index 5ce5552885..c48630c9b2 100644 --- a/apps/ehr/src/features/visits/telemed/utils/appointments.ts +++ b/apps/ehr/src/features/visits/telemed/utils/appointments.ts @@ -180,25 +180,10 @@ export const extractPhotoUrlsFromAppointmentData = (appointment: AppointmentReso ); }; -const findMainAppointment = (appointments: Appointment[], encounters: Encounter[]): Appointment | undefined => { - return ( - appointments.find((apt) => - encounters.some((enc) => !enc.partOf && enc.appointment?.some((ref) => ref.reference === `Appointment/${apt.id}`)) - ) ?? appointments[0] - ); -}; - -export const extractReviewAndSignAppointmentData = ( - data: AppointmentResources[], - context?: { appointmentId?: string; encounterId?: string } -): ReviewAndSignData | undefined => { - const appointments = data.filter( +export const extractReviewAndSignAppointmentData = (data: AppointmentResources[]): ReviewAndSignData | undefined => { + const appointment = data?.find( (resource: FhirResource) => resource.resourceType === 'Appointment' - ) as Appointment[]; - const encounters = data.filter((resource: FhirResource) => resource.resourceType === 'Encounter') as Encounter[]; - const appointment = context?.appointmentId - ? appointments.find((resource) => resource.id === context.appointmentId) - : findMainAppointment(appointments, encounters); + ) as unknown as Appointment; if (!appointment) { return; @@ -206,12 +191,9 @@ export const extractReviewAndSignAppointmentData = ( const appointmentStatus = appointment.status; - const encounter = context?.encounterId - ? encounters.find((resource) => resource.id === context.encounterId) - : encounters.find( - (resource) => - resource.appointment?.some((appointmentRef) => appointmentRef.reference === `Appointment/${appointment.id}`) - ) ?? encounters[0]; + const encounter = data?.find( + (resource: FhirResource) => resource.resourceType === 'Encounter' + ) as unknown as Encounter; if (!encounter) { return; diff --git a/apps/ehr/src/pages/AddPatient.tsx b/apps/ehr/src/pages/AddPatient.tsx index 306ec48862..74a7ee39d7 100644 --- a/apps/ehr/src/pages/AddPatient.tsx +++ b/apps/ehr/src/pages/AddPatient.tsx @@ -17,7 +17,7 @@ import { Location, Schedule, Slot } from 'fhir/r4b'; import { DateTime } from 'luxon'; import { enqueueSnackbar } from 'notistack'; import { useEffect, useMemo, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { AddVisitPatientInformationCard } from 'src/features/visits/shared/components/staff-add-visit/AddVisitPatientInformationCard'; import { BOOKING_CONFIG, @@ -82,28 +82,9 @@ enum VisitType { } export default function AddPatient(): JSX.Element { - const location = useLocation(); - const followUpState = location.state as - | { - parentEncounterId?: string; - parentLocation?: LocationWithWalkinSchedule; - patientId?: string; - patientInfo?: AddVisitPatientInfo; - } - | undefined; - const parentEncounterId = followUpState?.parentEncounterId; - const isScheduledFollowUp = !!parentEncounterId; - console.log('[AddPatient] location.state:', location.state, 'parentEncounterId:', parentEncounterId); - - const [selectedLocation, setSelectedLocation] = useState( - followUpState?.parentLocation - ); - const [birthDate, setBirthDate] = useState( - followUpState?.patientInfo?.dateOfBirth ? DateTime.fromISO(followUpState.patientInfo.dateOfBirth) : null - ); - const [patientInfo, setPatientInfo] = useState( - followUpState?.patientInfo || undefined - ); + const [selectedLocation, setSelectedLocation] = useState(); + const [birthDate, setBirthDate] = useState(null); // i would love to not have to handle this state but i think the date search component would have to change and i dont want to touch that right now + const [patientInfo, setPatientInfo] = useState(undefined); const [reasonForVisit, setReasonForVisit] = useState(''); const [reasonForVisitAdditional, setReasonForVisitAdditional] = useState(''); const [visitType, setVisitType] = useState(); @@ -121,9 +102,7 @@ export default function AddPatient(): JSX.Element { const [validDate, setValidDate] = useState(true); const [selectSlotDialogOpen, setSelectSlotDialogOpen] = useState(false); const [validReasonForVisit, setValidReasonForVisit] = useState(true); - const [showFields, setShowFields] = useState( - isScheduledFollowUp ? 'existingPatientSelected' : 'initialPatientSearch' - ); + const [showFields, setShowFields] = useState('initialPatientSearch'); useEffect(() => { setReasonForVisit(''); @@ -254,7 +233,6 @@ export default function AddPatient(): JSX.Element { reasonAdditional: reasonForVisitAdditional !== '' ? reasonForVisitAdditional : undefined, }, slotId: persistedSlot.id!, - parentEncounterId, }; let response; @@ -283,7 +261,7 @@ export default function AddPatient(): JSX.Element { @@ -295,7 +273,7 @@ export default function AddPatient(): JSX.Element { color={'primary.dark'} data-testid={dataTestIds.addPatientPage.pageTitle} > - {isScheduledFollowUp ? 'Add Scheduled Follow-up Visit' : 'Add Visit'} + Add Visit {/* form content */} @@ -363,24 +341,17 @@ export default function AddPatient(): JSX.Element { } /> - {!isScheduledFollowUp && ( - - )} - {isScheduledFollowUp && patientInfo && ( - - Patient: {patientInfo.firstName} {patientInfo.lastName} - - )} + {/* Visit Information */} {shouldShowReasonForVisitFields && ( diff --git a/apps/ehr/tests/component/AddPatientFollowup.test.tsx b/apps/ehr/tests/component/AddPatientFollowup.test.tsx deleted file mode 100644 index 83706dccb8..0000000000 --- a/apps/ehr/tests/component/AddPatientFollowup.test.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { ReactNode } from 'react'; -import { BrowserRouter } from 'react-router-dom'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import AddPatientFollowup from '../../src/features/visits/shared/components/patient/AddPatientFollowup'; - -vi.mock('react-router-dom', async () => { - const actual = await vi.importActual('react-router-dom'); - return { - ...actual, - useParams: vi.fn().mockReturnValue({ id: 'test-patient-123' }), - useNavigate: vi.fn().mockReturnValue(vi.fn()), - useLocation: vi.fn().mockReturnValue({ state: null, pathname: '', search: '', hash: '', key: '' }), - }; -}); - -vi.mock('../../src/hooks/useGetPatient', () => ({ - useGetPatient: () => ({ - patient: { - resourceType: 'Patient', - id: 'test-patient-123', - name: [{ given: ['Test'], family: 'Patient' }], - birthDate: '1990-01-01', - gender: 'male', - }, - }), -})); - -// Mock the child form components to avoid deep dependency issues -vi.mock('../../src/features/visits/shared/components/patient/PatientFollowupForm', () => ({ - default: () =>
    Annotation Form
    , -})); - -vi.mock('../../src/features/visits/shared/components/patient/ScheduledFollowupParentSelector', () => ({ - default: () =>
    Scheduled Form
    , -})); - -vi.mock('../../src/layout/PageContainer', () => ({ - default: ({ children }: { children: ReactNode }) =>
    {children}
    , -})); - -vi.mock('../../src/components/CustomBreadcrumbs', () => ({ - default: () =>
    Breadcrumbs
    , -})); - -describe('AddPatientFollowup', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('renders the page title', () => { - render( - - - - ); - expect(screen.getByText('Add Follow-up Visit')).toBeVisible(); - }); - - it('renders Annotation and Scheduled Visit radio buttons', () => { - render( - - - - ); - expect(screen.getByLabelText('Annotation')).toBeInTheDocument(); - expect(screen.getByLabelText('Scheduled Visit')).toBeInTheDocument(); - // Labels should be visible even though the radio input itself is visually hidden by MUI - expect(screen.getByText('Annotation')).toBeVisible(); - expect(screen.getByText('Scheduled Visit')).toBeVisible(); - }); - - it('defaults to Annotation mode and shows annotation form', () => { - render( - - - - ); - expect(screen.getByLabelText('Annotation')).toBeChecked(); - expect(screen.getByTestId('annotation-form')).toBeVisible(); - expect(screen.queryByTestId('scheduled-form')).not.toBeInTheDocument(); - }); - - it('switches to Scheduled form when Scheduled Visit is selected', async () => { - const user = userEvent.setup(); - render( - - - - ); - - await user.click(screen.getByLabelText('Scheduled Visit')); - - expect(screen.getByTestId('scheduled-form')).toBeVisible(); - expect(screen.queryByTestId('annotation-form')).not.toBeInTheDocument(); - }); - - it('switches back to Annotation form when Annotation is re-selected', async () => { - const user = userEvent.setup(); - render( - - - - ); - - await user.click(screen.getByLabelText('Scheduled Visit')); - expect(screen.getByTestId('scheduled-form')).toBeVisible(); - - await user.click(screen.getByLabelText('Annotation')); - expect(screen.getByTestId('annotation-form')).toBeVisible(); - expect(screen.queryByTestId('scheduled-form')).not.toBeInTheDocument(); - }); - - it('renders breadcrumbs', () => { - render( - - - - ); - expect(screen.getByTestId('breadcrumbs')).toBeVisible(); - }); -}); diff --git a/apps/ehr/tests/e2e/specs/in-person/scheduled-followup.spec.ts b/apps/ehr/tests/e2e/specs/in-person/scheduled-followup.spec.ts deleted file mode 100644 index ea4bb8d41f..0000000000 --- a/apps/ehr/tests/e2e/specs/in-person/scheduled-followup.spec.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { BrowserContext, expect, Page, test } from '@playwright/test'; -import { dataTestIds } from 'src/constants/data-test-ids'; -import { ResourceHandler } from '../../../e2e-utils/resource-handler'; - -const DEFAULT_TIMEOUT = { timeout: 15000 }; -const LONG_TIMEOUT = { timeout: 35000 }; - -let context: BrowserContext; -let page: Page; -const resourceHandler = new ResourceHandler('in-person'); - -test.beforeAll(async ({ browser }) => { - context = await browser.newContext(); - page = await context.newPage(); - await resourceHandler.setResources({ skipPaperwork: true }); -}); - -test.afterAll(async () => { - await page.close(); - await context.close(); - await resourceHandler.cleanupResources(); -}); - -test.describe.serial('Scheduled Follow-up Visit E2E', () => { - test('Follow-up page shows Annotation and Scheduled Visit toggle', async () => { - const patientId = resourceHandler.patient?.id; - expect(patientId).toBeTruthy(); - - await page.goto(`/patient/${patientId}/followup/add`); - - await test.step('Page title and radio buttons are visible', async () => { - await expect(page.getByText('Add Follow-up Visit')).toBeVisible(DEFAULT_TIMEOUT); - await expect(page.getByText('Annotation', { exact: true })).toBeVisible(DEFAULT_TIMEOUT); - await expect(page.getByText('Scheduled Visit', { exact: true })).toBeVisible(DEFAULT_TIMEOUT); - }); - - await test.step('Annotation is selected by default with annotation-specific fields', async () => { - await expect(page.getByLabel('Annotation', { exact: true })).toBeChecked(); - await expect(page.locator('label:has-text("Annotation provider")')).toBeVisible(DEFAULT_TIMEOUT); - }); - - await test.step('Switching to Scheduled shows initial visit selector', async () => { - await page.getByLabel('Scheduled Visit', { exact: true }).click(); - await expect(page.getByRole('combobox', { name: /initial visit/i })).toBeVisible(DEFAULT_TIMEOUT); - await expect(page.getByText('Continue to Add Visit')).toBeVisible(DEFAULT_TIMEOUT); - await expect(page.locator('label:has-text("Annotation provider")')).not.toBeVisible(); - }); - - await test.step('Switching back to Annotation restores annotation fields', async () => { - await page.getByLabel('Annotation', { exact: true }).click(); - await expect(page.locator('label:has-text("Annotation provider")')).toBeVisible(DEFAULT_TIMEOUT); - await expect(page.getByText('Continue to Add Visit')).not.toBeVisible(); - }); - }); - - test('Create a scheduled follow-up, verify on tracking board and patient record', async () => { - const patientId = resourceHandler.patient?.id; - expect(patientId).toBeTruthy(); - - // Step 1: Navigate to follow-up creation page - await test.step('Navigate to follow-up page and select Scheduled Visit', async () => { - await page.goto(`/patient/${patientId}/followup/add`); - await expect(page.getByText('Add Follow-up Visit')).toBeVisible(DEFAULT_TIMEOUT); - await page.getByLabel('Scheduled Visit', { exact: true }).click(); - }); - - // Step 2: Select parent encounter - await test.step('Select parent encounter', async () => { - const combobox = page.getByRole('combobox', { name: /initial visit/i }); - await expect(combobox).toBeVisible(DEFAULT_TIMEOUT); - await combobox.click(); - await page.getByRole('option').first().waitFor(DEFAULT_TIMEOUT); - await page.getByRole('option').first().click(); - }); - - // Step 3: Continue to Add Visit page - await test.step('Continue to Add Visit page', async () => { - await page.getByText('Continue to Add Visit').click(); - await expect(page.getByText('Add Scheduled Follow-up Visit')).toBeVisible(LONG_TIMEOUT); - await expect(page.getByText(/Patient:/)).toBeVisible(DEFAULT_TIMEOUT); - }); - - // Step 4: Select visit type - await test.step('Select walk-in visit type', async () => { - await page.getByTestId(dataTestIds.addPatientPage.visitTypeDropdown).click(); - await page.getByText('Walk-in In Person Visit').click(); - }); - - // Step 5: Select service category if not disabled (auto-selected when only one) - await test.step('Select service category', async () => { - const serviceCategoryDropdown = page.getByTestId(dataTestIds.addPatientPage.serviceCategoryDropdown); - const isDisabled = - (await serviceCategoryDropdown.locator('[aria-disabled="true"]').count()) > 0 || - (await serviceCategoryDropdown.locator('input[disabled]').count()) > 0; - if (!isDisabled) { - await serviceCategoryDropdown.click(); - await page.getByRole('option').first().waitFor(DEFAULT_TIMEOUT); - await page.getByRole('option').first().click(); - } - }); - - // Step 6: Select location from dropdown (must use dropdown to get walkinSchedule) - await test.step('Select location', async () => { - const locationSelect = page.getByTestId(dataTestIds.dashboard.locationSelect); - // Clear and re-select to ensure walkinSchedule data is loaded - await locationSelect.click(); - await page.locator('li[role="option"]').first().waitFor(DEFAULT_TIMEOUT); - await page.locator('li[role="option"]').first().click(); - }); - - // Step 7: Select reason for visit and submit - await test.step('Fill reason for visit and submit', async () => { - const reasonDropdown = page.getByTestId(dataTestIds.addPatientPage.reasonForVisitDropdown); - await expect(reasonDropdown).toBeVisible(LONG_TIMEOUT); - // Click the dropdown's combobox element to open it - await reasonDropdown.locator('[role="combobox"]').click(); - // Wait for menu and select Fever - const feverOption = page.getByRole('option', { name: 'Fever' }); - await expect(feverOption).toBeVisible(DEFAULT_TIMEOUT); - await feverOption.click(); - // Wait for menu to close - await page.waitForTimeout(500); - - // Submit the form — use force click to bypass any overlays - const addButton = page.getByTestId(dataTestIds.addPatientPage.addButton); - await expect(addButton).toBeEnabled(DEFAULT_TIMEOUT); - - // Log current form state for debugging - const currentUrl = page.url(); - console.log('Before submit, URL:', currentUrl); - - await addButton.click({ force: true }); - - // Wait for either navigation or loading state to complete - // The submission triggers createSlot + createAppointment which can take time - await page.waitForURL((url) => url.pathname === '/visits', { timeout: 60000, waitUntil: 'domcontentloaded' }); - console.log('After submit, URL:', page.url()); - }); - - // Step 7: Verify on tracking board — visit was created and we redirected - await test.step('Verify on tracking board', async () => { - await expect(page).toHaveURL(/\/visits/); - }); - - // Step 8: Verify on patient record with indentation - await test.step('Verify indented follow-up on patient record', async () => { - await page.goto(`/patient/${patientId}`); - const tableRows = page.locator('table tbody tr'); - await expect(tableRows.first()).toBeVisible(LONG_TIMEOUT); - - // Verify indentation icon exists (SubdirectoryArrowRightIcon) - const indentedRows = page.locator('svg[data-testid="SubdirectoryArrowRightIcon"]'); - await expect(indentedRows.first()).toBeVisible(DEFAULT_TIMEOUT); - }); - }); -}); diff --git a/packages/utils/lib/fhir/encounter.test.ts b/packages/utils/lib/fhir/encounter.test.ts deleted file mode 100644 index 399203481b..0000000000 --- a/packages/utils/lib/fhir/encounter.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { Encounter } from 'fhir/r4b'; -import { describe, expect, it } from 'vitest'; -import { - EncounterVisitType, - FOLLOWUP_SUBTYPE_SYSTEM, - FOLLOWUP_SYSTEMS, - getEncounterVisitType, - getFollowupSubtype, - isAnnotationFollowupEncounter, - isFollowupEncounter, - isScheduledFollowupEncounter, -} from './encounter'; - -const makeEncounter = (overrides: Partial = {}): Encounter => ({ - resourceType: 'Encounter', - status: 'in-progress', - class: { system: 'http://hl7.org/fhir/R4/v3/ActEncounterCode/vs.html', code: 'ACUTE' }, - ...overrides, -}); - -const makeFollowupType = (subtype?: string): Encounter['type'] => [ - { - coding: [ - { - system: FOLLOWUP_SYSTEMS.type.url, - code: FOLLOWUP_SYSTEMS.type.code, - display: 'Follow-up Encounter', - }, - ...(subtype - ? [ - { - system: FOLLOWUP_SUBTYPE_SYSTEM, - code: subtype, - display: subtype, - }, - ] - : []), - ], - text: 'Follow-up Encounter', - }, -]; - -describe('Encounter follow-up helpers', () => { - describe('isFollowupEncounter', () => { - it('returns false for a regular encounter', () => { - expect(isFollowupEncounter(makeEncounter())).toBe(false); - }); - - it('returns true for an annotation follow-up', () => { - expect(isFollowupEncounter(makeEncounter({ type: makeFollowupType('annotation') }))).toBe(true); - }); - - it('returns true for a scheduled follow-up', () => { - expect(isFollowupEncounter(makeEncounter({ type: makeFollowupType('scheduled') }))).toBe(true); - }); - - it('returns true for a follow-up without subtype coding', () => { - expect(isFollowupEncounter(makeEncounter({ type: makeFollowupType() }))).toBe(true); - }); - }); - - describe('getFollowupSubtype', () => { - it('returns undefined for a regular encounter', () => { - expect(getFollowupSubtype(makeEncounter())).toBeUndefined(); - }); - - it('returns "annotation" for an annotation follow-up', () => { - expect(getFollowupSubtype(makeEncounter({ type: makeFollowupType('annotation') }))).toBe('annotation'); - }); - - it('returns "scheduled" for a scheduled follow-up', () => { - expect(getFollowupSubtype(makeEncounter({ type: makeFollowupType('scheduled') }))).toBe('scheduled'); - }); - - it('defaults to "annotation" when follow-up has no subtype coding', () => { - expect(getFollowupSubtype(makeEncounter({ type: makeFollowupType() }))).toBe('annotation'); - }); - }); - - describe('isScheduledFollowupEncounter', () => { - it('returns false for a regular encounter', () => { - expect(isScheduledFollowupEncounter(makeEncounter())).toBe(false); - }); - - it('returns false for an annotation follow-up', () => { - expect(isScheduledFollowupEncounter(makeEncounter({ type: makeFollowupType('annotation') }))).toBe(false); - }); - - it('returns true for a scheduled follow-up', () => { - expect(isScheduledFollowupEncounter(makeEncounter({ type: makeFollowupType('scheduled') }))).toBe(true); - }); - - it('returns false for a follow-up without subtype coding', () => { - expect(isScheduledFollowupEncounter(makeEncounter({ type: makeFollowupType() }))).toBe(false); - }); - }); - - describe('isAnnotationFollowupEncounter', () => { - it('returns false for a regular encounter', () => { - expect(isAnnotationFollowupEncounter(makeEncounter())).toBe(false); - }); - - it('returns true for an annotation follow-up', () => { - expect(isAnnotationFollowupEncounter(makeEncounter({ type: makeFollowupType('annotation') }))).toBe(true); - }); - - it('returns false for a scheduled follow-up', () => { - expect(isAnnotationFollowupEncounter(makeEncounter({ type: makeFollowupType('scheduled') }))).toBe(false); - }); - - it('returns true for a follow-up without subtype coding (defaults to annotation)', () => { - expect(isAnnotationFollowupEncounter(makeEncounter({ type: makeFollowupType() }))).toBe(true); - }); - }); - - describe('getEncounterVisitType', () => { - it('returns "main" for a regular encounter', () => { - expect(getEncounterVisitType(makeEncounter())).toBe('main' as EncounterVisitType); - }); - - it('returns "main" for undefined encounter', () => { - expect(getEncounterVisitType(undefined)).toBe('main' as EncounterVisitType); - }); - - it('returns "follow-up" for an annotation follow-up', () => { - expect(getEncounterVisitType(makeEncounter({ type: makeFollowupType('annotation') }))).toBe( - 'follow-up' as EncounterVisitType - ); - }); - - it('returns "scheduled-follow-up" for a scheduled follow-up', () => { - expect(getEncounterVisitType(makeEncounter({ type: makeFollowupType('scheduled') }))).toBe( - 'scheduled-follow-up' as EncounterVisitType - ); - }); - - it('returns "follow-up" for a follow-up without subtype (defaults to annotation)', () => { - expect(getEncounterVisitType(makeEncounter({ type: makeFollowupType() }))).toBe( - 'follow-up' as EncounterVisitType - ); - }); - }); -}); diff --git a/packages/utils/lib/fhir/encounter.ts b/packages/utils/lib/fhir/encounter.ts index 0980741b34..2239f18e52 100644 --- a/packages/utils/lib/fhir/encounter.ts +++ b/packages/utils/lib/fhir/encounter.ts @@ -1,6 +1,6 @@ import Oystehr from '@oystehr/sdk'; import { Operation } from 'fast-json-patch'; -import { Appointment, Encounter, EncounterStatusHistory, Extension, Location, Resource } from 'fhir/r4b'; +import { Encounter, EncounterStatusHistory, Extension, Location } from 'fhir/r4b'; import { DateTime } from 'luxon'; import { CODE_SYSTEM_ACT_CODE_V3 } from '../helpers'; import { @@ -17,9 +17,6 @@ import { ENCOUNTER_PAYMENT_VARIANT_EXTENSION_URL, FHIR_BASE_URL, FHIR_EXTENSION export const FOLLOWUP_TYPES = ['Follow-up Encounter'] as const; export type FollowupType = (typeof FOLLOWUP_TYPES)[number]; -export type FollowupSubtype = 'annotation' | 'scheduled'; -export const FOLLOWUP_SUBTYPE_SYSTEM = `${FHIR_BASE_URL}/followup-subtype`; - export const FOLLOWUP_REASONS = [ 'Result - Lab', 'Result - Radiology', @@ -57,30 +54,10 @@ export const isFollowupEncounter = (encounter: Encounter): boolean => { ); }; -export const getFollowupSubtype = (encounter: Encounter): FollowupSubtype | undefined => { - if (!isFollowupEncounter(encounter)) return undefined; - const subtypeCoding = encounter.type - ?.flatMap((t) => t.coding ?? []) - .find((c) => c.system === FOLLOWUP_SUBTYPE_SYSTEM); - if (subtypeCoding?.code === 'scheduled') return 'scheduled'; - return 'annotation'; -}; - -export const isScheduledFollowupEncounter = (encounter: Encounter): boolean => { - return getFollowupSubtype(encounter) === 'scheduled'; -}; - -export const isAnnotationFollowupEncounter = (encounter: Encounter): boolean => { - return isFollowupEncounter(encounter) && !isScheduledFollowupEncounter(encounter); -}; - -export type EncounterVisitType = 'main' | 'follow-up' | 'scheduled-follow-up'; +export type EncounterVisitType = 'main' | 'follow-up'; export const getEncounterVisitType = (encounter?: Encounter): EncounterVisitType => { if (encounter && isFollowupEncounter(encounter)) { - if (isScheduledFollowupEncounter(encounter)) { - return 'scheduled-follow-up'; - } return 'follow-up'; } return 'main'; @@ -300,80 +277,3 @@ export const isEncounterSelfPay = (encounter?: Encounter): boolean => { const paymentVariant = getPaymentVariantFromEncounter(encounter); return paymentVariant === PaymentVariant.selfPay; }; - -export const buildAppointmentStartMap = (resources: Resource[]): Record => { - const map: Record = {}; - resources.forEach((r) => { - if (r.resourceType === 'Appointment' && r.id && (r as Appointment).start) { - map[r.id] = (r as Appointment).start!; - } - }); - return map; -}; - -// Resolves the best available datetime for an encounter -export const getEncounterDateTime = ( - encounter: Encounter, - appointmentStartMap: Record -): string | undefined => { - const apptId = encounter.appointment?.[0]?.reference?.replace('Appointment/', ''); - if (apptId && appointmentStartMap[apptId]) return appointmentStartMap[apptId]; - if (encounter.period?.start) return encounter.period.start; - return encounter.statusHistory?.[0]?.period?.start; -}; - -export const getEncounterDisplayName = ( - encounter: Encounter, - appointmentStartMap: Record, - formatDateTime: (iso: string) => string -): string => { - const dateTime = getEncounterDateTime(encounter, appointmentStartMap); - const dateStr = dateTime ? formatDateTime(dateTime) : ''; - if (!encounter.partOf) { - return `Main Visit${dateStr ? ` - ${dateStr}` : ''}`; - } - const typeText = encounter.type?.[0]?.text || 'Follow-up'; - return `${typeText}${dateStr ? ` - ${dateStr}` : ''}`; -}; - -export const getAnnotationFollowupStatusLabel = (encounterStatus: string | undefined): 'OPEN' | 'RESOLVED' => { - return encounterStatus === 'in-progress' ? 'OPEN' : 'RESOLVED'; -}; - -/** - * Determines which encounter should be pre-selected as the "initial visit" - * when creating a new follow-up from the current visit context. - * If the current encounter is itself a follow-up child (has partOf), the parent - * (followUpOriginEncounter) is the initial visit; otherwise the current encounter is. - */ -export const getInitialEncounterIdForFollowUp = ( - encounter: Encounter | undefined, - followUpOriginEncounter: Encounter | undefined -): string | undefined => { - return encounter?.partOf ? followUpOriginEncounter?.id : encounter?.id; -}; - -export const getFollowUpProgressNotePathSegment = ( - followupSubtype: FollowupSubtype | undefined, - encounterStatus: string | undefined -): 'review-and-sign' | 'follow-up-note' | null => { - if (followupSubtype === 'scheduled') { - if (encounterStatus === 'planned' || encounterStatus === 'arrived') return null; - return 'review-and-sign'; - } - return 'follow-up-note'; -}; - -export const getInteractionModeForEncounter = ( - encounter: Encounter, - followUpOriginEncounterId: string | undefined -): 'main' | 'follow-up' => { - if (encounter.id === followUpOriginEncounterId) return 'main'; - if (isScheduledFollowupEncounter(encounter)) return 'main'; - return 'follow-up'; -}; - -export const getEncounterLocationId = (encounter: Encounter | undefined): string | undefined => { - const locationRef = encounter?.location?.[0]?.location?.reference; - return locationRef?.split('/')[1]; -}; diff --git a/packages/utils/lib/types/api/encounter.types.ts b/packages/utils/lib/types/api/encounter.types.ts index f63bd2c6e0..3fb2621277 100644 --- a/packages/utils/lib/types/api/encounter.types.ts +++ b/packages/utils/lib/types/api/encounter.types.ts @@ -1,5 +1,5 @@ import { Location } from 'fhir/r4b'; -import { FollowupReason, FollowupSubtype, FollowupType } from '../../fhir'; +import { FollowupReason, FollowupType } from '../../fhir'; export interface FollowupEncounterDTO { encounterId?: string; @@ -15,7 +15,6 @@ export interface PatientFollowupDetails { encounterId?: string; // will only exist when updating patientId: string | null; followupType: FollowupType; - followupSubtype?: FollowupSubtype; reason?: FollowupReason; otherReason?: string; initialEncounterID?: string; diff --git a/packages/utils/lib/types/api/patient-visit-history.types.ts b/packages/utils/lib/types/api/patient-visit-history.types.ts index a41402f9a2..de5d997ea2 100644 --- a/packages/utils/lib/types/api/patient-visit-history.types.ts +++ b/packages/utils/lib/types/api/patient-visit-history.types.ts @@ -1,5 +1,4 @@ import { Encounter, Task } from 'fhir/r4b'; -import { FollowupSubtype } from '../../fhir'; import { ServiceMode } from '../common'; import { TelemedAppointmentStatusEnum } from '../data'; import { AppointmentType, VisitStatusLabel } from './appointment.types'; @@ -49,8 +48,6 @@ export interface FollowUpVisitHistoryRow { id: string; } | undefined; - followupSubtype?: FollowupSubtype; - appointmentId?: string; } type InPersonAppointmentHistoryRow = BaseAppointmentHistoryRow & { diff --git a/packages/utils/lib/types/api/prebook-create-appointment/prebook-create-appointment.types.ts b/packages/utils/lib/types/api/prebook-create-appointment/prebook-create-appointment.types.ts index ba26a9c92e..ba0bb33a11 100644 --- a/packages/utils/lib/types/api/prebook-create-appointment/prebook-create-appointment.types.ts +++ b/packages/utils/lib/types/api/prebook-create-appointment/prebook-create-appointment.types.ts @@ -11,7 +11,6 @@ export interface CreateAppointmentInputParams { locationState?: string; unconfirmedDateOfBirth?: string | undefined; appointmentMetadata?: Appointment['meta']; - parentEncounterId?: string; } export interface CreateAppointmentResponse { diff --git a/packages/utils/lib/types/data/appointments/appointments.types.ts b/packages/utils/lib/types/data/appointments/appointments.types.ts index 246674bb7e..5e2d12aaa0 100644 --- a/packages/utils/lib/types/data/appointments/appointments.types.ts +++ b/packages/utils/lib/types/data/appointments/appointments.types.ts @@ -131,9 +131,6 @@ export interface InPersonAppointmentInformation waitingMinutes?: number; serviceCategory?: string; location?: Location; - isFollowUp?: boolean; - parentEncounterId?: string; - parentAppointmentId?: string; } export interface TelemedAppointmentInformation extends Omit { diff --git a/packages/utils/lib/utils/scheduleUtils.ts b/packages/utils/lib/utils/scheduleUtils.ts index fd8207975f..f3c8b3d8dd 100644 --- a/packages/utils/lib/utils/scheduleUtils.ts +++ b/packages/utils/lib/utils/scheduleUtils.ts @@ -18,7 +18,7 @@ import { DEFAULT_APPOINTMENT_LENGTH_MINUTES, getFullName, getPatchOperationForNewMetaTag, - isAnnotationFollowupEncounter, + isFollowupEncounter, isLocationVirtual, makeBookingOriginExtensionEntry, SCHEDULE_EXTENSION_URL, @@ -189,7 +189,7 @@ export async function getWaitingMinutesAtSchedule( console.timeEnd('get_longest_waiting_patient'); const arrivedEncounters = searchForLongestWaitingPatient.filter( - (resource) => resource.resourceType === 'Encounter' && !isAnnotationFollowupEncounter(resource) + (resource) => resource.resourceType === 'Encounter' && !isFollowupEncounter(resource as Encounter) ); return getWaitingMinutes(nowForTimezone, arrivedEncounters); diff --git a/packages/zambdas/src/ehr/get-appointments/index.ts b/packages/zambdas/src/ehr/get-appointments/index.ts index d4b2726f03..0258a4ddcd 100644 --- a/packages/zambdas/src/ehr/get-appointments/index.ts +++ b/packages/zambdas/src/ehr/get-appointments/index.ts @@ -36,7 +36,7 @@ import { getVisitStatusHistory, InPersonAppointmentInformation, INSURANCE_CARD_CODE, - isAnnotationFollowupEncounter, + isFollowupEncounter, isNonPaperworkQuestionnaireResponse, isTruthy, PHOTO_ID_CARD_CODE, @@ -240,7 +240,7 @@ export const index = wrapHandler('get-appointments', async (input: ZambdaInput): if (patientId) patientIds.push(`Patient/${patientId}`); } else if (resource.resourceType === 'Patient' && resource.id) { patientIdMap[resource.id] = resource as Patient; - } else if (resource.resourceType === 'Encounter' && !isAnnotationFollowupEncounter(resource as Encounter)) { + } else if (resource.resourceType === 'Encounter' && !isFollowupEncounter(resource as Encounter)) { const asEnc = resource as Encounter; const apptRef = asEnc.appointment?.[0].reference; if (apptRef) { @@ -746,12 +746,5 @@ const makeAppointmentInformation = ( ?.flatMap((codeableConcept) => codeableConcept.coding ?? []) ?.find((coding) => coding.system === SERVICE_CATEGORY_SYSTEM)?.display, location: locationIdToResourceMap[encounter.location?.[0]?.location?.reference ?? ''], - isFollowUp: !!encounter.partOf, - parentEncounterId: encounter.partOf?.reference?.replace('Encounter/', ''), - parentAppointmentId: encounter.partOf - ? Object.entries(apptRefToEncounterMap) - .find(([, enc]) => `Encounter/${enc.id}` === encounter.partOf?.reference)?.[0] - ?.replace('Appointment/', '') - : undefined, }; }; diff --git a/packages/zambdas/src/ehr/patient-visit-history/get/index.ts b/packages/zambdas/src/ehr/patient-visit-history/get/index.ts index e4699dc11d..86a0495242 100644 --- a/packages/zambdas/src/ehr/patient-visit-history/get/index.ts +++ b/packages/zambdas/src/ehr/patient-visit-history/get/index.ts @@ -8,9 +8,7 @@ import { AppointmentTypeOptions, AppointmentTypeSchema, FHIR_RESOURCE_NOT_FOUND, - FOLLOWUP_SUBTYPE_SYSTEM, FOLLOWUP_SYSTEMS, - FollowupSubtype, FollowUpVisitHistoryRow, getAttendingPractitionerId, getCoding, @@ -150,9 +148,6 @@ const performEffect = async (input: EffectInput, oystehr: Oystehr): Promise = {}; - // Track appointment IDs belonging to scheduled follow-up encounters so we can - // exclude them from top-level appointment rows - const scheduledFollowUpAppointmentIds = new Set(); allResources.forEach((res) => { switch (res.resourceType) { case 'Appointment': @@ -163,17 +158,6 @@ const performEffect = async (input: EffectInput, oystehr: Oystehr): Promise t.coding ?? []) - .find((c) => c.system === FOLLOWUP_SUBTYPE_SYSTEM); - if (subtypeCoding?.code === 'scheduled') { - const ownApptRef = res.appointment?.[0]?.reference?.replace('Appointment/', ''); - if (ownApptRef) { - scheduledFollowUpAppointmentIds.add(ownApptRef); - } - } } else { encounters.push(res as Encounter); } @@ -198,11 +182,6 @@ const performEffect = async (input: EffectInput, oystehr: Oystehr): Promise appointment.slot?.some((s) => s.reference === `Slot/${slot.id}`)); const location = locations.find( (location) => appointment?.participant?.some((p) => p.actor?.reference?.replace('Location/', '') === location.id) @@ -224,7 +203,6 @@ const performEffect = async (input: EffectInput, oystehr: Oystehr): Promise fu !== undefined) @@ -430,7 +408,6 @@ interface FollowUpContext { locations: Location[]; originalEncounter: Encounter; serviceCategory?: string; - appointments: Appointment[]; } const followUpVisitHistoryRowFromEncounter = ( @@ -441,7 +418,7 @@ const followUpVisitHistoryRowFromEncounter = ( return undefined; } const followUpType = getFollowUpTypeFromEncounter(encounter); - const { practitioners, locations, originalEncounter, serviceCategory, appointments } = context; + const { practitioners, locations, originalEncounter, serviceCategory } = context; const location = locations.find( (location) => encounter?.location?.some((loc) => loc.location?.reference?.replace('Location/', '') === location.id) @@ -449,33 +426,17 @@ const followUpVisitHistoryRowFromEncounter = ( const office = location?.address?.state && location?.name ? `${location.address.state.toUpperCase()} - ${location.name}` : '-'; const originalAppointmentId = originalEncounter.appointment?.[0]?.reference?.replace('Appointment/', ''); - - // Determine follow-up subtype from encounter type coding - const subtypeCoding = encounter.type - ?.flatMap((t) => t.coding ?? []) - .find((c) => c.system === FOLLOWUP_SUBTYPE_SYSTEM); - const followupSubtype: FollowupSubtype = subtypeCoding?.code === 'scheduled' ? 'scheduled' : 'annotation'; - - // For scheduled follow-ups, get the encounter's own appointment ID and use its data - const ownAppointmentId = - followupSubtype === 'scheduled' ? encounter.appointment?.[0]?.reference?.replace('Appointment/', '') : undefined; - - // For scheduled follow-ups, fall back to appointment data for date and reason - const ownAppointment = ownAppointmentId ? appointments.find((a) => a.id === ownAppointmentId) : undefined; - return { encounterId: encounter.id, - dateTime: encounter.period?.start || ownAppointment?.start, + dateTime: encounter.period?.start, type: followUpType, serviceCategory, - visitReason: encounter.reasonCode?.[0]?.text || ownAppointment?.description, + visitReason: encounter.reasonCode?.[0]?.text, provider: getProviderFromEncounter(encounter, practitioners), office, status: encounter.status ?? '-', originalEncounterId: originalEncounter.id, originalAppointmentId, - followupSubtype, - appointmentId: ownAppointmentId, }; }; diff --git a/packages/zambdas/src/ehr/practice-kpis-report/index.ts b/packages/zambdas/src/ehr/practice-kpis-report/index.ts index 48c33939f7..8af447a58a 100644 --- a/packages/zambdas/src/ehr/practice-kpis-report/index.ts +++ b/packages/zambdas/src/ehr/practice-kpis-report/index.ts @@ -5,7 +5,7 @@ import { appointmentTypeForAppointment, getInPersonVisitStatus, getVisitStatusHistory, - isAnnotationFollowupEncounter, + isFollowupEncounter, isInPersonAppointment, LocationKpiMetrics, OTTEHR_MODULE, @@ -337,8 +337,7 @@ export const index = wrapHandler(ZAMBDA_NAME, async (input: ZambdaInput): Promis (resource): resource is Appointment => resource.resourceType === 'Appointment' ); const encounters = allResources.filter( - (resource): resource is Encounter => - resource.resourceType === 'Encounter' && !isAnnotationFollowupEncounter(resource) + (resource): resource is Encounter => resource.resourceType === 'Encounter' && !isFollowupEncounter(resource) ); console.log( diff --git a/packages/zambdas/src/ehr/save-followup-encounter/helpers.ts b/packages/zambdas/src/ehr/save-followup-encounter/helpers.ts index 49466a9644..4f5aaccfd8 100644 --- a/packages/zambdas/src/ehr/save-followup-encounter/helpers.ts +++ b/packages/zambdas/src/ehr/save-followup-encounter/helpers.ts @@ -2,7 +2,6 @@ import Oystehr from '@oystehr/sdk'; import { Operation } from 'fast-json-patch'; import { CodeableConcept, Coding, Encounter, EncounterParticipant, Location, Reference } from 'fhir/r4b'; import { - FOLLOWUP_SUBTYPE_SYSTEM, FOLLOWUP_SYSTEMS, FollowupReason, formatFhirEncounterToPatientFollowupDetails, @@ -29,7 +28,7 @@ export async function createEncounterResource( start: encounterDetails.start, end: encounterDetails?.end, }, - type: createEncounterType(encounterDetails.followupType, encounterDetails.followupSubtype || 'annotation'), + type: createEncounterType(encounterDetails.followupType), }; if (encounterDetails.location) { @@ -141,7 +140,7 @@ export async function updateEncounterResource( operations.push({ op: 'replace', path: '/type', - value: createEncounterType(encounterDetails.followupType, encounterDetails.followupSubtype || 'annotation'), + value: createEncounterType(encounterDetails.followupType), }); } @@ -323,7 +322,7 @@ export async function updateEncounterResource( } } -const createEncounterType = (type: string, subtype: string = 'annotation'): Encounter['type'] => { +const createEncounterType = (type: string): Encounter['type'] => { return [ { coding: [ @@ -332,11 +331,6 @@ const createEncounterType = (type: string, subtype: string = 'annotation'): Enco code: FOLLOWUP_SYSTEMS.type.code, display: type, }, - { - system: FOLLOWUP_SUBTYPE_SYSTEM, - code: subtype, - display: subtype, - }, ], text: type, }, diff --git a/packages/zambdas/src/ehr/visit-details/get-visit-details/index.ts b/packages/zambdas/src/ehr/visit-details/get-visit-details/index.ts index cfffe3a2b8..b5afd0fb41 100644 --- a/packages/zambdas/src/ehr/visit-details/get-visit-details/index.ts +++ b/packages/zambdas/src/ehr/visit-details/get-visit-details/index.ts @@ -26,7 +26,7 @@ import { getNameFromScheduleResource, getTimezone, INVALID_RESOURCE_ID_ERROR, - isAnnotationFollowupEncounter, + isFollowupEncounter, isValidUUID, MISSING_REQUEST_BODY, MISSING_REQUIRED_PARAMETERS, @@ -167,7 +167,7 @@ const complexValidation = async (input: Input, oystehr: Oystehr): Promise resource.resourceType === 'Appointment') as Appointment; const patient = searchResults.find((resource) => resource.resourceType === 'Patient') as Patient; const encounter = searchResults.find( - (resource) => resource.resourceType === 'Encounter' && !isAnnotationFollowupEncounter(resource as Encounter) + (resource) => resource.resourceType === 'Encounter' && !isFollowupEncounter(resource as Encounter) ) as Encounter; const location = searchResults.find((resource) => resource.resourceType === 'Location') as Location | undefined; const flags = searchResults.filter((resource) => resource.resourceType === 'Flag') as Flag[]; diff --git a/packages/zambdas/src/ehr/visits-overview-report/index.ts b/packages/zambdas/src/ehr/visits-overview-report/index.ts index 3196fb7c65..bf946a5706 100644 --- a/packages/zambdas/src/ehr/visits-overview-report/index.ts +++ b/packages/zambdas/src/ehr/visits-overview-report/index.ts @@ -8,7 +8,7 @@ import { getAttendingPractitionerId, getCoding, getInPersonVisitStatus, - isAnnotationFollowupEncounter, + isFollowupEncounter, isInPersonAppointment, isTelemedAppointment, LocationVisitCount, @@ -105,7 +105,7 @@ export const index = wrapHandler(ZAMBDA_NAME, async (input: ZambdaInput): Promis // Create encounter map for quick lookups to determine visit status const encounterMap = new Map(); encounters - .filter((encounter) => !isAnnotationFollowupEncounter(encounter)) + .filter((encounter) => !isFollowupEncounter(encounter)) .forEach((encounter) => { const appointmentRef = encounter.appointment?.[0]?.reference; if (appointmentRef && encounter.id) { diff --git a/packages/zambdas/src/patient/appointment/create-appointment/index.ts b/packages/zambdas/src/patient/appointment/create-appointment/index.ts index 5fad12960f..965c20d4cf 100644 --- a/packages/zambdas/src/patient/appointment/create-appointment/index.ts +++ b/packages/zambdas/src/patient/appointment/create-appointment/index.ts @@ -31,8 +31,6 @@ import { FHIR_EXTENSION, FhirAppointmentStatus, FhirEncounterStatus, - FOLLOWUP_SUBTYPE_SYSTEM, - FOLLOWUP_SYSTEMS, formatPhoneNumber, formatPhoneNumberDisplay, getAppointmentDurationFromSlot, @@ -82,7 +80,6 @@ interface CreateAppointmentInput { locationState?: string; unconfirmedDateOfBirth?: string; appointmentMetadata?: Appointment['meta']; - parentEncounterId?: string; } // Lifting up value to outside of the handler allows it to stay in memory across warm lambda invocations @@ -118,7 +115,6 @@ export const index = wrapHandler('create-appointment', async (input: ZambdaInput questionnaireCanonical, visitType, appointmentMetadata: maybeMetadata, - parentEncounterId, } = effectInput; console.log('effectInput', effectInput); console.timeEnd('performing-complex-validation'); @@ -152,7 +148,6 @@ export const index = wrapHandler('create-appointment', async (input: ZambdaInput unconfirmedDateOfBirth, questionnaireCanonical, appointmentMetadata, - parentEncounterId, }, oystehr ); @@ -283,7 +278,6 @@ export async function createAppointment( createdBy, slot, appointmentMetadata, - parentEncounterId: input.parentEncounterId, }); let relatedPersonId = ''; @@ -375,7 +369,6 @@ interface TransactionInput { formUser?: string; slot?: Slot; appointmentMetadata?: Appointment['meta']; - parentEncounterId?: string; } interface TransactionOutput { appointment: Appointment; @@ -409,20 +402,8 @@ export const performTransactionalFhirRequests = async (input: TransactionInput): serviceMode, slot, appointmentMetadata, - parentEncounterId, } = input; - // Validate parent encounter for scheduled follow-ups — no nesting allowed - if (parentEncounterId) { - const parentEncounter = await oystehr.fhir.get({ - resourceType: 'Encounter', - id: parentEncounterId, - }); - if (parentEncounter.partOf) { - throw new Error('Cannot create a follow-up of a follow-up. Please select a top-level encounter as the parent.'); - } - } - if (!patient && !createPatientRequest?.fullUrl) { throw new Error('Unexpectedly have no patient and no request to make one'); } @@ -573,26 +554,6 @@ export const performTransactionalFhirRequests = async (input: TransactionInput): ] : [], extension: encExtensions, - ...(parentEncounterId && { - partOf: { reference: `Encounter/${parentEncounterId}` }, - type: [ - { - coding: [ - { - system: FOLLOWUP_SYSTEMS.type.url, - code: FOLLOWUP_SYSTEMS.type.code, - display: 'Follow-up Encounter', - }, - { - system: FOLLOWUP_SUBTYPE_SYSTEM, - code: 'scheduled', - display: 'scheduled', - }, - ], - text: 'Follow-up Encounter', - }, - ], - }), }; const { documents, accountInfo } = await getRelatedResources(oystehr, patient?.id); diff --git a/packages/zambdas/src/patient/appointment/create-appointment/validateRequestParameters.ts b/packages/zambdas/src/patient/appointment/create-appointment/validateRequestParameters.ts index ac9a3a6483..a0822d39d1 100644 --- a/packages/zambdas/src/patient/appointment/create-appointment/validateRequestParameters.ts +++ b/packages/zambdas/src/patient/appointment/create-appointment/validateRequestParameters.ts @@ -45,8 +45,7 @@ export function validateCreateAppointmentParams(input: ZambdaInput, user: User): const isEHRUser = user && checkIsEHRUser(user); const bodyJSON = JSON.parse(input.body); - const { slotId, language, patient, unconfirmedDateOfBirth, locationState, appointmentMetadata, parentEncounterId } = - bodyJSON; + const { slotId, language, patient, unconfirmedDateOfBirth, locationState, appointmentMetadata } = bodyJSON; console.log('unconfirmedDateOfBirth', unconfirmedDateOfBirth); console.log('patient:', patient, 'slotId:', slotId); // Check existence of necessary fields @@ -145,7 +144,6 @@ export function validateCreateAppointmentParams(input: ZambdaInput, user: User): unconfirmedDateOfBirth, locationState, appointmentMetadata, - parentEncounterId, }; } @@ -159,7 +157,6 @@ export interface CreateAppointmentEffectInput { visitType: VisitType; locationState?: string; appointmentMetadata?: Appointment['meta']; - parentEncounterId?: string; } export const createAppointmentComplexValidation = async ( @@ -278,6 +275,5 @@ export const createAppointmentComplexValidation = async ( visitType, locationState, appointmentMetadata, - parentEncounterId: input.parentEncounterId, }; }; diff --git a/packages/zambdas/src/patient/appointment/get-past-visits/helpers.ts b/packages/zambdas/src/patient/appointment/get-past-visits/helpers.ts index eb959ec182..848f588285 100644 --- a/packages/zambdas/src/patient/appointment/get-past-visits/helpers.ts +++ b/packages/zambdas/src/patient/appointment/get-past-visits/helpers.ts @@ -1,13 +1,13 @@ import Oystehr, { FhirSearchParams } from '@oystehr/sdk'; import { Appointment, Encounter, Location, Patient, RelatedPerson, Resource, Schedule } from 'fhir/r4b'; -import { isAnnotationFollowupEncounter, OTTEHR_MODULE, removePrefix } from 'utils'; +import { isFollowupEncounter, OTTEHR_MODULE, removePrefix } from 'utils'; export type EncounterToAppointmentIdMap = { [appointmentId: string]: Encounter }; export function mapEncountersToAppointmentIds(allResources: Resource[]): EncounterToAppointmentIdMap { const result: EncounterToAppointmentIdMap = {}; allResources.forEach((resource) => { - if (!(resource.resourceType === 'Encounter' && !isAnnotationFollowupEncounter(resource as Encounter))) return; + if (!(resource.resourceType === 'Encounter' && !isFollowupEncounter(resource as Encounter))) return; const encounter = resource as Encounter; const appointmentReference = encounter?.appointment?.[0].reference || ''; diff --git a/packages/zambdas/src/patient/appointment/get-visit-details/index.ts b/packages/zambdas/src/patient/appointment/get-visit-details/index.ts index 98135d7562..9e74c52939 100644 --- a/packages/zambdas/src/patient/appointment/get-visit-details/index.ts +++ b/packages/zambdas/src/patient/appointment/get-visit-details/index.ts @@ -6,7 +6,6 @@ import { FileURLInfo, getSecret, GetVisitDetailsResponse, - isAnnotationFollowupEncounter, isFollowupEncounter, SecretsKeys, } from 'utils'; @@ -56,7 +55,7 @@ export const index = wrapHandler(ZAMBDA_NAME, async (input: ZambdaInput): Promis ).unbundle(); allEncounters = encounterResults.filter((e) => e.resourceType === 'Encounter') as Encounter[]; // Find the main encounter (not follow-up) - encounter = allEncounters.find((e) => !isAnnotationFollowupEncounter(e)) as Encounter; + encounter = allEncounters.find((e) => !isFollowupEncounter(e)) as Encounter; const appointment = encounterResults.find((e) => e.resourceType === 'Appointment') as Appointment; if (!encounter || !encounter.id) { throw new Error('Error getting appointment encounter'); diff --git a/packages/zambdas/src/patient/check-in/index.ts b/packages/zambdas/src/patient/check-in/index.ts index 542b050414..8e2db1915b 100644 --- a/packages/zambdas/src/patient/check-in/index.ts +++ b/packages/zambdas/src/patient/check-in/index.ts @@ -15,7 +15,7 @@ import { getLocationInformation, getPatchBinary, getTaskResource, - isAnnotationFollowupEncounter, + isFollowupEncounter, isNonPaperworkQuestionnaireResponse, Secrets, TaskIndicator, @@ -102,7 +102,7 @@ export const index = wrapHandler('check-in', async (input: ZambdaInput): Promise if (resource.resourceType === 'Patient') { patient = resource as Patient; } - if (resource.resourceType === 'Encounter' && !isAnnotationFollowupEncounter(resource as Encounter)) { + if (resource.resourceType === 'Encounter' && !isFollowupEncounter(resource as Encounter)) { encounter = resource as Encounter; } if (resource.resourceType === 'Location') { diff --git a/packages/zambdas/src/patient/paperwork/update-paperwork-in-progress/index.ts b/packages/zambdas/src/patient/paperwork/update-paperwork-in-progress/index.ts index 8008497de5..741217d48d 100644 --- a/packages/zambdas/src/patient/paperwork/update-paperwork-in-progress/index.ts +++ b/packages/zambdas/src/patient/paperwork/update-paperwork-in-progress/index.ts @@ -2,7 +2,7 @@ import Oystehr, { User } from '@oystehr/sdk'; import { APIGatewayProxyResult } from 'aws-lambda'; import { Appointment, Encounter, Flag } from 'fhir/r4b'; import { DateTime } from 'luxon'; -import { isAnnotationFollowupEncounter } from 'utils'; +import { isFollowupEncounter } from 'utils'; import { createOystehrClient, getAuth0Token, getUser, wrapHandler, ZambdaInput } from '../../../shared'; import { createOrUpdateFlags } from '../sharedHelpers'; import { validateUpdatePaperworkParams } from './validateRequestParameters'; @@ -72,7 +72,7 @@ async function flagPaperworkInProgress( const appointment = resources.find((resource) => resource.resourceType === 'Appointment'); const encounter = resources.find( - (resource) => resource.resourceType === 'Encounter' && !isAnnotationFollowupEncounter(resource as Encounter) + (resource) => resource.resourceType === 'Encounter' && !isFollowupEncounter(resource as Encounter) ); const existingFlags: Flag[] = ( resources.filter(