From c8f488ff7e57236f505064e3568bede038748215 Mon Sep 17 00:00:00 2001 From: aykhanahmadli Date: Mon, 21 Oct 2024 16:22:13 +0200 Subject: [PATCH 01/38] fix employee information form --- packages/ehr-utils/lib/types/user.types.ts | 14 + .../components/EmployeeInformationForm.tsx | 325 ++++++++++-------- 2 files changed, 193 insertions(+), 146 deletions(-) diff --git a/packages/ehr-utils/lib/types/user.types.ts b/packages/ehr-utils/lib/types/user.types.ts index 53a1ca81..42ce7b73 100644 --- a/packages/ehr-utils/lib/types/user.types.ts +++ b/packages/ehr-utils/lib/types/user.types.ts @@ -6,3 +6,17 @@ export type User = ZapEHRUser & { roles: { name: string }[]; profileResource?: Practitioner; }; + +export enum RoleType { + NewUser = 'NewUser', + Administrator = 'Administrator', + AssistantAdmin = 'AssistantAdmin', + RegionalTelemedLead = 'RegionalTelemedLead', + CallCentre = 'CallCentre', + Billing = 'Billing', + Manager = 'Manager', + Staff = 'Staff', + Provider = 'Provider', + FrontDesk = 'Front Desk', + Inactive = 'Inactive', +} diff --git a/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx b/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx index b3ae6fe4..fb918eec 100644 --- a/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx +++ b/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx @@ -30,15 +30,15 @@ import DeleteIcon from '@mui/icons-material/Delete'; import { useEffect, useState } from 'react'; import { Controller, useForm, useWatch } from 'react-hook-form'; import { Link } from 'react-router-dom'; -import { FHIR_IDENTIFIER_NPI, PractitionerLicense, PractitionerQualificationCode, User } from 'ehr-utils'; +import { FHIR_IDENTIFIER_NPI, PractitionerLicense, PractitionerQualificationCode, RoleType, User } from 'ehr-utils'; import { otherColors } from '../CustomThemeProvider'; import { updateUser } from '../api/api'; import { useApiClients } from '../hooks/useAppClients'; -import { RoleType } from '../types/types'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import AddIcon from '@mui/icons-material/Add'; import { AllStates } from '../types/types'; import { PractitionerQualificationCodesLabels } from 'ehr-utils'; +import useOttehrUser from '../hooks/useOttehrUser'; const displaystates = AllStates.map((state) => state.value); @@ -54,6 +54,7 @@ interface EmployeeForm { middleName: string; lastName: string; nameSuffix: string; + enabledLicenses: { [key: string]: boolean }; roles: string[]; } @@ -65,13 +66,12 @@ const AVAILABLE_ROLES: { { value: RoleType.Administrator, label: 'Administrator', - hint: `Adjust/edit frequency/slots; all copy/message edits; invite users or inactivate users`, + hint: `Adjust full settings for entire system`, }, { value: RoleType.Manager, label: 'Manager', - hint: `Grant existing users site/queue access; adjust operating hours or special hours/schedule overrides; - adjust # of providers (eventually provider level/type)`, + hint: `Adjust operating hours or schedule overrides; adjust pre-booked visits per hour`, }, { value: RoleType.Staff, @@ -118,8 +118,9 @@ export default function EmployeeInformationForm({ }: EditEmployeeInformationProps): JSX.Element { const { zambdaClient } = useApiClients(); const theme = useTheme(); + const currentUser = useOttehrUser(); const [loading, setLoading] = useState(false); - const [errors, setErrors] = useState({ submit: false, roles: false }); + const [errors, setErrors] = useState({ submit: false, roles: false, newLicense: false, duplicateLicense: false }); const [newLicenseState, setNewLicenseState] = useState(undefined); const [newLicenseCode, setNewLicenseCode] = useState(undefined); @@ -132,11 +133,13 @@ export default function EmployeeInformationForm({ id: '', profile: '', accessPolicy: {}, - phoneNumber: '', roles: [], + phoneNumber: '', profileResource: undefined, }); + console.log('existingUser', existingUser); + let npiText = 'n/a'; if (existingUser?.profileResource?.identifier) { const npi = existingUser.profileResource.identifier.find((identifier) => identifier.system === FHIR_IDENTIFIER_NPI); @@ -145,6 +148,14 @@ export default function EmployeeInformationForm({ } } + let phoneText = ''; + if (existingUser?.profileResource?.telecom) { + const phone = existingUser.profileResource.telecom.find((tel) => tel.system === 'sms')?.value; + if (phone) { + phoneText = phone; + } + } + let photoSrc = ''; if (existingUser?.profileResource?.photo) { const photo = existingUser.profileResource.photo[0]; @@ -180,6 +191,16 @@ export default function EmployeeInformationForm({ setValue('middleName', middleName); setValue('lastName', lastName); setValue('nameSuffix', nameSuffix); + + let enabledLicenses = {}; + if (existingUser.profileResource?.qualification) { + enabledLicenses = existingUser.profileResource.qualification.reduce((result, qualification) => { + const state = qualification.extension?.[0].extension?.[1].valueCodeableConcept?.coding?.[0].code; + return state ? { ...result, ...{ [state]: true } } : result; + }, {}); + } + + setValue('enabledLicenses', enabledLicenses); } }, [existingUser, setValue]); @@ -208,7 +229,7 @@ export default function EmployeeInformationForm({ lastName: data.lastName, nameSuffix: data.nameSuffix, selectedRoles: data.roles, - licenses: newLicenses, + licenses: licenses.filter((license) => data.enabledLicenses[license.state]), }); } catch (error) { console.log(`Failed to update user: ${error}`); @@ -310,7 +331,7 @@ export default function EmployeeInformationForm({ } - disabled={!isActive} + disabled={!isActive || !currentUser?.hasRole([RoleType.Administrator])} label={roleEntry.label} sx={{ '.MuiFormControlLabel-asterisk': { display: 'none' } }} /> @@ -372,7 +393,6 @@ export default function EmployeeInformationForm({ sx={{ ...theme.typography.h4, color: theme.palette.primary.dark, - mb: 2, mt: 3, fontWeight: '600 !important', }} @@ -394,21 +414,160 @@ export default function EmployeeInformationForm({ /> )} /> - + + {isProviderRoleSelected && ( + <> +
+ + Provider Qualifications + + + + + + + State + Qualification + Operate in state + Delete License + + + + {newLicenses.map((license, index) => ( + + {license.state} + {license.code} + + { + const updatedLicenses = [...newLicenses]; + updatedLicenses[index].active = !updatedLicenses[index].active; + + setNewLicenses(updatedLicenses); + + await updateLicenses(updatedLicenses); + }} + /> + + + { + const updatedLicenses = [...newLicenses]; + updatedLicenses.splice(index, 1); + + setNewLicenses(updatedLicenses); + await updateLicenses(updatedLicenses); + }} + > + + + + + ))} + +
+
+ + } + sx={{ + marginTop: '20px', + fontWeight: 'bold', + color: theme.palette.primary.main, + cursor: 'pointer', + }} + > + Add New State Qualification + + + + + option} + renderInput={(params) => } + onChange={(event, value) => setNewLicenseState(value || undefined)} + /> + + + option} + renderInput={(params) => } + onChange={(event, value) => setNewLicenseCode(value || undefined)} + /> + + + + + + + +
+ + )} )} + {/* Error on submit if request fails */} + {errors.submit && ( + {`Failed to update user. Please try again.`} + )} + {errors.newLicense && ( + {`Please select a state and qualification`} + )} + {errors.duplicateLicense && ( + {`License already exists`} + )} + {/* Update Employee and Cancel Buttons */} - {/* Error on submit if request fails */} - {errors.submit && ( - {`Failed to update user. Please try again.`} - )} - {isProviderRoleSelected && ( - <> -
- - Provider Qualifications - - - - - - - State - Qualification - Operate in state - Delete License - - - - {newLicenses.map((license, index) => ( - - {license.state} - {license.code} - - { - const updatedLicenses = [...newLicenses]; - updatedLicenses[index].active = !updatedLicenses[index].active; - - setNewLicenses(updatedLicenses); - - await updateLicenses(updatedLicenses); - }} - /> - - - { - const updatedLicenses = [...newLicenses]; - updatedLicenses.splice(index, 1); - - setNewLicenses(updatedLicenses); - await updateLicenses(updatedLicenses); - }} - > - - - - - ))} - -
-
- - } - sx={{ - marginTop: '20px', - fontWeight: 'bold', - color: theme.palette.primary.main, - cursor: 'pointer', - }} - > - Add New State Qualification - - - - - option} - renderInput={(params) => } - onChange={(event, value) => setNewLicenseState(value || undefined)} - /> - - - option} - renderInput={(params) => } - onChange={(event, value) => setNewLicenseCode(value || undefined)} - /> - - - - - - - -
- - )} ); From fdbf768955b7fb5de4b1e680ea230dc740b8f561 Mon Sep 17 00:00:00 2001 From: Olivia Bonitatibus Date: Tue, 15 Oct 2024 12:12:16 -0400 Subject: [PATCH 02/38] Add questionnaire responses to additional questions patient column in ehr portal --- .../paperwork/paperworkResponse.hepler.ts | 19 +++ .../appointment/AppointmentSidePanel.tsx | 27 ++-- .../AdditionalQuestionsPatientColumn.tsx | 130 ++++++++---------- .../state/appointment/appointment.queries.ts | 35 +++++ .../state/appointment/appointment.store.ts | 4 +- .../zambdas/src/shared/accessPolicies.ts | 4 + 6 files changed, 137 insertions(+), 82 deletions(-) diff --git a/packages/ehr-utils/lib/helpers/paperwork/paperworkResponse.hepler.ts b/packages/ehr-utils/lib/helpers/paperwork/paperworkResponse.hepler.ts index bafbe554..059d7383 100644 --- a/packages/ehr-utils/lib/helpers/paperwork/paperworkResponse.hepler.ts +++ b/packages/ehr-utils/lib/helpers/paperwork/paperworkResponse.hepler.ts @@ -39,6 +39,25 @@ export const mapPaperworkResponseItem = (item: QuestionnaireResponseItem): Quest return item as QuestionnaireResponseItemWithValueArray; }; +export enum QuestionnaireLinkIds { + PREFERRED_LANGUAGE = 'preferred-language', + ALLERGIES = 'allergies', + REASON_FOR_VISIT = 'reason-for-visit', + VITALS_TEMPERATURE = 'vitals-temperature', + VITALS_PULSE = 'vitals-pulse', + VITALS_HR = 'vitals-hr', + VITALS_RR = 'vitals-rr', + VITALS_BP = 'vitals-bp', + MEDICAL_HISTORY = 'medical-history', + SURGICAL_HISTORY = 'surgical-history', + CURRENT_MEDICATION = 'current-medications', + PATIENT_STREET_ADDRESS = 'patient-street-address', + PATIENT_NUMBER = 'patient-number', + GUARDIAN_NUMBER = 'guardian-number', + RELAY_PHONE = 'relay-phone', + CONSENT_FORMS = 'consent-forms', +} + export const getQuestionnaireResponseByLinkId = ( linkId: string, questionnaireResponse?: QuestionnaireResponse, diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentSidePanel.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentSidePanel.tsx index b51d5215..f41daeee 100644 --- a/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentSidePanel.tsx +++ b/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentSidePanel.tsx @@ -27,6 +27,7 @@ import { ApptStatus, AppointmentMessaging, UCAppointmentInformation, + QuestionnaireLinkIds, } from 'ehr-utils'; import ChatModal from '../../../features/chat/ChatModal'; import { calculatePatientAge } from '../../../helpers/formatDateTime'; @@ -71,17 +72,23 @@ export const AppointmentSidePanel: FC = ({ appointmen const [chatModalOpen, setChatModalOpen] = useState(false); const [isInviteParticipantOpen, setIsInviteParticipantOpen] = useState(false); - const reasonForVisit = getQuestionnaireResponseByLinkId('reason-for-visit', questionnaireResponse)?.answer?.[0] - .valueString; - const preferredLanguage = getQuestionnaireResponseByLinkId('preferred-language', questionnaireResponse)?.answer?.[0] - .valueString; - const relayPhone = getQuestionnaireResponseByLinkId('relay-phone', questionnaireResponse)?.answer?.[0].valueString; + const reasonForVisit = getQuestionnaireResponseByLinkId(QuestionnaireLinkIds.REASON_FOR_VISIT, questionnaireResponse) + ?.answer?.[0].valueString; + const preferredLanguage = getQuestionnaireResponseByLinkId( + QuestionnaireLinkIds.PREFERRED_LANGUAGE, + questionnaireResponse, + )?.answer?.[0].valueString; + const relayPhone = getQuestionnaireResponseByLinkId(QuestionnaireLinkIds.RELAY_PHONE, questionnaireResponse) + ?.answer?.[0].valueString; const number = - getQuestionnaireResponseByLinkId('patient-number', questionnaireResponse)?.answer?.[0].valueString || - getQuestionnaireResponseByLinkId('guardian-number', questionnaireResponse)?.answer?.[0].valueString; - const knownAllergies = getQuestionnaireResponseByLinkId('allergies', questionnaireResponse)?.answer[0].valueArray; - const address = getQuestionnaireResponseByLinkId('patient-street-address', questionnaireResponse)?.answer?.[0] - .valueString; + getQuestionnaireResponseByLinkId(QuestionnaireLinkIds.PATIENT_NUMBER, questionnaireResponse)?.answer?.[0] + .valueString || + getQuestionnaireResponseByLinkId(QuestionnaireLinkIds.GUARDIAN_NUMBER, questionnaireResponse)?.answer?.[0] + .valueString; + const knownAllergies = getQuestionnaireResponseByLinkId(QuestionnaireLinkIds.ALLERGIES, questionnaireResponse) + ?.answer[0].valueArray; + const address = getQuestionnaireResponseByLinkId(QuestionnaireLinkIds.PATIENT_STREET_ADDRESS, questionnaireResponse) + ?.answer?.[0].valueString; const handleERXLoadingStatusChange = useCallback<(status: boolean) => void>( (status) => setIsERXLoading(status), diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/AdditionalQuestions/AdditionalQuestionsPatientColumn.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/AdditionalQuestions/AdditionalQuestionsPatientColumn.tsx index d31e4c5c..b845d3a6 100644 --- a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/AdditionalQuestions/AdditionalQuestionsPatientColumn.tsx +++ b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/AdditionalQuestions/AdditionalQuestionsPatientColumn.tsx @@ -1,20 +1,59 @@ import React, { FC } from 'react'; import { Box, Divider, Skeleton, Typography } from '@mui/material'; -import { getQuestionnaireResponseByLinkId } from 'ehr-utils'; +import { FhirResource, Questionnaire, QuestionnaireItem, QuestionnaireResponse } from 'fhir/r4'; +import { QuestionnaireLinkIds } from 'ehr-utils'; import { getSelectors } from '../../../../../shared/store/getSelectors'; -import { useAppointmentStore } from '../../../../state'; +import { useAppointmentStore, useGetQuestionnaireDetails } from '../../../../state'; + +const omitKnownQuestions = Object.values(QuestionnaireLinkIds) as string[]; export const AdditionalQuestionsPatientColumn: FC = () => { - const { questionnaireResponse, isAppointmentLoading } = getSelectors(useAppointmentStore, [ + const { questionnaire, questionnaireResponse, isAppointmentLoading } = getSelectors(useAppointmentStore, [ + 'questionnaire', 'questionnaireResponse', 'isAppointmentLoading', ]); - const fluVaccine = getQuestionnaireResponseByLinkId('flu-vaccine', questionnaireResponse)?.answer[0].valueString; - const vaccinesUpToDate = getQuestionnaireResponseByLinkId('vaccines-up-to-date', questionnaireResponse)?.answer[0] - .valueString; - const travelUsa = getQuestionnaireResponseByLinkId('travel-usa', questionnaireResponse)?.answer[0].valueString; - const hospitalize = getQuestionnaireResponseByLinkId('hospitalize', questionnaireResponse)?.answer[0].valueString; + const { isFetching: isFetchingQuestionnaire } = useGetQuestionnaireDetails( + { + questionnaireName: questionnaireResponse?.questionnaire, + }, + (data) => { + const questionnaire = data?.find( + (resource: FhirResource) => resource.resourceType === 'Questionnaire', + ) as unknown as Questionnaire; + useAppointmentStore.setState({ + questionnaire: questionnaire, + }); + }, + ); + + const getQuestionBlock = ( + questionStructure: QuestionnaireItem, + questionnaireResponse: QuestionnaireResponse | undefined, + ): JSX.Element | null => { + if (questionStructure.type == 'group') { + const answerBlocks = questionStructure.item?.map((question) => getQuestionBlock(question, questionnaireResponse)); + return answerBlocks && answerBlocks.length > 0 ? ( + + + {questionStructure.text} + + {questionStructure.item?.map((question) => getQuestionBlock(question, questionnaireResponse))} + + ) : null; + } + const answer = questionnaireResponse?.item?.find((q) => q.linkId === questionStructure.linkId)?.answer?.[0] + ?.valueString; + return answer ? ( + + + {questionStructure.text} + + {answer} + + ) : null; + }; return ( { gap: 1, }} > - - Have you or your child had your flu vaccine? - {isAppointmentLoading ? ( - - Yes - - ) : ( - {fluVaccine} - )} - - - - Are your or your child's vaccines up to date? - {isAppointmentLoading ? ( - - Yes - - ) : ( - {vaccinesUpToDate} - )} - - - - Have you traveled out of the USA in the last 2 weeks? - {isAppointmentLoading ? ( - - Yes - - ) : ( - {travelUsa} - )} - - - - Has the patient been hospitalized in the past 6 months? - {isAppointmentLoading ? ( - - Yes - - ) : ( - {hospitalize} - )} - + {isFetchingQuestionnaire || isAppointmentLoading ? ( + + ) : ( + questionnaire?.item + ?.filter((question) => !omitKnownQuestions.includes(question.linkId)) + .map((question, index, array) => ( + <> + {getQuestionBlock(question, questionnaireResponse)} + {index < array.length - 1 && } + + )) + )} ); }; diff --git a/packages/telemed-ehr/app/src/telemed/state/appointment/appointment.queries.ts b/packages/telemed-ehr/app/src/telemed/state/appointment/appointment.queries.ts index b3ce083b..7c686914 100644 --- a/packages/telemed-ehr/app/src/telemed/state/appointment/appointment.queries.ts +++ b/packages/telemed-ehr/app/src/telemed/state/appointment/appointment.queries.ts @@ -192,6 +192,41 @@ export const useGetAppointmentInformation = ( ); }; +export const useGetQuestionnaireDetails = ( + { + questionnaireName, + }: { + questionnaireName: string | undefined; + }, + onSuccess: (data: Bundle[]) => void, + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type +) => { + const { fhirClient } = useApiClients(); + return useQuery( + ['telemed-questionnaire', { questionnaireName }], + () => { + if (fhirClient && questionnaireName) { + const questionnaireId = questionnaireName.split('/').pop(); + if (questionnaireId) { + return fhirClient.searchResources({ + resourceType: 'Questionnaire', + searchParams: [{ name: '_id', value: questionnaireId }], + }); + } + throw new Error('questionnaireName not valid'); + } + throw new Error('fhir client not defined or questionnaireName not provided'); + }, + { + onSuccess, + onError: (err) => { + console.error('Error during fetching get telemed appointment: ', err); + }, + enabled: !!questionnaireName, + }, + ); +}; + export const useGetMeetingData = ( getAccessTokenSilently: () => Promise, onSuccess: (data: MeetingData) => void, diff --git a/packages/telemed-ehr/app/src/telemed/state/appointment/appointment.store.ts b/packages/telemed-ehr/app/src/telemed/state/appointment/appointment.store.ts index 3d6af5dd..60d1a471 100644 --- a/packages/telemed-ehr/app/src/telemed/state/appointment/appointment.store.ts +++ b/packages/telemed-ehr/app/src/telemed/state/appointment/appointment.store.ts @@ -1,4 +1,4 @@ -import { Appointment, Encounter, Location, Patient, QuestionnaireResponse } from 'fhir/r4'; +import { Appointment, Encounter, Location, Patient, Questionnaire, QuestionnaireResponse } from 'fhir/r4'; import { GetChartDataResponse } from 'ehr-utils'; import { create } from 'zustand'; @@ -8,6 +8,7 @@ type AppointmentState = { location: Location | undefined; encounter: Encounter; questionnaireResponse: QuestionnaireResponse | undefined; + questionnaire: Questionnaire | undefined; patientPhotoUrls: string[]; schoolWorkNoteUrls: string[]; isAppointmentLoading: boolean; @@ -28,6 +29,7 @@ const APPOINTMENT_INITIAL: AppointmentState = { location: undefined, encounter: {} as Encounter, questionnaireResponse: undefined, + questionnaire: undefined, patientPhotoUrls: [], schoolWorkNoteUrls: [], isAppointmentLoading: false, diff --git a/packages/telemed-ehr/zambdas/src/shared/accessPolicies.ts b/packages/telemed-ehr/zambdas/src/shared/accessPolicies.ts index 091dec43..faaba59b 100644 --- a/packages/telemed-ehr/zambdas/src/shared/accessPolicies.ts +++ b/packages/telemed-ehr/zambdas/src/shared/accessPolicies.ts @@ -7,6 +7,7 @@ export const ADMINISTRATOR_RULES = [ 'FHIR:Coverage', 'FHIR:RelatedPerson', 'FHIR:Organization', + 'FHIR:Questionnaire', 'FHIR:QuestionnaireResponse', 'FHIR:DocumentReference', 'FHIR:Person', @@ -77,6 +78,7 @@ export const MANAGER_RULES = [ 'FHIR:Coverage', 'FHIR:RelatedPerson', 'FHIR:Organization', + 'FHIR:Questionnaire', 'FHIR:QuestionnaireResponse', 'FHIR:DocumentReference', 'FHIR:Person', @@ -125,6 +127,7 @@ export const STAFF_RULES = [ 'FHIR:Organization', 'FHIR:Location', 'FHIR:Encounter', + 'FHIR:Questionnaire', 'FHIR:QuestionnaireResponse', 'FHIR:DocumentReference', 'FHIR:Person', @@ -155,6 +158,7 @@ export const PROVIDER_RULES = [ 'FHIR:Organization', 'FHIR:Location', 'FHIR:Encounter', + 'FHIR:Questionnaire', 'FHIR:QuestionnaireResponse', 'FHIR:DocumentReference', 'FHIR:Person', From f9586db042547c8151d0594b88acfbbb704d5d5e Mon Sep 17 00:00:00 2001 From: aykhanahmadli Date: Mon, 21 Oct 2024 16:52:10 +0200 Subject: [PATCH 03/38] refactor error logic --- .../components/EmployeeInformationForm.tsx | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx b/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx index fb918eec..cf5a037a 100644 --- a/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx +++ b/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx @@ -257,6 +257,32 @@ export default function EmployeeInformationForm({ }); }; + const handleAddLicense = async (): Promise => { + try { + setErrors((prev) => ({ ...prev, duplicateLicense: false, newLicense: false })); + + if (newLicenses.find((license) => license.state === newLicenseState && license.code === newLicenseCode)) { + setErrors((prev) => ({ ...prev, duplicateLicense: true })); + return; + } + if (!newLicenseState || !newLicenseCode) { + setErrors((prev) => ({ ...prev, newLicense: true })); + return; + } + const updatedLicenses = [...newLicenses]; + updatedLicenses.push({ + state: newLicenseState, + code: newLicenseCode as PractitionerQualificationCode, + active: true, + }); + setNewLicenses(updatedLicenses); + await updateLicenses(updatedLicenses); + } catch (error) { + console.error('Error adding license:', error); + setErrors((prev) => ({ ...prev, submit: true })); + } + }; + // every time newLicenses changes, update the user return isActive === undefined ? ( @@ -518,29 +544,7 @@ export default function EmployeeInformationForm({ variant="contained" endIcon={} sx={{ textTransform: 'none', fontWeight: 'bold', borderRadius: 28 }} - onClick={async () => { - console.log('newLicenseState', newLicenseState); - console.log('newLicenseCode', newLicenseCode); - if ( - licenses.find( - (license) => license.state === newLicenseState && license.code === newLicenseCode, - ) - ) { - setErrors((prev) => ({ ...prev, duplicateLicense: true })); - } else if (newLicenseState && newLicenseCode) { - const updatedLicenses = [...newLicenses]; - updatedLicenses.push({ - state: newLicenseState, - code: newLicenseCode as PractitionerQualificationCode, - active: true, - }); - setErrors((prev) => ({ ...prev, newLicense: false, duplicateLicense: false })); - setNewLicenses(updatedLicenses); - await updateLicenses(updatedLicenses); - } else { - setErrors((prev) => ({ ...prev, newLicense: true })); - } - }} + onClick={handleAddLicense} fullWidth > Add From 4bc7a865806ffe1621dc5cd952d1a9184eceb841 Mon Sep 17 00:00:00 2001 From: Robert Zinger Date: Tue, 22 Oct 2024 15:06:44 -0400 Subject: [PATCH 04/38] add connection naming --- packages/telemed-ehr/app/env/.env.local-template | 5 ++++- packages/telemed-ehr/app/src/App.tsx | 1 + packages/telemed-ehr/app/src/photon.d.ts | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/telemed-ehr/app/env/.env.local-template b/packages/telemed-ehr/app/env/.env.local-template index 8d9c123d..78c74cb0 100644 --- a/packages/telemed-ehr/app/env/.env.local-template +++ b/packages/telemed-ehr/app/env/.env.local-template @@ -30,4 +30,7 @@ VITE_APP_DELETE_CHART_DATA_ZAMBDA_ID=delete-chart-data VITE_APP_GET_TOKEN_FOR_CONVERSATION_ZAMBDA_ID=get-token-for-conversation VITE_APP_CANCEL_TELEMED_APPOINTMENT_ZAMBDA_ID=cancel-telemed-appointment VITE_APP_CANCEL_IN_PERSON_APPOINTMENT_ZAMBDA_ID=cancel-in-person-appointment -VITE_APP_QRS_URL=http://localhost:3002 \ No newline at end of file +VITE_APP_QRS_URL=http://localhost:3002 +VITE_APP_PHOTON_ORG_ID= +VITE_APP_PHOTON_CLIENT_ID= +VITE_APP_PHOTON_CONNECTION_NAME= \ No newline at end of file diff --git a/packages/telemed-ehr/app/src/App.tsx b/packages/telemed-ehr/app/src/App.tsx index ab264686..7e66dce3 100644 --- a/packages/telemed-ehr/app/src/App.tsx +++ b/packages/telemed-ehr/app/src/App.tsx @@ -81,6 +81,7 @@ function App(): ReactElement { dev-mode="true" auto-login="true" redirect-uri={window.location.origin} + connection={import.meta.env.VITE_APP_PHOTON_CONNECTION_NAME} > diff --git a/packages/telemed-ehr/app/src/photon.d.ts b/packages/telemed-ehr/app/src/photon.d.ts index b92bba74..f79f0537 100644 --- a/packages/telemed-ehr/app/src/photon.d.ts +++ b/packages/telemed-ehr/app/src/photon.d.ts @@ -6,6 +6,7 @@ declare namespace JSX { 'dev-mode': string; 'auto-login': string; 'redirect-uri': string; + connection: string; children: Element; }; 'photon-prescribe-workflow': { From f48a44284650a3f6ce0a0277371da1b4de7b87c1 Mon Sep 17 00:00:00 2001 From: Robert Zinger Date: Tue, 22 Oct 2024 15:18:32 -0400 Subject: [PATCH 05/38] adding missing effects --- packages/ehr-utils/lib/fhir/index.ts | 53 ++++++++++++++++++- .../app/src/hooks/useOttehrUser.tsx | 39 +++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/packages/ehr-utils/lib/fhir/index.ts b/packages/ehr-utils/lib/fhir/index.ts index c55b8247..0b92ff3c 100644 --- a/packages/ehr-utils/lib/fhir/index.ts +++ b/packages/ehr-utils/lib/fhir/index.ts @@ -1,6 +1,6 @@ import { BatchInputRequest, FhirClient } from '@zapehr/sdk'; import { Operation } from 'fast-json-patch'; -import { Coding, Patient, Person, Practitioner, RelatedPerson, Resource, Appointment } from 'fhir/r4'; +import { Coding, Patient, Person, Practitioner, RelatedPerson, Resource, Appointment, Extension } from 'fhir/r4'; import { FHIR_EXTENSION } from './constants'; export * from './chat'; @@ -70,6 +70,57 @@ export const getPatchOperationForNewMetaTag = (resource: Resource, newTag: Codin } }; +export const getPatchOperationToUpdateExtension = ( + resource: { extension?: Extension[] }, + newExtension: { + url: Extension['url']; + valueString?: Extension['valueString']; + valueBoolean?: Extension['valueBoolean']; + }, +): Operation | undefined => { + if (!resource.extension) { + return { + op: 'add', + path: '/extension', + value: [newExtension], + }; + } + + const extension = resource.extension; + let requiresUpdate = false; + + if (extension.length > 0) { + const existingExtIndex = extension.findIndex((ext) => ext.url === newExtension.url); + // check if formUser exists and needs to be updated and if so, update + if ( + existingExtIndex >= 0 && + (extension[existingExtIndex].valueString !== newExtension.valueString || + extension[existingExtIndex].valueBoolean !== newExtension.valueBoolean) + ) { + extension[existingExtIndex] = newExtension; + requiresUpdate = true; + } else if (existingExtIndex < 0) { + // if form user does not exist within the extension + // push to patientExtension array + extension.push(newExtension); + requiresUpdate = true; + } + } else { + // since no extensions exist, it must be added via patch operations + extension.push(newExtension); + requiresUpdate = true; + } + + if (requiresUpdate) { + return { + op: 'replace', + path: '/extension', + value: extension, + }; + } + + return undefined; +}; export interface GetPatchBinaryInput { resourceId: string; resourceType: string; diff --git a/packages/telemed-ehr/app/src/hooks/useOttehrUser.tsx b/packages/telemed-ehr/app/src/hooks/useOttehrUser.tsx index 60c34204..1cf4419b 100644 --- a/packages/telemed-ehr/app/src/hooks/useOttehrUser.tsx +++ b/packages/telemed-ehr/app/src/hooks/useOttehrUser.tsx @@ -13,6 +13,7 @@ import { getFullestAvailableName, getPatchOperationForNewMetaTag, getPractitionerNPIIdentitifier, + getPatchOperationToUpdateExtension, initialsFromName, } from 'ehr-utils'; import { create } from 'zustand'; @@ -176,6 +177,40 @@ export default function useOttehrUser(): OttehrUser | undefined { } }, [auth0User?.updated_at]); + useEffect(() => { + if ( + !isPractitionerEnrolledInERX && + hasRole([RoleType.Provider]) && + _practitionerSyncFinished && + isProviderHasEverythingToBeEnrolledInErx && + !_practitionerERXEnrollmentStarted + ) { + _practitionerERXEnrollmentStarted = true; + mutateEnrollPractitionerInERX() + .then(async () => { + if (profile) { + const op = getPatchOperationToUpdateExtension(profile, { + url: ERX_PRACTITIONER_ENROLLED, + valueBoolean: true, + }); + if (op) { + await mutatePractitionerAsync([op]); + void refetchProfile(); + } + } + }) + .catch(console.error); + } + }, [ + hasRole, + isPractitionerEnrolledInERX, + isProviderHasEverythingToBeEnrolledInErx, + mutateEnrollPractitionerInERX, + mutatePractitionerAsync, + profile, + refetchProfile, + ]); + const { userName, userInitials, lastLogin } = useMemo(() => { if (profile) { const userName = getFullestAvailableName(profile) ?? 'Ottehr Team'; @@ -195,13 +230,15 @@ export default function useOttehrUser(): OttehrUser | undefined { userInitials, lastLogin, profileResource: profile, + isERXPrescriber, + isPractitionerEnrolledInERX, hasRole: (role: RoleType[]) => { return userRoles.find((r) => role.includes(r.name as RoleType)) != undefined; }, }; } return undefined; - }, [lastLogin, profile, user, userInitials, userName]); + }, [lastLogin, isERXPrescriber, isPractitionerEnrolledInERX, profile, user, userInitials, userName]); } const MINUTE = 1000 * 60; From 5bd0a01ae87b48443c0a81ebd26335ed325e506c Mon Sep 17 00:00:00 2001 From: aykhanahmadli Date: Wed, 23 Oct 2024 14:24:58 +0200 Subject: [PATCH 06/38] remove inputs once qualification is added --- .../app/src/components/EmployeeInformationForm.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx b/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx index cf5a037a..7c595121 100644 --- a/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx +++ b/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx @@ -220,7 +220,6 @@ export default function EmployeeInformationForm({ setLoading(true); - // Update the user try { await updateUser(zambdaClient, { userId: user.id, @@ -276,6 +275,8 @@ export default function EmployeeInformationForm({ active: true, }); setNewLicenses(updatedLicenses); + setNewLicenseState(undefined); + setNewLicenseCode(undefined); await updateLicenses(updatedLicenses); } catch (error) { console.error('Error adding license:', error); @@ -528,6 +529,7 @@ export default function EmployeeInformationForm({ options={displaystates} getOptionLabel={(option: string) => option} renderInput={(params) => } + value={newLicenseState || null} // Set the value prop onChange={(event, value) => setNewLicenseState(value || undefined)} /> @@ -536,6 +538,7 @@ export default function EmployeeInformationForm({ options={Object.keys(PractitionerQualificationCodesLabels)} getOptionLabel={(option: string) => option} renderInput={(params) => } + value={newLicenseCode || null} // Set the value prop onChange={(event, value) => setNewLicenseCode(value || undefined)} /> From 49c1f046caab2b96a3db131c5aac43f47fb7ea43 Mon Sep 17 00:00:00 2001 From: Robert Zinger Date: Wed, 23 Oct 2024 12:09:43 -0400 Subject: [PATCH 07/38] make connection name optional --- packages/telemed-ehr/app/src/photon.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/telemed-ehr/app/src/photon.d.ts b/packages/telemed-ehr/app/src/photon.d.ts index f79f0537..0ab85344 100644 --- a/packages/telemed-ehr/app/src/photon.d.ts +++ b/packages/telemed-ehr/app/src/photon.d.ts @@ -6,7 +6,7 @@ declare namespace JSX { 'dev-mode': string; 'auto-login': string; 'redirect-uri': string; - connection: string; + connection?: string; children: Element; }; 'photon-prescribe-workflow': { From 351d58fda5318cd1c7e1901b044223c2ed68383f Mon Sep 17 00:00:00 2001 From: Robert Zinger Date: Wed, 23 Oct 2024 13:23:09 -0400 Subject: [PATCH 08/38] for now, do not include connection name --- packages/telemed-ehr/app/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/telemed-ehr/app/src/App.tsx b/packages/telemed-ehr/app/src/App.tsx index 7e66dce3..da3da7e3 100644 --- a/packages/telemed-ehr/app/src/App.tsx +++ b/packages/telemed-ehr/app/src/App.tsx @@ -81,7 +81,7 @@ function App(): ReactElement { dev-mode="true" auto-login="true" redirect-uri={window.location.origin} - connection={import.meta.env.VITE_APP_PHOTON_CONNECTION_NAME} + // connection={import.meta.env.VITE_APP_PHOTON_CONNECTION_NAME} > From de54852739344b950890d8e9377e2ff3c9d414f2 Mon Sep 17 00:00:00 2001 From: Robert Zinger Date: Wed, 23 Oct 2024 14:55:20 -0400 Subject: [PATCH 09/38] adding connection details back in --- packages/telemed-ehr/app/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/telemed-ehr/app/src/App.tsx b/packages/telemed-ehr/app/src/App.tsx index da3da7e3..7e66dce3 100644 --- a/packages/telemed-ehr/app/src/App.tsx +++ b/packages/telemed-ehr/app/src/App.tsx @@ -81,7 +81,7 @@ function App(): ReactElement { dev-mode="true" auto-login="true" redirect-uri={window.location.origin} - // connection={import.meta.env.VITE_APP_PHOTON_CONNECTION_NAME} + connection={import.meta.env.VITE_APP_PHOTON_CONNECTION_NAME} > From ebf6e866bc9d4d3eb66a1bbeaa92db98c7f20817 Mon Sep 17 00:00:00 2001 From: Robert Zinger Date: Wed, 23 Oct 2024 15:50:18 -0400 Subject: [PATCH 10/38] need to keep this suppressed for now --- packages/telemed-ehr/app/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/telemed-ehr/app/src/App.tsx b/packages/telemed-ehr/app/src/App.tsx index 7e66dce3..da3da7e3 100644 --- a/packages/telemed-ehr/app/src/App.tsx +++ b/packages/telemed-ehr/app/src/App.tsx @@ -81,7 +81,7 @@ function App(): ReactElement { dev-mode="true" auto-login="true" redirect-uri={window.location.origin} - connection={import.meta.env.VITE_APP_PHOTON_CONNECTION_NAME} + // connection={import.meta.env.VITE_APP_PHOTON_CONNECTION_NAME} > From 121d7a67fb77db6336f6a7a150c88c6d9161e7d9 Mon Sep 17 00:00:00 2001 From: aykhanahmadli Date: Mon, 28 Oct 2024 15:04:17 +0100 Subject: [PATCH 11/38] fix ottehr deploy script --- packages/telemed-ehr/app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/telemed-ehr/app/package.json b/packages/telemed-ehr/app/package.json index 9e7c1bae..dcdb3701 100644 --- a/packages/telemed-ehr/app/package.json +++ b/packages/telemed-ehr/app/package.json @@ -30,7 +30,7 @@ "test": "react-scripts test", "eject": "react-scripts eject", "deploy:development": " PREFIX=development CLOUDFRONT_ID=E10TA6FN58D1OS ENV=development pnpm run ci-deploy-skeleton", - "ci-deploy-skeleton": "ENV=${ENV} VITE_APP_SHA=${GIT_HEAD:-$(git rev-parse --short HEAD)} VITE_APP_VERSION=$(node -pe 'require(\"./package.json\").version') pnpm run build:${ENV} && aws s3 sync build/ s3://ehr.ottehr.com --region us-east-1 --delete --profile ottehr && aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_ID} --paths '/*' --region us-east-1" + "ci-deploy-skeleton": "ENV=${ENV} VITE_APP_SHA=${GIT_HEAD:-$(git rev-parse --short HEAD)} VITE_APP_VERSION=$(node -pe 'require(\"./package.json\").version') pnpm run build:${ENV} && aws s3 sync build/ s3://ehr.ottehr.com --region us-east-1 --delete && aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_ID} --paths '/*' --region us-east-1" }, "dependencies": { "@mui/icons-material": "^5.14.9", From da7706d02b3399d6fd445dafc8e90b07a762fda7 Mon Sep 17 00:00:00 2001 From: Gilad Schneider Date: Tue, 29 Oct 2024 08:47:18 -0400 Subject: [PATCH 12/38] stashing changes --- .../zambdas/src/appointment/create-appointment/index.ts | 6 ++---- .../zambdas/src/shared/appointment/helpers.ts | 2 -- .../data/telemed/appointments/create-appointment.types.ts | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/telemed-intake/zambdas/src/appointment/create-appointment/index.ts b/packages/telemed-intake/zambdas/src/appointment/create-appointment/index.ts index 66318e26..6113ab01 100644 --- a/packages/telemed-intake/zambdas/src/appointment/create-appointment/index.ts +++ b/packages/telemed-intake/zambdas/src/appointment/create-appointment/index.ts @@ -104,8 +104,8 @@ async function performEffect(props: PerformEffectInputProps): Promise { - const ENVIRONMENT = getSecret(SecretsKeys.ENVIRONMENT, secrets); - let maybeFhirPatient: Patient | undefined = undefined; let updatePatientRequest: BatchInputRequest | undefined = undefined; let createPatientRequest: BatchInputPostRequest | undefined = undefined; @@ -204,7 +202,7 @@ export async function createAppointment( } /** !!! Start time should be the appointment creation time here, - * cause the "Estimated waiting time" calulations are based on this, + * cause the "Estimated waiting time" calculations are based on this, * and we can't search appointments by "created" prop **/ const originalDate = DateTime.fromISO(slot); diff --git a/packages/telemed-intake/zambdas/src/shared/appointment/helpers.ts b/packages/telemed-intake/zambdas/src/shared/appointment/helpers.ts index 6d6b7d6c..8c3b638b 100644 --- a/packages/telemed-intake/zambdas/src/shared/appointment/helpers.ts +++ b/packages/telemed-intake/zambdas/src/shared/appointment/helpers.ts @@ -104,8 +104,6 @@ export async function createUpdateUserRelatedResources( const relatedPerson = userResource.relatedPerson; const person = userResource.person; - console.log(5, person.telecom?.find((telecomTemp) => telecomTemp.system === 'phone')?.value); - if (!person.id) { throw new Error('Person resource does not have an ID'); } diff --git a/packages/utils/lib/types/data/telemed/appointments/create-appointment.types.ts b/packages/utils/lib/types/data/telemed/appointments/create-appointment.types.ts index 0ea9181e..aaaad5fa 100644 --- a/packages/utils/lib/types/data/telemed/appointments/create-appointment.types.ts +++ b/packages/utils/lib/types/data/telemed/appointments/create-appointment.types.ts @@ -4,7 +4,7 @@ import { UserType, PatientBaseInfo } from '../../../common'; export interface CreateAppointmentUCTelemedParams { patient?: PatientInfo; slot?: string; - scheduleType?: 'location' | 'provider'; + scheduleType?: 'location' | 'provider' | 'group'; visitType?: 'prebook' | 'now'; visitService?: 'in-person' | 'telemedicine'; locationID?: string; From 4b5042b7b27cffa35779fcc9e1872a623b2a90b9 Mon Sep 17 00:00:00 2001 From: Gilad Schneider Date: Tue, 29 Oct 2024 10:33:53 -0400 Subject: [PATCH 13/38] cancelled appointments show up in cancelled tab --- packages/telemed-ehr/zambdas/src/sync-user/index.ts | 2 +- .../src/appointment/cancel-in-person-appointment/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/telemed-ehr/zambdas/src/sync-user/index.ts b/packages/telemed-ehr/zambdas/src/sync-user/index.ts index 9e9c3dbc..7677fe67 100644 --- a/packages/telemed-ehr/zambdas/src/sync-user/index.ts +++ b/packages/telemed-ehr/zambdas/src/sync-user/index.ts @@ -108,7 +108,7 @@ async function getRemotePractitionerAndCredentials( console.log('Preparing search parameters for remote practitioner'); const myEhrUser = await appClient.getMe(); const myEmail = myEhrUser.email.toLocaleLowerCase(); - console.log(`Preparing search for local ractitioner email: ${myEmail}`); + console.log(`Preparing search for local practitioner email: ${myEmail}`); const clinicianSearchResults: RemotePractitionerData[] = []; diff --git a/packages/telemed-intake/zambdas/src/appointment/cancel-in-person-appointment/index.ts b/packages/telemed-intake/zambdas/src/appointment/cancel-in-person-appointment/index.ts index 167d3afb..62adc0e6 100644 --- a/packages/telemed-intake/zambdas/src/appointment/cancel-in-person-appointment/index.ts +++ b/packages/telemed-intake/zambdas/src/appointment/cancel-in-person-appointment/index.ts @@ -142,7 +142,7 @@ export const index = async (input: ZambdaInput): Promise }); } // why do we have two separate sets of batch requests here, with updates related to the cancelation spread between both? - const status = 'CANCELLED'; + const status = 'cancelled'; appointmentPatchOperations.push(...getPatchOperationsToUpdateVisitStatus(appointment, status, now || undefined)); const appointmentPatchRequest = getPatchBinary({ resourceType: 'Appointment', @@ -213,7 +213,7 @@ export const index = async (input: ZambdaInput): Promise console.log('Send cancel message request'); if (!zapehrMessagingToken) { - zapehrMessagingToken = await getAccessToken(secrets, 'messaging'); + // zapehrMessagingToken = await getAccessToken(secrets, 'messaging'); } const WEBSITE_URL = getSecret(SecretsKeys.WEBSITE_URL, secrets); const message = `Your visit for ${getPatientFirstName(patient)} with Ottehr Urgent Care ${ From 8b85777b49a8e77a3bc1140fe6723b0cca3c3ec8 Mon Sep 17 00:00:00 2001 From: Gilad Schneider Date: Tue, 29 Oct 2024 13:38:04 -0400 Subject: [PATCH 14/38] more stashing --- .../ehr-utils/lib/types/appointment.types.ts | 2 + .../src/components/AppointmentSidePanel.tsx | 381 ++++++++++++++++++ .../app/src/pages/AppointmentPage.tsx | 2 +- .../CurrentMedicationsProviderColumn.tsx | 4 +- .../KnownAllergiesProviderColumn.tsx | 4 +- .../MedicalConditionsProviderColumn.tsx | 4 +- .../index.ts | 2 +- .../helpers/mappers.ts | 9 + 8 files changed, 400 insertions(+), 8 deletions(-) create mode 100644 packages/telemed-ehr/app/src/components/AppointmentSidePanel.tsx diff --git a/packages/ehr-utils/lib/types/appointment.types.ts b/packages/ehr-utils/lib/types/appointment.types.ts index 7d211464..db052a9e 100644 --- a/packages/ehr-utils/lib/types/appointment.types.ts +++ b/packages/ehr-utils/lib/types/appointment.types.ts @@ -26,6 +26,8 @@ export const mapStatusToTelemed = ( case 'finished': if (appointmentStatus === 'fulfilled') return ApptStatus.complete; else return ApptStatus.unsigned; + case 'cancelled': + return ApptStatus.cancelled; } return undefined; }; diff --git a/packages/telemed-ehr/app/src/components/AppointmentSidePanel.tsx b/packages/telemed-ehr/app/src/components/AppointmentSidePanel.tsx new file mode 100644 index 00000000..03b2c224 --- /dev/null +++ b/packages/telemed-ehr/app/src/components/AppointmentSidePanel.tsx @@ -0,0 +1,381 @@ +import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; +import ChatOutlineIcon from '@mui/icons-material/ChatOutlined'; +import DateRangeOutlinedIcon from '@mui/icons-material/DateRangeOutlined'; +import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; +import MedicationOutlinedIcon from '@mui/icons-material/MedicationOutlined'; +import PersonAddAltOutlinedIcon from '@mui/icons-material/PersonAddAltOutlined'; +import { LoadingButton } from '@mui/lab'; +import { + Badge, + Box, + Button, + Divider, + Drawer, + IconButton, + Link, + Toolbar, + Tooltip, + Typography, + useTheme, +} from '@mui/material'; +import { DateTime } from 'luxon'; +import { FC, useCallback, useState } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { + getQuestionnaireResponseByLinkId, + mapStatusToTelemed, + ApptStatus, + AppointmentMessaging, + UCAppointmentInformation, +} from 'ehr-utils'; +import ChatModal from '../features/chat/ChatModal'; +import { calculatePatientAge } from '../helpers/formatDateTime'; +import useOttehrUser from '../hooks/useOttehrUser'; +import { getSelectors } from '../shared/store/getSelectors'; +import CancelVisitDialog from '../telemed/components/CancelVisitDialog'; +import EditPatientDialog from '../telemed/components/EditPatientDialog'; +import InviteParticipant from '../telemed/components/InviteParticipant'; +import { useGetAppointmentAccessibility } from '../telemed'; +import { useAppointmentStore } from '../telemed'; +import { getAppointmentStatusChip } from './AppointmentTableRow'; +import { getPatientName } from '../telemed/utils'; +// import { ERX } from './ERX'; +import { PastVisits } from '../telemed/features/appointment/PastVisits'; +import { addSpacesAfterCommas } from '../helpers/formatString'; +import { INTERPRETER_PHONE_NUMBER } from 'ehr-utils'; + +enum Gender { + 'male' = 'Male', + 'female' = 'Female', + 'other' = 'Other', + 'unknown' = 'Unknown', +} + +interface AppointmentSidePanelProps { + appointmentType: 'telemed' | 'in-person'; +} + +export const AppointmentSidePanel: FC = ({ appointmentType }) => { + const theme = useTheme(); + + const { appointment, encounter, patient, location, isReadOnly, questionnaireResponse } = getSelectors( + useAppointmentStore, + ['appointment', 'patient', 'encounter', 'location', 'isReadOnly', 'questionnaireResponse'], + ); + + const user = useOttehrUser(); + + const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false); + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [isERXOpen, setIsERXOpen] = useState(false); + const [isERXLoading, setIsERXLoading] = useState(false); + const [chatModalOpen, setChatModalOpen] = useState(false); + const [isInviteParticipantOpen, setIsInviteParticipantOpen] = useState(false); + + const reasonForVisit = getQuestionnaireResponseByLinkId('reason-for-visit', questionnaireResponse)?.answer?.[0] + .valueString; + const preferredLanguage = getQuestionnaireResponseByLinkId('preferred-language', questionnaireResponse)?.answer?.[0] + .valueString; + const relayPhone = getQuestionnaireResponseByLinkId('relay-phone', questionnaireResponse)?.answer?.[0].valueString; + const number = + getQuestionnaireResponseByLinkId('patient-number', questionnaireResponse)?.answer?.[0].valueString || + getQuestionnaireResponseByLinkId('guardian-number', questionnaireResponse)?.answer?.[0].valueString; + const knownAllergies = getQuestionnaireResponseByLinkId('allergies', questionnaireResponse)?.answer[0].valueArray; + const address = getQuestionnaireResponseByLinkId('patient-street-address', questionnaireResponse)?.answer?.[0] + .valueString; + + const handleERXLoadingStatusChange = useCallback<(status: boolean) => void>( + (status) => setIsERXLoading(status), + [setIsERXLoading], + ); + const appointmentAccessibility = useGetAppointmentAccessibility(); + + const isCancellableStatus = + appointmentAccessibility.status !== ApptStatus.complete && + appointmentAccessibility.status !== ApptStatus.cancelled && + appointmentAccessibility.status !== ApptStatus.unsigned; + + const isPractitionerAllowedToCancelThisVisit = + // appointmentAccessibility.isPractitionerLicensedInState && + // appointmentAccessibility.isEncounterAssignedToCurrentPractitioner && + isCancellableStatus; + + const [hasUnread, setHasUnread] = useState( + (appointment as unknown as UCAppointmentInformation)?.smsModel?.hasUnreadMessages || false, + ); + + if (!patient) { + return null; + } + + const weight = patient.extension?.find( + (extension) => extension.url === 'https://fhir.zapehr.com/r4/StructureDefinitions/weight', + )?.valueString; + const weightLastUpdated = patient.extension?.find( + (extension) => extension.url === 'https://fhir.zapehr.com/r4/StructureDefinitions/weight-last-updated', + )?.valueString; + + const weightString = + weight && + weightLastUpdated && + `${Math.round(+weight * 0.45359237 * 100) / 100} kg (updated ${DateTime.fromFormat( + weightLastUpdated, + 'yyyy-MM-dd', + ).toFormat('MM/dd/yyyy')})`; + + function isSpanish(language: string): boolean { + return language.toLowerCase() === 'Spanish'.toLowerCase(); + } + + const delimeterString = preferredLanguage && isSpanish(preferredLanguage) ? `\u00A0|\u00A0` : ''; + const interpreterString = + preferredLanguage && isSpanish(preferredLanguage) ? `Interpreter: ${INTERPRETER_PHONE_NUMBER}` : ''; + + return ( + + + + + + {getAppointmentStatusChip(appointment?.status ?? '')} + + {appointment?.id && ( + + + VID: {appointment.id} + + + )} + + + + + {getPatientName(patient.name).lastFirstName} + + + {!isReadOnly && ( + setIsEditDialogOpen(true)}> + + + )} + + + + + + + PID: + + {patient.id} + + + + + + + {Gender[patient.gender!]} + + + DOB: {DateTime.fromFormat(patient.birthDate!, 'yyyy-MM-dd').toFormat('MM/dd/yyyy')}, Age:{' '} + {calculatePatientAge(patient.birthDate!)} + + + {weightString && Wt: {weightString}} + + {knownAllergies && ( + + Allergies: {knownAllergies.map((answer: any) => answer['allergies-form-agent-substance']).join(', ')} + + )} + + {/* Location: {location.address?.state} */} + + Address: {address} + + {reasonForVisit && addSpacesAfterCommas(reasonForVisit)} + + + + + + + ) : ( + + ) + } + onClick={() => setChatModalOpen(true)} + /> + + + + {/* {user?.isPractitionerEnrolledInPhoton && ( + } + onClick={() => setIsERXOpen(true)} + loading={isERXLoading} + disabled={appointmentAccessibility.isAppointmentReadOnly} + > + RX + + )} */} + + + + + + + + Preferred Language + + + {preferredLanguage} {delimeterString} {interpreterString} + + + + + + Hearing Impaired Relay Service? (711) + + {relayPhone} + + + + + Patient number + + + {number} + + + + + + {appointmentAccessibility.status && + [ApptStatus['pre-video'], ApptStatus['on-video']].includes(appointmentAccessibility.status) && ( + + )} + {isPractitionerAllowedToCancelThisVisit && ( + + )} + + + {isCancelDialogOpen && ( + setIsCancelDialogOpen(false)} appointmentType={appointmentType} /> + )} + {/* {isERXOpen && setIsERXOpen(false)} onLoadingStatusChange={handleERXLoadingStatusChange} />} */} + {isEditDialogOpen && ( + setIsEditDialogOpen(false)} /> + )} + {chatModalOpen && ( + setChatModalOpen(false)} + onMarkAllRead={() => setHasUnread(false)} + /> + )} + {isInviteParticipantOpen && ( + setIsInviteParticipantOpen(false)} /> + )} + + + + ); +}; diff --git a/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx b/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx index 26b81023..e3d37fdf 100644 --- a/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx +++ b/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx @@ -4,10 +4,10 @@ import { useNavigate, useParams } from 'react-router-dom'; import { AppointmentFooter, AppointmentHeader, - AppointmentSidePanel, AppointmentTabs, AppointmentTabsHeader, } from '../telemed/features/appointment'; +import { AppointmentSidePanel } from '../components/AppointmentSidePanel'; import { PATIENT_PHOTO_CODE, getQuestionnaireResponseByLinkId } from 'ehr-utils'; import { useAppointmentStore, diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/CurrentMedications/CurrentMedicationsProviderColumn.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/CurrentMedications/CurrentMedicationsProviderColumn.tsx index aa39664d..4de9003a 100644 --- a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/CurrentMedications/CurrentMedicationsProviderColumn.tsx +++ b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/CurrentMedications/CurrentMedicationsProviderColumn.tsx @@ -66,9 +66,9 @@ export const CurrentMedicationsProviderColumn: FC = () => { )} - {medications.length === 0 && isReadOnly && !isChartDataLoading && ( + {/* {medications.length === 0 && isReadOnly && !isChartDataLoading && ( Missing. Patient input must be reconciled by provider - )} + )} */} {!isReadOnly && (
diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/KnownAllergies/KnownAllergiesProviderColumn.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/KnownAllergies/KnownAllergiesProviderColumn.tsx index fc8938c0..5b2bfab5 100644 --- a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/KnownAllergies/KnownAllergiesProviderColumn.tsx +++ b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/KnownAllergies/KnownAllergiesProviderColumn.tsx @@ -83,9 +83,9 @@ export const KnownAllergiesProviderColumn: FC = () => { )} - {allergies.length === 0 && isReadOnly && !isChartDataLoading && ( + {/* {allergies.length === 0 && isReadOnly && !isChartDataLoading && ( Missing. Patient input must be reconciled by provider - )} + )} */} {!isReadOnly && ( diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/MedicalConditions/MedicalConditionsProviderColumn.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/MedicalConditions/MedicalConditionsProviderColumn.tsx index bed61d53..2886c54c 100644 --- a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/MedicalConditions/MedicalConditionsProviderColumn.tsx +++ b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/MedicalConditions/MedicalConditionsProviderColumn.tsx @@ -75,9 +75,9 @@ export const MedicalConditionsProviderColumn: FC = () => { )} - {conditions.length === 0 && isReadOnly && !isChartDataLoading && ( + {/* {conditions.length === 0 && isReadOnly && !isChartDataLoading && ( Missing. Patient input must be reconciled by provider - )} + )} */} {!isReadOnly && ( diff --git a/packages/telemed-ehr/zambdas/src/change-telemed-appointment-status/index.ts b/packages/telemed-ehr/zambdas/src/change-telemed-appointment-status/index.ts index d8dedef6..68f765d1 100644 --- a/packages/telemed-ehr/zambdas/src/change-telemed-appointment-status/index.ts +++ b/packages/telemed-ehr/zambdas/src/change-telemed-appointment-status/index.ts @@ -73,7 +73,7 @@ export const performEffect = async ( console.debug(`Status change detected from ${currentStatus} to ${newStatus}`); if (visitResources.account?.id === undefined) { - throw new Error(`No account has been found associated with the encouter ${visitResources.encounter?.id}`); + throw new Error(`No account has been found associated with the encounter ${visitResources.encounter?.id}`); } // see if charge item already exists for the encounter and if not, create it diff --git a/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/mappers.ts b/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/mappers.ts index 509db599..022f07b7 100644 --- a/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/mappers.ts +++ b/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/mappers.ts @@ -199,6 +199,15 @@ function mapCommunicationsToRelatedPersonRef( ): Record { const commsToRpRefMap: Record = {}; + // todo: remove temporary fix + if ( + allCommunications.length === 0 || + Object.keys(rpToIdMap).length === 0 || + Object.keys(rpsRefsToPhoneNumberMap).length === 0 + ) { + return commsToRpRefMap; + } + allCommunications.forEach((comm) => { const communication = comm as Communication; const rpRef = communication.sender?.reference; From 9c72edcb4500269eb43ea4e54d9f48e09921ca1d Mon Sep 17 00:00:00 2001 From: Gilad Schneider Date: Tue, 29 Oct 2024 16:59:30 -0400 Subject: [PATCH 15/38] progress --- .../data/urgent-care/appointment.types.ts | 2 +- .../app/src/components/AppointmentFooter.tsx | 60 +++++++++ .../src/components/AppointmentSidePanel.tsx | 4 +- .../components/AppointmentStatusSwitcher.tsx | 114 ++++++++++++++++++ .../app/src/pages/AppointmentPage.tsx | 10 +- .../zambdas/src/shared/queueingUtils.ts | 2 +- 6 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 packages/telemed-ehr/app/src/components/AppointmentFooter.tsx create mode 100644 packages/telemed-ehr/app/src/components/AppointmentStatusSwitcher.tsx diff --git a/packages/ehr-utils/lib/types/data/urgent-care/appointment.types.ts b/packages/ehr-utils/lib/types/data/urgent-care/appointment.types.ts index 83fa2946..40a5cbee 100644 --- a/packages/ehr-utils/lib/types/data/urgent-care/appointment.types.ts +++ b/packages/ehr-utils/lib/types/data/urgent-care/appointment.types.ts @@ -63,7 +63,7 @@ const PRIVATE_EXTENSION_BASE_URL = 'https://fhir.zapehr.com/r4/StructureDefiniti const visitStatusExtensionCode = 'visit-history'; export const visitStatusExtensionUrl = `${PRIVATE_EXTENSION_BASE_URL}/${visitStatusExtensionCode}`; -const getStatusFromExtension = (resource: Appointment): VisitStatus | undefined => { +export const getStatusFromExtension = (resource: Appointment): VisitStatus | undefined => { const history = getVisitStatusHistory(resource); if (history) { const historySorted = [...history] diff --git a/packages/telemed-ehr/app/src/components/AppointmentFooter.tsx b/packages/telemed-ehr/app/src/components/AppointmentFooter.tsx new file mode 100644 index 00000000..cbb77a23 --- /dev/null +++ b/packages/telemed-ehr/app/src/components/AppointmentFooter.tsx @@ -0,0 +1,60 @@ +import { AppBar, Box, Typography, useTheme } from '@mui/material'; +import { FC, useState } from 'react'; +import { ApptStatus, getVisitStatusHistory, mapEncounterStatusHistory, VisitStatusHistoryEntry } from 'ehr-utils'; +import { getSelectors } from '../shared/store/getSelectors'; +import InviteParticipant from '../telemed/components/InviteParticipant'; +import { useGetAppointmentAccessibility } from '../telemed'; +import { useAppointmentStore, useVideoCallStore } from '../telemed'; +import { getAppointmentWaitingTime } from '../telemed/utils'; +import { AppointmentFooterButton } from '../telemed/features/appointment'; +import AppointmentStatusSwitcher from './AppointmentStatusSwitcher'; +import { Appointment } from 'fhir/r4'; + +export const AppointmentFooter: FC = () => { + const theme = useTheme(); + + const appointmentAccessibility = useGetAppointmentAccessibility(); + const { appointment, encounter } = getSelectors(useAppointmentStore, ['appointment', 'encounter']); + const [appointmentStatus, setAppointmentStatus] = useState([]); + + const statuses = + encounter.statusHistory && appointment?.status + ? mapEncounterStatusHistory(encounter.statusHistory, appointment.status) + : undefined; + const waitingTime = getAppointmentWaitingTime(statuses); + + return ( + theme.zIndex.drawer + 1, + }} + > + {appointmentAccessibility.status && + [ApptStatus.ready, ApptStatus['pre-video']].includes(appointmentAccessibility.status) && ( + + + Patient waiting + {waitingTime} mins + + + + + + )} + + ); +}; diff --git a/packages/telemed-ehr/app/src/components/AppointmentSidePanel.tsx b/packages/telemed-ehr/app/src/components/AppointmentSidePanel.tsx index 03b2c224..ef4bd0f0 100644 --- a/packages/telemed-ehr/app/src/components/AppointmentSidePanel.tsx +++ b/packages/telemed-ehr/app/src/components/AppointmentSidePanel.tsx @@ -27,6 +27,7 @@ import { ApptStatus, AppointmentMessaging, UCAppointmentInformation, + getStatusFromExtension, } from 'ehr-utils'; import ChatModal from '../features/chat/ChatModal'; import { calculatePatientAge } from '../helpers/formatDateTime'; @@ -43,6 +44,7 @@ import { getPatientName } from '../telemed/utils'; import { PastVisits } from '../telemed/features/appointment/PastVisits'; import { addSpacesAfterCommas } from '../helpers/formatString'; import { INTERPRETER_PHONE_NUMBER } from 'ehr-utils'; +import { Appointment } from 'fhir/r4'; enum Gender { 'male' = 'Male', @@ -144,7 +146,7 @@ export const AppointmentSidePanel: FC = ({ appointmen - {getAppointmentStatusChip(appointment?.status ?? '')} + {getAppointmentStatusChip(getStatusFromExtension(appointment as Appointment) as ApptStatus)} {appointment?.id && ( diff --git a/packages/telemed-ehr/app/src/components/AppointmentStatusSwitcher.tsx b/packages/telemed-ehr/app/src/components/AppointmentStatusSwitcher.tsx new file mode 100644 index 00000000..9bb336e5 --- /dev/null +++ b/packages/telemed-ehr/app/src/components/AppointmentStatusSwitcher.tsx @@ -0,0 +1,114 @@ +import React, { Dispatch, ReactElement, SetStateAction } from 'react'; +import { + OutlinedInput, + InputLabel, + MenuItem, + FormControl, + ListItemText, + Checkbox, + Select, + SelectChangeEvent, + CircularProgress, +} from '@mui/material'; +import { FhirClient } from '@zapehr/sdk'; +import { Appointment, Encounter } from 'fhir/r4'; +import { getPatchOperationsToUpdateVisitStatus } from '../helpers/mappingUtils'; +import { Operation } from 'fast-json-patch'; +import { getPatchBinary } from 'ehr-utils'; +import { VisitStatus, STATI } from '../helpers/mappingUtils'; +import { useApiClients } from '../hooks/useAppClients'; +import { Label } from 'amazon-chime-sdk-component-library-react'; +import { AppointmentStatusChip } from '../telemed'; +import { getAppointmentStatusChip } from './AppointmentTableRow'; +import { set } from 'react-hook-form'; + +const statuses = STATI; + +export const switchStatus = async ( + fhirClient: FhirClient | undefined, + appointment: Appointment, + encounter: Encounter, + status: VisitStatus, +): Promise => { + const statusOperations = getPatchOperationsToUpdateVisitStatus(appointment, status); + + if (!fhirClient) { + throw new Error('error getting fhir client'); + } + + if (!appointment.id || !encounter.id) { + throw new Error('Appointment or Encounter ID is missing'); + } + + const patchOp: Operation = { + op: 'replace', + path: '/status', + value: status, + }; + + await fhirClient?.batchRequest({ + requests: [ + getPatchBinary({ + resourceId: appointment.id, + resourceType: 'Appointment', + patchOperations: [patchOp, ...statusOperations], + }), + getPatchBinary({ + resourceId: encounter.id, + resourceType: 'Encounter', + patchOperations: [patchOp], + }), + ], + }); +}; + +interface AppointmentStatusSwitcherProps { + appointment: Appointment; + encounter: Encounter; +} + +export default function AppointmentStatusSwitcher({ + appointment, + encounter, +}: AppointmentStatusSwitcherProps): ReactElement { + const { fhirClient } = useApiClients(); + const [statusLoading, setStatusLoading] = React.useState(false); + const [currentAppointment, setCurrentAppointment] = React.useState(appointment); + + const handleChange = async (event: SelectChangeEvent): Promise => { + const value = event.target.value; + setStatusLoading(true); + await switchStatus(fhirClient, currentAppointment, encounter, value as VisitStatus); + const newAppointment = (await fhirClient?.readResource({ + resourceType: 'Appointment', + resourceId: appointment.id || '', + })) as Appointment; + setCurrentAppointment(newAppointment); + setStatusLoading(false); + }; + + return ( + <> + + {statusLoading ? ( + + ) : ( + + )} + + + ); +} diff --git a/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx b/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx index e3d37fdf..8d09147b 100644 --- a/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx +++ b/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx @@ -1,12 +1,8 @@ import { Box, Container } from '@mui/material'; import { FC, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { - AppointmentFooter, - AppointmentHeader, - AppointmentTabs, - AppointmentTabsHeader, -} from '../telemed/features/appointment'; +import { AppointmentHeader, AppointmentTabs, AppointmentTabsHeader } from '../telemed/features/appointment'; +import { AppointmentFooter } from '../components/AppointmentFooter'; import { AppointmentSidePanel } from '../components/AppointmentSidePanel'; import { PATIENT_PHOTO_CODE, getQuestionnaireResponseByLinkId } from 'ehr-utils'; import { @@ -102,6 +98,8 @@ export const AppointmentPage: FC = () => { + + ); }; diff --git a/packages/telemed-ehr/zambdas/src/shared/queueingUtils.ts b/packages/telemed-ehr/zambdas/src/shared/queueingUtils.ts index 31b9b3cb..e3560835 100644 --- a/packages/telemed-ehr/zambdas/src/shared/queueingUtils.ts +++ b/packages/telemed-ehr/zambdas/src/shared/queueingUtils.ts @@ -281,7 +281,7 @@ class QueueBuilder { this.insertNew(appointment, this.queues.inOffice.inExam.provider); } else if (status === 'ready for discharge') { this.insertNew(appointment, this.queues.inOffice.inExam['ready for discharge']); - } else if (status === 'canceled' || status === 'no show') { + } else if (status === 'cancelled' || status === 'no show') { this.insertNew(appointment, this.queues.canceled); } else if (status === 'checked out') { this.insertNew(appointment, this.queues.checkedOut); From 0a8b56e79145272ccb5f4e4e810952526ad1bbbf Mon Sep 17 00:00:00 2001 From: aykhanahmadli Date: Wed, 30 Oct 2024 13:51:26 +0100 Subject: [PATCH 16/38] change submit logic and error handling --- .../components/EmployeeInformationForm.tsx | 318 +++++++++--------- .../telemed-intake/app/src/pages/AuthPage.tsx | 24 +- .../telemed-intake/app/src/pages/Welcome.tsx | 2 +- 3 files changed, 178 insertions(+), 166 deletions(-) diff --git a/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx b/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx index 7c595121..da7123de 100644 --- a/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx +++ b/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx @@ -120,7 +120,13 @@ export default function EmployeeInformationForm({ const theme = useTheme(); const currentUser = useOttehrUser(); const [loading, setLoading] = useState(false); - const [errors, setErrors] = useState({ submit: false, roles: false, newLicense: false, duplicateLicense: false }); + const [errors, setErrors] = useState({ + submit: false, + roles: false, + qualification: false, + state: false, + duplicateLicense: false, + }); const [newLicenseState, setNewLicenseState] = useState(undefined); const [newLicenseCode, setNewLicenseCode] = useState(undefined); @@ -228,7 +234,7 @@ export default function EmployeeInformationForm({ lastName: data.lastName, nameSuffix: data.nameSuffix, selectedRoles: data.roles, - licenses: licenses.filter((license) => data.enabledLicenses[license.state]), + licenses: newLicenses, }); } catch (error) { console.log(`Failed to update user: ${error}`); @@ -238,53 +244,32 @@ export default function EmployeeInformationForm({ } }; - const updateLicenses = async (licenses: PractitionerLicense[]): Promise => { - if (!zambdaClient) { - throw new Error('Zambda Client not found'); - } - - const data = getValues(); - - await updateUser(zambdaClient, { - userId: user.id, - firstName: data.firstName, - middleName: data.middleName, - lastName: data.lastName, - nameSuffix: data.nameSuffix, - selectedRoles: data.roles, - licenses: licenses, - }); - }; - const handleAddLicense = async (): Promise => { - try { - setErrors((prev) => ({ ...prev, duplicateLicense: false, newLicense: false })); + setErrors((prev) => ({ ...prev, state: false, qualification: false, duplicateLicense: false })); - if (newLicenses.find((license) => license.state === newLicenseState && license.code === newLicenseCode)) { - setErrors((prev) => ({ ...prev, duplicateLicense: true })); - return; - } - if (!newLicenseState || !newLicenseCode) { - setErrors((prev) => ({ ...prev, newLicense: true })); - return; - } - const updatedLicenses = [...newLicenses]; - updatedLicenses.push({ - state: newLicenseState, - code: newLicenseCode as PractitionerQualificationCode, - active: true, - }); - setNewLicenses(updatedLicenses); - setNewLicenseState(undefined); - setNewLicenseCode(undefined); - await updateLicenses(updatedLicenses); - } catch (error) { - console.error('Error adding license:', error); - setErrors((prev) => ({ ...prev, submit: true })); + if (newLicenses.find((license) => license.state === newLicenseState && license.code === newLicenseCode)) { + setErrors((prev) => ({ ...prev, duplicateLicense: true })); + return; } + if (!newLicenseCode || !newLicenseState) { + setErrors((prev) => ({ + ...prev, + qualification: !newLicenseCode, + state: !newLicenseState, + })); + return; + } + const updatedLicenses = [...newLicenses]; + updatedLicenses.push({ + state: newLicenseState, + code: newLicenseCode as PractitionerQualificationCode, + active: true, + }); + setNewLicenses(updatedLicenses); + setNewLicenseState(undefined); + setNewLicenseCode(undefined); }; - // every time newLicenses changes, update the user return isActive === undefined ? ( ) : ( @@ -446,117 +431,138 @@ export default function EmployeeInformationForm({ {isProviderRoleSelected && ( <>
- - Provider Qualifications - - - - - - - State - Qualification - Operate in state - Delete License - - - - {newLicenses.map((license, index) => ( - - {license.state} - {license.code} - - { - const updatedLicenses = [...newLicenses]; - updatedLicenses[index].active = !updatedLicenses[index].active; - - setNewLicenses(updatedLicenses); - - await updateLicenses(updatedLicenses); - }} - /> - - - { - const updatedLicenses = [...newLicenses]; - updatedLicenses.splice(index, 1); - - setNewLicenses(updatedLicenses); - await updateLicenses(updatedLicenses); - }} - > - - - + + + Provider Qualifications + + + +
+ + + State + Qualification + Operate in state + Delete License - ))} - -
-
- - } - sx={{ - marginTop: '20px', - fontWeight: 'bold', - color: theme.palette.primary.main, - cursor: 'pointer', - }} - > - Add New State Qualification - - - - - option} - renderInput={(params) => } - value={newLicenseState || null} // Set the value prop - onChange={(event, value) => setNewLicenseState(value || undefined)} - /> + + + {newLicenses.map((license, index) => ( + + {license.state} + {license.code} + + { + const updatedLicenses = [...newLicenses]; + updatedLicenses[index].active = !updatedLicenses[index].active; + + setNewLicenses(updatedLicenses); + }} + /> + + + { + const updatedLicenses = [...newLicenses]; + updatedLicenses.splice(index, 1); + + setNewLicenses(updatedLicenses); + }} + > + + + + + ))} + + + + + } + sx={{ + marginTop: '20px', + fontWeight: 'bold', + color: theme.palette.primary.main, + cursor: 'pointer', + }} + > + Add New State Qualification + + + + + option} + renderInput={(params) => ( + + )} + value={newLicenseState || null} + onChange={(event, value) => setNewLicenseState(value || undefined)} + /> + + + option} + renderInput={(params) => ( + + )} + value={newLicenseCode || null} + onChange={(event, value) => setNewLicenseCode(value || undefined)} + /> + + + + + {errors.duplicateLicense && ( + {`License already exists.`} + )} - - option} - renderInput={(params) => } - value={newLicenseCode || null} // Set the value prop - onChange={(event, value) => setNewLicenseCode(value || undefined)} - /> - - - - - - - -
+ + +
+ )} @@ -566,12 +572,6 @@ export default function EmployeeInformationForm({ {errors.submit && ( {`Failed to update user. Please try again.`} )} - {errors.newLicense && ( - {`Please select a state and qualification`} - )} - {errors.duplicateLicense && ( - {`License already exists`} - )} {/* Update Employee and Cancel Buttons */} diff --git a/packages/telemed-intake/app/src/pages/AuthPage.tsx b/packages/telemed-intake/app/src/pages/AuthPage.tsx index b08d482d..3e0f7803 100644 --- a/packages/telemed-intake/app/src/pages/AuthPage.tsx +++ b/packages/telemed-intake/app/src/pages/AuthPage.tsx @@ -5,8 +5,12 @@ import { IntakeFlowPageRoute } from '../App'; import { ErrorFallbackScreen, LoadingScreen } from '../features/common'; const AuthPage: FC = () => { - const { isAuthenticated, loginWithRedirect, isLoading, error } = useAuth0(); + const { isAuthenticated, loginWithRedirect, isLoading, error, user } = useAuth0(); const authRef = useRef | null>(null); + + const searchParams = new URLSearchParams(location.search); + const welcomePath = searchParams.get('flow') === 'welcome'; + if (error) { return ; } @@ -17,15 +21,23 @@ const AuthPage: FC = () => { if (!isAuthenticated) { if (!authRef.current) { - authRef.current = loginWithRedirect(); + authRef.current = loginWithRedirect({ + appState: { welcomePath }, + }); } return ; } - if (!localStorage.getItem('welcomePath')) { - return ; + + console.log('user appState', user?.appState?.welcomePath); + + const appState = user?.appState || {}; + const redirectToWelcome = appState.welcomePath || welcomePath; + + if (redirectToWelcome) { + return ; } - localStorage.removeItem('welcomePath'); - return ; + + return ; }; export default AuthPage; diff --git a/packages/telemed-intake/app/src/pages/Welcome.tsx b/packages/telemed-intake/app/src/pages/Welcome.tsx index bfb97910..8a33bc0e 100644 --- a/packages/telemed-intake/app/src/pages/Welcome.tsx +++ b/packages/telemed-intake/app/src/pages/Welcome.tsx @@ -75,7 +75,7 @@ const Welcome = (): JSX.Element => { localStorage.setItem('welcomePath', location.pathname); if (!isAuthenticated) { - navigate(IntakeFlowPageRoute.AuthPage.path); + navigate(`${IntakeFlowPageRoute.AuthPage.path}?flow=welcome`); return; } From b57b149e3644cb456ee75cb6ea80f8d90dffbb63 Mon Sep 17 00:00:00 2001 From: Gilad Schneider Date: Wed, 30 Oct 2024 14:50:20 -0400 Subject: [PATCH 17/38] allows editing appointment status for in person appointments --- .../src/components/AppointmentSidePanel.tsx | 4 +- .../components/AppointmentStatusSwitcher.tsx | 86 +++++++++++-------- .../src/components/AppointmentTableRow.tsx | 8 +- .../app/src/helpers/mappingUtils.ts | 31 +++---- .../app/src/pages/AppointmentPage.tsx | 2 - .../src/shared/fhirStatusMappingUtils.ts | 6 +- .../utils/lib/helpers/resources/appoinment.ts | 7 +- 7 files changed, 78 insertions(+), 66 deletions(-) diff --git a/packages/telemed-ehr/app/src/components/AppointmentSidePanel.tsx b/packages/telemed-ehr/app/src/components/AppointmentSidePanel.tsx index ef4bd0f0..8b53ffa9 100644 --- a/packages/telemed-ehr/app/src/components/AppointmentSidePanel.tsx +++ b/packages/telemed-ehr/app/src/components/AppointmentSidePanel.tsx @@ -45,6 +45,7 @@ import { PastVisits } from '../telemed/features/appointment/PastVisits'; import { addSpacesAfterCommas } from '../helpers/formatString'; import { INTERPRETER_PHONE_NUMBER } from 'ehr-utils'; import { Appointment } from 'fhir/r4'; +import AppointmentStatusSwitcher from './AppointmentStatusSwitcher'; enum Gender { 'male' = 'Male', @@ -146,8 +147,6 @@ export const AppointmentSidePanel: FC = ({ appointmen - {getAppointmentStatusChip(getStatusFromExtension(appointment as Appointment) as ApptStatus)} - {appointment?.id && ( = ({ appointmen )} + {} diff --git a/packages/telemed-ehr/app/src/components/AppointmentStatusSwitcher.tsx b/packages/telemed-ehr/app/src/components/AppointmentStatusSwitcher.tsx index 9bb336e5..be31fdf6 100644 --- a/packages/telemed-ehr/app/src/components/AppointmentStatusSwitcher.tsx +++ b/packages/telemed-ehr/app/src/components/AppointmentStatusSwitcher.tsx @@ -1,26 +1,14 @@ -import React, { Dispatch, ReactElement, SetStateAction } from 'react'; -import { - OutlinedInput, - InputLabel, - MenuItem, - FormControl, - ListItemText, - Checkbox, - Select, - SelectChangeEvent, - CircularProgress, -} from '@mui/material'; +import React, { ReactElement } from 'react'; +import { MenuItem, FormControl, Select, SelectChangeEvent, CircularProgress } from '@mui/material'; import { FhirClient } from '@zapehr/sdk'; import { Appointment, Encounter } from 'fhir/r4'; -import { getPatchOperationsToUpdateVisitStatus } from '../helpers/mappingUtils'; +import { getPatchOperationsToUpdateVisitStatus, mapVisitStatusToFhirAppointmentStatus } from '../helpers/mappingUtils'; import { Operation } from 'fast-json-patch'; -import { getPatchBinary } from 'ehr-utils'; +import { getPatchBinary, getStatusFromExtension } from 'ehr-utils'; import { VisitStatus, STATI } from '../helpers/mappingUtils'; import { useApiClients } from '../hooks/useAppClients'; -import { Label } from 'amazon-chime-sdk-component-library-react'; -import { AppointmentStatusChip } from '../telemed'; import { getAppointmentStatusChip } from './AppointmentTableRow'; -import { set } from 'react-hook-form'; +import { Box } from '@mui/system'; const statuses = STATI; @@ -30,7 +18,9 @@ export const switchStatus = async ( encounter: Encounter, status: VisitStatus, ): Promise => { - const statusOperations = getPatchOperationsToUpdateVisitStatus(appointment, status); + if (status === 'unknown') { + throw new Error(`Invalid status: ${status}`); + } if (!fhirClient) { throw new Error('error getting fhir client'); @@ -40,10 +30,12 @@ export const switchStatus = async ( throw new Error('Appointment or Encounter ID is missing'); } + const statusOperations = getPatchOperationsToUpdateVisitStatus(appointment, status); + const patchOp: Operation = { op: 'replace', path: '/status', - value: status, + value: mapVisitStatusToFhirAppointmentStatus(status), }; await fhirClient?.batchRequest({ @@ -88,27 +80,47 @@ export default function AppointmentStatusSwitcher({ }; return ( - <> - - {statusLoading ? ( - - ) : ( - await handleChange(event)} + renderValue={(selected) => getAppointmentStatusChip(selected)} + sx={{ + boxShadow: 'none', + '.MuiOutlinedInput-notchedOutline': { border: 0 }, + '&.MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline': { + border: 0, + }, + '&.MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { + border: 0, + }, + }} + > + {statuses + .filter((status) => status !== 'unknown') + .map((status) => ( {getAppointmentStatusChip(status)} ))} - - )} - - + + )} + ); } diff --git a/packages/telemed-ehr/app/src/components/AppointmentTableRow.tsx b/packages/telemed-ehr/app/src/components/AppointmentTableRow.tsx index 98915dd0..a0f81ecf 100644 --- a/packages/telemed-ehr/app/src/components/AppointmentTableRow.tsx +++ b/packages/telemed-ehr/app/src/components/AppointmentTableRow.tsx @@ -133,7 +133,7 @@ export const CHIP_STATUS_MAP: { primary: '#684e5d', }, }, - 'provider-ready': { + 'ready for provider': { background: { primary: '#EEEEEE', secondary: '#444444', @@ -150,7 +150,7 @@ export const CHIP_STATUS_MAP: { primary: '#6F6D1A', }, }, - discharge: { + 'ready for discharge': { background: { primary: '#B2EBF2', }, @@ -158,7 +158,7 @@ export const CHIP_STATUS_MAP: { primary: '#006064', }, }, - 'checked-out': { + 'checked out': { background: { primary: '#FFFFFF', }, @@ -174,7 +174,7 @@ export const CHIP_STATUS_MAP: { primary: '#B71C1C', }, }, - 'no-show': { + 'no show': { background: { primary: '#DFE5E9', }, diff --git a/packages/telemed-ehr/app/src/helpers/mappingUtils.ts b/packages/telemed-ehr/app/src/helpers/mappingUtils.ts index 68f901aa..618adaa7 100644 --- a/packages/telemed-ehr/app/src/helpers/mappingUtils.ts +++ b/packages/telemed-ehr/app/src/helpers/mappingUtils.ts @@ -5,16 +5,16 @@ import { DateTime } from 'luxon'; export const ZAPEHR_ID_TYPE = 'ZapEHR ID'; export const STATI = [ + 'pending', 'arrived', - 'cancelled', + 'ready', 'intake', - 'no-show', - 'pending', + 'ready for provider', 'provider', - 'ready', - 'checked-out', - 'discharge', - 'provider-ready', + 'ready for discharge', + 'checked out', + 'cancelled', + 'no show', 'unknown', ] as const; type STATI_LIST = typeof STATI; @@ -56,17 +56,18 @@ export type VisitStatusExtension = { }; export type VisitStatusWithoutUnknown = Exclude; +// todo this should be customizable const otherEHRToFhirAppointmentStatusMap: Record = { pending: 'booked', arrived: 'arrived', ready: 'checked-in', intake: 'checked-in', - 'provider-ready': 'fulfilled', + 'ready for provider': 'fulfilled', provider: 'fulfilled', - discharge: 'fulfilled', - 'checked-out': 'fulfilled', + 'ready for discharge': 'fulfilled', + 'checked out': 'fulfilled', cancelled: 'cancelled', - 'no-show': 'noshow', + 'no show': 'noshow', }; export function makeVisitStatusExtensionEntry( @@ -94,12 +95,12 @@ const otherEHRToFhirEncounterStatusMap: Record { diff --git a/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx b/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx index 8d09147b..a6c39c25 100644 --- a/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx +++ b/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx @@ -98,8 +98,6 @@ export const AppointmentPage: FC = () => { - - ); }; diff --git a/packages/telemed-ehr/zambdas/src/shared/fhirStatusMappingUtils.ts b/packages/telemed-ehr/zambdas/src/shared/fhirStatusMappingUtils.ts index ef42b834..5d676342 100644 --- a/packages/telemed-ehr/zambdas/src/shared/fhirStatusMappingUtils.ts +++ b/packages/telemed-ehr/zambdas/src/shared/fhirStatusMappingUtils.ts @@ -6,12 +6,12 @@ const STATI = [ 'CANCELLED', 'CHECKED-IN', 'INTAKE', - 'NO-SHOW', + 'NO SHOW', 'PENDING', 'PROVIDER', 'READY', - 'DISCHARGE', - 'PROVIDER-READY', + 'READY FOR DISCHARGE', + 'READY FOR PROVIDER', ]; type VISIT_STATUS_LABEL_TYPE = typeof STATI; diff --git a/packages/utils/lib/helpers/resources/appoinment.ts b/packages/utils/lib/helpers/resources/appoinment.ts index 0a9f0a8d..47c87b1b 100644 --- a/packages/utils/lib/helpers/resources/appoinment.ts +++ b/packages/utils/lib/helpers/resources/appoinment.ts @@ -30,13 +30,14 @@ const STATI = [ 'CANCELLED', 'CHECKED-IN', 'INTAKE', - 'NO-SHOW', + 'NO SHOW', 'PENDING', 'PROVIDER', 'READY', - 'DISCHARGE', - 'PROVIDER-READY', + 'READY FOR DISCHARGE', + 'READY FOR PROVIDER', ]; + type STATI_LIST = typeof STATI; export type VisitStatus = STATI_LIST[number]; From 7cecc9e46237e6547fc24de597c7e70a1a24b96c Mon Sep 17 00:00:00 2001 From: Gilad Schneider Date: Wed, 30 Oct 2024 15:39:56 -0400 Subject: [PATCH 18/38] telemed tab debugging --- .../helpers/fhir-utils.ts | 5 +++++ .../get-telemed-appointments/helpers/mappers.ts | 16 ++++++++-------- .../src/get-telemed-appointments/index.ts | 12 ++++++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/fhir-utils.ts b/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/fhir-utils.ts index 4d2324ce..8cdab2bb 100644 --- a/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/fhir-utils.ts +++ b/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/fhir-utils.ts @@ -116,7 +116,9 @@ export const getPractLicensesLocationsAbbreviations = async ( fhirClient: FhirClient, appClient: AppClient, ): Promise => { + console.log('Getting practitioner licenses locations abbreviations'); const practitionerId = (await appClient.getMe()).profile.replace('Practitioner/', ''); + console.log('My practitioner ID: ' + practitionerId); const practitioner: Practitioner = (await fhirClient.readResource({ @@ -159,6 +161,7 @@ export const locationIdsForAppointmentsSearch = async ( // In this case we want to return appointments with location == stateFilter if (patientFilter === 'my-patients') { + console.log('my patients'); const practitionerStates = await getPractLicensesLocationsAbbreviations(fhirClient, appClient); console.log('Practitioner states: ' + JSON.stringify(practitionerStates)); if (!stateFilter) { @@ -190,6 +193,7 @@ export const getAllPrefilteredFhirResources = async ( const { dateFilter, providersFilter, stateFilter, statusesFilter, groupsFilter, patientFilter } = params; let allResources: Resource[] = []; + console.log('######'); const locationsIdsToSearchWith = await locationIdsForAppointmentsSearch( stateFilter, patientFilter, @@ -197,6 +201,7 @@ export const getAllPrefilteredFhirResources = async ( fhirClient, appClient, ); + console.log('Locations IDs to search with: ' + JSON.stringify(locationsIdsToSearchWith)); if (!locationsIdsToSearchWith) return undefined; const encounterStatusesToSearchWith = mapTelemedStatusToEncounter(statusesFilter); const dateFilterConverted = DateTime.fromISO(dateFilter); diff --git a/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/mappers.ts b/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/mappers.ts index 022f07b7..f3713acc 100644 --- a/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/mappers.ts +++ b/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/mappers.ts @@ -199,14 +199,14 @@ function mapCommunicationsToRelatedPersonRef( ): Record { const commsToRpRefMap: Record = {}; - // todo: remove temporary fix - if ( - allCommunications.length === 0 || - Object.keys(rpToIdMap).length === 0 || - Object.keys(rpsRefsToPhoneNumberMap).length === 0 - ) { - return commsToRpRefMap; - } + // // todo: remove temporary fix + // if ( + // allCommunications.length === 0 || + // Object.keys(rpToIdMap).length === 0 || + // Object.keys(rpsRefsToPhoneNumberMap).length === 0 + // ) { + // return commsToRpRefMap; + // } allCommunications.forEach((comm) => { const communication = comm as Communication; diff --git a/packages/telemed-ehr/zambdas/src/get-telemed-appointments/index.ts b/packages/telemed-ehr/zambdas/src/get-telemed-appointments/index.ts index bd3cd9c5..64b7b21d 100644 --- a/packages/telemed-ehr/zambdas/src/get-telemed-appointments/index.ts +++ b/packages/telemed-ehr/zambdas/src/get-telemed-appointments/index.ts @@ -62,18 +62,30 @@ export const performEffect = async ( const virtualLocationsMap = await getAllVirtualLocationsMap(fhirClient); console.log('Created virtual locations map.'); + console.log(19879871); const allResources = await getAllPrefilteredFhirResources(fhirClient, appClient, params, virtualLocationsMap); + console.log(19879872); + if (!allResources) { + console.log(19879873); + return { message: 'Successfully retrieved all appointments', appointments: [], }; } + console.log(19879874); + const allPackages = filterAppointmentsFromResources(allResources, statusesFilter, virtualLocationsMap); + console.log(19879875); + console.log('Received all appointments with type "virtual":', allPackages.length); + console.log(19879876); const resultAppointments: TelemedAppointmentInformation[] = []; + console.log(19879877); + if (allResources.length > 0) { const allRelatedPersonMaps = await relatedPersonAndCommunicationMaps(fhirClient, allResources); From 7a18cd74f5d3d18bff24b38cbdfd0ccd7900ade9 Mon Sep 17 00:00:00 2001 From: Gilad Schneider Date: Wed, 30 Oct 2024 16:56:09 -0400 Subject: [PATCH 19/38] removes console logs and fixes telemedicine tab bug --- .../src/get-telemed-appointments/helpers/fhir-utils.ts | 9 ++------- .../zambdas/src/get-telemed-appointments/index.ts | 9 --------- .../zambdas/src/appointment/create-appointment/index.ts | 1 - 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/fhir-utils.ts b/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/fhir-utils.ts index 8cdab2bb..fd3d0092 100644 --- a/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/fhir-utils.ts +++ b/packages/telemed-ehr/zambdas/src/get-telemed-appointments/helpers/fhir-utils.ts @@ -106,8 +106,8 @@ export const getCommunicationsAndSenders = async ( resourceType: 'Communication', searchParams: [ { name: 'medium', value: `${ZAP_SMS_MEDIUM_CODE}` }, - // { name: 'sender:RelatedPerson.telecom', value: uniqueNumbers.join(',') }, - // { name: '_include', value: 'Communication:sender' }, + { name: 'sender:RelatedPerson.telecom', value: uniqueNumbers.join(',') }, + { name: '_include', value: 'Communication:sender' }, ], }); }; @@ -116,9 +116,7 @@ export const getPractLicensesLocationsAbbreviations = async ( fhirClient: FhirClient, appClient: AppClient, ): Promise => { - console.log('Getting practitioner licenses locations abbreviations'); const practitionerId = (await appClient.getMe()).profile.replace('Practitioner/', ''); - console.log('My practitioner ID: ' + practitionerId); const practitioner: Practitioner = (await fhirClient.readResource({ @@ -161,7 +159,6 @@ export const locationIdsForAppointmentsSearch = async ( // In this case we want to return appointments with location == stateFilter if (patientFilter === 'my-patients') { - console.log('my patients'); const practitionerStates = await getPractLicensesLocationsAbbreviations(fhirClient, appClient); console.log('Practitioner states: ' + JSON.stringify(practitionerStates)); if (!stateFilter) { @@ -193,7 +190,6 @@ export const getAllPrefilteredFhirResources = async ( const { dateFilter, providersFilter, stateFilter, statusesFilter, groupsFilter, patientFilter } = params; let allResources: Resource[] = []; - console.log('######'); const locationsIdsToSearchWith = await locationIdsForAppointmentsSearch( stateFilter, patientFilter, @@ -201,7 +197,6 @@ export const getAllPrefilteredFhirResources = async ( fhirClient, appClient, ); - console.log('Locations IDs to search with: ' + JSON.stringify(locationsIdsToSearchWith)); if (!locationsIdsToSearchWith) return undefined; const encounterStatusesToSearchWith = mapTelemedStatusToEncounter(statusesFilter); const dateFilterConverted = DateTime.fromISO(dateFilter); diff --git a/packages/telemed-ehr/zambdas/src/get-telemed-appointments/index.ts b/packages/telemed-ehr/zambdas/src/get-telemed-appointments/index.ts index 64b7b21d..3c4d125c 100644 --- a/packages/telemed-ehr/zambdas/src/get-telemed-appointments/index.ts +++ b/packages/telemed-ehr/zambdas/src/get-telemed-appointments/index.ts @@ -62,30 +62,21 @@ export const performEffect = async ( const virtualLocationsMap = await getAllVirtualLocationsMap(fhirClient); console.log('Created virtual locations map.'); - console.log(19879871); const allResources = await getAllPrefilteredFhirResources(fhirClient, appClient, params, virtualLocationsMap); - console.log(19879872); if (!allResources) { - console.log(19879873); - return { message: 'Successfully retrieved all appointments', appointments: [], }; } - console.log(19879874); const allPackages = filterAppointmentsFromResources(allResources, statusesFilter, virtualLocationsMap); - console.log(19879875); console.log('Received all appointments with type "virtual":', allPackages.length); - console.log(19879876); const resultAppointments: TelemedAppointmentInformation[] = []; - console.log(19879877); - if (allResources.length > 0) { const allRelatedPersonMaps = await relatedPersonAndCommunicationMaps(fhirClient, allResources); diff --git a/packages/telemed-intake/zambdas/src/appointment/create-appointment/index.ts b/packages/telemed-intake/zambdas/src/appointment/create-appointment/index.ts index 6113ab01..9e3ae942 100644 --- a/packages/telemed-intake/zambdas/src/appointment/create-appointment/index.ts +++ b/packages/telemed-intake/zambdas/src/appointment/create-appointment/index.ts @@ -105,7 +105,6 @@ async function performEffect(props: PerformEffectInputProps): Promise Date: Thu, 31 Oct 2024 00:28:17 +0100 Subject: [PATCH 20/38] fix for get patients errors --- .../app/src/features/patients/patients.queries.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/telemed-intake/app/src/features/patients/patients.queries.ts b/packages/telemed-intake/app/src/features/patients/patients.queries.ts index b5d0fccb..7519b005 100644 --- a/packages/telemed-intake/app/src/features/patients/patients.queries.ts +++ b/packages/telemed-intake/app/src/features/patients/patients.queries.ts @@ -16,7 +16,7 @@ export const useGetPatients = ( throw new Error('api client not defined'); }, { - enabled: false, + enabled: !!apiClient, onSuccess, onError: (err) => { console.error('Error during fetching get patients: ', err); From 57e8b9df22b36575bafb0ace39da9b5ee064daae Mon Sep 17 00:00:00 2001 From: aykhanahmadli Date: Thu, 31 Oct 2024 00:28:58 +0100 Subject: [PATCH 21/38] feat: add loading indicator for patients data fetching --- .../telemed-intake/app/src/pages/PatientPortal.tsx | 4 ++-- packages/telemed-intake/app/src/pages/Welcome.tsx | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/telemed-intake/app/src/pages/PatientPortal.tsx b/packages/telemed-intake/app/src/pages/PatientPortal.tsx index 9aea8d88..7694dd3e 100644 --- a/packages/telemed-intake/app/src/pages/PatientPortal.tsx +++ b/packages/telemed-intake/app/src/pages/PatientPortal.tsx @@ -21,7 +21,7 @@ const PatientPortal = (): JSX.Element => { ['ready', 'pre-video', 'on-video'].includes(appointment.telemedStatus), ); - const { data: patientsData } = useGetPatients(apiClient, (data) => { + const { data: patientsData, isFetching: isPatientsFetching } = useGetPatients(apiClient, (data) => { usePatientsStore.setState({ patients: data?.patients }); }); @@ -40,7 +40,7 @@ const PatientPortal = (): JSX.Element => { bgVariant={IntakeFlowPageRoute.PatientPortal.path} isFirstPage={true} > - {isFetching ? ( + {isFetching || isPatientsFetching ? ( { useFilesStore.setState({ fileURLs: undefined }); }; - const { refetch: refetchPatients } = useGetPatients(apiClient, (data) => { + const { refetch: refetchPatients, isFetching: isPatientsFetching } = useGetPatients(apiClient, (data) => { usePatientsStore.setState({ patients: data?.patients }); }); @@ -121,7 +122,8 @@ const Welcome = (): JSX.Element => { {visitType === 'prebook' && } - + )} From 22438e9b7182b5a66457eed1308eee329fcf7304 Mon Sep 17 00:00:00 2001 From: aykhanahmadli Date: Thu, 31 Oct 2024 14:14:38 +0100 Subject: [PATCH 22/38] revert auth changes --- .../telemed-intake/app/src/pages/AuthPage.tsx | 24 +++++-------------- .../telemed-intake/app/src/pages/Welcome.tsx | 2 +- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/packages/telemed-intake/app/src/pages/AuthPage.tsx b/packages/telemed-intake/app/src/pages/AuthPage.tsx index 3e0f7803..b08d482d 100644 --- a/packages/telemed-intake/app/src/pages/AuthPage.tsx +++ b/packages/telemed-intake/app/src/pages/AuthPage.tsx @@ -5,12 +5,8 @@ import { IntakeFlowPageRoute } from '../App'; import { ErrorFallbackScreen, LoadingScreen } from '../features/common'; const AuthPage: FC = () => { - const { isAuthenticated, loginWithRedirect, isLoading, error, user } = useAuth0(); + const { isAuthenticated, loginWithRedirect, isLoading, error } = useAuth0(); const authRef = useRef | null>(null); - - const searchParams = new URLSearchParams(location.search); - const welcomePath = searchParams.get('flow') === 'welcome'; - if (error) { return ; } @@ -21,23 +17,15 @@ const AuthPage: FC = () => { if (!isAuthenticated) { if (!authRef.current) { - authRef.current = loginWithRedirect({ - appState: { welcomePath }, - }); + authRef.current = loginWithRedirect(); } return ; } - - console.log('user appState', user?.appState?.welcomePath); - - const appState = user?.appState || {}; - const redirectToWelcome = appState.welcomePath || welcomePath; - - if (redirectToWelcome) { - return ; + if (!localStorage.getItem('welcomePath')) { + return ; } - - return ; + localStorage.removeItem('welcomePath'); + return ; }; export default AuthPage; diff --git a/packages/telemed-intake/app/src/pages/Welcome.tsx b/packages/telemed-intake/app/src/pages/Welcome.tsx index 8a33bc0e..bfb97910 100644 --- a/packages/telemed-intake/app/src/pages/Welcome.tsx +++ b/packages/telemed-intake/app/src/pages/Welcome.tsx @@ -75,7 +75,7 @@ const Welcome = (): JSX.Element => { localStorage.setItem('welcomePath', location.pathname); if (!isAuthenticated) { - navigate(`${IntakeFlowPageRoute.AuthPage.path}?flow=welcome`); + navigate(IntakeFlowPageRoute.AuthPage.path); return; } From d24cd5ebdb3e166dc4f404535140c4b42303eec7 Mon Sep 17 00:00:00 2001 From: aykhanahmadli Date: Thu, 31 Oct 2024 14:17:31 +0100 Subject: [PATCH 23/38] remove unhandled lincese state --- .../app/src/components/EmployeeInformationForm.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx b/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx index da7123de..4dad606b 100644 --- a/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx +++ b/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx @@ -54,7 +54,6 @@ interface EmployeeForm { middleName: string; lastName: string; nameSuffix: string; - enabledLicenses: { [key: string]: boolean }; roles: string[]; } @@ -197,16 +196,6 @@ export default function EmployeeInformationForm({ setValue('middleName', middleName); setValue('lastName', lastName); setValue('nameSuffix', nameSuffix); - - let enabledLicenses = {}; - if (existingUser.profileResource?.qualification) { - enabledLicenses = existingUser.profileResource.qualification.reduce((result, qualification) => { - const state = qualification.extension?.[0].extension?.[1].valueCodeableConcept?.coding?.[0].code; - return state ? { ...result, ...{ [state]: true } } : result; - }, {}); - } - - setValue('enabledLicenses', enabledLicenses); } }, [existingUser, setValue]); From d977a85b8d407b8b936b8495f8d55f0ec490f12a Mon Sep 17 00:00:00 2001 From: aykhanahmadli Date: Thu, 31 Oct 2024 16:15:51 +0100 Subject: [PATCH 24/38] feat: fix add patient page --- .../telemed-ehr/app/env/.env.local-template | 1 + .../app/src/CustomThemeProvider.tsx | 4 +-- packages/telemed-ehr/app/src/api/api.ts | 21 +++++-------- .../app/src/components/SlotPicker.tsx | 31 +++++++++++-------- .../telemed-ehr/app/src/components/Slots.tsx | 8 +++-- .../telemed-ehr/app/src/pages/AddPatient.tsx | 12 +++---- 6 files changed, 41 insertions(+), 36 deletions(-) diff --git a/packages/telemed-ehr/app/env/.env.local-template b/packages/telemed-ehr/app/env/.env.local-template index 621cbd92..0192c6b4 100644 --- a/packages/telemed-ehr/app/env/.env.local-template +++ b/packages/telemed-ehr/app/env/.env.local-template @@ -18,6 +18,7 @@ VITE_APP_GET_APPOINTMENTS_ZAMBDA_ID=get-appointments VITE_APP_CREATE_APPOINTMENT_ZAMBDA_ID=create-appointment VITE_APP_UPDATE_USER_ZAMBDA_ID=update-user VITE_APP_GET_USER_ZAMBDA_ID=get-user +VITE_APP_GET_SCHEDULE_ZAMBDA_ID=get-schedule VITE_APP_SYNC_USER_ZAMBDA_ID=sync-user VITE_APP_DEACTIVATE_USER_ZAMBDA_ID=deactivate-user VITE_APP_GET_EMPLOYEES_ZAMBDA_ID=get-employees diff --git a/packages/telemed-ehr/app/src/CustomThemeProvider.tsx b/packages/telemed-ehr/app/src/CustomThemeProvider.tsx index ba7c0642..451c3a07 100644 --- a/packages/telemed-ehr/app/src/CustomThemeProvider.tsx +++ b/packages/telemed-ehr/app/src/CustomThemeProvider.tsx @@ -113,8 +113,8 @@ const typography: TypographyOptions = { lineHeight: '140%', }, h3: { - fontSize: 32, - fontWeight: '500 !important', + fontSize: 20, + fontWeight: '600 !important', fontFamily: headerFonts.join(','), lineHeight: '140%', }, diff --git a/packages/telemed-ehr/app/src/api/api.ts b/packages/telemed-ehr/app/src/api/api.ts index b96fb1ca..1133af03 100644 --- a/packages/telemed-ehr/app/src/api/api.ts +++ b/packages/telemed-ehr/app/src/api/api.ts @@ -27,7 +27,7 @@ const UPDATE_USER_ZAMBDA_ID = import.meta.env.VITE_APP_UPDATE_USER_ZAMBDA_ID; const GET_USER_ZAMBDA_ID = import.meta.env.VITE_APP_GET_USER_ZAMBDA_ID; const DEACTIVATE_USER_ZAMBDA_ID = import.meta.env.VITE_APP_DEACTIVATE_USER_ZAMBDA_ID; const GET_CONVERSATION_ZAMBDA_ID = import.meta.env.VITE_APP_GET_CONVERSATION_ZAMBDA_ID; -const GET_LOCATION_ZAMBDA_ID = import.meta.env.VITE_APP_GET_LOCATION_ZAMBDA_ID; +const GET_SCHEDULE_ZAMBDA_ID = import.meta.env.VITE_APP_GET_SCHEDULE_ZAMBDA_ID; const CANCEL_IN_PERSON_APPOINTMENT_ZAMBDA_ID = import.meta.env.VITE_APP_CANCEL_IN_PERSON_APPOINTMENT_ZAMBDA_ID; const GET_EMPLOYEES_ZAMBDA_ID = import.meta.env.VITE_APP_GET_EMPLOYEES_ZAMBDA_ID; @@ -229,7 +229,7 @@ export interface AvailableLocationInformation { otherOffices: { display: string; url: string }[]; } -export interface GetLocationResponse { +export interface GetScheduleResponse { message: string; state: string; name: string; @@ -242,24 +242,19 @@ export interface GetLocationResponse { timezone: string; } -export interface GetLocationParameters { - slug?: string; +export interface GetScheduleParameters { scheduleType?: 'location' | 'provider' | 'group'; + slug?: string; locationState?: string; - fetchAll?: boolean; } -export const getLocations = async ( - zambdaClient: ZambdaClient, - parameters: GetLocationParameters, -): Promise => { +export const getSchedule = async (zambdaClient: ZambdaClient, parameters: GetScheduleParameters): Promise => { try { - if (GET_LOCATION_ZAMBDA_ID == null || VITE_APP_IS_LOCAL == null) { - throw new Error('get location environment variable could not be loaded'); + if (GET_SCHEDULE_ZAMBDA_ID == null || VITE_APP_IS_LOCAL == null) { + throw new Error('get schedule environment variable could not be loaded'); } - console.log(import.meta.env); const response = await zambdaClient?.invokePublicZambda({ - zambdaId: GET_LOCATION_ZAMBDA_ID, + zambdaId: GET_SCHEDULE_ZAMBDA_ID, payload: parameters, }); return chooseJson(response, VITE_APP_IS_LOCAL); diff --git a/packages/telemed-ehr/app/src/components/SlotPicker.tsx b/packages/telemed-ehr/app/src/components/SlotPicker.tsx index 3f425af6..a1826e37 100644 --- a/packages/telemed-ehr/app/src/components/SlotPicker.tsx +++ b/packages/telemed-ehr/app/src/components/SlotPicker.tsx @@ -170,7 +170,7 @@ const SlotPicker = ({ const slotsExist = getSlotsForDate(firstAvailableDay).length > 0 || getSlotsForDate(secondAvailableDay).length > 0; return ( - + {secondAvailableDay && ( @@ -265,6 +277,7 @@ const SlotPicker = ({ opacity: 1, textTransform: 'capitalize', fontWeight: 700, + fontSize: '16px', }} /> )} @@ -272,11 +285,7 @@ const SlotPicker = ({ - + {firstAvailableDay?.toFormat(DATE_FULL_NO_YEAR)} - + {secondAvailableDay?.toFormat(DATE_FULL_NO_YEAR)} + - - {/* {user?.isPractitionerEnrolledInPhoton && ( - } - onClick={() => setIsERXOpen(true)} - loading={isERXLoading} - disabled={appointmentAccessibility.isAppointmentReadOnly} - > - RX - - )} */} - - - - - - - - Preferred Language - - - {preferredLanguage} {delimeterString} {interpreterString} - - - - - - Hearing Impaired Relay Service? (711) - - {relayPhone} - - - - - Patient number - - - {number} - - - - - - {appointmentAccessibility.status && - [ApptStatus['pre-video'], ApptStatus['on-video']].includes(appointmentAccessibility.status) && ( - - )} - {isPractitionerAllowedToCancelThisVisit && ( - - )} - - - {isCancelDialogOpen && ( - setIsCancelDialogOpen(false)} appointmentType={appointmentType} /> - )} - {/* {isERXOpen && setIsERXOpen(false)} onLoadingStatusChange={handleERXLoadingStatusChange} />} */} - {isEditDialogOpen && ( - setIsEditDialogOpen(false)} /> - )} - {chatModalOpen && ( - setChatModalOpen(false)} - onMarkAllRead={() => setHasUnread(false)} - /> - )} - {isInviteParticipantOpen && ( - setIsInviteParticipantOpen(false)} /> - )} - - - - ); -}; diff --git a/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx b/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx index 6d1efc85..dafe4756 100644 --- a/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx +++ b/packages/telemed-ehr/app/src/pages/AppointmentPage.tsx @@ -2,7 +2,7 @@ import { Box, Container } from '@mui/material'; import { FC, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { AppointmentHeader, AppointmentTabs, AppointmentTabsHeader } from '../telemed/features/appointment'; -import { AppointmentSidePanel } from '../components/AppointmentSidePanel'; +import { AppointmentSidePanel } from '../telemed/features/appointment'; import { PATIENT_PHOTO_CODE, getQuestionnaireResponseByLinkId } from 'ehr-utils'; import { useAppointmentStore, diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentSidePanel.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentSidePanel.tsx index b51d5215..cdcf4408 100644 --- a/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentSidePanel.tsx +++ b/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentSidePanel.tsx @@ -42,6 +42,8 @@ import { getAppointmentStatusChip, getPatientName } from '../../utils'; import { PastVisits } from './PastVisits'; import { addSpacesAfterCommas } from '../../../helpers/formatString'; import { INTERPRETER_PHONE_NUMBER } from 'ehr-utils'; +import { Appointment } from 'fhir/r4'; +import AppointmentStatusSwitcher from '../../../components/AppointmentStatusSwitcher'; enum Gender { 'male' = 'Male', @@ -143,7 +145,8 @@ export const AppointmentSidePanel: FC = ({ appointmen - {getAppointmentStatusChip(mapStatusToTelemed(encounter.status, appointment?.status))} + {appointmentType === 'telemed' && + getAppointmentStatusChip(mapStatusToTelemed(encounter.status, appointment?.status))} {appointment?.id && ( @@ -161,6 +164,10 @@ export const AppointmentSidePanel: FC = ({ appointmen )} + {appointmentType === 'in-person' && ( + + )} + {getPatientName(patient.name).lastFirstName} From 190ba0e9bc9f926b216ce19fec9a912f3616c7e8 Mon Sep 17 00:00:00 2001 From: aykhanahmadli Date: Fri, 1 Nov 2024 17:11:46 +0100 Subject: [PATCH 30/38] add required --- .../telemed-ehr/app/src/components/EmployeeInformationForm.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx b/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx index 0c0d54be..7c33ca0e 100644 --- a/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx +++ b/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx @@ -504,6 +504,7 @@ export default function EmployeeInformationForm({ {...params} label="State" error={errors.state} + required helperText={errors.state ? 'Please select a state' : null} /> )} @@ -520,6 +521,7 @@ export default function EmployeeInformationForm({ {...params} label="Qualification" error={errors.qualification} + required helperText={errors.qualification ? 'Please select a qualification' : null} /> )} From 5f795429fe16831a77c309f7d7c832d351ded003 Mon Sep 17 00:00:00 2001 From: Gilad Schneider Date: Fri, 1 Nov 2024 12:34:42 -0400 Subject: [PATCH 31/38] resolved comments --- .../CurrentMedications/CurrentMedicationsProviderColumn.tsx | 4 ++-- .../KnownAllergies/KnownAllergiesProviderColumn.tsx | 4 ++-- .../MedicalConditions/MedicalConditionsProviderColumn.tsx | 4 ++-- .../zambdas/src/change-telemed-appointment-status/index.ts | 1 - 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/CurrentMedications/CurrentMedicationsProviderColumn.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/CurrentMedications/CurrentMedicationsProviderColumn.tsx index 4de9003a..aa39664d 100644 --- a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/CurrentMedications/CurrentMedicationsProviderColumn.tsx +++ b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/CurrentMedications/CurrentMedicationsProviderColumn.tsx @@ -66,9 +66,9 @@ export const CurrentMedicationsProviderColumn: FC = () => { )} - {/* {medications.length === 0 && isReadOnly && !isChartDataLoading && ( + {medications.length === 0 && isReadOnly && !isChartDataLoading && ( Missing. Patient input must be reconciled by provider - )} */} + )} {!isReadOnly && ( diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/KnownAllergies/KnownAllergiesProviderColumn.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/KnownAllergies/KnownAllergiesProviderColumn.tsx index 5b2bfab5..fc8938c0 100644 --- a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/KnownAllergies/KnownAllergiesProviderColumn.tsx +++ b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/KnownAllergies/KnownAllergiesProviderColumn.tsx @@ -83,9 +83,9 @@ export const KnownAllergiesProviderColumn: FC = () => { )} - {/* {allergies.length === 0 && isReadOnly && !isChartDataLoading && ( + {allergies.length === 0 && isReadOnly && !isChartDataLoading && ( Missing. Patient input must be reconciled by provider - )} */} + )} {!isReadOnly && ( diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/MedicalConditions/MedicalConditionsProviderColumn.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/MedicalConditions/MedicalConditionsProviderColumn.tsx index 2886c54c..bed61d53 100644 --- a/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/MedicalConditions/MedicalConditionsProviderColumn.tsx +++ b/packages/telemed-ehr/app/src/telemed/features/appointment/MedicalHistoryTab/MedicalConditions/MedicalConditionsProviderColumn.tsx @@ -75,9 +75,9 @@ export const MedicalConditionsProviderColumn: FC = () => { )} - {/* {conditions.length === 0 && isReadOnly && !isChartDataLoading && ( + {conditions.length === 0 && isReadOnly && !isChartDataLoading && ( Missing. Patient input must be reconciled by provider - )} */} + )} {!isReadOnly && ( diff --git a/packages/telemed-ehr/zambdas/src/change-telemed-appointment-status/index.ts b/packages/telemed-ehr/zambdas/src/change-telemed-appointment-status/index.ts index fd15fcdf..100b452f 100644 --- a/packages/telemed-ehr/zambdas/src/change-telemed-appointment-status/index.ts +++ b/packages/telemed-ehr/zambdas/src/change-telemed-appointment-status/index.ts @@ -129,6 +129,5 @@ export const performEffect = async ( // console.log(9); // console.log(`Charge outcome: ${JSON.stringify(chargeOutcome)}`); // } - console.log(10); return { message: 'Appointment status successfully changed and charged issued.' }; }; From 59c3c2e49868df45db07ab4a6dd06bcb20e43534 Mon Sep 17 00:00:00 2001 From: aykhanahmadli Date: Fri, 1 Nov 2024 17:42:17 +0100 Subject: [PATCH 32/38] edit chat modal and fix for appointment side panel --- packages/ehr-utils/lib/fhir/chat.ts | 63 +++++ packages/ehr-utils/lib/fhir/index.ts | 4 + packages/ehr-utils/lib/helpers/index.ts | 1 + packages/ehr-utils/lib/helpers/mappers.ts | 99 +++++++ .../src/components/AppointmentTableRow.tsx | 2 + .../telemed-ehr/app/src/constants/index.ts | 5 + .../app/src/features/chat/ChatModal.tsx | 261 +++++++++++------- .../appointment/AppointmentSidePanel.tsx | 24 +- .../state/appointment/appointment.queries.ts | 55 ++++ .../app/src/telemed/utils/index.ts | 1 + .../app/src/telemed/utils/removeHtmlTags.ts | 4 + 11 files changed, 414 insertions(+), 105 deletions(-) create mode 100644 packages/ehr-utils/lib/helpers/mappers.ts create mode 100644 packages/telemed-ehr/app/src/telemed/utils/removeHtmlTags.ts diff --git a/packages/ehr-utils/lib/fhir/chat.ts b/packages/ehr-utils/lib/fhir/chat.ts index 305990d6..8e7cc7ee 100644 --- a/packages/ehr-utils/lib/fhir/chat.ts +++ b/packages/ehr-utils/lib/fhir/chat.ts @@ -4,6 +4,9 @@ import { OTTEHR_PATIENT_MESSAGE_CODE, OTTEHR_PATIENT_MESSAGE_SYSTEM, OttehrPatientMessageStatus, + RelatedPersonMaps, + SMSModel, + SMSRecipient, } from '../types'; import { BatchInputRequest, FhirClient, User } from '@zapehr/sdk'; import { DateTime } from 'luxon'; @@ -41,6 +44,52 @@ export const getChatContainsUnreadMessages = (chat: Communication[]): boolean => return readStatusList.find((stat) => stat === false) !== undefined; }; +export const getCommunicationsAndSenders = async ( + fhirClient: FhirClient, + uniqueNumbers: string[], +): Promise<(Communication | RelatedPerson)[]> => { + return await fhirClient.searchResources({ + resourceType: 'Communication', + searchParams: [ + { name: 'medium', value: ZAP_SMS_MEDIUM_CODE }, + { name: 'sender:RelatedPerson.telecom', value: uniqueNumbers.join(',') }, + { name: '_include', value: 'Communication:sender:RelatedPerson' }, + ], + }); +}; + +export function getUniquePhonesNumbers(allRps: RelatedPerson[]): string[] { + const uniquePhoneNumbers: string[] = []; + + allRps.forEach((rp) => { + const phone = getSMSNumberForIndividual(rp); + if (phone && !uniquePhoneNumbers.includes(phone)) uniquePhoneNumbers.push(phone); + }); + + return uniquePhoneNumbers; +} + +export const createSmsModel = (patientId: string, allRelatedPersonMaps: RelatedPersonMaps): SMSModel | undefined => { + let rps: RelatedPerson[] = []; + try { + rps = allRelatedPersonMaps.rpsToPatientIdMap[patientId]; + const recipients = filterValidRecipients(rps); + if (recipients.length) { + const allComs = recipients.flatMap((recip) => { + return allRelatedPersonMaps.commsToRpRefMap[recip.relatedPersonId] ?? []; + }); + return { + hasUnreadMessages: getChatContainsUnreadMessages(allComs), + recipients, + }; + } + } catch (e) { + console.log('error building sms model: ', e); + console.log('related persons value prior to error: ', rps); + } + return undefined; +}; + export interface MakeOttehrMessageReadStatusInput { userId: string; timeRead: string; @@ -148,3 +197,17 @@ export const initialsFromName = (name: string): string => { }); return parts.join(''); }; + +function filterValidRecipients(relatedPersons: RelatedPerson[]): SMSRecipient[] { + // some slack alerts suggest this could be undefined, but that would mean there are patients with no RP + // or some bug preventing rp from being returned with the query + return relatedPersons + .map((rp) => { + return { + recipientResourceUri: rp.id ? `RelatedPerson/${rp.id}` : undefined, + smsNumber: getSMSNumberForIndividual(rp), + relatedPersonId: rp.id, + }; + }) + .filter((rec) => rec.recipientResourceUri !== undefined && rec.smsNumber !== undefined) as SMSRecipient[]; +} diff --git a/packages/ehr-utils/lib/fhir/index.ts b/packages/ehr-utils/lib/fhir/index.ts index 0b92ff3c..d0409795 100644 --- a/packages/ehr-utils/lib/fhir/index.ts +++ b/packages/ehr-utils/lib/fhir/index.ts @@ -27,6 +27,10 @@ export const getFullestAvailableName = ( return undefined; }; +export function filterResources(allResources: Resource[], resourceType: string): Resource[] { + return allResources.filter((res) => res.resourceType === resourceType && res.id); +} + export const getPatchOperationForNewMetaTag = (resource: Resource, newTag: Coding): Operation => { if (resource.meta == undefined) { return { diff --git a/packages/ehr-utils/lib/helpers/index.ts b/packages/ehr-utils/lib/helpers/index.ts index d7b741c5..5f0a8718 100644 --- a/packages/ehr-utils/lib/helpers/index.ts +++ b/packages/ehr-utils/lib/helpers/index.ts @@ -3,3 +3,4 @@ export * from './formatPhoneNumber'; export * from './paperwork'; export * from './practitioner'; export * from './telemed-appointment.helper'; +export * from './mappers'; diff --git a/packages/ehr-utils/lib/helpers/mappers.ts b/packages/ehr-utils/lib/helpers/mappers.ts new file mode 100644 index 00000000..b4a5b3d0 --- /dev/null +++ b/packages/ehr-utils/lib/helpers/mappers.ts @@ -0,0 +1,99 @@ +import { FhirClient } from '@zapehr/sdk'; +import { Communication, RelatedPerson, Resource } from 'fhir/r4'; +import { removePrefix } from '.'; +import { + filterResources, + getCommunicationsAndSenders, + getSMSNumberForIndividual, + getUniquePhonesNumbers, +} from '../fhir'; +import { RelatedPersonMaps } from '../types'; + +export const relatedPersonAndCommunicationMaps = async ( + fhirClient: FhirClient, + inputResources: Resource[], +): Promise => { + const allRelatedPersons = filterResources(inputResources, 'RelatedPerson') as RelatedPerson[]; + const rpsPhoneNumbers = getUniquePhonesNumbers(allRelatedPersons); + const rpsToPatientIdMap = mapRelatedPersonToPatientId(allRelatedPersons); + const rpToIdMap = mapRelatedPersonToId(allRelatedPersons); + const foundResources = await getCommunicationsAndSenders(fhirClient, rpsPhoneNumbers); + const foundRelatedPersons = filterResources(foundResources, 'RelatedPerson') as RelatedPerson[]; + Object.assign(rpToIdMap, mapRelatedPersonToId(foundRelatedPersons)); + rpsPhoneNumbers.concat(getUniquePhonesNumbers(foundRelatedPersons)); // do better here + const rpsRefsToPhoneNumberMap = mapRelatedPersonsRefsToPhoneNumber(foundRelatedPersons); + + const foundCommunications = filterResources(foundResources, 'Communication') as Communication[]; + const commsToRpRefMap = mapCommunicationsToRelatedPersonRef(foundCommunications, rpToIdMap, rpsRefsToPhoneNumberMap); + + return { + rpsToPatientIdMap, + commsToRpRefMap, + }; +}; + +function mapRelatedPersonToPatientId(allRps: RelatedPerson[]): Record { + const rpsToPatientIdMap: Record = {}; + + allRps.forEach((rp) => { + const patientId = removePrefix('Patient/', rp.patient.reference || ''); + if (patientId) { + if (rpsToPatientIdMap[patientId]) rpsToPatientIdMap[patientId].push(rp); + else rpsToPatientIdMap[patientId] = [rp]; + } + }); + + return rpsToPatientIdMap; +} + +function mapRelatedPersonToId(allRps: RelatedPerson[]): Record { + const rpToIdMap: Record = {}; + + allRps.forEach((rp) => { + rpToIdMap['RelatedPerson/' + rp.id] = rp; + }); + + return rpToIdMap; +} + +function mapRelatedPersonsRefsToPhoneNumber(allRps: RelatedPerson[]): Record { + const relatedPersonRefToPhoneNumber: Record = {}; + + allRps.forEach((rp) => { + const rpRef = `RelatedPerson/${rp.id}`; + const pn = getSMSNumberForIndividual(rp as RelatedPerson); + if (pn) { + if (relatedPersonRefToPhoneNumber[pn]) relatedPersonRefToPhoneNumber[pn].push(rpRef); + else relatedPersonRefToPhoneNumber[pn] = [rpRef]; + } + }); + return relatedPersonRefToPhoneNumber; +} + +function mapCommunicationsToRelatedPersonRef( + allCommunications: Communication[], + rpToIdMap: Record, + rpsRefsToPhoneNumberMap: Record, +): Record { + const commsToRpRefMap: Record = {}; + + allCommunications.forEach((comm) => { + const communication = comm as Communication; + const rpRef = communication.sender?.reference; + if (rpRef) { + const senderResource = rpToIdMap[rpRef]; + if (senderResource) { + const smsNumber = getSMSNumberForIndividual(senderResource); + if (smsNumber) { + const allRPsWithThisNumber = rpsRefsToPhoneNumberMap[smsNumber]; + allRPsWithThisNumber.forEach((rpRef) => { + if (commsToRpRefMap[rpRef]) commsToRpRefMap[rpRef].push(communication); + else commsToRpRefMap[rpRef] = [communication]; + }); + } + } + } + }); + + return commsToRpRefMap; +} diff --git a/packages/telemed-ehr/app/src/components/AppointmentTableRow.tsx b/packages/telemed-ehr/app/src/components/AppointmentTableRow.tsx index a0f81ecf..aa41e56f 100644 --- a/packages/telemed-ehr/app/src/components/AppointmentTableRow.tsx +++ b/packages/telemed-ehr/app/src/components/AppointmentTableRow.tsx @@ -45,6 +45,7 @@ import { ApptTab } from './AppointmentTabs'; import CustomChip from './CustomChip'; import { GenericToolTip, PaperworkToolTipContent } from './GenericToolTip'; import { VisitStatus, StatusLabel } from '../helpers/mappingUtils'; +import { quickTexts } from '../telemed/utils'; interface AppointmentTableProps { appointment: UCAppointmentInformation; @@ -795,6 +796,7 @@ export default function AppointmentTableRow({ currentLocation={location} onClose={() => setChatModalOpen(false)} onMarkAllRead={() => setHasUnread(false)} + quickTexts={quickTexts} /> )} diff --git a/packages/telemed-ehr/app/src/constants/index.ts b/packages/telemed-ehr/app/src/constants/index.ts index 79e67287..632be5b9 100644 --- a/packages/telemed-ehr/app/src/constants/index.ts +++ b/packages/telemed-ehr/app/src/constants/index.ts @@ -17,6 +17,11 @@ export const ReasonForVisitOptions = [ 'Eye concern', ]; +export enum LANGUAGES { + spanish = 'spanish', + english = 'english', +} + export const phoneNumberRegex = /^\d{10}$/; export const MAXIMUM_CHARACTER_LIMIT = 160; diff --git a/packages/telemed-ehr/app/src/features/chat/ChatModal.tsx b/packages/telemed-ehr/app/src/features/chat/ChatModal.tsx index 4189789f..91fa0d15 100644 --- a/packages/telemed-ehr/app/src/features/chat/ChatModal.tsx +++ b/packages/telemed-ehr/app/src/features/chat/ChatModal.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/prop-types */ import { Close } from '@mui/icons-material'; import SendIcon from '@mui/icons-material/Send'; import { LoadingButton } from '@mui/lab'; @@ -13,29 +14,20 @@ import { Modal, TextField, useTheme, + ToggleButtonGroup, + ToggleButton, + Box, } from '@mui/material'; import Typography from '@mui/material/Typography'; -import { Location } from 'fhir/r4'; +import { Location, Patient } from 'fhir/r4'; import { DateTime } from 'luxon'; import { ChangeEvent, ReactElement, UIEvent, memo, useEffect, useMemo, useState } from 'react'; -import { - AppointmentMessaging, - ConversationMessage, - initialsFromName, - markAllMessagesRead, - standardizePhoneNumber, -} from 'ehr-utils'; +import { LANGUAGES } from '../../constants'; +import { AppointmentMessaging, ConversationMessage, initialsFromName, markAllMessagesRead } from 'ehr-utils'; import { useApiClients } from '../../hooks/useAppClients'; import useOttehrUser, { OttehrUser } from '../../hooks/useOttehrUser'; import { useFetchChatMessagesQuery, useSendMessagesMutation } from './chat.queries'; -import { TIMEZONE_EXTENSION_URL } from '../../constants'; - -interface PatientParticipant { - firstName: string; - name: string; - initials: string; - appointmentId: string; -} +import { getPatientName, removeHtmlTags } from '../../telemed/utils'; function scrollToBottomOfChat(): void { // this helps with the scroll working, @@ -73,18 +65,21 @@ const makePendingSentMessage = (text: string, timezone: string, sender: OttehrUs }; }; -// eslint-disable-next-line react/display-name const ChatModal = memo( ({ appointment, + patient, currentLocation, onClose, onMarkAllRead, + quickTexts, }: { appointment: AppointmentMessaging; + patient?: Patient; currentLocation?: Location; onClose: () => void; onMarkAllRead: () => void; + quickTexts: { [key in LANGUAGES]: string }[] | string[]; }): ReactElement => { const theme = useTheme(); const { fhirClient } = useApiClients(); @@ -92,20 +87,23 @@ const ChatModal = memo( const [_messages, _setMessages] = useState([]); const [messageText, setMessageText] = useState(''); const [quickTextsOpen, setQuickTextsOpen] = useState(false); + const [language, setLanguage] = useState(LANGUAGES.english); const [pendingMessageSend, setPendingMessageSend] = useState(); - const model = appointment?.smsModel; + const { patient: patientFromAppointment, smsModel: model } = appointment; const timezone = useMemo(() => { // const state = currentLocation?.address?.state; return ( currentLocation?.extension?.find((ext) => { - return ext.url === TIMEZONE_EXTENSION_URL; + return ext.url === 'http://hl7.org/fhir/StructureDefinition/timezone'; })?.valueString ?? 'America/New_York' ); }, [currentLocation]); - const patientParticipant = getPatientParticipantFromAppointment(appointment); + let patientName; + if (patientFromAppointment?.firstName || patientFromAppointment?.lastName) + patientName = `${patientFromAppointment?.firstName || ''} ${patientFromAppointment?.lastName || ''}`; const numbersToSendTo = useMemo(() => { const numbers = (model?.recipients ?? []).map((recip) => recip.smsNumber); @@ -159,13 +157,7 @@ const ChatModal = memo( })?.id; }, [messages]); - useEffect(() => { - if (messages.length) { - scrollToBottomOfChat(); - } - }, [messages]); - - const hasUnreadMessages = appointment?.smsModel?.hasUnreadMessages; + const hasUnreadMessages = model?.hasUnreadMessages; const markAllRead = async (): Promise => { if (currentUser && fhirClient && hasUnreadMessages) { @@ -218,24 +210,6 @@ const ChatModal = memo( p: '8px 16px', }; - const locationInStorage = localStorage.getItem('selectedLocation'); - let officePhoneNumber: string | undefined; - if (locationInStorage) { - const location: Location | undefined = JSON.parse(locationInStorage); - officePhoneNumber = location?.telecom?.find((telecomTemp) => telecomTemp.system === 'phone')?.value; - officePhoneNumber = standardizePhoneNumber(officePhoneNumber); - } - - const VITE_APP_QRS_URL = import.meta.env.VITE_APP_QRS_URL; - - const quickTexts = [ - // todo need to make url dynamic or pull from location - `Please complete the paperwork and sign consent forms to avoid a delay in check-in. For ${patientParticipant?.firstName}, click here: ${VITE_APP_QRS_URL}/visit/${patientParticipant?.appointmentId}`, - 'We are now ready to check you in. Please head to the front desk to complete the process.', - 'We are ready for the patient to be seen, please enter the facility.', - `Ottehr is trying to get ahold of you. Please call us at ${officePhoneNumber} or respond to this text message.`, - ]; - const selectQuickText = (text: string): void => { setMessageText(text); setQuickTextsOpen(false); @@ -246,6 +220,46 @@ const ChatModal = memo( onClose(); }; + const hasQuickTextTranslations = (quickTexts: any): quickTexts is { [key in LANGUAGES]: string }[] => { + return typeof quickTexts[0] === 'object'; + }; + + /* + https://github.com/masslight/pmp-ehr/issues/1984 + prior to memoizing this list of messages, the component was rerendering + the entire message list with each key stroke. + + we might consider other scale-related changes in the future (limiting the messages that get + displayed by total number or some cut off date, with option to look back further, virtualizing the list so + that only immediately visible messages are rendered, etc.). for now this fairly simple change eliminates + the bad behavior. + */ + const MessageBodies: JSX.Element[] = useMemo(() => { + if (pendingMessageSend === undefined && isMessagesFetching) { + return []; + } + return messages.map((message) => { + const contentKey = message.resolvedId ?? message.id; + const isPending = message.id === pendingMessageSend?.id || message.id === pendingMessageSend?.resolvedId; + return ( + + ); + }); + }, [isMessagesFetching, messages, newMessagesStartId, pendingMessageSend]); + + useEffect(() => { + if (MessageBodies.length) { + scrollToBottomOfChat(); + } + }, [MessageBodies.length]); + return ( - Chat with {patientParticipant?.name} + Chat with {patientName || getPatientName(patient?.name).firstLastName} ) : ( - messages.map((message) => { - const contentKey = message.resolvedId ?? message.id; - const isPending = - message.id === pendingMessageSend?.id || message.id === pendingMessageSend?.resolvedId; - return ( - - ); - }) + MessageBodies )} @@ -375,30 +375,86 @@ const ChatModal = memo( }} > - - - Quick texts - + + + + Quick texts + + {hasQuickTextTranslations(quickTexts) && ( + , value: LANGUAGES) => { + setLanguage(value); + }} + > + + English + + + Spanish + + + )} + Select the text to populate the message to the patient - {quickTexts.map((text) => { - return ( - selectQuickText(text)} - > - - {text} - - - ); - })} + {hasQuickTextTranslations(quickTexts) + ? quickTexts + .filter((text) => text[language]) + .map((text) => ( + selectQuickText(text[language])} + > + {text[language]} + + )) + : quickTexts.map((text) => { + return ( + selectQuickText(removeHtmlTags(text))} + > + {parseTextToJSX(text)} + + ); + })} @@ -409,6 +465,8 @@ const ChatModal = memo( }, ); +ChatModal.displayName = 'ChatModal'; + export default ChatModal; interface MessageBodyProps { @@ -418,12 +476,14 @@ interface MessageBodyProps { contentKey: string; showDaySent: boolean; } -const MessageBody: React.FC = (props: MessageBodyProps) => { + +const MessageBody: React.FC = (props) => { const { isPending, message, contentKey, hasNewMessageLine, showDaySent } = props; const theme = useTheme(); const authorInitials = useMemo(() => { return initialsFromName(message.sender); }, [message.sender]); + return ( {hasNewMessageLine && ( @@ -455,9 +515,9 @@ const MessageBody: React.FC = (props: MessageBodyProps) => { )} = (props: MessageBodyProps) => { > 3 ? '12px' : '16px', marginInlineEnd: '8px', @@ -513,19 +574,23 @@ const MessageBody: React.FC = (props: MessageBodyProps) => { ); }; -const getPatientParticipantFromAppointment = (appointment: AppointmentMessaging): PatientParticipant | undefined => { - if (!appointment.patient) { - return undefined; - } - const firstName = appointment.patient.firstName || ''; - const lastName = appointment.patient.lastName || ''; - const initials = `${firstName?.charAt(0) || ''} ${lastName?.charAt(0) || ''}`; - const name = `${firstName} ${lastName}`; +function parseTextToJSX(text: string): JSX.Element[] { + // Split the string at the custom HTML tag + const parts = text.split(/()/g).filter(Boolean); + + return parts.map((part, index) => { + if (part.startsWith(']*>(.*?)<\/phone>/); + if (match) { + return ( + + {match[1]} + + ); + } + } - return { - firstName, - name, - initials, - appointmentId: appointment.id, - }; -}; + return {part}; + }); +} diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentSidePanel.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentSidePanel.tsx index cdcf4408..464e82a8 100644 --- a/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentSidePanel.tsx +++ b/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentSidePanel.tsx @@ -36,8 +36,8 @@ import CancelVisitDialog from '../../components/CancelVisitDialog'; import EditPatientDialog from '../../components/EditPatientDialog'; import InviteParticipant from '../../components/InviteParticipant'; import { useGetAppointmentAccessibility } from '../../hooks'; -import { useAppointmentStore } from '../../state'; -import { getAppointmentStatusChip, getPatientName } from '../../utils'; +import { useAppointmentStore, useGetTelemedAppointmentWithSMSModel } from '../../state'; +import { getAppointmentStatusChip, getPatientName, quickTexts } from '../../utils'; // import { ERX } from './ERX'; import { PastVisits } from './PastVisits'; import { addSpacesAfterCommas } from '../../../helpers/formatString'; @@ -101,10 +101,18 @@ export const AppointmentSidePanel: FC = ({ appointmen // appointmentAccessibility.isEncounterAssignedToCurrentPractitioner && isCancellableStatus; - const [hasUnread, setHasUnread] = useState( - (appointment as unknown as UCAppointmentInformation)?.smsModel?.hasUnreadMessages || false, + const { data: appointmentMessaging, isFetching } = useGetTelemedAppointmentWithSMSModel( + { + appointmentId: appointment?.id, + patientId: patient?.id, + }, + (data) => { + setHasUnread(data.smsModel?.hasUnreadMessages || false); + }, ); + const [hasUnread, setHasUnread] = useState(appointmentMessaging?.smsModel?.hasUnreadMessages || false); + if (!patient) { return null; } @@ -261,6 +269,7 @@ export const AppointmentSidePanel: FC = ({ appointmen ) } onClick={() => setChatModalOpen(true)} + loading={isFetching && !appointmentMessaging} />