Skip to content

Commit

Permalink
Merge pull request #553 from masslight/develop
Browse files Browse the repository at this point in the history
Release Oswego v0.15
  • Loading branch information
GiladSchneider authored Nov 4, 2024
2 parents 36e8a62 + 04c4ad9 commit 2467fda
Show file tree
Hide file tree
Showing 55 changed files with 1,151 additions and 510 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ottehr",
"version": "0.14.0",
"version": "0.15.0",
"private": true,
"scripts": {
"test": "pnpm recursive run test",
Expand Down
63 changes: 63 additions & 0 deletions packages/ehr-utils/lib/fhir/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Communication | RelatedPerson>({
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;
Expand Down Expand Up @@ -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[];
}
57 changes: 56 additions & 1 deletion packages/ehr-utils/lib/fhir/index.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/ehr-utils/lib/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './formatPhoneNumber';
export * from './paperwork';
export * from './practitioner';
export * from './telemed-appointment.helper';
export * from './mappers';
99 changes: 99 additions & 0 deletions packages/ehr-utils/lib/helpers/mappers.ts
Original file line number Diff line number Diff line change
@@ -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<RelatedPersonMaps> => {
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<string, RelatedPerson[]> {
const rpsToPatientIdMap: Record<string, RelatedPerson[]> = {};

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<string, RelatedPerson> {
const rpToIdMap: Record<string, RelatedPerson> = {};

allRps.forEach((rp) => {
rpToIdMap['RelatedPerson/' + rp.id] = rp;
});

return rpToIdMap;
}

function mapRelatedPersonsRefsToPhoneNumber(allRps: RelatedPerson[]): Record<string, string[]> {
const relatedPersonRefToPhoneNumber: Record<string, string[]> = {};

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<string, RelatedPerson>,
rpsRefsToPhoneNumberMap: Record<string, string[]>,
): Record<string, Communication[]> {
const commsToRpRefMap: Record<string, Communication[]> = {};

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/ehr-utils/lib/types/appointment.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
14 changes: 14 additions & 0 deletions packages/ehr-utils/lib/types/user.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
2 changes: 1 addition & 1 deletion packages/ehr-utils/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion packages/ottehr-components/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
6 changes: 5 additions & 1 deletion packages/telemed-ehr/app/env/.env.local-template
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
VITE_APP_QRS_URL=http://localhost:3002
VITE_APP_PHOTON_ORG_ID=
VITE_APP_PHOTON_CLIENT_ID=
VITE_APP_PHOTON_CONNECTION_NAME=
4 changes: 2 additions & 2 deletions packages/telemed-ehr/app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "telemed-ehr-app",
"version": "0.14.0",
"version": "0.15.0",
"private": true,
"browserslist": {
"production": [
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/telemed-ehr/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
>
<Outlet />
</photon-client>
Expand Down
Loading

0 comments on commit 2467fda

Please sign in to comment.