diff --git a/.env b/.env
index e0aed43..75e6c54 100644
--- a/.env
+++ b/.env
@@ -3,7 +3,7 @@ HTTPS_CERT_PATH = server.cert
HTTPS_KEY_PATH = server.key
VITE_ALT_DRUG = true
VITE_AUTH = http://localhost:8180
-VITE_CDS_SERVICE = http://localhost:8090/cds-services
+VITE_CDS_SERVICE = http://localhost:8090/etasu/reset
VITE_CLIENT = app-login
VITE_CLIENT_SCOPES = launch offline_access openid profile user/Patient.read patient/Patient.read user/Practitioner.read
VITE_DEFAULT_USER = pra1234
@@ -15,11 +15,8 @@ VITE_GENERATE_JWT = true
VITE_GH_PAGES=false
VITE_HOMEPAGE = http://localhost:8080
VITE_LAUNCH_URL = http://localhost:4040/launch
-VITE_ORDER_SELECT = rems-order-select
-VITE_ORDER_SIGN = rems-order-sign
VITE_PASSWORD = alice
VITE_PATIENT_FHIR_QUERY = Patient?_sort=identifier&_count=12
-VITE_PATIENT_VIEW = rems-patient-view
VITE_PIMS_SERVER = http://localhost:5051/doctorOrders/api/addRx
VITE_PUBLIC_KEYS = http://localhost:3000/request-generator/.well-known/jwks.json
VITE_REALM = ClientFhirServer
diff --git a/.gitignore b/.gitignore
index 5c7ad5e..fa3e111 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,3 +64,6 @@ src/db.json
# Environment variable files
.env.local
+
+# Mac OS files
+**/.DS_Store
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index b98d3f8..6c28420 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28,6 +28,7 @@
"react-dom": "^17.0.0",
"react-markdown": "^8.0.7",
"react-router-dom": "^6.17.0",
+ "uuid": "^9.0.1",
"vite": "^5.1.6",
"vite-tsconfig-paths": "^4.3.2"
},
@@ -2993,6 +2994,16 @@
"safe-json-stringify": "~1"
}
},
+ "node_modules/@expo/bunyan/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/@expo/cli": {
"version": "0.17.7",
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.17.7.tgz",
@@ -4577,6 +4588,16 @@
"node": ">=12"
}
},
+ "node_modules/@expo/rudder-sdk-node/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/@expo/sdk-runtime-versions": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz",
@@ -22810,11 +22831,13 @@
}
},
"node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "optional": true,
- "peer": true,
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
"bin": {
"uuid": "dist/bin/uuid"
}
diff --git a/src/.DS_Store b/src/.DS_Store
deleted file mode 100644
index 4efe7f2..0000000
Binary files a/src/.DS_Store and /dev/null differ
diff --git a/src/components/RequestBox/RequestBox.jsx b/src/components/RequestBox/RequestBox.jsx
index 8ec1e8f..2adb992 100644
--- a/src/components/RequestBox/RequestBox.jsx
+++ b/src/components/RequestBox/RequestBox.jsx
@@ -17,7 +17,6 @@ const RequestBox = props => {
});
const {
- callback,
prefetchedResources,
submitInfo,
patient,
@@ -29,7 +28,8 @@ const RequestBox = props => {
defaultUser,
smartAppUrl,
client,
- pimsUrl
+ pimsUrl,
+ getRemsAdminUrl
} = props;
const emptyField = empty;
@@ -61,12 +61,8 @@ const RequestBox = props => {
};
const submitOrderSign = request => {
- submitInfo(prepPrefetch(), request, patient, 'order-sign');
- };
-
- const submit = () => {
if (!_.isEmpty(request)) {
- submitOrderSign(request);
+ submitInfo(prepPrefetch(), request, patient, 'order-sign');
}
};
@@ -149,8 +145,7 @@ const RequestBox = props => {
renderedPrefetches.set(resourceKey, renderedList);
});
- console.log(renderedPrefetches);
- console.log(Object.entries(renderedPrefetches));
+
return (
Prefetched
@@ -284,6 +279,7 @@ const RequestBox = props => {
const disableSendToCRD = isOrderNotSelected() || loading;
const disableSendRx = isOrderNotSelected() || loading;
const disableLaunchSmartOnFhir = isPatientNotSelected();
+ const orderSignRemsAdmin = getRemsAdminUrl(request, 'order-sign');
return (
<>
@@ -311,7 +307,7 @@ const RequestBox = props => {
-
@@ -230,9 +243,7 @@ const SettingsSection = props => {
control={
{
- updateSetting(key, event.target.checked);
- }}
+ onChange={event => updateSetting(key, event.target.checked)}
/>
}
label={display}
@@ -248,35 +259,162 @@ const SettingsSection = props => {
);
}
})}
+
+
+
+
+
+
+
+ Medication Display
+ Medication RxNorm Code
+ CDS Hook
+ REMS Admin Endpoint
+ {/* This empty TableCell corresponds to the add and delete
+ buttons. It is used to fill up the sticky header which
+ will appear over the gray/white table rows. */}
+
+
+
+
+ {Object.entries(state.medicationRequestToRemsAdmins).map(([key, row]) => {
+ return (
+
+
+
+ dispatch({
+ type: actionTypes.updateCdsHookSetting,
+ settingId: key,
+ value: { display: event.target.value }
+ })
+ }
+ sx={{ width: '100%' }}
+ />
+
+
+
+ dispatch({
+ type: actionTypes.updateCdsHookSetting,
+ settingId: key,
+ value: { rxnorm: event.target.value }
+ })
+ }
+ sx={{ width: '100%' }}
+ />
+
+
+
+
+
+
+ dispatch({
+ type: actionTypes.updateCdsHookSetting,
+ settingId: key,
+ value: { remsAdmin: event.target.value }
+ })
+ }
+ sx={{ width: '100%' }}
+ />
+
+
+
+
+ dispatch({ type: actionTypes.addCdsHookSetting, settingId: key })
+ }
+ size="large"
+ >
+
+
+
+
+
+ dispatch({ type: actionTypes.deleteCdsHookSetting, settingId: key })
+ }
+ size="large"
+ >
+
+
+
+
+
+ );
+ })}
+
+
+
+
+
+ {/* spacer */}
+
+
+
{resetHeaderDefinitions.map(({ key, display, reset, variant }) => {
return (
-
+
{display}
);
})}
- {/* spacer */}
-
-
+
-
+
+
Reset
-
+
Save
-
+
);
};
diff --git a/src/components/SMARTBox/PatientBox.jsx b/src/components/SMARTBox/PatientBox.jsx
index c347976..fea2e08 100644
--- a/src/components/SMARTBox/PatientBox.jsx
+++ b/src/components/SMARTBox/PatientBox.jsx
@@ -463,11 +463,7 @@ const PatientBox = props => {
const makeResponseTable = (columns, options, type, patient) => {
return (
-
+
diff --git a/src/containers/ContextProvider/reducer.js b/src/containers/ContextProvider/reducer.js
index 051b364..d73f500 100644
--- a/src/containers/ContextProvider/reducer.js
+++ b/src/containers/ContextProvider/reducer.js
@@ -1,12 +1,80 @@
-import { headerDefinitions } from '../../util/data';
+import { headerDefinitions, medicationRequestToRemsAdmins } from '../../util/data';
+import { v4 as uuidv4 } from 'uuid';
+
export const actionTypes = Object.freeze({
updatePatient: 'update_patient', // {type, value}
updateSetting: 'update_setting', // {type, settingId, value}
- flagStartup: 'flag_startup' // {type}
+ flagStartup: 'flag_startup', // {type}
+ deleteCdsHookSetting: 'DELETE_CDS_HOOK_SETTING', // {type, settingId}
+ addCdsHookSetting: 'ADD_CDS_HOOK_SETTING', // {type}
+ updateCdsHookSetting: 'UPDATE_CDS_HOOK_SETTING', // {type, settingId, value}
+ resetSettings: 'RESET_SETTINGS' // {type}
});
+
+const getNewStateWithoutCdsHookSetting = (state, settingId) => {
+ const newState = { ...state, medicationRequestToRemsAdmins: {} };
+ for (const key of Object.keys(state.medicationRequestToRemsAdmins)) {
+ if (key !== settingId) {
+ newState.medicationRequestToRemsAdmins[key] = state.medicationRequestToRemsAdmins[key];
+ }
+ }
+ return newState;
+};
+
+const getNewStateWithNewCdsHookSetting = (state, settingId) => {
+ const original = Object.entries(state.medicationRequestToRemsAdmins);
+ const indexOfSiblingToInsertBelow = original.findIndex(([key, _value]) => key === settingId);
+ const firstHalf =
+ indexOfSiblingToInsertBelow === 0
+ ? [original[0]]
+ : original.slice(0, indexOfSiblingToInsertBelow + 1);
+ const secondHalf =
+ indexOfSiblingToInsertBelow === 0
+ ? original.slice(indexOfSiblingToInsertBelow)
+ : original.slice(indexOfSiblingToInsertBelow - 1);
+
+ const newState = { ...state, medicationRequestToRemsAdmins: {} };
+
+ for (const pair of firstHalf) {
+ const [key, value] = pair;
+ newState.medicationRequestToRemsAdmins[key] = value;
+ }
+
+ newState.medicationRequestToRemsAdmins[uuidv4()] = {
+ rxnorm: 'Fill out Medication RxNorm Code',
+ display: 'Fill out Medication Display Name',
+ hook: 'order-sign',
+ remsAdmin: 'REMS Admin URL for CDS Hook'
+ };
+
+ for (const pair of secondHalf) {
+ const [key, value] = pair;
+ newState.medicationRequestToRemsAdmins[key] = value;
+ }
+
+ return newState;
+};
+
// todo: add an enum that defines possible settings
export const reducer = (state, action) => {
switch (action.type) {
+ case actionTypes.deleteCdsHookSetting:
+ return getNewStateWithoutCdsHookSetting(state, action.settingId);
+ case actionTypes.addCdsHookSetting:
+ return getNewStateWithNewCdsHookSetting(state, action.settingId);
+ case actionTypes.updateCdsHookSetting:
+ return {
+ ...state,
+ medicationRequestToRemsAdmins: {
+ ...state.medicationRequestToRemsAdmins,
+ [action.settingId]: {
+ ...state.medicationRequestToRemsAdmins[action.settingId],
+ ...action.value
+ }
+ }
+ };
+ case actionTypes.resetSettings:
+ return { ...initialState, startup: true };
case actionTypes.updateSetting:
return {
...state,
@@ -27,13 +95,24 @@ export const reducer = (state, action) => {
}
};
-const initialState = {
- patient: null,
- startup: false,
- redirect: ''
-};
-Object.keys(headerDefinitions).forEach(e => {
- initialState[e] = headerDefinitions[e].default; // fill default settings values
-});
+export const initialState = (() => {
+ let state = {
+ patient: null,
+ startup: false,
+ redirect: '',
+ medicationRequestToRemsAdmins: {}
+ };
+
+ Object.keys(headerDefinitions).forEach(key => {
+ state[key] = headerDefinitions[key].default;
+ });
-export { initialState };
+ medicationRequestToRemsAdmins.forEach(row => {
+ const { rxnorm, display, hookEndpoints } = row;
+ hookEndpoints.forEach(({ hook, remsAdmin }) => {
+ const key = `${rxnorm}_${hook}`;
+ state.medicationRequestToRemsAdmins[key] = { rxnorm, display, hook, remsAdmin };
+ });
+ });
+ return state;
+})();
diff --git a/src/containers/RequestBuilder.jsx b/src/containers/RequestBuilder.jsx
index 5bb65cf..76d176c 100644
--- a/src/containers/RequestBuilder.jsx
+++ b/src/containers/RequestBuilder.jsx
@@ -8,6 +8,7 @@ import RequestBox from '../components/RequestBox/RequestBox.jsx';
import buildRequest from '../util/buildRequest.js';
import { types } from '../util/data.js';
import { createJwt } from '../util/auth.js';
+import { getMedicationSpecificRemsAdminUrl } from '../util/util.js';
import Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary';
@@ -105,11 +106,18 @@ const RequestBuilder = props => {
const submitInfo = (prefetch, request, patient, hook) => {
console.log('Initiating form submission ', types.info);
+ const remsAdminUrl = getMedicationSpecificRemsAdminUrl(request, globalState, hook);
+
setState(prevState => ({
...prevState,
- loading: true,
+ loading: !!remsAdminUrl,
patient
}));
+
+ if (!remsAdminUrl) {
+ return;
+ }
+
const hookConfig = {
includeConfig: globalState.includeConfig,
alternativeTherapy: globalState.alternativeTherapy
@@ -126,17 +134,6 @@ const RequestBuilder = props => {
hook,
hookConfig
);
- let cdsUrl = globalState.cdsUrl;
- if (hook === 'order-sign') {
- cdsUrl = cdsUrl + '/' + globalState.orderSign;
- } else if (hook === 'order-select') {
- cdsUrl = cdsUrl + '/' + globalState.orderSelect;
- } else if (hook === 'patient-view') {
- cdsUrl = cdsUrl + '/' + globalState.patientView;
- } else {
- console.log("ERROR: unknown hook type: '", hook);
- return;
- }
let baseUrl = globalState.baseUrl;
@@ -144,12 +141,12 @@ const RequestBuilder = props => {
'Content-Type': 'application/json'
};
if (globalState.generateJsonToken) {
- const jwt = 'Bearer ' + createJwt(baseUrl, cdsUrl);
+ const jwt = 'Bearer ' + createJwt(baseUrl, remsAdminUrl);
headers.authorization = jwt;
}
try {
- fetch(cdsUrl, {
+ fetch(remsAdminUrl, {
method: 'POST',
headers: new Headers(headers),
body: JSON.stringify(json_request)
@@ -298,6 +295,9 @@ const RequestBuilder = props => {
defaultUser={globalState.defaultUser}
loading={state.loading}
patientFhirQuery={globalState.patientFhirQuery}
+ getRemsAdminUrl={(request, hook) =>
+ getMedicationSpecificRemsAdminUrl(request, globalState, hook)
+ }
/>
)}
diff --git a/src/util/data.js b/src/util/data.js
index 49e8428..d308619 100644
--- a/src/util/data.js
+++ b/src/util/data.js
@@ -12,7 +12,7 @@ const headerDefinitions = {
default: env.get('VITE_EHR_BASE').asString()
},
cdsUrl: {
- display: 'REMS Admin',
+ display: 'REMS Admin Reset Url',
type: 'input',
default: env.get('VITE_CDS_SERVICE').asString()
},
@@ -46,26 +46,11 @@ const headerDefinitions = {
type: 'input',
default: env.get('VITE_LAUNCH_URL').asString()
},
- orderSelect: {
- display: 'Order Select Rest End Point',
- type: 'input',
- default: env.get('VITE_ORDER_SELECT').asString()
- },
- orderSign: {
- display: 'Order Sign Rest End Point',
- type: 'input',
- default: env.get('VITE_ORDER_SIGN').asString()
- },
patientFhirQuery: {
display: 'Patient FHIR Query',
type: 'input',
default: env.get('VITE_PATIENT_FHIR_QUERY').asString()
},
- patientView: {
- display: 'Patient View Rest End Point',
- type: 'input',
- default: env.get('VITE_PATIENT_VIEW').asString()
- },
pimsUrl: {
display: 'PIMS Server',
type: 'input',
@@ -93,6 +78,45 @@ const headerDefinitions = {
}
};
+const medicationRequestToRemsAdmins = Object.freeze([
+ {
+ rxnorm: 2183126,
+ display: 'Turalio 200 MG Oral Capsule',
+ hookEndpoints: [
+ { hook: 'order-sign', remsAdmin: 'http://localhost:8090/cds-services/rems-order-sign' },
+ { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/rems-order-select' },
+ { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/rems-patient-view' }
+ ]
+ },
+ {
+ rxnorm: 6064,
+ display: 'Isotretinoin 20 MG Oral Capsule',
+ hookEndpoints: [
+ { hook: 'order-sign', remsAdmin: 'http://localhost:8090/cds-services/rems-order-sign' },
+ { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/rems-order-select' },
+ { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/rems-patient-view' }
+ ]
+ },
+ {
+ rxnorm: 1237051,
+ display: 'TIRF 200 UG Oral Transmucosal Lozenge',
+ hookEndpoints: [
+ { hook: 'order-sign', remsAdmin: 'http://localhost:8090/cds-services/rems-order-sign' },
+ { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/rems-order-select' },
+ { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/rems-patient-view' }
+ ]
+ },
+ {
+ rxnorm: 1666386,
+ display: 'Addyi 100 MG Oral Tablet',
+ hookEndpoints: [
+ { hook: 'order-sign', remsAdmin: 'http://localhost:8090/cds-services/rems-order-sign' },
+ { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/rems-order-select' },
+ { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/rems-patient-view' }
+ ]
+ }
+]);
+
const types = {
error: 'errorClass',
info: 'infoClass',
@@ -218,4 +242,12 @@ const shortNameMap = {
'http://hl7.org/fhir/sid/ndc': 'NDC'
};
-export { defaultValues, genderOptions, headerDefinitions, shortNameMap, stateOptions, types };
+export {
+ defaultValues,
+ genderOptions,
+ headerDefinitions,
+ shortNameMap,
+ stateOptions,
+ types,
+ medicationRequestToRemsAdmins
+};
diff --git a/src/util/util.js b/src/util/util.js
index 244e805..b76c684 100644
--- a/src/util/util.js
+++ b/src/util/util.js
@@ -92,4 +92,30 @@ function getEtasu(etasuUrl, responseCallback) {
);
}
-export { retrieveLaunchContext, getEtasu };
+const getMedicationSpecificRemsAdminUrl = (request, globalState, hook) => {
+ const display = request.medicationCodeableConcept?.coding?.[0]?.display;
+ const rxnorm = request.medicationCodeableConcept?.coding?.[0]?.code;
+
+ if (!rxnorm) {
+ console.log("ERROR: unknown MedicationRequest code: '", rxnorm);
+ return undefined;
+ }
+
+ if (!(hook === 'patient-view' || hook === 'order-sign' || hook === 'order-select')) {
+ console.log(`ERROR: unknown hook type: ${hook}`);
+ return undefined;
+ }
+
+ const cdsUrl = Object.values(globalState.medicationRequestToRemsAdmins).find(
+ value => Number(value.rxnorm) === Number(rxnorm) && value.hook === hook
+ )?.remsAdmin;
+
+ if (!cdsUrl) {
+ console.log(`Medication ${display} is not a REMS medication`);
+ return undefined;
+ }
+
+ return cdsUrl;
+};
+
+export { retrieveLaunchContext, getEtasu, getMedicationSpecificRemsAdminUrl };