Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update appointment page for telemed #394

Merged
merged 7 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/ehr-utils/lib/types/appointment.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum ApptStatus {
'on-video' = 'on-video',
'unsigned' = 'unsigned',
'complete' = 'complete',
'cancelled' = 'cancelled',
}

export type TelemedCallStatuses = `${ApptStatus}`;
Expand Down
2 changes: 2 additions & 0 deletions packages/ehr-utils/lib/types/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export const PRACTITIONER_QUALIFICATION_EXTENSION_URL =
export const PRACTITIONER_QUALIFICATION_CODE_SYSTEM = 'http://terminology.hl7.org/CodeSystem/v2-0360|2.7';

export const PRACTITIONER_QUALIFICATION_STATE_SYSTEM = 'http://hl7.org/fhir/us/core/ValueSet/us-core-usps-state';

export const INTERPRETER_PHONE_NUMBER = '(123) 456-7890';
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export type VisitStatus = VISIT_STATUS_LABEL_TYPE[number];

export const getStatusLabelForAppointmentAndEncounter = (appointment: Appointment): VisitStatus => {
const statusToMap = getStatusFromExtension(appointment);
console.log('statusToMap', statusToMap);
if (statusToMap == undefined) {
console.log(
`Unable to derive Visit status from ${
Expand Down Expand Up @@ -98,7 +97,6 @@ type AppOrEncounter = Appointment | Encounter;
export const getVisitStatusHistory = <T extends AppOrEncounter>(resource: T): VisitStatusHistoryEntry[] => {
const extensions = (resource.extension ?? []).find((ext) => ext.url === visitStatusExtensionUrl)?.extension ?? [];
const history = extensions.map((ext) => {
console.log(2, ext);
const reduced: VisitStatusHistoryEntry = (ext.extension ?? []).reduce(
(accum, currentExt) => {
if (currentExt.url === 'status') {
Expand Down
7 changes: 6 additions & 1 deletion packages/ehr-utils/lib/types/messaging.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Appointment, Coding } from 'fhir/r4';
import { Appointment, Coding, Communication, RelatedPerson } from 'fhir/r4';

export const OTTEHR_PATIENT_MESSAGE_SYSTEM = 'ottehr-patient-message-status';
export const OTTEHR_PATIENT_MESSAGE_CODE = 'read-by-ottehr';
Expand All @@ -17,6 +17,11 @@ export interface ConversationMessage {
isFromPatient: boolean;
}

export type RelatedPersonMaps = {
rpsToPatientIdMap: Record<string, RelatedPerson[]>;
commsToRpRefMap: Record<string, Communication[]>;
};

export interface SMSRecipient {
relatedPersonId: string;
smsNumber: string;
Expand Down
1 change: 0 additions & 1 deletion packages/ottehr-components/lib/helpers/form/form.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export function formInputStringToBoolean(data: any, questions: Question[]): any
export function checkEnable(item: FormInputType, values: FieldValues): boolean {
if (item.enableWhen) {
const value = values[item.enableWhen.question];
// console.log(item.name, item.enableWhen.answer, value);
if (item.enableWhen.operator === '=') {
const test = value === item.enableWhen.answer;
// handle validatiation item field
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,6 @@ export default function AppointmentTableRow({
).years >= 3;

const showChatIcon = appointment.smsModel !== undefined;
// console.log('sms model', appointment.smsModel);

const saveNote = async (_event: React.MouseEvent<HTMLElement>): Promise<void> => {
if (!fhirClient) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export default function Navbar(): ReactElement {
});
}, [currentTab, currentUrl, location.pathname, navbarItems]);

if (location.pathname.match(/^\/telemed\/appointments\//)) {
if (location.pathname.match(/^\/telemed\/appointments\//) || location.pathname.match(/^\/visit\//)) {
return <></>;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/telemed-ehr/app/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ export const HOP_QUEUE_URI = 'hop-queue';

export const TIMEZONE_EXTENSION_URL = 'http://hl7.org/fhir/StructureDefinition/timezone';
export const TIMEZONES = ['America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles'];

export const CHAT_REFETCH_INTERVAL = 15000;
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ const generateRandomPatientInfo = async (
timezone: 'UTC',
isDemo: true,
phoneNumber: phoneNumber,
locationID: randomLocationId,
};
}

Expand Down
8 changes: 8 additions & 0 deletions packages/telemed-ehr/app/src/helpers/formatString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Adds a space after each comma in a string, if it doesn't already exist.
* @param str - The original string
* @returns A string with spaces after commas
*/
export function addSpacesAfterCommas(str: string): string {
return str.replace(/,(?=[^\s])/g, ', ');
}
158 changes: 158 additions & 0 deletions packages/telemed-ehr/app/src/hooks/useGetPatient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { useEffect, useState } from 'react';
import { Appointment, Encounter, EncounterStatusHistory, Location, Patient, RelatedPerson, Resource } from 'fhir/r4';
import { DateTime } from 'luxon';
import { SearchParam } from '@zapehr/sdk';
import { getFirstName, getLastName, OTTEHR_MODULE } from 'ehr-utils';
import { getPatientNameSearchParams } from '../helpers/patientSearch';
import { getVisitTypeLabelForAppointment } from '../types/types';
import { getVisitTotalTime } from '../helpers/visitDurationUtils';
import { useApiClients } from './useAppClients';

const getTelemedLength = (history?: EncounterStatusHistory[]): number => {
const value = history?.find((item) => item.status === 'in-progress');
if (!value || !value.period.start) {
return 0;
}

const { start, end } = value.period;
const duration = DateTime.fromISO(start).diff(end ? DateTime.fromISO(end) : DateTime.now(), ['minute']);

return Math.abs(duration.minutes);
};

export type AppointmentHistoryRow = {
id: string | undefined;
type: string | undefined;
office: string | undefined;
dateTime: string | undefined;
length: number;
};

export const useGetPatient = (
id?: string,
): {
loading: boolean;
otherPatientsWithSameName: boolean;
setOtherPatientsWithSameName: (value: boolean) => void;
patient?: Patient;
appointments?: AppointmentHistoryRow[];
relatedPerson?: RelatedPerson;
} => {
const { fhirClient } = useApiClients();
const [loading, setLoading] = useState<boolean>(true);
const [otherPatientsWithSameName, setOtherPatientsWithSameName] = useState<boolean>(false);
const [patient, setPatient] = useState<Patient>();
const [appointments, setAppointments] = useState<AppointmentHistoryRow[]>();
const [relatedPerson, setRelatedPerson] = useState<RelatedPerson>();

useEffect(() => {
async function getPatient(): Promise<void> {
if (!fhirClient || !id) {
throw new Error('fhirClient or patient ID is not defined');
}

setLoading(true);
const resourcesTemp = await fhirClient.searchResources<Resource>({
resourceType: 'Patient',
searchParams: [
{ name: '_id', value: id },
{
name: '_revinclude',
value: 'Appointment:patient',
},
{
name: '_include:iterate',
value: 'Appointment:location',
},
{
name: '_revinclude:iterate',
value: 'RelatedPerson:patient',
},
{
name: '_revinclude:iterate',
value: 'Encounter:appointment',
},
],
});

const patientTemp: Patient = resourcesTemp.find((resource) => resource.resourceType === 'Patient') as Patient;
const appointmentsTemp: Appointment[] = resourcesTemp.filter(
(resource) =>
resource.resourceType === 'Appointment' &&
resource.meta?.tag?.find((tag) => tag.code === OTTEHR_MODULE.UC || tag.code === OTTEHR_MODULE.TM),
) as Appointment[];
const locations: Location[] = resourcesTemp.filter(
(resource) => resource.resourceType === 'Location',
) as Location[];
const relatedPersonTemp: RelatedPerson = resourcesTemp.find(
(resource) => resource.resourceType === 'RelatedPerson',
) as RelatedPerson;
const encounters: Encounter[] = resourcesTemp.filter(
(resource) => resource.resourceType === 'Encounter',
) as Encounter[];

appointmentsTemp.sort((a, b) => {
const createdA = DateTime.fromISO(a.start ?? '');
const createdB = DateTime.fromISO(b.start ?? '');
return createdB.diff(createdA).milliseconds;
});

const first = getFirstName(patientTemp);
const last = getLastName(patientTemp);
const otherPatientParams: SearchParam[] = getPatientNameSearchParams({
firstLast: { first, last },
narrowByRelatedPersonAndAppointment: false,
maxResultOverride: 2,
});
const otherPatientsWithSameNameTemp = await fhirClient.searchResources<Resource>({
resourceType: 'Patient',
searchParams: otherPatientParams,
});

if (otherPatientsWithSameNameTemp?.length > 1) {
setOtherPatientsWithSameName(true);
} else {
setOtherPatientsWithSameName(false);
}

const appointmentRows: AppointmentHistoryRow[] = appointmentsTemp.map((appointment: Appointment) => {
const appointmentLocationID = appointment.participant
.find((participant) => participant.actor?.reference?.startsWith('Location/'))
?.actor?.reference?.replace('Location/', '');
const location = locations.find((location) => location.id === appointmentLocationID);
const encounter = appointment.id
? encounters.find((encounter) => encounter.appointment?.[0]?.reference?.endsWith(appointment.id!))
: undefined;
const type = getVisitTypeLabelForAppointment(appointment);

return {
id: appointment.id,
type,
office:
location?.address?.state &&
location?.name &&
`${location?.address?.state?.toUpperCase()} - ${location?.name}`,
dateTime: appointment.start,
length: getTelemedLength(encounter?.statusHistory),
};
});

setAppointments(appointmentRows);
setPatient(patientTemp);
setRelatedPerson(relatedPersonTemp);
}

getPatient()
.catch((error) => console.log(error))
.finally(() => setLoading(false));
}, [fhirClient, id]);

return {
loading,
appointments,
otherPatientsWithSameName,
setOtherPatientsWithSameName,
patient,
relatedPerson,
};
};
19 changes: 15 additions & 4 deletions packages/telemed-ehr/app/src/pages/AppointmentPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Box, Container } from '@mui/material';
import { FC, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { AppointmentFooter, AppointmentHeader, AppointmentTabs } from '../telemed/features/appointment';
import {
AppointmentFooter,
AppointmentHeader,
AppointmentSidePanel,
AppointmentTabs,
AppointmentTabsHeader,
} from '../telemed/features/appointment';
import { PATIENT_PHOTO_CODE, getQuestionnaireResponseByLinkId } from 'ehr-utils';
import {
useAppointmentStore,
Expand Down Expand Up @@ -86,9 +92,14 @@ export const AppointmentPage: FC = () => {
}}
>
<AppointmentHeader />
<Container maxWidth="xl" sx={{ my: 3 }}>
<AppointmentTabs />
</Container>

<Box sx={{ display: 'flex', flex: 1, width: '100%' }}>
<AppointmentSidePanel />

<Container maxWidth="xl" sx={{ my: 3 }}>
<AppointmentTabs />
</Container>
</Box>

<AppointmentFooter />
</Box>
Expand Down
12 changes: 12 additions & 0 deletions packages/telemed-ehr/app/src/telemed/assets/icons/Reminder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React, { FC } from 'react';
import { SvgIcon, SvgIconProps } from '@mui/material';

export const ReminderIcon: FC<SvgIconProps> = (props) => {
return (
<SvgIcon {...props}>
<svg width="18" height="18" viewBox="0 0 18 18" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M4.87498 6H5.62498V5.25C5.62498 5.0375 5.55311 4.85938 5.40936 4.71562C5.26561 4.57187 5.08748 4.5 4.87498 4.5C4.66248 4.5 4.48436 4.57187 4.34061 4.71562C4.19686 4.85938 4.12498 5.0375 4.12498 5.25C4.12498 5.4625 4.19686 5.64062 4.34061 5.78438C4.48436 5.92812 4.66248 6 4.87498 6ZM8.24998 6C8.46248 6 8.64061 5.92812 8.78436 5.78438C8.92811 5.64062 8.99998 5.4625 8.99998 5.25C8.99998 5.0375 8.92811 4.85938 8.78436 4.71562C8.64061 4.57187 8.46248 4.5 8.24998 4.5C8.03748 4.5 7.85936 4.57187 7.71561 4.71562C7.57186 4.85938 7.49998 5.0375 7.49998 5.25V6H8.24998ZM7.85623 16.5C7.50623 16.5 7.17811 16.425 6.87186 16.275C6.56561 16.125 6.30623 15.9125 6.09373 15.6375L2.32498 10.8563C2.22498 10.7438 2.18123 10.6094 2.19373 10.4531C2.20623 10.2969 2.26248 10.1687 2.36248 10.0688C2.61248 9.80625 2.91248 9.65 3.26248 9.6C3.61248 9.55 3.93748 9.61875 4.23748 9.80625L5.62498 10.65V7.5H4.87498C4.24998 7.5 3.71873 7.28125 3.28123 6.84375C2.84373 6.40625 2.62498 5.875 2.62498 5.25C2.62498 4.625 2.84373 4.09375 3.28123 3.65625C3.71873 3.21875 4.24998 3 4.87498 3C5.01248 3 5.14061 3.0125 5.25936 3.0375C5.37811 3.0625 5.49998 3.09375 5.62498 3.13125V2.25C5.62498 2.0375 5.69686 1.85938 5.84061 1.71562C5.98436 1.57187 6.16248 1.5 6.37498 1.5C6.58748 1.5 6.76873 1.57187 6.91873 1.71562C7.06873 1.85938 7.14373 2.0375 7.14373 2.25V3.3C7.31873 3.2 7.49686 3.125 7.67811 3.075C7.85936 3.025 8.04998 3 8.24998 3C8.87498 3 9.40623 3.21875 9.84373 3.65625C10.2812 4.09375 10.5 4.625 10.5 5.25C10.5 5.875 10.2812 6.40625 9.84373 6.84375C9.40623 7.28125 8.87498 7.5 8.24998 7.5H7.14373V12C7.14373 12.2875 7.01561 12.5031 6.75936 12.6469C6.50311 12.7906 6.24998 12.7875 5.99998 12.6375L5.32498 12.225L7.27498 14.7188C7.34998 14.8062 7.43748 14.875 7.53748 14.925C7.63748 14.975 7.74373 15 7.85623 15H12C12.4125 15 12.7656 14.8531 13.0594 14.5594C13.3531 14.2656 13.5 13.9125 13.5 13.5V10.5C13.5 10.2875 13.4281 10.1094 13.2844 9.96563C13.1406 9.82188 12.9625 9.75 12.75 9.75H9.39373C9.18123 9.75 9.00311 9.67813 8.85936 9.53438C8.71561 9.39063 8.64373 9.2125 8.64373 9C8.64373 8.7875 8.71561 8.60938 8.85936 8.46563C9.00311 8.32188 9.18123 8.25 9.39373 8.25H12.75C13.375 8.25 13.9062 8.46875 14.3437 8.90625C14.7812 9.34375 15 9.875 15 10.5V13.5C15 14.325 14.7062 15.0313 14.1187 15.6188C13.5312 16.2063 12.825 16.5 12 16.5H7.85623Z" />
</svg>
</SvgIcon>
);
};
1 change: 1 addition & 0 deletions packages/telemed-ehr/app/src/telemed/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './Diagnosis';
export * from './ContractEdit';
export * from './PatientList';
export * from './icons';
export * from './Reminder';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IconButton, styled, alpha, useTheme } from '@mui/material';
import { IconButton, styled, alpha, useTheme, darken } from '@mui/material';
import { otherColors } from '../../CustomThemeProvider';

export const IconButtonContained = styled(IconButton)<{ variant?: string }>(({ variant }) => {
Expand All @@ -8,7 +8,7 @@ export const IconButtonContained = styled(IconButton)<{ variant?: string }>(({ v
switch (variant) {
case 'disabled': {
colors = {
backgroundColor: theme.palette.primary.contrastText,
backgroundColor: darken(theme.palette.primary.contrastText, 0.125),
'&:hover': { backgroundColor: alpha(theme.palette.primary.contrastText, 0.9) },
};
break;
Expand Down

This file was deleted.

Loading
Loading