From 334399cc3065a6e9568b01ea94b4fc2dc7faf812 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Wed, 27 Mar 2024 12:39:32 -0400 Subject: [PATCH 01/13] Ignore .DS_Store files --- .gitignore | 3 +++ src/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 3 insertions(+) delete mode 100644 src/.DS_Store diff --git a/.gitignore b/.gitignore index 5c7ad5ef..fa3e1111 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/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 4efe7f2488c12a7abaa0448d03911fb783443795..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJ5EC}5S)b+5i}_&eFbh{Md1Wo07xj1BA!H`e--D-(K7ofh+fi#CYqJjW3P8? zd5X7h0od|q^9U>e%;}E!^@1ciWrg3GhXq5PrTy`yGi!xfOGG##z5ZpU;bvd z-o6dP))Qn>Knh3!DIf);z{M1((mKDt*oitw3P^!#QNX_sjqcbB$He$_aEKOwIAb`B z^XMgr%>%?=eJ0Q^+b(QKnffy zaGT48*Z(K_ng0Knq?Htq0#~Jg%~sE=C7)Eab@Di`wT=El_nbZ5jq{*zh;mGfa?FL7 e}w-uuR From dbc71c39c1b30b0349e2903f8cc0a26fcbd47c60 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Thu, 28 Mar 2024 15:50:04 -0400 Subject: [PATCH 02/13] Fix typo in sx prop --- src/components/SMARTBox/PatientBox.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SMARTBox/PatientBox.jsx b/src/components/SMARTBox/PatientBox.jsx index c3479761..f08cff42 100644 --- a/src/components/SMARTBox/PatientBox.jsx +++ b/src/components/SMARTBox/PatientBox.jsx @@ -466,7 +466,7 @@ const PatientBox = props => { From 17e837056e84c25decb4963bd5b443041acff570 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Thu, 28 Mar 2024 15:50:30 -0400 Subject: [PATCH 03/13] Update package-lock.json --- package-lock.json | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index b98d3f8a..6c28420e 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" } From 791e2007e87efe733ca7ca51752d3763313feb24 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Thu, 28 Mar 2024 16:00:44 -0400 Subject: [PATCH 04/13] Create table in SettingsSection and store initial state for all REMS drugs --- .../RequestDashboard/SettingsSection.jsx | 220 ++++++++++++------ src/containers/ContextProvider/reducer.js | 12 +- src/util/data.js | 49 +++- 3 files changed, 210 insertions(+), 71 deletions(-) diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index c373a9a2..e4c4345a 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -1,31 +1,65 @@ import React, { memo, useState, useEffect } from 'react'; -import { Button, Box, FormControlLabel, Grid, Checkbox, TextField } from '@mui/material'; +import { + Box, + Button, + Checkbox, + FormControlLabel, + Grid, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField +} from '@mui/material'; -import useStyles from './styles'; import env from 'env-var'; import FHIR from 'fhirclient'; -import { headerDefinitions, types } from '../../util/data'; +import { headerDefinitions, medicationRequestToRemsAdmins } from '../../util/data'; import { actionTypes } from '../../containers/ContextProvider/reducer'; import { SettingsContext } from '../../containers/ContextProvider/SettingsProvider'; const SettingsSection = props => { - const classes = useStyles(); const [state, dispatch] = React.useContext(SettingsContext); const [headers, setHeaders] = useState([]); const [originalValues, setOriginalValues] = useState([]); useEffect(() => { - const headers = Object.keys(headerDefinitions) - .map(key => ({ ...headerDefinitions[key], key })) + const remsAdminHookEndpoints = {}; + medicationRequestToRemsAdmins.forEach(row => { + const { rxnorm, display, hookEndpoints } = row; + hookEndpoints.forEach(endpoint => { + const { hook, remsAdmin } = endpoint; + const key = `${rxnorm}_${hook}`; + remsAdminHookEndpoints[key] = { + display, + hook, + type: 'tableInput', + default: remsAdmin + }; + }); + }); + + const headers = [ + ...Object.keys(headerDefinitions).map(key => ({ ...headerDefinitions[key], key })), + ...Object.keys(remsAdminHookEndpoints).map(key => ({ ...remsAdminHookEndpoints[key], key })) + ] // Display the fields in descending order of type. If two fields are the same type, then sort by ascending order of display text. .sort( (self, other) => -self.type.localeCompare(other.type) || self.display.localeCompare(other.display) ); setHeaders(headers); - const originals = Object.keys(headerDefinitions).map(key => [key, state[key]]); + + const originals = [ + ...Object.keys(headerDefinitions).map(key => [key, state[key]]), + ...Object.keys(remsAdminHookEndpoints).map(key => [key, state[key]]) + ]; setOriginalValues(originals); + JSON.parse(localStorage.getItem('reqgenSettings') || '[]').forEach(element => { try { updateSetting(element[0], element[1]); @@ -41,6 +75,7 @@ const SettingsSection = props => { type: actionTypes.flagStartup }); }, []); + const updateSetting = (key, value) => { dispatch({ type: actionTypes.updateSetting, @@ -48,6 +83,7 @@ const SettingsSection = props => { value: value }); }; + const saveSettings = () => { const headers = Object.keys(headerDefinitions).map(key => [key, state[key]]); localStorage.setItem('reqgenSettings', JSON.stringify(headers)); @@ -62,6 +98,7 @@ const SettingsSection = props => { } }); }; + const clearQuestionnaireResponses = ({ defaultUser }) => () => { @@ -105,6 +142,7 @@ const SettingsSection = props => { console.log(error); }); }; + const resetRemsAdmin = ({ cdsUrl }) => () => { @@ -123,6 +161,7 @@ const SettingsSection = props => { console.log(error); }); }; + const clearMedicationDispenses = ({ ehrUrl, access_token }) => () => { @@ -153,6 +192,7 @@ const SettingsSection = props => { console.log(e); }); }; + const reconnectEhr = ({ baseUrl, redirect }) => () => { @@ -163,6 +203,7 @@ const SettingsSection = props => { scope: env.get('VITE_CLIENT_SCOPES').asString() }); }; + const resetHeaderDefinitions = [ { display: 'Reset PIMS Database', @@ -195,59 +236,61 @@ const SettingsSection = props => { let firstCheckbox = true; let showBreak = true; return ( - - - {headers.map(({ key, type, display }) => { - switch (type) { - case 'input': - return ( - -
- { - updateSetting(key, event.target.value); - }} - sx={{ width: '100%' }} - /> -
-
- ); - case 'check': - if (firstCheckbox) { - firstCheckbox = false; - showBreak = true; - } else { - showBreak = false; - } - return ( - - {showBreak ? : ''} - - { - updateSetting(key, event.target.checked); - }} - /> - } - label={display} - /> + + + {headers + .filter(header => header.type !== 'tableInput') + .map(({ key, type, display }) => { + switch (type) { + case 'input': + return ( + +
+ { + updateSetting(key, event.target.value); + }} + sx={{ width: '100%' }} + /> +
-
- ); - default: - return ( -
-

{display}

-
- ); - } - })} + ); + case 'check': + if (firstCheckbox) { + firstCheckbox = false; + showBreak = true; + } else { + showBreak = false; + } + return ( + + {showBreak ? : ''} + + { + updateSetting(key, event.target.checked); + }} + /> + } + label={display} + /> + + + ); + default: + return ( +
+

{display}

+
+ ); + } + })} {resetHeaderDefinitions.map(({ key, display, reset, variant }) => { return ( @@ -257,26 +300,65 @@ const SettingsSection = props => { ); })} - {/* spacer */} -
- + + + + +
+ + + Medication + CDS Hook + REMS Admin Endpoint + + + + {headers + .filter(header => header.type === 'tableInput') + .map(({ key, hook, display }) => ( + + {display} + {hook} + + { + updateSetting(key, event.target.value); + }} + sx={{ width: '100%' }} + /> + + + ))} + +
+
+ + + {/* spacer */} +
- + + - + - + ); }; diff --git a/src/containers/ContextProvider/reducer.js b/src/containers/ContextProvider/reducer.js index 051b3643..04a26e04 100644 --- a/src/containers/ContextProvider/reducer.js +++ b/src/containers/ContextProvider/reducer.js @@ -1,4 +1,4 @@ -import { headerDefinitions } from '../../util/data'; +import { headerDefinitions, medicationRequestToRemsAdmins } from '../../util/data'; export const actionTypes = Object.freeze({ updatePatient: 'update_patient', // {type, value} updateSetting: 'update_setting', // {type, settingId, value} @@ -32,8 +32,18 @@ const initialState = { startup: false, redirect: '' }; + Object.keys(headerDefinitions).forEach(e => { initialState[e] = headerDefinitions[e].default; // fill default settings values }); +medicationRequestToRemsAdmins.forEach(row => { + const { rxnorm, hookEndpoints } = row; + hookEndpoints.forEach(endpoint => { + const { hook, remsAdmin } = endpoint; + const key = `${rxnorm}_${hook}`; + initialState[key] = remsAdmin; + }); +}); + export { initialState }; diff --git a/src/util/data.js b/src/util/data.js index 49e8428a..099b88dc 100644 --- a/src/util/data.js +++ b/src/util/data.js @@ -93,6 +93,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/order-sign' }, + { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/order-select' }, + { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/patient-view' } + ] + }, + { + rxnorm: 6064, + display: 'Isotretinoin 20 MG Oral Capsule', + hookEndpoints: [ + { hook: 'order-sign', remsAdmin: 'http://localhost:8090/cds-services/order-sign' }, + { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/order-select' }, + { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/patient-view' } + ] + }, + { + rxnorm: 1237051, + display: 'TIRF 200 UG Oral Transmucosal Lozenge', + hookEndpoints: [ + { hook: 'order-sign', remsAdmin: 'http://localhost:8090/cds-services/order-sign' }, + { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/order-select' }, + { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/patient-view' } + ] + }, + { + rxnorm: 1666386, + display: 'Addyi 100 MG Oral Tablet', + hookEndpoints: [ + { hook: 'order-sign', remsAdmin: 'http://localhost:8090/cds-services/order-sign' }, + { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/order-select' }, + { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/patient-view' } + ] + } +]); + const types = { error: 'errorClass', info: 'infoClass', @@ -218,4 +257,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 +}; From f80e2cfb8061189a3f64b1c37f0437f8f8e108d2 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Thu, 28 Mar 2024 19:01:44 -0400 Subject: [PATCH 05/13] Remove gray background for QuestionnaireResponse table entirely --- src/components/SMARTBox/PatientBox.jsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/SMARTBox/PatientBox.jsx b/src/components/SMARTBox/PatientBox.jsx index f08cff42..fea2e08c 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 ( - + From bbed0b7f0381a4115b8726a16ba1e4f85b1704f0 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Thu, 28 Mar 2024 19:16:16 -0400 Subject: [PATCH 06/13] Fire order-sign hook only on REMS drugs, remove env vars, and fix typo in REMS Admin urls for all drugs --- .env | 3 -- src/components/RequestBox/RequestBox.jsx | 19 ++++++----- src/containers/RequestBuilder.jsx | 20 ++++++------ src/util/data.js | 41 ++++++++---------------- src/util/util.js | 26 +++++++++++++++ 5 files changed, 57 insertions(+), 52 deletions(-) diff --git a/.env b/.env index e0aed43a..2f3b9eb8 100644 --- a/.env +++ b/.env @@ -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/src/components/RequestBox/RequestBox.jsx b/src/components/RequestBox/RequestBox.jsx index 8ec1e8fe..ab032fab 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,10 @@ const RequestBox = props => { - diff --git a/src/containers/RequestBuilder.jsx b/src/containers/RequestBuilder.jsx index 5bb65cf1..5dfdb9e2 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'; @@ -126,15 +127,9 @@ 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); + + const remsAdminUrl = getMedicationSpecificRemsAdminUrl(request, globalState, hook); + if (!remsAdminUrl) { return; } @@ -144,12 +139,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 +293,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 099b88dc..d3086192 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', @@ -98,36 +83,36 @@ const medicationRequestToRemsAdmins = Object.freeze([ rxnorm: 2183126, display: 'Turalio 200 MG Oral Capsule', hookEndpoints: [ - { hook: 'order-sign', remsAdmin: 'http://localhost:8090/cds-services/order-sign' }, - { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/order-select' }, - { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/patient-view' } + { 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/order-sign' }, - { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/order-select' }, - { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/patient-view' } + { 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/order-sign' }, - { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/order-select' }, - { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/patient-view' } + { 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/order-sign' }, - { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/order-select' }, - { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/patient-view' } + { 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' } ] } ]); diff --git a/src/util/util.js b/src/util/util.js index 244e805a..7b80185b 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -93,3 +93,29 @@ 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 key = `${rxnorm}_${hook}`; + const cdsUrl = globalState[key]; + + if (!cdsUrl) { + console.log(`Medication ${display} is not a REMS medication`); + return undefined; + } + + return cdsUrl; +}; + +export { retrieveLaunchContext, getMedicationSpecificRemsAdminUrl }; From eef1a27ff861acf8f58d16c7818f6184df4d08e2 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Mon, 1 Apr 2024 14:02:21 -0400 Subject: [PATCH 07/13] WIP SettingsSection add delete/add buttons, change table cell widths, store table row state instead of just REMS admin url, fix bug where Send Rx and Sign Order buttons are grayed out --- src/components/RequestBox/RequestBox.jsx | 5 +- .../RequestDashboard/SettingsSection.jsx | 178 +++++++++++++----- src/containers/ContextProvider/reducer.js | 18 +- src/containers/RequestBuilder.jsx | 14 +- src/util/util.js | 2 +- 5 files changed, 149 insertions(+), 68 deletions(-) diff --git a/src/components/RequestBox/RequestBox.jsx b/src/components/RequestBox/RequestBox.jsx index ab032fab..2adb9927 100644 --- a/src/components/RequestBox/RequestBox.jsx +++ b/src/components/RequestBox/RequestBox.jsx @@ -307,10 +307,7 @@ const RequestBox = props => { - diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index e4c4345a..87406c5c 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -5,7 +5,10 @@ import { Checkbox, FormControlLabel, Grid, + IconButton, Paper, + Select, + MenuItem, Table, TableBody, TableCell, @@ -14,6 +17,8 @@ import { TableRow, TextField } from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; +import AddIcon from '@mui/icons-material/Add'; import env from 'env-var'; import FHIR from 'fhirclient'; @@ -22,44 +27,52 @@ import { headerDefinitions, medicationRequestToRemsAdmins } from '../../util/dat import { actionTypes } from '../../containers/ContextProvider/reducer'; import { SettingsContext } from '../../containers/ContextProvider/SettingsProvider'; +function getRemsAdminHookEndpoints() { + const remsAdminHookEndpoints = {}; + medicationRequestToRemsAdmins.forEach(row => { + const { rxnorm, display, hookEndpoints } = row; + hookEndpoints.forEach(endpoint => { + const { hook, remsAdmin } = endpoint; + const key = `${rxnorm}_${hook}`; + remsAdminHookEndpoints[key] = { + rxnorm, + display, + hook, + remsAdmin, + type: 'tableInput' + }; + }); + }); + return remsAdminHookEndpoints; +} + +const remsAdminHookEndpoints = getRemsAdminHookEndpoints(); + +const CDS_HOOKS = ['order-sign', 'order-select', 'patient-view']; + const SettingsSection = props => { const [state, dispatch] = React.useContext(SettingsContext); - const [headers, setHeaders] = useState([]); - const [originalValues, setOriginalValues] = useState([]); - - useEffect(() => { - const remsAdminHookEndpoints = {}; - medicationRequestToRemsAdmins.forEach(row => { - const { rxnorm, display, hookEndpoints } = row; - hookEndpoints.forEach(endpoint => { - const { hook, remsAdmin } = endpoint; - const key = `${rxnorm}_${hook}`; - remsAdminHookEndpoints[key] = { - display, - hook, - type: 'tableInput', - default: remsAdmin - }; - }); - }); - const headers = [ - ...Object.keys(headerDefinitions).map(key => ({ ...headerDefinitions[key], key })), - ...Object.keys(remsAdminHookEndpoints).map(key => ({ ...remsAdminHookEndpoints[key], key })) - ] - // Display the fields in descending order of type. If two fields are the same type, then sort by ascending order of display text. + const [fieldHeaders, _setFieldHeaders] = useState( + Object.keys(headerDefinitions) + .map(key => ({ ...headerDefinitions[key], key })) .sort( (self, other) => -self.type.localeCompare(other.type) || self.display.localeCompare(other.display) - ); - setHeaders(headers); + ) + ); + + const [tableHeaders, setTableHeaders] = useState( + Object.keys(remsAdminHookEndpoints).map(key => ({ + ...remsAdminHookEndpoints[key], + key + })) + ); - const originals = [ - ...Object.keys(headerDefinitions).map(key => [key, state[key]]), - ...Object.keys(remsAdminHookEndpoints).map(key => [key, state[key]]) - ]; - setOriginalValues(originals); + const originalFieldHeaders = Object.keys(headerDefinitions).map(key => [key, state[key]]); + const originalTableHeaders = Object.keys(remsAdminHookEndpoints).map(key => [key, state[key]]); + useEffect(() => { JSON.parse(localStorage.getItem('reqgenSettings') || '[]').forEach(element => { try { updateSetting(element[0], element[1]); @@ -90,13 +103,14 @@ const SettingsSection = props => { }; const resetSettings = () => { - originalValues.forEach(e => { + [...originalFieldHeaders, ...originalTableHeaders].forEach(e => { try { updateSetting(e[0], e[1]); } catch { console.log('Failed to reset setting value'); } }); + setTableHeaders(originalTableHeaders); }; const clearQuestionnaireResponses = @@ -238,7 +252,7 @@ const SettingsSection = props => { return ( - {headers + {fieldHeaders .filter(header => header.type !== 'tableInput') .map(({ key, type, display }) => { switch (type) { @@ -310,30 +324,90 @@ const SettingsSection = props => {
- Medication - CDS Hook - REMS Admin Endpoint + Medication Display + Medication RxNorm Code + CDS Hook + REMS Admin Endpoint - {headers + {tableHeaders .filter(header => header.type === 'tableInput') - .map(({ key, hook, display }) => ( - - {display} - {hook} - - { - updateSetting(key, event.target.value); - }} - sx={{ width: '100%' }} - /> - - - ))} + .map(({ key, rxnorm, display, hook, remsAdmin }) => { + return ( + + + { + updateSetting(key, { ...state[key], display: event.target.value }); + }} + sx={{ width: '100%' }} + /> + + + { + updateSetting(key, { ...state[key], rxnorm: event.target.value }); + }} + sx={{ width: '100%' }} + /> + + + + + + { + updateSetting(key, { ...state[key], remsAdmin: event.target.value }); + }} + sx={{ width: '100%' }} + /> + + + { + console.log('clicked add row function'); + }} + size="large" + > + + + { + console.log('clicked delete row function'); + dispatch({ type: actionTypes.deleteCdsHookSetting, settingId: key }); + setTableHeaders(tableHeaders.filter(header => header.key === key)); + }} + size="large" + > + + + + + ); + })}
diff --git a/src/containers/ContextProvider/reducer.js b/src/containers/ContextProvider/reducer.js index 04a26e04..613c34d8 100644 --- a/src/containers/ContextProvider/reducer.js +++ b/src/containers/ContextProvider/reducer.js @@ -2,11 +2,20 @@ import { headerDefinitions, medicationRequestToRemsAdmins } from '../../util/dat 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' }); // todo: add an enum that defines possible settings export const reducer = (state, action) => { switch (action.type) { + case actionTypes.deleteCdsHookSetting: + return Object.keys(state).reduce((previousState, currentKey) => { + const newState = { ...previousState }; + if (currentKey !== action.settingId) { + newState[currentKey] = state[currentKey]; + } + return newState; + }, {}); case actionTypes.updateSetting: return { ...state, @@ -38,11 +47,10 @@ Object.keys(headerDefinitions).forEach(e => { }); medicationRequestToRemsAdmins.forEach(row => { - const { rxnorm, hookEndpoints } = row; - hookEndpoints.forEach(endpoint => { - const { hook, remsAdmin } = endpoint; + const { rxnorm, display, hookEndpoints } = row; + hookEndpoints.forEach(({ hook, remsAdmin }) => { const key = `${rxnorm}_${hook}`; - initialState[key] = remsAdmin; + initialState[key] = { rxnorm, display, hook, remsAdmin }; }); }); diff --git a/src/containers/RequestBuilder.jsx b/src/containers/RequestBuilder.jsx index 5dfdb9e2..76d176cf 100644 --- a/src/containers/RequestBuilder.jsx +++ b/src/containers/RequestBuilder.jsx @@ -106,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 @@ -128,11 +135,6 @@ const RequestBuilder = props => { hookConfig ); - const remsAdminUrl = getMedicationSpecificRemsAdminUrl(request, globalState, hook); - if (!remsAdminUrl) { - return; - } - let baseUrl = globalState.baseUrl; const headers = { diff --git a/src/util/util.js b/src/util/util.js index 7b80185b..82397f85 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -108,7 +108,7 @@ const getMedicationSpecificRemsAdminUrl = (request, globalState, hook) => { } const key = `${rxnorm}_${hook}`; - const cdsUrl = globalState[key]; + const cdsUrl = globalState[key]?.remsAdmin; if (!cdsUrl) { console.log(`Medication ${display} is not a REMS medication`); From ec4c56185ea33210f15432d58b61e89335eba957 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Mon, 1 Apr 2024 16:51:06 -0400 Subject: [PATCH 08/13] Fix adding/deleting/reset settings functionality --- .../RequestDashboard/SettingsSection.jsx | 328 ++++++++---------- src/containers/ContextProvider/reducer.js | 84 +++-- src/util/util.js | 5 +- 3 files changed, 210 insertions(+), 207 deletions(-) diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index 87406c5c..3a9947e3 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -24,61 +24,28 @@ import env from 'env-var'; import FHIR from 'fhirclient'; import { headerDefinitions, medicationRequestToRemsAdmins } from '../../util/data'; -import { actionTypes } from '../../containers/ContextProvider/reducer'; +import { actionTypes, initialState } from '../../containers/ContextProvider/reducer'; import { SettingsContext } from '../../containers/ContextProvider/SettingsProvider'; -function getRemsAdminHookEndpoints() { - const remsAdminHookEndpoints = {}; - medicationRequestToRemsAdmins.forEach(row => { - const { rxnorm, display, hookEndpoints } = row; - hookEndpoints.forEach(endpoint => { - const { hook, remsAdmin } = endpoint; - const key = `${rxnorm}_${hook}`; - remsAdminHookEndpoints[key] = { - rxnorm, - display, - hook, - remsAdmin, - type: 'tableInput' - }; - }); - }); - return remsAdminHookEndpoints; -} - -const remsAdminHookEndpoints = getRemsAdminHookEndpoints(); - const CDS_HOOKS = ['order-sign', 'order-select', 'patient-view']; const SettingsSection = props => { const [state, dispatch] = React.useContext(SettingsContext); - const [fieldHeaders, _setFieldHeaders] = useState( - Object.keys(headerDefinitions) - .map(key => ({ ...headerDefinitions[key], key })) - .sort( - (self, other) => - -self.type.localeCompare(other.type) || self.display.localeCompare(other.display) - ) - ); - - const [tableHeaders, setTableHeaders] = useState( - Object.keys(remsAdminHookEndpoints).map(key => ({ - ...remsAdminHookEndpoints[key], - key - })) - ); - - const originalFieldHeaders = Object.keys(headerDefinitions).map(key => [key, state[key]]); - const originalTableHeaders = Object.keys(remsAdminHookEndpoints).map(key => [key, state[key]]); + const fieldHeaders = Object.keys(headerDefinitions) + .map(key => ({ ...headerDefinitions[key], key })) + .sort( + (self, other) => + -self.type.localeCompare(other.type) || self.display.localeCompare(other.display) + ); useEffect(() => { - JSON.parse(localStorage.getItem('reqgenSettings') || '[]').forEach(element => { + JSON.parse(localStorage.getItem('reqgenSettings') || '[]').forEach(([key, value]) => { try { - updateSetting(element[0], element[1]); + updateSetting(key, value); } catch { - if (element[0]) { - console.log('Could not load setting:' + element[0]); + if (!key) { + console.log('Could not load setting:' + key); } } }); @@ -98,19 +65,12 @@ const SettingsSection = props => { }; const saveSettings = () => { - const headers = Object.keys(headerDefinitions).map(key => [key, state[key]]); + const headers = Object.keys(state).map(key => [key, state[key]]); localStorage.setItem('reqgenSettings', JSON.stringify(headers)); }; const resetSettings = () => { - [...originalFieldHeaders, ...originalTableHeaders].forEach(e => { - try { - updateSetting(e[0], e[1]); - } catch { - console.log('Failed to reset setting value'); - } - }); - setTableHeaders(originalTableHeaders); + dispatch({ type: actionTypes.resetSettings }); }; const clearQuestionnaireResponses = @@ -252,59 +212,53 @@ const SettingsSection = props => { return ( - {fieldHeaders - .filter(header => header.type !== 'tableInput') - .map(({ key, type, display }) => { - switch (type) { - case 'input': - return ( - -
- { - updateSetting(key, event.target.value); - }} - sx={{ width: '100%' }} - /> -
-
- ); - case 'check': - if (firstCheckbox) { - firstCheckbox = false; - showBreak = true; - } else { - showBreak = false; - } - return ( - - {showBreak ? : ''} - - { - updateSetting(key, event.target.checked); - }} - /> - } - label={display} - /> - - - ); - default: - return ( -
-

{display}

+ {fieldHeaders.map(({ key, type, display }) => { + switch (type) { + case 'input': + return ( + +
+ updateSetting(key, event.target.value)} + sx={{ width: '100%' }} + />
- ); - } - })} +
+ ); + case 'check': + if (firstCheckbox) { + firstCheckbox = false; + showBreak = true; + } else { + showBreak = false; + } + return ( + + {showBreak ? : ''} + + updateSetting(key, event.target.checked)} + /> + } + label={display} + /> + + + ); + default: + return ( +
+

{display}

+
+ ); + } + })} {resetHeaderDefinitions.map(({ key, display, reset, variant }) => { return ( @@ -331,83 +285,93 @@ const SettingsSection = props => { - {tableHeaders - .filter(header => header.type === 'tableInput') - .map(({ key, rxnorm, display, hook, remsAdmin }) => { - return ( - - - { - updateSetting(key, { ...state[key], display: event.target.value }); - }} - sx={{ width: '100%' }} - /> - - - { - updateSetting(key, { ...state[key], rxnorm: event.target.value }); - }} - sx={{ width: '100%' }} - /> - - - - - - { - updateSetting(key, { ...state[key], remsAdmin: event.target.value }); - }} - sx={{ width: '100%' }} - /> - - - { - console.log('clicked add row function'); - }} - size="large" - > - - - { - console.log('clicked delete row function'); - dispatch({ type: actionTypes.deleteCdsHookSetting, settingId: key }); - setTableHeaders(tableHeaders.filter(header => header.key === key)); - }} - size="large" - > - - - - - ); - })} + {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 })} + size="large" + > + + + + dispatch({ type: actionTypes.deleteCdsHookSetting, settingId: key }) + } + size="large" + > + + + + + ); + })} diff --git a/src/containers/ContextProvider/reducer.js b/src/containers/ContextProvider/reducer.js index 613c34d8..b9f18c1d 100644 --- a/src/containers/ContextProvider/reducer.js +++ b/src/containers/ContextProvider/reducer.js @@ -1,21 +1,57 @@ 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} - deleteCdsHookSetting: 'DELETE_CDS_HOOK_SETTING' + 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; +}; + // todo: add an enum that defines possible settings export const reducer = (state, action) => { switch (action.type) { case actionTypes.deleteCdsHookSetting: - return Object.keys(state).reduce((previousState, currentKey) => { - const newState = { ...previousState }; - if (currentKey !== action.settingId) { - newState[currentKey] = state[currentKey]; + return getNewStateWithoutCdsHookSetting(state, action.settingId); + case actionTypes.addCdsHookSetting: + return { + ...state, + medicationRequestToRemsAdmins: { + ...state.medicationRequestToRemsAdmins, + [uuidv4()]: { + rxnorm: 'Fill out Medication RxNorm Code', + display: 'Fill out Medication Display Name', + hook: 'order-sign', + remsAdmin: 'REMS Admin URL for CDS Hook' + } } - return newState; - }, {}); + }; + 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, @@ -36,22 +72,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: {} + }; -medicationRequestToRemsAdmins.forEach(row => { - const { rxnorm, display, hookEndpoints } = row; - hookEndpoints.forEach(({ hook, remsAdmin }) => { - const key = `${rxnorm}_${hook}`; - initialState[key] = { rxnorm, display, hook, remsAdmin }; + 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/util/util.js b/src/util/util.js index 82397f85..b9e237ea 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -107,8 +107,9 @@ const getMedicationSpecificRemsAdminUrl = (request, globalState, hook) => { return undefined; } - const key = `${rxnorm}_${hook}`; - const cdsUrl = globalState[key]?.remsAdmin; + 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`); From 7521aac27db83aa827b11fd3a61109397fdcd043 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Mon, 1 Apr 2024 17:24:24 -0400 Subject: [PATCH 09/13] Allow adding of new row directly under a given row --- .../RequestDashboard/SettingsSection.jsx | 4 +- src/containers/ContextProvider/reducer.js | 47 ++++++++++++++----- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index 3a9947e3..918292c9 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -354,7 +354,9 @@ const SettingsSection = props => { dispatch({ type: actionTypes.addCdsHookSetting })} + onClick={() => + dispatch({ type: actionTypes.addCdsHookSetting, settingId: key }) + } size="large" > diff --git a/src/containers/ContextProvider/reducer.js b/src/containers/ContextProvider/reducer.js index b9f18c1d..d73f500c 100644 --- a/src/containers/ContextProvider/reducer.js +++ b/src/containers/ContextProvider/reducer.js @@ -21,24 +21,47 @@ const getNewStateWithoutCdsHookSetting = (state, settingId) => { 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 { - ...state, - medicationRequestToRemsAdmins: { - ...state.medicationRequestToRemsAdmins, - [uuidv4()]: { - rxnorm: 'Fill out Medication RxNorm Code', - display: 'Fill out Medication Display Name', - hook: 'order-sign', - remsAdmin: 'REMS Admin URL for CDS Hook' - } - } - }; + return getNewStateWithNewCdsHookSetting(state, action.settingId); case actionTypes.updateCdsHookSetting: return { ...state, From 20daaf1c996d9abed95a88fa194946cc28d7ba4a Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Mon, 1 Apr 2024 17:37:02 -0400 Subject: [PATCH 10/13] Add back deleted comment --- src/components/RequestDashboard/SettingsSection.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index 918292c9..3cc543c6 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -34,6 +34,8 @@ const SettingsSection = props => { const fieldHeaders = Object.keys(headerDefinitions) .map(key => ({ ...headerDefinitions[key], key })) + // Display the fields in descending order of type. If two fields are the same type, + // then sort by ascending order of display text. .sort( (self, other) => -self.type.localeCompare(other.type) || self.display.localeCompare(other.display) From 26a4ec432040e03d8f4801866b4c5beb7818375d Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Tue, 2 Apr 2024 16:08:22 -0400 Subject: [PATCH 11/13] Make REMS admin reset field use full path instead of the URL origin --- .env | 2 +- src/components/RequestDashboard/SettingsSection.jsx | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 2f3b9eb8..75e6c543 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 diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index 3cc543c6..5b836924 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -122,10 +122,7 @@ const SettingsSection = props => { const resetRemsAdmin = ({ cdsUrl }) => () => { - let url = new URL(cdsUrl); - const resetUrl = url.origin + '/etasu/reset'; - - fetch(resetUrl, { + fetch(cdsUrl, { method: 'POST' }) .then(response => { From 4df8769199201312a79b26cfec55b74314bb47ad Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Tue, 2 Apr 2024 16:34:43 -0400 Subject: [PATCH 12/13] Make table scrollable and have sticky header, add tooltips over add/delete table row buttons, move reset and reconnect buttons to left of save and reset buttons --- .../RequestDashboard/SettingsSection.jsx | 81 +++++++++++-------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index 5b836924..0c8b0105 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -15,6 +15,7 @@ import { TableContainer, TableHead, TableRow, + Tooltip, TextField } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; @@ -258,29 +259,29 @@ const SettingsSection = props => { ); } })} - {resetHeaderDefinitions.map(({ key, display, reset, variant }) => { - return ( - - - - ); - })} - + - +
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. */} + @@ -350,25 +351,29 @@ const SettingsSection = props => { sx={{ width: '100%' }} /> - - - dispatch({ type: actionTypes.addCdsHookSetting, settingId: key }) - } - size="large" - > - - - - dispatch({ type: actionTypes.deleteCdsHookSetting, settingId: key }) - } - size="large" - > - - + + + + dispatch({ type: actionTypes.addCdsHookSetting, settingId: key }) + } + size="large" + > + + + + + + dispatch({ type: actionTypes.deleteCdsHookSetting, settingId: key }) + } + size="large" + > + + + ); @@ -385,7 +390,19 @@ const SettingsSection = props => { }} /> - + + {resetHeaderDefinitions.map(({ key, display, reset, variant }) => { + return ( + + + + ); + })} + + +