diff --git a/package.json b/package.json index 70454218..f026628f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ottehr", - "version": "0.14.0", + "version": "0.15.0", "private": true, "scripts": { "test": "pnpm recursive run test", 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 c55b8247..d0409795 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'; @@ -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 { @@ -70,6 +74,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/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/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/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/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/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/ehr-utils/package.json b/packages/ehr-utils/package.json index 2f20394b..f47f6d41 100644 --- a/packages/ehr-utils/package.json +++ b/packages/ehr-utils/package.json @@ -1,7 +1,7 @@ { "name": "ehr-utils", "private": true, - "version": "0.14.0", + "version": "0.15.0", "main": "lib/main.ts", "types": "lib/main.ts", "scripts": { diff --git a/packages/ottehr-components/package.json b/packages/ottehr-components/package.json index cc6e2527..c45c2d43 100644 --- a/packages/ottehr-components/package.json +++ b/packages/ottehr-components/package.json @@ -1,7 +1,7 @@ { "name": "ottehr-components", "private": true, - "version": "0.14.0", + "version": "0.15.0", "main": "lib/main.ts", "types": "lib/main.ts", "scripts": { diff --git a/packages/telemed-ehr/app/env/.env.local-template b/packages/telemed-ehr/app/env/.env.local-template index bdd38c5b..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 @@ -30,4 +31,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/package.json b/packages/telemed-ehr/app/package.json index 9e7c1bae..36a3be19 100644 --- a/packages/telemed-ehr/app/package.json +++ b/packages/telemed-ehr/app/package.json @@ -1,6 +1,6 @@ { "name": "telemed-ehr-app", - "version": "0.14.0", + "version": "0.15.0", "private": true, "browserslist": { "production": [ @@ -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", diff --git a/packages/telemed-ehr/app/src/App.tsx b/packages/telemed-ehr/app/src/App.tsx index ab264686..da3da7e3 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/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/AppointmentStatusSwitcher.tsx b/packages/telemed-ehr/app/src/components/AppointmentStatusSwitcher.tsx new file mode 100644 index 00000000..be31fdf6 --- /dev/null +++ b/packages/telemed-ehr/app/src/components/AppointmentStatusSwitcher.tsx @@ -0,0 +1,126 @@ +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, mapVisitStatusToFhirAppointmentStatus } from '../helpers/mappingUtils'; +import { Operation } from 'fast-json-patch'; +import { getPatchBinary, getStatusFromExtension } from 'ehr-utils'; +import { VisitStatus, STATI } from '../helpers/mappingUtils'; +import { useApiClients } from '../hooks/useAppClients'; +import { getAppointmentStatusChip } from './AppointmentTableRow'; +import { Box } from '@mui/system'; + +const statuses = STATI; + +export const switchStatus = async ( + fhirClient: FhirClient | undefined, + appointment: Appointment, + encounter: Encounter, + status: VisitStatus, +): Promise => { + if (status === 'unknown') { + throw new Error(`Invalid status: ${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 statusOperations = getPatchOperationsToUpdateVisitStatus(appointment, status); + + const patchOp: Operation = { + op: 'replace', + path: '/status', + value: mapVisitStatusToFhirAppointmentStatus(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/components/AppointmentTableRow.tsx b/packages/telemed-ehr/app/src/components/AppointmentTableRow.tsx index 98915dd0..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; @@ -133,7 +134,7 @@ export const CHIP_STATUS_MAP: { primary: '#684e5d', }, }, - 'provider-ready': { + 'ready for provider': { background: { primary: '#EEEEEE', secondary: '#444444', @@ -150,7 +151,7 @@ export const CHIP_STATUS_MAP: { primary: '#6F6D1A', }, }, - discharge: { + 'ready for discharge': { background: { primary: '#B2EBF2', }, @@ -158,7 +159,7 @@ export const CHIP_STATUS_MAP: { primary: '#006064', }, }, - 'checked-out': { + 'checked out': { background: { primary: '#FFFFFF', }, @@ -174,7 +175,7 @@ export const CHIP_STATUS_MAP: { primary: '#B71C1C', }, }, - 'no-show': { + 'no show': { background: { primary: '#DFE5E9', }, @@ -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/components/EmployeeInformationForm.tsx b/packages/telemed-ehr/app/src/components/EmployeeInformationForm.tsx index b3ae6fe4..7c33ca0e 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); @@ -65,13 +65,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 +117,15 @@ 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, + qualification: false, + state: false, + duplicateLicense: false, + }); const [newLicenseState, setNewLicenseState] = useState(undefined); const [newLicenseCode, setNewLicenseCode] = useState(undefined); @@ -132,11 +138,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 +153,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]; @@ -199,7 +215,6 @@ export default function EmployeeInformationForm({ setLoading(true); - // Update the user try { await updateUser(zambdaClient, { userId: user.id, @@ -218,25 +233,32 @@ export default function EmployeeInformationForm({ } }; - const updateLicenses = async (licenses: PractitionerLicense[]): Promise => { - if (!zambdaClient) { - throw new Error('Zambda Client not found'); - } + const handleAddLicense = async (): Promise => { + setErrors((prev) => ({ ...prev, state: false, qualification: false, duplicateLicense: false })); - 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, + 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 ? ( ) : ( @@ -310,7 +332,7 @@ export default function EmployeeInformationForm({ } - disabled={!isActive} + disabled={!isActive || !currentUser?.hasRole([RoleType.Administrator])} label={roleEntry.label} sx={{ '.MuiFormControlLabel-asterisk': { display: 'none' } }} /> @@ -372,7 +394,6 @@ export default function EmployeeInformationForm({ sx={{ ...theme.typography.h4, color: theme.palette.primary.dark, - mb: 2, mt: 3, fontWeight: '600 !important', }} @@ -394,21 +415,156 @@ 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); + }} + /> + + + { + 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.`} + )} + + + +
+
+ + )} )} + {/* Error on submit if request fails */} + {errors.submit && ( + {`Failed to update user. Please try again.`} + )} + {/* 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)} - /> - - - - - - - -
- - )} ); 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)} + + )} diff --git a/packages/telemed-intake/zambdas/package.json b/packages/telemed-intake/zambdas/package.json index db88a93d..301d2024 100644 --- a/packages/telemed-intake/zambdas/package.json +++ b/packages/telemed-intake/zambdas/package.json @@ -1,6 +1,6 @@ { "name": "telemed-intake-zambdas", - "version": "0.14.0", + "version": "0.15.0", "private": true, "scripts": { "start": "npm run start:local", 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 ${ 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..9e3ae942 100644 --- a/packages/telemed-intake/zambdas/src/appointment/create-appointment/index.ts +++ b/packages/telemed-intake/zambdas/src/appointment/create-appointment/index.ts @@ -104,7 +104,6 @@ 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 +201,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/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]; 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; diff --git a/packages/utils/package.json b/packages/utils/package.json index 712f2a7e..4bd7e9bc 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,7 +1,7 @@ { "name": "ottehr-utils", "private": true, - "version": "0.14.0", + "version": "0.15.0", "main": "lib/main.ts", "types": "lib/main.ts", "scripts": {