diff --git a/packages/ehr-utils/lib/fhir/constants.ts b/packages/ehr-utils/lib/fhir/constants.ts
index 345824b55..9ef5e5699 100644
--- a/packages/ehr-utils/lib/fhir/constants.ts
+++ b/packages/ehr-utils/lib/fhir/constants.ts
@@ -7,4 +7,9 @@ export const FHIR_EXTENSION = {
url: `${PRIVATE_EXTENSION_BASE_URL}/date-of-birth-not-confirmed`,
},
},
+ Patient: {
+ weight: {
+ url: `${PRIVATE_EXTENSION_BASE_URL}/weight`,
+ },
+ },
};
diff --git a/packages/ehr-utils/lib/fhir/index.ts b/packages/ehr-utils/lib/fhir/index.ts
index c55b82474..52681e609 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,58 @@ 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/practitioner.ts b/packages/ehr-utils/lib/helpers/practitioner.ts
index b876aca95..e0a87d20d 100644
--- a/packages/ehr-utils/lib/helpers/practitioner.ts
+++ b/packages/ehr-utils/lib/helpers/practitioner.ts
@@ -1,5 +1,6 @@
-import { Encounter, Practitioner } from 'fhir/r4';
+import { Encounter, Identifier, Practitioner } from 'fhir/r4';
import {
+ FHIR_IDENTIFIER_NPI,
PRACTITIONER_QUALIFICATION_CODE_SYSTEM,
PRACTITIONER_QUALIFICATION_EXTENSION_URL,
PRACTITIONER_QUALIFICATION_STATE_SYSTEM,
@@ -54,3 +55,7 @@ export const checkIsEncounterForPractitioner = (encounter: Encounter, practition
return !!practitioner && !!encounterPractitioner && practitionerId === encounterPractitionerId;
};
+
+export const getPractitionerNPIIdentitifier = (practitioner: Practitioner | undefined): Identifier | undefined => {
+ return practitioner?.identifier?.find((existIdentifier) => existIdentifier.system === FHIR_IDENTIFIER_NPI);
+};
diff --git a/packages/ehr-utils/lib/types/api/chart-data/chart-data.types.ts b/packages/ehr-utils/lib/types/api/chart-data/chart-data.types.ts
index ee4198547..8230d0c89 100644
--- a/packages/ehr-utils/lib/types/api/chart-data/chart-data.types.ts
+++ b/packages/ehr-utils/lib/types/api/chart-data/chart-data.types.ts
@@ -5,6 +5,7 @@ export interface ChartDataFields {
ros?: FreeTextNoteDTO;
conditions?: MedicalConditionDTO[];
medications?: MedicationDTO[];
+ prescribedMedications?: PrescribedMedicationDTO[];
allergies?: AllergyDTO[];
procedures?: ProcedureDTO[];
proceduresNote?: FreeTextNoteDTO;
@@ -41,6 +42,11 @@ export interface MedicationDTO extends SaveableDTO {
id?: string;
}
+export interface PrescribedMedicationDTO extends SaveableDTO {
+ name?: string;
+ instructions?: string;
+}
+
export interface ProcedureDTO extends SaveableDTO {
name?: string;
}
diff --git a/packages/ehr-utils/lib/types/api/index.ts b/packages/ehr-utils/lib/types/api/index.ts
index 3eb81f4cd..828cad28d 100644
--- a/packages/ehr-utils/lib/types/api/index.ts
+++ b/packages/ehr-utils/lib/types/api/index.ts
@@ -2,5 +2,6 @@ export * from './change-telemed-appointment-status';
export * from './chart-data';
export * from './get-telemed-appointments';
export * from './icd-search';
+export * from './sync-user';
export * from './init-telemed-session';
export * from './patient-instructions';
diff --git a/packages/ehr-utils/lib/types/api/sync-user/index.ts b/packages/ehr-utils/lib/types/api/sync-user/index.ts
new file mode 100644
index 000000000..7b398fb61
--- /dev/null
+++ b/packages/ehr-utils/lib/types/api/sync-user/index.ts
@@ -0,0 +1 @@
+export * from './sync-user.types';
diff --git a/packages/ehr-utils/lib/types/api/sync-user/sync-user.types.ts b/packages/ehr-utils/lib/types/api/sync-user/sync-user.types.ts
new file mode 100644
index 000000000..e574f495f
--- /dev/null
+++ b/packages/ehr-utils/lib/types/api/sync-user/sync-user.types.ts
@@ -0,0 +1,4 @@
+export interface SyncUserResponse {
+ message: string;
+ updated: boolean;
+}
diff --git a/packages/ehr-utils/lib/types/constants.ts b/packages/ehr-utils/lib/types/constants.ts
index 9ecb6a483..1c7e45169 100644
--- a/packages/ehr-utils/lib/types/constants.ts
+++ b/packages/ehr-utils/lib/types/constants.ts
@@ -1,5 +1,5 @@
export const TELEMED_VIDEO_ROOM_CODE = 'chime-video-meetings';
-export const PHOTON_PATIENT_IDENTIFIER_SYSTEM = 'http://api.zapehr.com/photon-patient-id';
+export const ERX_PATIENT_IDENTIFIER_SYSTEM = 'http://api.zapehr.com/photon-patient-id';
export const PRACTITIONER_QUALIFICATION_EXTENSION_URL =
'http://hl7.org/fhir/us/davinci-pdex-plan-net/StructureDefinition/practitioner-qualification';
diff --git a/packages/ehr-utils/lib/types/practitioner.types.ts b/packages/ehr-utils/lib/types/practitioner.types.ts
index dfdbc1369..af0260277 100644
--- a/packages/ehr-utils/lib/types/practitioner.types.ts
+++ b/packages/ehr-utils/lib/types/practitioner.types.ts
@@ -281,3 +281,6 @@ export interface PractitionerLicense {
code: PractitionerQualificationCode;
active: boolean;
}
+
+export const ERX_PRESCRIBER_SYSTEM_URL = 'http://api.zapehr.com/photon-prescriber-id';
+export const ERX_PRACTITIONER_ENROLLED = 'http://api.zapehr.com/photon-practitioner-enrolled';
diff --git a/packages/telemed-ehr/app/env/.env.local-template b/packages/telemed-ehr/app/env/.env.local-template
index bc9e23acc..b9231c629 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_SYNC_USER_ZAMBDA_ID=sync-user
VITE_APP_DEACTIVATE_USER_ZAMBDA_ID=deactivate-user
VITE_APP_GET_EMPLOYEES_ZAMBDA_ID=get-employees
VITE_APP_GET_TELEMED_APPOINTMENTS_ZAMBDA_ID=get-telemed-appointments
@@ -28,4 +29,6 @@ VITE_APP_CHANGE_TELEMED_APPOINTMENT_STATUS_ZAMBDA_ID=change-telemed-appointment-
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-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=
diff --git a/packages/telemed-ehr/app/index.html b/packages/telemed-ehr/app/index.html
index 62bda6bf9..7cd5c13ab 100644
--- a/packages/telemed-ehr/app/index.html
+++ b/packages/telemed-ehr/app/index.html
@@ -2,11 +2,11 @@
-
+
-
+
Ottehr EHR
diff --git a/packages/telemed-ehr/app/package.json b/packages/telemed-ehr/app/package.json
index 5b4c2bd04..6c3c40c8e 100644
--- a/packages/telemed-ehr/app/package.json
+++ b/packages/telemed-ehr/app/package.json
@@ -37,12 +37,13 @@
"@mui/x-data-grid-pro": "^6.3.0",
"@mui/x-date-pickers": "^5.0.20",
"@mui/x-date-pickers-pro": "^5.0.20",
- "@photonhealth/elements": "^0.9.1-rc.1",
+ "@photonhealth/elements": "^0.12.2",
"@twilio/conversations": "^2.4.1",
"@zapehr/sdk": "1.0.15",
"amazon-chime-sdk-component-library-react": "^3.7.0",
"amazon-chime-sdk-js": "^3.20.0",
"chart.js": "^4.4.1",
+ "notistack": "^3.0.1",
"fast-json-patch": "^3.1.1",
"react-chartjs-2": "^5.2.0",
"react-draggable": "^4.4.6",
diff --git a/packages/telemed-ehr/app/public/manifest.json b/packages/telemed-ehr/app/public/manifest.json
index 92f769d36..d85ef579d 100644
--- a/packages/telemed-ehr/app/public/manifest.json
+++ b/packages/telemed-ehr/app/public/manifest.json
@@ -3,8 +3,8 @@
"name": "Ottehr EHR",
"icons": [
{
- "src": "favicon.ico",
- "sizes": "32x32 16x16",
+ "src": "ottehr-icon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
diff --git a/packages/telemed-ehr/app/public/ottehr-icon-256.png b/packages/telemed-ehr/app/public/ottehr-icon-256.png
new file mode 100644
index 000000000..dbf10a9bc
Binary files /dev/null and b/packages/telemed-ehr/app/public/ottehr-icon-256.png differ
diff --git a/packages/telemed-ehr/app/public/ottehr-icon.ico b/packages/telemed-ehr/app/public/ottehr-icon.ico
new file mode 100644
index 000000000..b82190225
Binary files /dev/null and b/packages/telemed-ehr/app/public/ottehr-icon.ico differ
diff --git a/packages/telemed-ehr/app/src/App.tsx b/packages/telemed-ehr/app/src/App.tsx
index c5c1c5253..a6101c565 100644
--- a/packages/telemed-ehr/app/src/App.tsx
+++ b/packages/telemed-ehr/app/src/App.tsx
@@ -8,7 +8,7 @@ import { LoadingScreen } from './components/LoadingScreen';
import Navbar from './components/navigation/Navbar';
import { ProtectedRoute } from './components/routing/ProtectedRoute';
import { useApiClients } from './hooks/useAppClients';
-import useOttehrUser from './hooks/useOttehrUser';
+import useOttehrUser, { useProviderERXStateStore } from './hooks/useOttehrUser';
import AddPatient from './pages/AddPatient';
import AppointmentsPage from './pages/Appointments';
import EditEmployeePage from './pages/EditEmployee';
@@ -22,18 +22,12 @@ import { TelemedAdminPage } from './pages/TelemedAdminPage';
import { useNavStore } from './state/nav.store';
import EditInsurance from './telemed/features/telemed-admin/EditInsurance';
import EditStatePage from './telemed/features/telemed-admin/EditState';
-import { isLocalOrDevOrTestingOrTrainingEnv } from './telemed/utils/env.helper';
import { RoleType } from './types/types';
import { AppointmentPage } from './pages/AppointmentPage';
import AddSchedulePage from './pages/AddSchedulePage';
+import('@photonhealth/elements').catch(console.log);
import Version from './pages/Version';
-const enablePhoton = false && isLocalOrDevOrTestingOrTrainingEnv;
-
-if (enablePhoton) {
- import('@photonhealth/elements').catch(console.log);
-}
-
const TelemedTrackingBoardPageLazy = lazy(async () => {
const TrackingBoardPage = await import('./telemed/pages/TrackingBoardPage');
return { default: TrackingBoardPage.TrackingBoardPage };
@@ -56,6 +50,8 @@ function App(): ReactElement {
const currentUser = useOttehrUser();
const currentTab = useNavStore((state: any) => state.currentTab) || 'In Person';
+ const wasEnrolledInERX = useProviderERXStateStore((state) => state.wasEnrolledInERX);
+
const roleUnknown =
!currentUser || !currentUser.hasRole([RoleType.Administrator, RoleType.Staff, RoleType.Manager, RoleType.Provider]);
@@ -72,11 +68,12 @@ function App(): ReactElement {
- {currentUser?.hasRole([RoleType.Provider]) && enablePhoton ? (
+ {(currentUser?.hasRole([RoleType.Provider]) && currentUser.isPractitionerEnrolledInERX) ||
+ wasEnrolledInERX ? (
diff --git a/packages/telemed-ehr/app/src/hooks/useOttehrUser.tsx b/packages/telemed-ehr/app/src/hooks/useOttehrUser.tsx
index a55bba73e..bd3324ed7 100644
--- a/packages/telemed-ehr/app/src/hooks/useOttehrUser.tsx
+++ b/packages/telemed-ehr/app/src/hooks/useOttehrUser.tsx
@@ -2,10 +2,21 @@ import { useAuth0 } from '@auth0/auth0-react';
import { ClientConfig, FhirClient } from '@zapehr/sdk';
import { Practitioner } from 'fhir/r4';
import { DateTime, Duration } from 'luxon';
-import { useEffect, useMemo } from 'react';
-import { useQuery } from 'react-query';
-import { User, getFullestAvailableName, getPatchOperationForNewMetaTag, initialsFromName } from 'ehr-utils';
+import { Operation } from 'fast-json-patch';
+import { useCallback, useEffect, useMemo } from 'react';
+import { useMutation, useQuery, useQueryClient } from 'react-query';
+import {
+ ERX_PRACTITIONER_ENROLLED,
+ ERX_PRESCRIBER_SYSTEM_URL,
+ SyncUserResponse,
+ User,
+ getFullestAvailableName,
+ getPatchOperationToUpdateExtension,
+ getPractitionerNPIIdentitifier,
+ initialsFromName,
+} from 'ehr-utils';
import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
import { getUser } from '../api/api';
import { useZapEHRAPIClient } from '../telemed/hooks/useZapEHRAPIClient';
import { RoleType } from '../types/types';
@@ -17,6 +28,8 @@ export interface OttehrUser extends User {
userInitials: string;
lastLogin: string | undefined;
hasRole: (role: RoleType[]) => boolean;
+ isERXPrescriber?: boolean;
+ isPractitionerEnrolledInERX?: boolean;
}
interface OttehrUserState {
@@ -32,17 +45,67 @@ enum LoadingState {
idle,
}
-// extracting it here, cause even if we use store - it will still initiate requests as much as we have usages of this hook,
-// so just to use this var as a synchronization mechanism - lifted it here
+export const useProviderERXStateStore = create<{
+ wasEnrolledInERX?: boolean;
+}>()(persist(() => ({}), { name: 'ottehr-ehr-provider-erx-store' }));
let _profileLoadingState = LoadingState.initial;
-let _lastLoginUpdating = LoadingState.initial;
let _userLoading = LoadingState.initial;
+let _practitionerSyncStarted = false;
+let _practitionerSyncFinished = false;
+let _practitionerERXEnrollmentStarted = false;
export default function useOttehrUser(): OttehrUser | undefined {
const { fhirClient } = useApiClients();
const { isAuthenticated, getAccessTokenSilently, user: auth0User } = useAuth0();
const user = useOttehrUserStore((state) => state.user);
const profile = useOttehrUserStore((state) => state.profile);
+ const isERXPrescriber = profile?.identifier?.some((x) => x.system === ERX_PRESCRIBER_SYSTEM_URL && Boolean(x.value));
+ const isPractitionerEnrolledInERX = profile?.extension?.some(
+ (x) => x.url === ERX_PRACTITIONER_ENROLLED && Boolean(x.valueBoolean),
+ );
+
+ useEffect(() => {
+ if (isPractitionerEnrolledInERX) {
+ useProviderERXStateStore.setState({ wasEnrolledInERX: true });
+ }
+ }, [isPractitionerEnrolledInERX]);
+
+ const isProviderHasEverythingToBeEnrolledInErx = Boolean(
+ profile?.id &&
+ profile?.telecom?.find((phone) => phone.system === 'sms' || phone.system === 'phone')?.value &&
+ getPractitionerNPIIdentitifier(profile)?.value &&
+ profile?.name?.[0]?.given?.[0] &&
+ profile?.name?.[0]?.family,
+ );
+ // console.log(
+ // `profile id: ${profile?.id}
+ // & NPI: ${getPractitionerNPIIdentitifier(profile)?.value}
+ // & phone: ${profile?.telecom?.find((phone) => phone.system === 'sms' || phone.system === 'phone')?.value}
+ // & given name: ${profile?.name?.[0]?.given?.[0]}
+ // & family name: ${profile?.name?.[0]?.family}`,
+ // );
+ // console.log(
+ // isProviderHasEverythingToBeEnrolledInErx
+ // ? 'Provider has everything to be enrolled in ERX'
+ // : 'Provider NOT ready to be enrolled in ERX',
+ // );
+
+ const userRoles = user?.roles;
+ const hasRole = useCallback(
+ (role: RoleType[]): boolean => {
+ return userRoles?.find((r) => role.includes(r.name as RoleType)) != undefined;
+ },
+ [userRoles],
+ );
+ useGetUser();
+ useSyncPractitioner((data) => {
+ if (data.updated) {
+ console.log('Practitioner sync success');
+ }
+ });
+ const { refetch: refetchProfile } = useGetProfile();
+ const { mutateAsync: mutatePractitionerAsync } = useUpdatePractitioner();
+ const { mutateAsync: mutateEnrollPractitionerInERX } = useEnrollPractitionerInERX();
useEffect(() => {
async function getUserRequest(): Promise {
@@ -87,32 +150,6 @@ export default function useOttehrUser(): OttehrUser | undefined {
}
}, [fhirClient, profile, user]);
- useEffect(() => {
- async function updateLastLogin(user: User, fhirClient: FhirClient): Promise {
- _lastLoginUpdating = LoadingState.pending;
- try {
- await fhirClient.patchResource({
- resourceType: 'Practitioner',
- resourceId: user.profile.replace('Practitioner/', ''),
- operations: [
- getPatchOperationForNewMetaTag(profile!, {
- system: 'last-login',
- code: DateTime.now().toISO() ?? 'Unknown',
- }),
- ],
- });
- } catch (error) {
- console.log(error);
- } finally {
- _lastLoginUpdating = LoadingState.idle;
- }
- }
-
- if (user && fhirClient && profile && _lastLoginUpdating !== LoadingState.pending) {
- void updateLastLogin(user, fhirClient);
- }
- }, [fhirClient, profile, user]);
-
useEffect(() => {
const lastLogin = auth0User?.updated_at;
if (lastLogin) {
@@ -123,6 +160,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';
@@ -142,14 +213,189 @@ 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;
const DAY = MINUTE * 60 * 24;
+
+interface StreetAddress {
+ street1: string;
+ street2?: string;
+ city: string;
+ state: string;
+ postal_code: string;
+}
+
+interface ERXEnrollmentProps {
+ providerId: Required;
+ address: StreetAddress;
+ npi?: string;
+ phone: string;
+ given_name: string;
+ family_name: string;
+}
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+const useEnrollPractitionerInERX = () => {
+ const token = useAuthToken();
+ const profile = useOttehrUserStore((state) => state.profile);
+
+ return useMutation(
+ ['enroll-provider-erx'],
+ async (): Promise => {
+ try {
+ const address: StreetAddress = {
+ street1: '1 Hollow Lane',
+ street2: 'Ste 301',
+ city: 'Lake Success',
+ postal_code: '11042',
+ state: 'NY',
+ };
+ const payload: ERXEnrollmentProps = {
+ providerId: profile?.id,
+ address,
+ phone: profile?.telecom?.find((phone) => phone.system === 'sms' || phone.system === 'phone')?.value || '',
+ npi: profile && getPractitionerNPIIdentitifier(profile)?.value,
+ given_name: profile?.name?.[0]?.given?.[0] || '',
+ family_name: profile?.name?.[0]?.family || '',
+ };
+ _practitionerERXEnrollmentStarted = true;
+
+ const response = await fetch(`${import.meta.env.VITE_APP_PROJECT_API_URL}/erx/enrollment`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify(payload),
+ method: 'POST',
+ });
+ if (!response.ok) {
+ throw new Error(`ERX practitioner enrollment call failed: ${response.statusText}`);
+ }
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+ },
+ { retry: 2 },
+ );
+};
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+const useGetUser = () => {
+ const token = useAuthToken();
+ const user = useOttehrUserStore((state) => state.user);
+
+ return useQuery(
+ ['get-user'],
+ async (): Promise => {
+ try {
+ const user = await getUser(token!);
+ useOttehrUserStore.setState({ user: user as User });
+ } catch (error) {
+ console.error(error);
+ }
+ },
+ {
+ enabled: Boolean(token && !user),
+ },
+ );
+};
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+const useGetProfile = () => {
+ const token = useAuthToken();
+ const user = useOttehrUserStore((state) => state.user);
+ const profile = useOttehrUserStore((state) => state.profile);
+ const { fhirClient } = useApiClients();
+
+ return useQuery(
+ ['get-practitioner-profile'],
+ async (): Promise => {
+ try {
+ if (!user?.profile) {
+ useOttehrUserStore.setState({ profile: undefined });
+ return;
+ }
+
+ const [resourceType, resourceId] = (user?.profile || '').split('/');
+ if (resourceType && resourceId && resourceType === 'Practitioner') {
+ const practitioner = await fhirClient?.readResource({ resourceType, resourceId });
+ useOttehrUserStore.setState({ profile: practitioner });
+ console.log('practitioner', practitioner);
+ }
+ } catch (e) {
+ console.error(`error fetching user's fhir profile: ${JSON.stringify(e)}`);
+ useOttehrUserStore.setState({ profile: undefined });
+ }
+ },
+ {
+ enabled: Boolean(token && fhirClient && user?.profile && !profile),
+ },
+ );
+};
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+const useSyncPractitioner = (onSuccess: (data: SyncUserResponse) => void) => {
+ const client = useZapEHRAPIClient();
+ const token = useAuthToken();
+ const { zambdaClient } = useApiClients();
+ const queryClient = useQueryClient();
+ return useQuery(
+ ['sync-user', zambdaClient],
+ async () => {
+ console.log('zambdaClient: ', zambdaClient);
+ if (!client) return undefined;
+ _practitionerSyncStarted = true;
+ const result = await client?.syncUser();
+ _practitionerSyncFinished = true;
+ if (result.updated) {
+ void queryClient.refetchQueries('get-practitioner-profile');
+ } else {
+ useOttehrUserStore.setState((state) => ({ profile: { ...(state.profile! || {}) } }));
+ }
+ return result;
+ },
+ {
+ onSuccess,
+ cacheTime: DAY,
+ staleTime: DAY,
+ enabled: Boolean(
+ token && zambdaClient && (zambdaClient as unknown as ClientConfig).accessToken && !_practitionerSyncStarted,
+ ),
+ },
+ );
+};
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+const useUpdatePractitioner = () => {
+ const user = useOttehrUserStore((state) => state.user);
+ const { fhirClient } = useApiClients();
+
+ return useMutation(
+ ['update-practitioner'],
+ async (patchOps: Operation[]): Promise => {
+ try {
+ if (!fhirClient || !user) return;
+
+ await fhirClient.patchResource({
+ resourceType: 'Practitioner',
+ resourceId: user.profile.replace('Practitioner/', ''),
+ operations: [...patchOps],
+ });
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+ },
+ { retry: 3 },
+ );
+};
diff --git a/packages/telemed-ehr/app/src/index.tsx b/packages/telemed-ehr/app/src/index.tsx
index 8b529b4ce..2ba9d0cf4 100644
--- a/packages/telemed-ehr/app/src/index.tsx
+++ b/packages/telemed-ehr/app/src/index.tsx
@@ -29,6 +29,8 @@ root.render(
audience={import.meta.env.VITE_APP_ZAPEHR_APPLICATION_AUDIENCE}
redirectUri={AUTH0_REDIRECT_URI}
connection={import.meta.env.VITE_APP_ZAPEHR_CONNECTION_NAME}
+ cacheLocation="localstorage"
+ skipRedirectCallback={window.location.href.includes('photon=true')}
>
diff --git a/packages/telemed-ehr/app/src/photon.d.ts b/packages/telemed-ehr/app/src/photon.d.ts
index 56e88de24..b92bba74c 100644
--- a/packages/telemed-ehr/app/src/photon.d.ts
+++ b/packages/telemed-ehr/app/src/photon.d.ts
@@ -8,6 +8,11 @@ declare namespace JSX {
'redirect-uri': string;
children: Element;
};
- 'photon-prescribe-workflow': { 'enable-order': string; 'patient-id'?: string };
+ 'photon-prescribe-workflow': {
+ 'enable-order': string;
+ 'patient-id'?: string;
+ weight?: number;
+ 'weight-unit'?: 'lbs' | 'kg';
+ };
}
}
diff --git a/packages/telemed-ehr/app/src/telemed/data/types.ts b/packages/telemed-ehr/app/src/telemed/data/types.ts
index a4f0f16b6..107ea1a90 100644
--- a/packages/telemed-ehr/app/src/telemed/data/types.ts
+++ b/packages/telemed-ehr/app/src/telemed/data/types.ts
@@ -6,6 +6,7 @@ export type GetZapEHRTelemedAPIParams = {
saveChartDataZambdaID?: string;
deleteChartDataZambdaID?: string;
changeTelemedAppointmentStatusZambdaID?: string;
+ syncUserZambdaID?: string;
getPatientInstructionsZambdaID?: string;
savePatientInstructionZambdaID?: string;
deletePatientInstructionZambdaID?: string;
diff --git a/packages/telemed-ehr/app/src/telemed/data/zapEHRApi.ts b/packages/telemed-ehr/app/src/telemed/data/zapEHRApi.ts
index 3a3f11cbd..8373408a4 100644
--- a/packages/telemed-ehr/app/src/telemed/data/zapEHRApi.ts
+++ b/packages/telemed-ehr/app/src/telemed/data/zapEHRApi.ts
@@ -17,6 +17,7 @@ import {
SaveChartDataRequest,
SaveChartDataResponse,
SavePatientInstructionInput,
+ SyncUserResponse,
} from 'ehr-utils';
import { GetAppointmentsRequestParams } from '../utils';
import { ApiError, GetZapEHRTelemedAPIParams } from './types';
@@ -28,6 +29,7 @@ enum ZambdaNames {
'save chart data' = 'save chart data',
'delete chart data' = 'delete chart data',
'change telemed appointment status' = 'change telemed appointment status',
+ 'sync user' = 'sync user',
'get patient instructions' = 'get patient instructions',
'save patient instruction' = 'save patient instruction',
'delete patient instruction' = 'delete patient instruction',
@@ -41,6 +43,7 @@ const zambdasPublicityMap: Record = {
'save chart data': false,
'delete chart data': false,
'change telemed appointment status': false,
+ 'sync user': false,
'get patient instructions': false,
'save patient instruction': false,
'delete patient instruction': false,
@@ -59,6 +62,7 @@ export const getZapEHRTelemedAPI = (
saveChartData: typeof saveChartData;
deleteChartData: typeof deleteChartData;
changeTelemedAppointmentStatus: typeof changeTelemedAppointmentStatus;
+ syncUser: typeof syncUser;
getPatientInstructions: typeof getPatientInstructions;
savePatientInstruction: typeof savePatientInstruction;
deletePatientInstruction: typeof deletePatientInstruction;
@@ -71,6 +75,7 @@ export const getZapEHRTelemedAPI = (
saveChartDataZambdaID,
deleteChartDataZambdaID,
changeTelemedAppointmentStatusZambdaID,
+ syncUserZambdaID,
getPatientInstructionsZambdaID,
savePatientInstructionZambdaID,
deletePatientInstructionZambdaID,
@@ -84,6 +89,7 @@ export const getZapEHRTelemedAPI = (
'save chart data': saveChartDataZambdaID,
'delete chart data': deleteChartDataZambdaID,
'change telemed appointment status': changeTelemedAppointmentStatusZambdaID,
+ 'sync user': syncUserZambdaID,
'get patient instructions': getPatientInstructionsZambdaID,
'save patient instruction': savePatientInstructionZambdaID,
'delete patient instruction': deletePatientInstructionZambdaID,
@@ -171,6 +177,10 @@ export const getZapEHRTelemedAPI = (
return await makeZapRequest('change telemed appointment status', parameters);
};
+ const syncUser = async (): Promise => {
+ return await makeZapRequest('sync user');
+ };
+
const getPatientInstructions = async (parameters: GetPatientInstructionsInput): Promise => {
return await makeZapRequest('get patient instructions', parameters);
};
@@ -194,6 +204,7 @@ export const getZapEHRTelemedAPI = (
saveChartData,
deleteChartData,
changeTelemedAppointmentStatus,
+ syncUser,
getPatientInstructions,
savePatientInstruction,
deletePatientInstruction,
diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentHeader.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentHeader.tsx
index fbb74ba2c..59add7146 100644
--- a/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentHeader.tsx
+++ b/packages/telemed-ehr/app/src/telemed/features/appointment/AppointmentHeader.tsx
@@ -1,11 +1,31 @@
-import { FC } from 'react';
-import { AppBar, Box, IconButton, useTheme } from '@mui/material';
+import { AppBar, Box, Container, IconButton, Skeleton, Typography, useTheme } from '@mui/material';
+import { DateTime } from 'luxon';
+import { FC, useState } from 'react';
+import { ERX_PATIENT_IDENTIFIER_SYSTEM, mapStatusToTelemed, getQuestionnaireResponseByLinkId } from 'ehr-utils';
+import CustomBreadcrumbs from '../../../components/CustomBreadcrumbs';
+import { getSelectors } from '../../../shared/store/getSelectors';
+import CancelVisitDialog from '../../components/CancelVisitDialog';
+import { useAppointmentStore } from '../../state';
+import { getAppointmentStatusChip, getPatientName } from '../../utils';
import CloseIcon from '@mui/icons-material/Close';
import { useNavigate } from 'react-router-dom';
import { AppointmentTabsHeader } from './AppointmentTabsHeader';
export const AppointmentHeader: FC = () => {
const theme = useTheme();
+
+ const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false);
+ const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
+ const [isERXOpen, setIsERXOpen] = useState(false);
+
+ const { appointment, encounter, patient, location, isReadOnly, questionnaireResponse } = getSelectors(
+ useAppointmentStore,
+ ['appointment', 'patient', 'encounter', 'location', 'isReadOnly', 'questionnaireResponse'],
+ );
+
+ const patientPhotonId = patient?.identifier?.find((id) => id.system === ERX_PATIENT_IDENTIFIER_SYSTEM)?.value;
+ const reasonForVisit = getQuestionnaireResponseByLinkId('reason-for-visit', questionnaireResponse)?.answer?.[0]
+ .valueString;
const navigate = useNavigate();
return (
diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/AssessmentTab/ERxCard.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/AssessmentTab/ERxCard.tsx
index 7476e1f44..5747e773d 100644
--- a/packages/telemed-ehr/app/src/telemed/features/appointment/AssessmentTab/ERxCard.tsx
+++ b/packages/telemed-ehr/app/src/telemed/features/appointment/AssessmentTab/ERxCard.tsx
@@ -1,19 +1,38 @@
-import React, { FC } from 'react';
import { Box } from '@mui/material';
-import { AccordionCard, RoundedButton } from '../../../components';
+import { FC, useCallback, useState } from 'react';
import { getSelectors } from '../../../../shared/store/getSelectors';
+import { AccordionCard, RoundedButton } from '../../../components';
import { useAppointmentStore } from '../../../state';
+import { ERX } from '../ERX';
+import { PrescribedMedicationReviewItem } from '../ReviewTab/components/PrescribedMedicationReviewItem';
export const ERxCard: FC = () => {
- const { isReadOnly } = getSelectors(useAppointmentStore, ['isReadOnly']);
+ const [collapsed, setCollapsed] = useState(false);
+ const { isReadOnly, chartData } = getSelectors(useAppointmentStore, ['isReadOnly', 'chartData']);
+
+ const [isERXOpen, setIsERXOpen] = useState(false);
+ const [isERXLoading, setIsERXLoading] = useState(false);
+
+ const handleERXLoadingStatusChange = useCallback<(status: boolean) => void>(
+ (status) => setIsERXLoading(status),
+ [setIsERXLoading],
+ );
return (
-
-
-
- Add eRx
+ setCollapsed((prevState) => !prevState)}>
+
+ {(chartData?.prescribedMedications?.length || -1) >= 0 && (
+
+ {chartData?.prescribedMedications?.map((med) => (
+
+ ))}
+
+ )}
+ setIsERXOpen(true)}>
+ Add RX
+ {isERXOpen && setIsERXOpen(false)} onLoadingStatusChange={handleERXLoadingStatusChange} />}
);
};
diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/ERX.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/ERX.tsx
new file mode 100644
index 000000000..50d0362cf
--- /dev/null
+++ b/packages/telemed-ehr/app/src/telemed/features/appointment/ERX.tsx
@@ -0,0 +1,78 @@
+import { Patient } from 'fhir/r4';
+import { enqueueSnackbar } from 'notistack';
+import { FC, useCallback, useEffect, useState } from 'react';
+import { ERX_PATIENT_IDENTIFIER_SYSTEM } from 'ehr-utils';
+import { getSelectors } from '../../../shared/store/getSelectors';
+import { useAppointmentStore, useSyncERXPatient } from '../../state';
+import { ERXDialog } from './ERXDialog';
+
+export const ERX: FC<{
+ onClose: () => void;
+ onLoadingStatusChange: (loading: boolean) => void;
+}> = ({ onClose, onLoadingStatusChange }) => {
+ const { patient } = getSelectors(useAppointmentStore, ['patient']);
+ const phoneNumber = patient?.telecom?.find((telecom) => telecom.system === 'phone')?.value;
+ const [syncedPatient, setSyncedPatient] = useState(false);
+
+ const { isLoading: isSyncingPatient, mutateAsync: syncPatient, isError } = useSyncERXPatient();
+
+ const photonPatientId = patient?.identifier?.find((id) => id.system === ERX_PATIENT_IDENTIFIER_SYSTEM)?.value;
+
+ const syncPatientFn = useCallback(async () => {
+ try {
+ const response = await syncPatient(patient!);
+ if (response.photonPatientId) {
+ useAppointmentStore.setState((state) => {
+ const oldPatient = (state.patient || {}) as Patient;
+ return {
+ patient: {
+ ...oldPatient,
+ identifier: [
+ ...(oldPatient?.identifier || []),
+ { system: ERX_PATIENT_IDENTIFIER_SYSTEM, value: response.photonPatientId },
+ ],
+ },
+ };
+ });
+ }
+ setSyncedPatient(true);
+ } catch (err: any) {
+ let errorMsg = 'Something went wrong while trying to open RX';
+
+ if (err.status === 400) {
+ if (err.message && err.message.includes('phone')) {
+ errorMsg = `Patient has specified some wrong phone number: ${phoneNumber}. RX can be used only when a real phone number is provided`;
+ } else {
+ errorMsg = `Something is wrong with patient data.`;
+ }
+ }
+
+ enqueueSnackbar(errorMsg, {
+ variant: 'error',
+ });
+
+ console.error('Error trying to sync patient to photon: ', err);
+ }
+ }, [patient, phoneNumber, syncPatient]);
+
+ useEffect(() => {
+ onLoadingStatusChange(isSyncingPatient);
+ }, [onLoadingStatusChange, isSyncingPatient]);
+
+ useEffect(() => {
+ if (isError) {
+ onClose();
+ }
+ if (!syncedPatient && patient && !isSyncingPatient && !isError) {
+ void syncPatientFn();
+ }
+ }, [syncedPatient, isError, isSyncingPatient, onClose, patient, syncPatientFn]);
+
+ return (
+ <>
+ {photonPatientId && !isSyncingPatient && (
+ onClose()} />
+ )}
+ >
+ );
+};
diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/ERXDialog.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/ERXDialog.tsx
index 15434e841..8985af6ff 100644
--- a/packages/telemed-ehr/app/src/telemed/features/appointment/ERXDialog.tsx
+++ b/packages/telemed-ehr/app/src/telemed/features/appointment/ERXDialog.tsx
@@ -1,5 +1,15 @@
-import { Dialog } from '@mui/material';
-import { ReactElement } from 'react';
+import CloseIcon from '@mui/icons-material/Close';
+import { Box, Dialog, DialogProps, IconButton } from '@mui/material';
+import { ReactElement, useEffect, useMemo } from 'react';
+import { PrescribedMedicationDTO } from 'ehr-utils';
+import { FHIR_EXTENSION } from 'ehr-utils/lib/fhir/constants';
+import { getSelectors } from '../../../shared/store/getSelectors';
+import { useAppointmentStore } from '../../state';
+
+interface PhotonPrescription {
+ treatment: { id: string; name: string };
+ instructions: string;
+}
export const ERXDialog = ({
onClose,
@@ -8,20 +18,94 @@ export const ERXDialog = ({
onClose: () => void;
patientPhotonId?: string;
}): ReactElement => {
+ const { patient, setPartialChartData, chartData } = getSelectors(useAppointmentStore, [
+ 'patient',
+ 'setPartialChartData',
+ 'chartData',
+ ]);
+ let weight: number | undefined = Number.parseFloat(
+ patient?.extension?.find((ext) => ext.url === FHIR_EXTENSION.Patient.weight.url)?.valueString ?? '',
+ );
+ if (isNaN(weight)) {
+ weight = undefined;
+ }
+
+ const existingPrescribedMeds = useMemo(
+ () => chartData?.prescribedMedications || [],
+ [chartData?.prescribedMedications],
+ );
+
+ useEffect(() => {
+ const photonListener = (e: Event): void => {
+ const prescriptionsEvent = e as unknown as { detail?: { prescriptions?: PhotonPrescription[] } };
+ const prescribedMeds =
+ prescriptionsEvent.detail?.prescriptions?.map(
+ (detail) =>
+ ({
+ name: detail.treatment.name,
+ instructions: detail.instructions,
+ }) as PrescribedMedicationDTO,
+ ) || [];
+ if (prescribedMeds.length > 0) {
+ setPartialChartData({
+ prescribedMedications: [
+ ...existingPrescribedMeds,
+ ...prescribedMeds.filter(
+ (newMed) =>
+ existingPrescribedMeds.findIndex(
+ (existingMed) => existingMed.instructions === newMed.instructions && existingMed.name === newMed.name,
+ ) < 0,
+ ),
+ ],
+ });
+ }
+ onClose();
+ };
+ document.addEventListener('photon-prescriptions-created', photonListener);
+
+ return () => {
+ document.removeEventListener('photon-prescriptions-created', photonListener);
+ };
+ }, [existingPrescribedMeds, onClose, setPartialChartData]);
+
+ const handleClose: DialogProps['onClose'] = (_, reason) => {
+ if (reason === 'backdropClick') return;
+ onClose();
+ };
return (
);
};
diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/ReviewTab/components/PrescribedMedicationReviewItem.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/ReviewTab/components/PrescribedMedicationReviewItem.tsx
new file mode 100644
index 000000000..483f18c8d
--- /dev/null
+++ b/packages/telemed-ehr/app/src/telemed/features/appointment/ReviewTab/components/PrescribedMedicationReviewItem.tsx
@@ -0,0 +1,16 @@
+import { Typography } from '@mui/material';
+import { FC } from 'react';
+import { PrescribedMedicationDTO } from 'ehr-utils';
+
+export const PrescribedMedicationReviewItem: FC<{ medication: PrescribedMedicationDTO }> = (props) => {
+ const { medication } = props;
+
+ return (
+ <>
+
+ {medication.name}
+
+ {medication.instructions}
+ >
+ );
+};
diff --git a/packages/telemed-ehr/app/src/telemed/features/appointment/ReviewTab/components/PrescribedMedicationsContainer.tsx b/packages/telemed-ehr/app/src/telemed/features/appointment/ReviewTab/components/PrescribedMedicationsContainer.tsx
new file mode 100644
index 000000000..a4bacbd3b
--- /dev/null
+++ b/packages/telemed-ehr/app/src/telemed/features/appointment/ReviewTab/components/PrescribedMedicationsContainer.tsx
@@ -0,0 +1,22 @@
+import { Box, Typography } from '@mui/material';
+import { FC } from 'react';
+import { getSelectors } from '../../../../../shared/store/getSelectors';
+import { useAppointmentStore } from '../../../../state';
+import { PrescribedMedicationReviewItem } from './PrescribedMedicationReviewItem';
+
+export const PrescribedMedicationsContainer: FC = () => {
+ const { chartData } = getSelectors(useAppointmentStore, ['chartData']);
+
+ return (
+
+
+ Prescriptions
+
+
+ {chartData?.prescribedMedications?.map((med) => (
+
+ ))}
+
+
+ );
+};
diff --git a/packages/telemed-ehr/app/src/telemed/hooks/useZapEHRAPIClient.ts b/packages/telemed-ehr/app/src/telemed/hooks/useZapEHRAPIClient.ts
index 84a5365bc..b741049d0 100644
--- a/packages/telemed-ehr/app/src/telemed/hooks/useZapEHRAPIClient.ts
+++ b/packages/telemed-ehr/app/src/telemed/hooks/useZapEHRAPIClient.ts
@@ -16,6 +16,7 @@ export const useZapEHRAPIClient = (): ReturnType | n
deleteChartDataZambdaID: import.meta.env.VITE_APP_DELETE_CHART_DATA_ZAMBDA_ID,
changeTelemedAppointmentStatusZambdaID: import.meta.env.VITE_APP_CHANGE_TELEMED_APPOINTMENT_STATUS_ZAMBDA_ID,
getPatientInstructionsZambdaID: import.meta.env.VITE_APP_GET_PATIENT_INSTRUCTIONS_ZAMBDA_ID,
+ syncUserZambdaID: import.meta.env.VITE_APP_SYNC_USER_ZAMBDA_ID,
savePatientInstructionZambdaID: import.meta.env.VITE_APP_SAVE_PATIENT_INSTRUCTION_ZAMBDA_ID,
deletePatientInstructionZambdaID: import.meta.env.VITE_APP_DELETE_PATIENT_INSTRUCTION_ZAMBDA_ID,
icdSearchZambdaId: import.meta.env.VITE_APP_ICD_SEARCH_ZAMBDA_ID,
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 bb6e05b92..cbb313ed4 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
@@ -1,5 +1,5 @@
import { useAuth0 } from '@auth0/auth0-react';
-import { Bundle, FhirResource } from 'fhir/r4';
+import { Bundle, FhirResource, Patient } from 'fhir/r4';
import { DateTime } from 'luxon';
import { useMutation, useQuery } from 'react-query';
import {
@@ -16,6 +16,7 @@ import { useZapEHRAPIClient } from '../../hooks/useZapEHRAPIClient';
import { useAppointmentStore } from './appointment.store';
import useOttehrUser from '../../../hooks/useOttehrUser';
import { getSelectors } from '../../../shared/store/getSelectors';
+import { useAuthToken } from '../../../hooks/useAuthToken';
import { CHAT_REFETCH_INTERVAL } from '../../../constants';
import { extractPhotoUrlsFromAppointmentData } from '../../utils';
@@ -500,3 +501,32 @@ export const useDeletePatientInstruction = () => {
},
});
};
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+export const useSyncERXPatient = () => {
+ const token = useAuthToken();
+
+ return useMutation(
+ ['sync-erx-patient'],
+ async (patient: Patient) => {
+ if (token) {
+ console.log(`Start syncing eRx patient ${patient.id}`);
+ const resp = await fetch(`${import.meta.env.VITE_APP_PROJECT_API_URL}/erx/sync-patient/${patient.id}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ method: 'POST',
+ });
+ if (!resp.ok) {
+ throw { ...(await resp.json()), status: resp.status };
+ }
+ console.log('Successfuly synced eRx patient');
+ return (await resp.json()) as { photonPatientId: string };
+ }
+ throw new Error('auth token is not defined');
+ },
+ {
+ retry: 2,
+ },
+ );
+};
diff --git a/packages/telemed-ehr/zambdas/serverless.yml b/packages/telemed-ehr/zambdas/serverless.yml
index 4cb674b5c..908e74713 100644
--- a/packages/telemed-ehr/zambdas/serverless.yml
+++ b/packages/telemed-ehr/zambdas/serverless.yml
@@ -102,6 +102,21 @@ functions:
PROJECT_API: ${file(./.env/${self:provider.stage}.json):PROJECT_API}
FHIR_API: ${file(./.env/${self:provider.stage}.json):FHIR_API}
ENVIRONMENT: ${file(./.env/${self:provider.stage}.json):ENVIRONMENT}
+ sync-user:
+ handler: src/sync-user/index.index
+ events:
+ - http:
+ path: zambda/sync-user/execute
+ method: POST
+ timeout: 25
+ environment:
+ AUTH0_ENDPOINT: ${file(./.env/${self:provider.stage}.json):AUTH0_ENDPOINT}
+ URGENT_CARE_AUTH0_CLIENT: ${file(./.env/${self:provider.stage}.json):URGENT_CARE_AUTH0_CLIENT}
+ URGENT_CARE_AUTH0_SECRET: ${file(./.env/${self:provider.stage}.json):URGENT_CARE_AUTH0_SECRET}
+ AUTH0_AUDIENCE: ${file(./.env/${self:provider.stage}.json):AUTH0_AUDIENCE}
+ PROJECT_API: ${file(./.env/${self:provider.stage}.json):PROJECT_API}
+ FHIR_API: ${file(./.env/${self:provider.stage}.json):FHIR_API}
+ ENVIRONMENT: ${file(./.env/${self:provider.stage}.json):ENVIRONMENT}
deactivate-user:
handler: src/deactivate-user/index.index
events:
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 d8dedef63..142dd575e 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
@@ -49,13 +49,18 @@ export const performEffect = async (
): Promise => {
const { appointmentId, newStatus } = params;
+ console.log(`Changing status for appointment ${appointmentId} to ${newStatus}`);
+
const visitResources = await getVideoResources(fhirClient, appointmentId);
if (!visitResources) {
{
+ console.error('No visit resources found');
throw new Error(`Visit resources are not properly defined for appointment ${appointmentId}`);
}
}
+ console.log(`Visit resources: ${JSON.stringify(visitResources)}`);
+
if (visitResources.encounter?.subject?.reference === undefined) {
throw new Error(`No subject reference defined for encoutner ${visitResources.encounter?.id}`);
}
diff --git a/packages/telemed-ehr/zambdas/src/sync-user/index.ts b/packages/telemed-ehr/zambdas/src/sync-user/index.ts
new file mode 100644
index 000000000..9e9c3dbcd
--- /dev/null
+++ b/packages/telemed-ehr/zambdas/src/sync-user/index.ts
@@ -0,0 +1,300 @@
+import { AppClient, FhirClient } from '@zapehr/sdk';
+import { APIGatewayProxyResult } from 'aws-lambda';
+import { ContactPoint, Identifier, Practitioner } from 'fhir/r4';
+import {
+ FHIR_IDENTIFIER_NPI,
+ PractitionerLicense,
+ Secrets,
+ SyncUserResponse,
+ allLicensesForPractitioner,
+ getPractitionerNPIIdentitifier,
+} from 'ehr-utils';
+// import { SecretsKeys, getSecret } from '../shared';
+import { checkOrCreateM2MClientToken, createAppClient, createFhirClient } from '../shared/helpers';
+import { makeQualificationForPractitioner } from '../shared/practitioners';
+import { ZambdaInput } from '../types';
+import { validateRequestParameters } from './validateRequestParameters';
+
+// Lifting up value to outside of the handler allows it to stay in memory across warm lambda invocations
+let m2mtoken: string;
+
+export const index = async (input: ZambdaInput): Promise => {
+ try {
+ const { secrets } = validateRequestParameters(input);
+ console.log('Parameters: ' + JSON.stringify(input));
+
+ m2mtoken = await checkOrCreateM2MClientToken(m2mtoken, secrets);
+ const fhirClient = createFhirClient(m2mtoken, secrets);
+ const appClient = createAppClient(input.headers.Authorization.replace('Bearer ', ''), secrets);
+
+ const response = await performEffect(appClient, fhirClient, secrets);
+
+ return {
+ statusCode: 200,
+ body: JSON.stringify(response),
+ };
+ } catch (error: any) {
+ console.log(JSON.stringify(error));
+ return {
+ statusCode: 500,
+ body: JSON.stringify({ message: 'Error synchronizing practitioner with remote credentialing authority.' }),
+ };
+ }
+};
+
+async function performEffect(
+ appClient: AppClient,
+ fhirClient: FhirClient,
+ secrets: Secrets | null,
+): Promise {
+ const [remotePractitioner, localPractitioner] = await Promise.all([
+ getRemotePractitionerAndCredentials(appClient, secrets),
+ getLocalEHRPractitioner(appClient, fhirClient),
+ ]);
+ if (!remotePractitioner) {
+ return {
+ message: 'Remote provider licenses and qualifications not found for current practitioner.',
+ updated: false,
+ };
+ }
+
+ // TODO: As it stands, practitioner will never get synchronized with the remote, as we have not
+ // implemented the logic to update the remote authority.
+ // This code will never get called
+ //
+ // We will also need to properly handle how data from remote authoirity is reconciled with
+ // the local EHR data that already exists.
+ //
+ let ehrPractitioner = { ...localPractitioner };
+ console.log(`remotePractitioner: ${JSON.stringify(remotePractitioner)}`);
+ console.log(`localPractitioner: ${JSON.stringify(ehrPractitioner)}`);
+ ehrPractitioner = updatePractitionerName(ehrPractitioner, remotePractitioner);
+ ehrPractitioner.birthDate = remotePractitioner.date_of_birth;
+ ehrPractitioner = updatePractitionerPhone(ehrPractitioner, remotePractitioner);
+ ehrPractitioner = updatePractitionerPhoto(ehrPractitioner, remotePractitioner);
+ ehrPractitioner = updatePractitionerQualification(ehrPractitioner, remotePractitioner);
+ ehrPractitioner = updatePractitionerCredentials(ehrPractitioner, remotePractitioner);
+ ehrPractitioner = updatePractitionerNPI(ehrPractitioner, remotePractitioner);
+ const result = await updatePractitioner(fhirClient, ehrPractitioner);
+ console.log(`Practitioner updated successfully: ${JSON.stringify(result)}`);
+ if (result)
+ return {
+ message: 'Practitioner credentials have been synchronized with remote credentials authority successfully.',
+ updated: true,
+ };
+ throw new Error('Failed updating practitioner...');
+}
+
+interface RemotePractitionerData {
+ id: string;
+ first_name?: string;
+ middle_name?: string;
+ last_name?: string;
+ date_of_birth?: string;
+ primary_phone?: string;
+ profession?: string;
+ user?: {
+ picture?: string;
+ };
+ photoUrl?: string;
+ licenses?: PractitionerLicense[];
+ npi?: string;
+}
+
+async function getRemotePractitionerAndCredentials(
+ appClient: AppClient,
+ secrets: Secrets | null,
+): Promise {
+ 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}`);
+
+ const clinicianSearchResults: RemotePractitionerData[] = [];
+
+ // TODO: this is where you could handle provider search results
+ // from a remote credentialing authority
+ //
+ // const searchResults: RemotePractitionerData[] = await searchRemoteCredentialsAuthority(
+ // `/api/v1/org/providers/?search=${myEmail}`,
+ // secrets,
+ // );
+
+ console.log(`Response: ${JSON.stringify(clinicianSearchResults)}`);
+
+ if (clinicianSearchResults) {
+ const provider = clinicianSearchResults.find((provider: any) => provider.email === myEmail);
+ if (provider?.id) {
+ // TODO: this is where you could handle provider credentials and licenses
+ // from a remote credentialing authority
+ //
+ // const licensesResponse = await searchRemoteCredentialsAuthority(
+ // `api/v1/org/licenses/?provider=${provider.id}`,
+ // secrets,
+ // );
+ // const licenses: PractitionerLicense[] = [];
+ // if (licensesResponse) {
+ // licensesResponse?.forEach((license: any) => {
+ // const code = license.certificate_type;
+ // const state = license.state;
+ // if (code && state) licenses.push({ code, state, active: true });
+ // });
+ // }
+
+ return undefined;
+ // {
+ // ...provider,
+ // licenses,
+ // };
+ }
+ }
+ return undefined;
+}
+
+async function getLocalEHRPractitioner(appClient: AppClient, fhirClient: FhirClient): Promise {
+ const practitionerId = (await appClient.getMe()).profile.replace('Practitioner/', '');
+ return await fhirClient.readResource({
+ resourceType: 'Practitioner',
+ resourceId: practitionerId,
+ });
+}
+
+async function searchRemoteCredentialsAuthority(path: string, secrets: Secrets | null): Promise {
+ // const url = getSecret(SecretsKeys.REMOT_AUTHORITY_URL, secrets);
+ // const apiKey = getSecret(SecretsKeys.REMOT_AUTHORITY_API_KEY, secrets);
+ // return await fetch(`{url}{path}`, {
+ // method: 'GET',
+ // headers: {
+ // accept: 'application/json',
+ // 'x-api-key': apiKey,
+ // },
+ // })
+ // .then((res) => res.json())
+ // .then((res) => res.results)
+ // .catch(console.error);
+}
+
+function updatePractitionerName(localClinician: Practitioner, remoteClinician: RemotePractitionerData): Practitioner {
+ if (!(remoteClinician.first_name || remoteClinician.middle_name || remoteClinician.last_name)) return localClinician;
+ const firstName = remoteClinician.first_name;
+ const secondName = remoteClinician.middle_name;
+ const lastName = remoteClinician.last_name;
+ if (firstName || secondName || lastName) {
+ if (!localClinician.name) localClinician.name = [{}];
+ const given = [];
+ if (!localClinician.name[0].given) {
+ if (firstName) given.push(firstName);
+ if (secondName) given.push(secondName);
+ if (given.length > 0) {
+ localClinician.name[0].given = given;
+ }
+ }
+ if (lastName) localClinician.name[0].family = lastName;
+ }
+ return localClinician;
+}
+
+function updatePractitionerPhone(localClinician: Practitioner, remoteClinician: RemotePractitionerData): Practitioner {
+ if (!remoteClinician.primary_phone) return localClinician;
+ localClinician = findTelecomAndUpdateOrAddNew('phone', localClinician, remoteClinician.primary_phone);
+ localClinician = findTelecomAndUpdateOrAddNew('sms', localClinician, remoteClinician.primary_phone);
+ return localClinician;
+}
+
+function findTelecomAndUpdateOrAddNew(
+ system: ContactPoint['system'],
+ practitioner: Practitioner,
+ newPhone: string,
+): Practitioner {
+ const newPractitioner = { ...practitioner };
+ const foundTelecomPhone = newPractitioner.telecom?.find((phone, id) => {
+ if (phone.system === system) {
+ if (newPractitioner.telecom) newPractitioner.telecom[id].value = newPhone;
+ return true;
+ }
+ return false;
+ });
+ if (!foundTelecomPhone) {
+ const phoneRecord: ContactPoint = {
+ system: system,
+ value: newPhone,
+ };
+ if (newPractitioner.telecom) {
+ newPractitioner.telecom.push(phoneRecord);
+ } else {
+ newPractitioner.telecom = [phoneRecord];
+ }
+ }
+ return newPractitioner;
+}
+
+function updatePractitionerPhoto(localClinician: Practitioner, remoteClinician: RemotePractitionerData): Practitioner {
+ if (!remoteClinician.photoUrl) return localClinician;
+ if (localClinician?.photo) {
+ if (localClinician.photo[0]) {
+ localClinician.photo[0] = { url: remoteClinician.photoUrl };
+ }
+ } else {
+ localClinician.photo = [{ url: remoteClinician.photoUrl }];
+ }
+ return localClinician;
+}
+
+function updatePractitionerQualification(
+ localPractitioner: Practitioner,
+ remotePractitioner: RemotePractitionerData,
+): Practitioner {
+ if (!remotePractitioner.licenses) return localPractitioner;
+ if (localPractitioner.qualification) {
+ const existedLicenses = allLicensesForPractitioner(localPractitioner);
+ const missingLicenses: PractitionerLicense[] = [];
+ remotePractitioner.licenses?.forEach((license) => {
+ if (!existedLicenses.find((existed) => existed.state === license.state && existed.code === license.code))
+ missingLicenses.push(license);
+ });
+ missingLicenses?.forEach((license) => {
+ localPractitioner.qualification?.push(makeQualificationForPractitioner(license));
+ });
+ } else {
+ localPractitioner.qualification = [];
+ remotePractitioner.licenses.forEach((license) =>
+ localPractitioner.qualification!.push(makeQualificationForPractitioner(license)),
+ );
+ }
+ return localPractitioner;
+}
+
+function updatePractitionerNPI(localClinician: Practitioner, remoteClinician: RemotePractitionerData): Practitioner {
+ if (!remoteClinician.npi) return localClinician;
+ const identifier: Identifier = {
+ system: FHIR_IDENTIFIER_NPI,
+ value: `${remoteClinician.npi}`,
+ };
+
+ if (localClinician.identifier) {
+ const foundIdentifier = getPractitionerNPIIdentitifier(localClinician);
+ if (foundIdentifier && foundIdentifier.value !== identifier.value) {
+ foundIdentifier.value = identifier.value;
+ } else if (!foundIdentifier) localClinician.identifier.push(identifier);
+ } else {
+ localClinician.identifier = [identifier];
+ }
+ return localClinician;
+}
+
+function updatePractitionerCredentials(
+ localClinician: Practitioner,
+ remoteClinician: RemotePractitionerData,
+): Practitioner {
+ if (!remoteClinician.profession) return localClinician;
+ if (!localClinician.name) localClinician.name = [{}];
+ if (!localClinician.name[0].suffix?.includes(remoteClinician.profession)) {
+ if (!localClinician.name[0].suffix) localClinician.name[0].suffix = [];
+ localClinician.name[0].suffix.push(remoteClinician.profession);
+ }
+ return localClinician;
+}
+
+async function updatePractitioner(fhirClient: FhirClient, practitioner: Practitioner): Promise {
+ return await fhirClient.updateResource(practitioner);
+}
diff --git a/packages/telemed-ehr/zambdas/src/sync-user/validateRequestParameters.ts b/packages/telemed-ehr/zambdas/src/sync-user/validateRequestParameters.ts
new file mode 100644
index 000000000..fff74559c
--- /dev/null
+++ b/packages/telemed-ehr/zambdas/src/sync-user/validateRequestParameters.ts
@@ -0,0 +1,12 @@
+import { ZambdaInput } from '../types';
+
+export function validateRequestParameters(input: ZambdaInput): Pick {
+ console.group('validateRequestParameters');
+
+ console.groupEnd();
+ console.debug('validateRequestParameters success');
+
+ return {
+ secrets: input.secrets,
+ };
+}
diff --git a/packages/telemed-intake/app/index.html b/packages/telemed-intake/app/index.html
index 9c82b14a7..69660b50a 100644
--- a/packages/telemed-intake/app/index.html
+++ b/packages/telemed-intake/app/index.html
@@ -2,7 +2,8 @@
-
+
+
Ottehr Telemedicine
diff --git a/packages/telemed-intake/app/ottehr-icon-256.png b/packages/telemed-intake/app/ottehr-icon-256.png
new file mode 100644
index 000000000..dbf10a9bc
Binary files /dev/null and b/packages/telemed-intake/app/ottehr-icon-256.png differ
diff --git a/packages/telemed-intake/app/ottehr-icon.ico b/packages/telemed-intake/app/ottehr-icon.ico
new file mode 100644
index 000000000..b82190225
Binary files /dev/null and b/packages/telemed-intake/app/ottehr-icon.ico differ
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c6f344499..ff54cb218 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -338,8 +338,8 @@ importers:
specifier: ^5.0.20
version: 5.0.20(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.16.5(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.5(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(date-fns@2.30.0)(dayjs@1.11.12)(luxon@3.4.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@photonhealth/elements':
- specifier: ^0.9.1-rc.1
- version: 0.9.2(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(solid-js@1.8.19)(ts-node@10.9.2(@types/node@18.19.42)(typescript@4.9.5))
+ specifier: ^0.12.2
+ version: 0.12.3(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(solid-js@1.8.19)(ts-node@10.9.2(@types/node@20.14.12)(typescript@4.9.5))
'@twilio/conversations':
specifier: ^2.4.1
version: 2.5.0
@@ -361,6 +361,9 @@ importers:
fast-json-patch:
specifier: ^3.1.1
version: 3.1.1
+ notistack:
+ specifier: ^3.0.1
+ version: 3.0.1(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-chartjs-2:
specifier: ^5.2.0
version: 5.2.0(chart.js@4.4.3)(react@18.3.1)
@@ -2582,8 +2585,8 @@ packages:
resolution: {integrity: sha512-SQa/6mJky63OkZw/gcA1N1SnprugqiP9VsTtE1VD2j4inqePlhD9xe4mU3WLlGtcbSBdlWbkuEpj2JexQ/cLDQ==}
engines: {node: '>=16'}
- '@photonhealth/elements@0.9.2':
- resolution: {integrity: sha512-HHKi9ABYJEwhaGjmnw4xcEoNQ5ATOLTizyKp40MJtP8Pxuf7ZxTVzDENg2abDPrMmzyMptO18zyEsLgV6afB1g==}
+ '@photonhealth/elements@0.12.3':
+ resolution: {integrity: sha512-ASNHp/nVENOfGI6izFD+2YqzTH1biRusblIJTJTFLu8V2Hbw8CblUW6NJ142Tcrgp3FtuxhxGYTYUwbZp99xrg==}
'@photonhealth/sdk@1.3.3':
resolution: {integrity: sha512-YNEICmz+nvwfOnYJlHsEqNjiXhLbnS0utcROv1iyfTVmAPQQVaAmZNPzmxbBTb2egX56JU/DKdGeGjkpRfT2hg==}
@@ -6176,6 +6179,11 @@ packages:
globrex@0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
+ goober@2.1.14:
+ resolution: {integrity: sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==}
+ peerDependencies:
+ csstype: ^3.0.10
+
gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
@@ -7882,6 +7890,13 @@ packages:
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
engines: {node: '>=10'}
+ notistack@3.0.1:
+ resolution: {integrity: sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==}
+ engines: {node: '>=12.0.0', npm: '>=6.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+
npm-registry-utilities@1.0.0:
resolution: {integrity: sha512-9xYfSJy2IFQw1i6462EJzjChL9e65EfSo2Cw6kl0EFeDp05VvU+anrQk3Fc0d1MbVCq7rWIxeer89O9SUQ/uOg==}
engines: {node: '>=12.0'}
@@ -14258,17 +14273,17 @@ snapshots:
dependencies:
pako: 1.0.11
- '@photonhealth/components@0.0.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.42)(typescript@4.9.5))':
+ '@photonhealth/components@0.0.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@20.14.12)(typescript@4.9.5))':
dependencies:
'@photonhealth/sdk': 1.3.3(@types/react@18.3.3)(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@tailwindcss/typography': 0.5.13(tailwindcss@3.4.7(ts-node@10.9.2(@types/node@18.19.42)(typescript@4.9.5)))
+ '@tailwindcss/typography': 0.5.13(tailwindcss@3.4.7(ts-node@10.9.2(@types/node@20.14.12)(typescript@4.9.5)))
clsx: 1.2.1
graphql: 16.9.0
graphql-tag: 2.12.6(graphql@16.9.0)
jwt-decode: 3.1.2
solid-js: 1.8.19
superstruct: 1.0.4
- tailwindcss: 3.4.7(ts-node@10.9.2(@types/node@18.19.42)(typescript@4.9.5))
+ tailwindcss: 3.4.7(ts-node@10.9.2(@types/node@20.14.12)(typescript@4.9.5))
transitivePeerDependencies:
- '@types/react'
- graphql-ws
@@ -14277,9 +14292,9 @@ snapshots:
- subscriptions-transport-ws
- ts-node
- '@photonhealth/elements@0.9.2(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(solid-js@1.8.19)(ts-node@10.9.2(@types/node@18.19.42)(typescript@4.9.5))':
+ '@photonhealth/elements@0.12.3(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(solid-js@1.8.19)(ts-node@10.9.2(@types/node@20.14.12)(typescript@4.9.5))':
dependencies:
- '@photonhealth/components': 0.0.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@18.19.42)(typescript@4.9.5))
+ '@photonhealth/components': 0.0.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@types/node@20.14.12)(typescript@4.9.5))
'@photonhealth/sdk': 1.3.3(@types/react@18.3.3)(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@shoelace-style/shoelace': 2.15.1(@types/react@18.3.3)
'@solid-primitives/scheduled': 1.4.3(solid-js@1.8.19)
@@ -15246,13 +15261,13 @@ snapshots:
dependencies:
defer-to-connect: 2.0.1
- '@tailwindcss/typography@0.5.13(tailwindcss@3.4.7(ts-node@10.9.2(@types/node@18.19.42)(typescript@4.9.5)))':
+ '@tailwindcss/typography@0.5.13(tailwindcss@3.4.7(ts-node@10.9.2(@types/node@20.14.12)(typescript@4.9.5)))':
dependencies:
lodash.castarray: 4.4.0
lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2
postcss-selector-parser: 6.0.10
- tailwindcss: 3.4.7(ts-node@10.9.2(@types/node@18.19.42)(typescript@4.9.5))
+ tailwindcss: 3.4.7(ts-node@10.9.2(@types/node@20.14.12)(typescript@4.9.5))
'@tanstack/solid-virtual@3.8.3(solid-js@1.8.19)':
dependencies:
@@ -16789,7 +16804,7 @@ snapshots:
buffer@4.9.2:
dependencies:
base64-js: 1.5.1
- ieee754: 1.1.13
+ ieee754: 1.2.1
isarray: 1.0.0
buffer@5.7.1:
@@ -18873,6 +18888,10 @@ snapshots:
globrex@0.1.2: {}
+ goober@2.1.14(csstype@3.1.3):
+ dependencies:
+ csstype: 3.1.3
+
gopd@1.0.1:
dependencies:
get-intrinsic: 1.2.4
@@ -21221,6 +21240,15 @@ snapshots:
normalize-url@6.1.0: {}
+ notistack@3.0.1(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ clsx: 1.2.1
+ goober: 2.1.14(csstype@3.1.3)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ transitivePeerDependencies:
+ - csstype
+
npm-registry-utilities@1.0.0:
dependencies:
ext: 1.7.0
@@ -21767,6 +21795,14 @@ snapshots:
postcss: 8.4.40
ts-node: 10.9.2(@types/node@18.19.42)(typescript@4.9.5)
+ postcss-load-config@4.0.2(postcss@8.4.40)(ts-node@10.9.2(@types/node@20.14.12)(typescript@4.9.5)):
+ dependencies:
+ lilconfig: 3.1.2
+ yaml: 2.5.0
+ optionalDependencies:
+ postcss: 8.4.40
+ ts-node: 10.9.2(@types/node@20.14.12)(typescript@4.9.5)
+
postcss-loader@6.2.1(postcss@8.4.40)(webpack@5.93.0(esbuild@0.18.20)):
dependencies:
cosmiconfig: 7.1.0
@@ -23633,6 +23669,33 @@ snapshots:
transitivePeerDependencies:
- ts-node
+ tailwindcss@3.4.7(ts-node@10.9.2(@types/node@20.14.12)(typescript@4.9.5)):
+ dependencies:
+ '@alloc/quick-lru': 5.2.0
+ arg: 5.0.2
+ chokidar: 3.6.0
+ didyoumean: 1.2.2
+ dlv: 1.1.3
+ fast-glob: 3.3.2
+ glob-parent: 6.0.2
+ is-glob: 4.0.3
+ jiti: 1.21.6
+ lilconfig: 2.1.0
+ micromatch: 4.0.7
+ normalize-path: 3.0.0
+ object-hash: 3.0.0
+ picocolors: 1.0.1
+ postcss: 8.4.40
+ postcss-import: 15.1.0(postcss@8.4.40)
+ postcss-js: 4.0.1(postcss@8.4.40)
+ postcss-load-config: 4.0.2(postcss@8.4.40)(ts-node@10.9.2(@types/node@20.14.12)(typescript@4.9.5))
+ postcss-nested: 6.2.0(postcss@8.4.40)
+ postcss-selector-parser: 6.1.1
+ resolve: 1.22.8
+ sucrase: 3.35.0
+ transitivePeerDependencies:
+ - ts-node
+
tapable@1.1.3: {}
tapable@2.2.1: {}
@@ -23827,6 +23890,25 @@ snapshots:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
+ ts-node@10.9.2(@types/node@20.14.12)(typescript@4.9.5):
+ dependencies:
+ '@cspotcode/source-map-support': 0.8.1
+ '@tsconfig/node10': 1.0.11
+ '@tsconfig/node12': 1.0.11
+ '@tsconfig/node14': 1.0.3
+ '@tsconfig/node16': 1.0.4
+ '@types/node': 20.14.12
+ acorn: 8.12.1
+ acorn-walk: 8.3.3
+ arg: 4.1.3
+ create-require: 1.1.1
+ diff: 4.0.2
+ make-error: 1.3.6
+ typescript: 4.9.5
+ v8-compile-cache-lib: 3.0.1
+ yn: 3.1.1
+ optional: true
+
tsconfck@3.1.1(typescript@4.9.5):
optionalDependencies:
typescript: 4.9.5