From 2879c1b807fa3628ebcc28477d283010f8a19d00 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Thu, 7 Dec 2023 19:04:55 -0500 Subject: [PATCH 01/11] include healthcheck --- Dockerfile | 2 ++ Dockerfile.dev | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index efc4563f..5c77e8e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,4 +4,6 @@ COPY --chown=node:node . . RUN npm install COPY --chown=node:node . . EXPOSE 3000 +RUN apk add --no-cache curl +HEALTHCHECK --interval=60s --timeout=10m --retries=10 CMD curl --fail http://localhost:3000 || exit 1 CMD npm run start diff --git a/Dockerfile.dev b/Dockerfile.dev index 2d637e9b..2b87b008 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -4,4 +4,6 @@ COPY --chown=node:node . . RUN npm install EXPOSE 3000 EXPOSE 3001 +RUN apk add --no-cache curl +HEALTHCHECK --interval=60s --timeout=10m --retries=10 CMD curl --fail http://localhost:3000 || exit 1 CMD ./dockerRunnerDev.sh From 046d9d6a4ac9d730e175857c20a8d16dc16e7336 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Thu, 7 Dec 2023 19:11:32 -0500 Subject: [PATCH 02/11] docker add curl --- Dockerfile | 5 ++++- Dockerfile.dev | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5c77e8e2..17da8a2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,9 @@ COPY --chown=node:node . . RUN npm install COPY --chown=node:node . . EXPOSE 3000 -RUN apk add --no-cache curl +RUN apk update +RUN apk upgrade +RUN apk search curl +RUN apk add curl HEALTHCHECK --interval=60s --timeout=10m --retries=10 CMD curl --fail http://localhost:3000 || exit 1 CMD npm run start diff --git a/Dockerfile.dev b/Dockerfile.dev index 2b87b008..552eb3d6 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -4,6 +4,9 @@ COPY --chown=node:node . . RUN npm install EXPOSE 3000 EXPOSE 3001 -RUN apk add --no-cache curl +RUN apk update +RUN apk upgrade +RUN apk search curl +RUN apk add curl HEALTHCHECK --interval=60s --timeout=10m --retries=10 CMD curl --fail http://localhost:3000 || exit 1 CMD ./dockerRunnerDev.sh From 70d8045667f8092c207228cef7c8ececad7cbe16 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Mon, 26 Aug 2024 14:39:56 -0400 Subject: [PATCH 03/11] health check config --- Dockerfile | 5 ++++- Dockerfile.dev | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6c4e884c..1f0f4996 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ FROM node:21-alpine +ARG VITE_URL +ENV VITE_URL=$VITE_URL + WORKDIR /home/node/app/request-generator COPY --chown=node:node . . RUN npm install @@ -8,7 +11,7 @@ RUN apk update RUN apk upgrade RUN apk search curl RUN apk add curl -HEALTHCHECK --interval=60s --timeout=10m --retries=10 CMD curl --fail http://localhost:3000 || exit 1 +HEALTHCHECK --interval=60s --timeout=10m --retries=10 CMD curl --fail $VITE_URL || exit 1 COPY --chown=node:node . . CMD npm run start diff --git a/Dockerfile.dev b/Dockerfile.dev index cb7c316f..cbb2e872 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,5 +1,9 @@ FROM node:21-alpine WORKDIR /home/node/app/request-generator + +ARG VITE_URL +ENV VITE_URL=$VITE_URL + COPY --chown=node:node . . RUN npm install EXPOSE 3000 @@ -8,5 +12,5 @@ RUN apk update RUN apk upgrade RUN apk search curl RUN apk add curl -HEALTHCHECK --interval=60s --timeout=10m --retries=10 CMD curl --fail http://localhost:3000 || exit 1 +HEALTHCHECK --interval=60s --timeout=10m --retries=10 CMD curl --fail $VITE_URL || exit 1 CMD ./dockerRunnerDev.sh From c075ef208375fbc6de8795580818177ded35f68f Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Tue, 27 Aug 2024 12:14:47 -0400 Subject: [PATCH 04/11] update healthcheck --- Dockerfile | 7 ++----- Dockerfile.dev | 8 +++----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1f0f4996..9f417013 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,11 +7,8 @@ WORKDIR /home/node/app/request-generator COPY --chown=node:node . . RUN npm install EXPOSE 3000 -RUN apk update -RUN apk upgrade -RUN apk search curl -RUN apk add curl -HEALTHCHECK --interval=60s --timeout=10m --retries=10 CMD curl --fail $VITE_URL || exit 1 + +HEALTHCHECK --interval=30s --start-period=15s --timeout=10m --retries=10 CMD wget --no-verbose --tries=1 --spider ${VITE_URL} || exit 1 COPY --chown=node:node . . CMD npm run start diff --git a/Dockerfile.dev b/Dockerfile.dev index cbb2e872..4bc72bb3 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -8,9 +8,7 @@ COPY --chown=node:node . . RUN npm install EXPOSE 3000 EXPOSE 3001 -RUN apk update -RUN apk upgrade -RUN apk search curl -RUN apk add curl -HEALTHCHECK --interval=60s --timeout=10m --retries=10 CMD curl --fail $VITE_URL || exit 1 + +HEALTHCHECK --interval=30s --start-period=15s --timeout=10m --retries=10 CMD wget --no-verbose --tries=1 --spider ${VITE_URL} || exit 1 + CMD ./dockerRunnerDev.sh From df11bb4a672f356842de676a9424e42eecb24eb3 Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Wed, 23 Oct 2024 13:01:49 -0400 Subject: [PATCH 05/11] Add button to delete all Tasks from the EHR --- .../RequestDashboard/SettingsSection.jsx | 66 +++++++------------ 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index c368a284..a3d6fe8b 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -84,30 +84,6 @@ const SettingsSection = props => { dispatch({ type: actionTypes.resetSettings }); }; - const clearQuestionnaireResponses = - ({ defaultUser }) => - () => { - props.client - .request('QuestionnaireResponse?author=' + defaultUser, { flat: true }) - .then(result => { - result.forEach(resource => { - props.client - .delete('QuestionnaireResponse/' + resource.id) - .then(result => { - console.log(result); - }) - .catch(e => { - console.log('Failed to delete QuestionnaireResponse ' + resource.id); - console.log(e); - }); - }); - }) - .catch(e => { - console.log('Failed to retrieve list of QuestionnaireResponses'); - console.log(e); - }); - }; - const resetPims = ({ pimsUrl }) => () => { @@ -144,33 +120,33 @@ const SettingsSection = props => { }); }; - const clearMedicationDispenses = - ({ ehrUrl, access_token }) => + const clearResource = + ({ ehrUrl, access_token }, type) => () => { - console.log('Clear MedicationDispenses from the EHR: ' + ehrUrl); + console.log('Clear ' + type + 's from the EHR: ' + ehrUrl); const client = FHIR.client({ serverUrl: ehrUrl, ...(access_token ? { tokenResponse: access_token } : {}) }); client - .request('MedicationDispense', { flat: true }) + .request(type, { flat: true }) .then(result => { console.log(result); result.forEach(resource => { - console.log(resource.id); + console.log('Delete ' + type + ': ' + resource.id); client - .delete('MedicationDispense/' + resource.id) + .delete(type + '/' + resource.id) .then(result => { console.log(result); }) .catch(e => { - console.log('Failed to delete MedicationDispense ' + resource.id); + console.log('Failed to delete ' + type + ' ' + resource.id); console.log(e); }); }); }) .catch(e => { - console.log('Failed to retrieve list of MedicationDispense'); + console.log('Failed to retrieve list of ' + type + 's'); console.log(e); }); }; @@ -192,20 +168,28 @@ const SettingsSection = props => { key: 'resetPims', reset: resetPims }, - { - display: 'Clear In-Progress Forms', - key: 'clearQuestionnaireResponses', - reset: clearQuestionnaireResponses - }, { display: 'Reset REMS-Admin Database', key: 'resetRemsAdmin', reset: resetRemsAdmin }, { - display: 'Clear EHR MedicationDispenses', + display: 'Clear EHR In-Progress Forms', + key: 'clearQuestionnaireResponses', + reset: clearResource, + parameter: 'QuestionnaireResponse' + }, + { + display: 'Clear EHR Dispense Statuses', key: 'clearMedicationDispenses', - reset: clearMedicationDispenses + reset: clearResource, + parameter: 'MedicationDispense' + }, + { + display: 'Clear EHR Tasks', + key: 'clearTasks', + reset: clearResource, + parameter: 'Task' }, { display: 'Reconnect EHR', @@ -428,10 +412,10 @@ const SettingsSection = props => { /> - {resetHeaderDefinitions.map(({ key, display, reset, variant }) => { + {resetHeaderDefinitions.map(({ key, display, reset, variant, parameter }) => { return ( - From bd688ddf0f9b2982901fc18c3d152dff32737484 Mon Sep 17 00:00:00 2001 From: Patrick LaRocque <41444457+plarocque4@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:01:02 -0500 Subject: [PATCH 06/11] 723 - Build workflow for back-office staff (#138) * add ui for backoffice * Some updated UI to match design of others * Back office staff workflow updates * fix styling --------- Co-authored-by: KeeyanGhoreshi Co-authored-by: Ariel Virgulto Co-authored-by: Sahil Malhotra <88040167+smalho01@users.noreply.github.com> --- .env | 1 + README.md | 1 + src/components/App.jsx | 7 +- src/components/Auth/Login.jsx | 1 + src/components/Dashboard/Dashboard.jsx | 2 +- .../PatientSearchBar/PatientSearchBar.jsx | 22 ++- src/components/RequestBox/RequestBox.jsx | 6 +- src/components/RequestDashboard/Home.jsx | 29 +++- .../RequestDashboard/PatientSection.jsx | 2 +- .../RequestDashboard/SettingsSection.jsx | 33 +--- .../RequestDashboard/TasksSection.jsx | 127 +++++++++++--- src/components/RequestDashboard/styles.jsx | 23 +++ src/components/SMARTBox/PatientBox.jsx | 145 ++++++++-------- src/containers/BackOffice/BackOffice.jsx | 58 +++++++ src/containers/BackOffice/Dashboard.jsx | 81 +++++++++ src/containers/BackOffice/TaskTab.jsx | 164 ++++++++++++++++++ src/containers/BackOffice/styles.jsx | 79 +++++++++ .../ContextProvider/SettingsProvider.jsx | 39 ++++- src/containers/Gateway/Gateway.jsx | 9 +- src/containers/Index.jsx | 36 +++- src/containers/PatientPortal.jsx | 45 +++-- src/containers/RequestBuilder.jsx | 29 +++- src/index.css | 27 +++ src/util/auth.js | 8 +- src/util/data.js | 6 + vite.config.ts | 3 + 26 files changed, 822 insertions(+), 161 deletions(-) create mode 100644 src/containers/BackOffice/BackOffice.jsx create mode 100644 src/containers/BackOffice/Dashboard.jsx create mode 100644 src/containers/BackOffice/TaskTab.jsx create mode 100644 src/containers/BackOffice/styles.jsx diff --git a/.env b/.env index 39c1fd4a..e9b49c84 100644 --- a/.env +++ b/.env @@ -6,6 +6,7 @@ VITE_AUTH = http://localhost:8180 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_USE_DEFAULT_USER = false VITE_DEFAULT_USER = pra1234 VITE_EHR_BASE = http://localhost:8080/test-ehr/r4 VITE_EHR_SERVER = http://localhost:8080/test-ehr/r4 diff --git a/README.md b/README.md index 4c7dd503..0d76231b 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ Following are a list of modifiable paths: | VITE_CDS_SERVICE | `http://localhost:8090/cds-services` | The base URL of the CDS Service. This will typically be the REMS Admin. | | VITE_CLIENT | `app-login` | The default client to use for the SMART launch. Can be modified directly when launching the app. | | VITE_CLIENT_SCOPES | `launch offline_access openid profile user/Patient.read patient/Patient.read user/Practitioner.read` | The default scopes to use for the SMART launch. Can be modified directly when launching the app. | +| VITE_USE_DEFAULT_USER | `false` | When true, override the logged in user with the default user. | | VITE_DEFAULT_USER | `pra1234` | The default user to log in as when SMART launching. It should be the FHIR id of a practitioner resource. | | VITE_EHR_BASE | `http://localhost:8080/test-ehr/r4` | The default base url for the EHR. Can be modified directly when launching the app. | | VITE_EHR_SERVER | `http://localhost:8080/test-ehr/r4` | The default base url for the EHR FHIR Server. Generally, this should be the same as the EHR_BASE. | diff --git a/src/components/App.jsx b/src/components/App.jsx index f26f03dc..ca55ec36 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -1,4 +1,5 @@ -import { ThemeProvider } from '@mui/styles'; +import { ThemeProvider } from '@mui/material'; + import React, { useEffect } from 'react'; import { BrowserRouter, HashRouter, Route, Routes } from 'react-router-dom'; import Gateway from '../containers/Gateway/Gateway'; @@ -26,8 +27,10 @@ const App = () => { } /> - } /> + } /> } /> + {/* forcibly enter backoffice workflow */} + } /> { params.append('client_id', env.get('VITE_CLIENT').asString()); axios .post( + // this change breaks the patient portal login! `${env.get('VITE_AUTH').asString()}/realms/${env .get('VITE_REALM') .asString()}/protocol/openid-connect/token`, diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index e6b3ddaa..ba7b505e 100644 --- a/src/components/Dashboard/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -102,7 +102,7 @@ const Dashboard = props => { }} > - + {createIcons().map((option, index) => (
diff --git a/src/components/RequestBox/PatientSearchBar/PatientSearchBar.jsx b/src/components/RequestBox/PatientSearchBar/PatientSearchBar.jsx index 54e23626..0f73050e 100644 --- a/src/components/RequestBox/PatientSearchBar/PatientSearchBar.jsx +++ b/src/components/RequestBox/PatientSearchBar/PatientSearchBar.jsx @@ -1,4 +1,7 @@ -import { Autocomplete, Box, TextField, IconButton } from '@mui/material'; +import { Autocomplete, Box, TextField } from '@mui/material'; +import { Grid, Button } from '@mui/material'; +import PeopleIcon from '@mui/icons-material/People'; + import { useEffect, useState } from 'react'; import { PrefetchTemplate } from '../../../PrefetchTemplate'; import { defaultValues } from '../../../util/data'; @@ -32,9 +35,16 @@ const PatientSearchBar = props => { return filteredListOfPatients.length; } + const showAllPatients = () => { + props.callback('patient', {}); + props.callback('expanded', false); + }; + function patientSearchBar() { return ( + +

Filter patient list

{ records

+
+ + + +
{displayFilteredPatientList(input, listOfPatients[0])}
); @@ -82,7 +99,8 @@ const PatientSearchBar = props => { clearCallback={props.clearCallback} options={options} responseExpirationDays={props.responseExpirationDays} - defaultUser={props.defaultUser} + user={props.user} + showButtons={props.showButtons} /> ); diff --git a/src/components/RequestBox/RequestBox.jsx b/src/components/RequestBox/RequestBox.jsx index 6416a266..54ed70a6 100644 --- a/src/components/RequestBox/RequestBox.jsx +++ b/src/components/RequestBox/RequestBox.jsx @@ -38,7 +38,7 @@ const RequestBox = props => { code, codeSystem, display, - defaultUser, + user, smartAppUrl, client, pimsUrl, @@ -183,9 +183,9 @@ const RequestBox = props => { let userId = prefetchedResources?.practitioner?.id; if (!userId) { console.log( - 'Practitioner not populated from prefetch, using default from config: ' + defaultUser + 'Practitioner not populated from prefetch, using user: ' + user ); - userId = defaultUser; + userId = user; } let link = { diff --git a/src/components/RequestDashboard/Home.jsx b/src/components/RequestDashboard/Home.jsx index 788cacd7..9ce8648a 100644 --- a/src/components/RequestDashboard/Home.jsx +++ b/src/components/RequestDashboard/Home.jsx @@ -3,14 +3,19 @@ import { Button, Grid, Tooltip } from '@mui/material'; import PersonIcon from '@mui/icons-material/Person'; import AssignmentIcon from '@mui/icons-material/Assignment'; import SettingsIcon from '@mui/icons-material/Settings'; +import AccountBoxIcon from '@mui/icons-material/AccountBox'; +import MedicalServicesIcon from '@mui/icons-material/MedicalServices'; import useStyles from './styles'; import PatientSection from './PatientSection'; import SettingsSection from './SettingsSection'; import TasksSection from './TasksSection'; +import { logout } from '../../util/auth'; + const Home = props => { const classes = useStyles(); + const { client, token } = props; const patientButton = 'Select a Patient'; const taskButton = 'View Tasks'; const settingsButton = 'Settings'; @@ -58,6 +63,7 @@ const Home = props => { gridClass = `${classes.mainDiv} ${classes.tabDivView}`; } return ( +
{section ? '' : } {/* spacer */} {renderMainButton(patientButton, )} @@ -65,13 +71,30 @@ const Home = props => { {renderMainButton(settingsButton, )} {section ? ( -
+ +   EHR Request Generator +
) : ( )} {/* spacer */} + {/** */} + {section ? ( + + + {token.name} + + + + ) : ( + + )} + {/**/}
+
); }; @@ -85,10 +108,10 @@ const Home = props => { return (
- +
- +
diff --git a/src/components/RequestDashboard/PatientSection.jsx b/src/components/RequestDashboard/PatientSection.jsx index 21512725..0439e998 100644 --- a/src/components/RequestDashboard/PatientSection.jsx +++ b/src/components/RequestDashboard/PatientSection.jsx @@ -8,7 +8,7 @@ const PatientSection = props => { return (
{state.startup ? ( - + ) : ( <>Loading... )} diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index a3d6fe8b..4bd6efdc 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -39,7 +39,7 @@ import { SettingsContext } from '../../containers/ContextProvider/SettingsProvid const ENDPOINT = [ORDER_SIGN, ORDER_SELECT, PATIENT_VIEW, ENCOUNTER_START, REMS_ETASU]; const SettingsSection = props => { - const [state, dispatch] = React.useContext(SettingsContext); + const [state, dispatch, updateSetting, readSettings, saveSettings] = React.useContext(SettingsContext); const fieldHeaders = Object.keys(headerDefinitions) .map(key => ({ ...headerDefinitions[key], key })) @@ -51,35 +51,9 @@ const SettingsSection = props => { ); useEffect(() => { - JSON.parse(localStorage.getItem('reqgenSettings') || '[]').forEach(([key, value]) => { - try { - updateSetting(key, value); - } catch { - if (!key) { - console.log('Could not load setting:' + key); - } - } - }); - - // indicate to the rest of the app that the settings have been loaded - dispatch({ - type: actionTypes.flagStartup - }); + readSettings(); }, []); - const updateSetting = (key, value) => { - dispatch({ - type: actionTypes.updateSetting, - settingId: key, - value: value - }); - }; - - const saveSettings = () => { - const headers = Object.keys(state).map(key => [key, state[key]]); - localStorage.setItem('reqgenSettings', JSON.stringify(headers)); - }; - const resetSettings = () => { dispatch({ type: actionTypes.resetSettings }); }; @@ -201,6 +175,7 @@ const SettingsSection = props => { let firstCheckbox = true; let showBreak = true; + return ( @@ -209,6 +184,7 @@ const SettingsSection = props => { case 'input': return ( + { ( (state['useDefaultUser'] && key === 'defaultUser') || key != 'defaultUser' ) ? (
{ sx={{ width: '100%' }} />
+ ) : ('') }
); case 'check': diff --git a/src/components/RequestDashboard/TasksSection.jsx b/src/components/RequestDashboard/TasksSection.jsx index b712b876..f6279576 100644 --- a/src/components/RequestDashboard/TasksSection.jsx +++ b/src/components/RequestDashboard/TasksSection.jsx @@ -29,6 +29,7 @@ const taskStatus = Object.freeze({ }); const TasksSection = props => { const classes = useStyles(); + const {client, userName, userId} = props; const [tasks, setTasks] = useState([]); const [state] = React.useContext(SettingsContext); const [value, setValue] = useState(0); @@ -36,7 +37,7 @@ const TasksSection = props => { const [taskToDelete, setTaskToDelete] = useState(''); const [anchorStatus, setAnchorStatus] = useState(null); const [anchorAssign, setAnchorAssign] = useState(null); // R4 Task - const [practitioner, setPractitioner] = useState(null); // R4 Practitioner + const [practitionerDefault, setPractitionerDefault] = useState(null); // R4 Practitioner const menuOpen = Boolean(anchorStatus); const assignMenuOpen = Boolean(anchorAssign); @@ -58,8 +59,14 @@ const TasksSection = props => { const taskClone = structuredClone(task); if (val === 'me') { assignTaskToMe(taskClone); - } else { + } else if (val === 'requester') { + assignTaskToRequester(taskClone); + } else if (val === 'defaultPractitioner') { + assignTaskToDefaultPractitioner(taskClone); + } else if (val === 'patient') { assignTaskToPatient(taskClone); + } else { //'unassign' + unassignTask(taskClone); } handleAssignMenuClose(); }; @@ -94,20 +101,56 @@ const TasksSection = props => { reference: `${task.for.resourceType}/${task.for.id}` }; } + if (task.requester && task.requester.id) { + task.requester = { + reference: `${task.requester.resourceType}/${task.requester.id}` + }; + } return task; }; const assignTaskToMe = task => { if (task) { task = washTask(task); - let user = props.client.user.id; - if (!user) { - user = `Practitioner/${state.defaultUser}`; + let user = `Practitioner/${userId}`; + console.log(user); + task.owner = { + reference: user + }; + + client.update(task).then(() => { + fetchTasks(); + }); + } + } + const assignTaskToRequester = task => { + if (task) { + task = washTask(task); + // default to logged in user + let user = client.user.id; + + // assign to requester if available + if (task?.requester) { + user = task.requester?.reference } task.owner = { reference: user }; - props.client.update(task).then(() => { + client.update(task).then(() => { + fetchTasks(); + }); + } + };; + const assignTaskToDefaultPractitioner = task => { + if (task) { + task = washTask(task); + // assign to default user if none set yet + let user = `Practitioner/${state.defaultUser}`; + task.owner = { + reference: user + }; + + client.update(task).then(() => { fetchTasks(); }); } @@ -115,17 +158,27 @@ const TasksSection = props => { const assignTaskToPatient = task => { if (task) { task = washTask(task); + console.log(task.for.reference); task.owner = { reference: task.for.reference }; - props.client.update(task).then(() => { + client.update(task).then(() => { + fetchTasks(); + }); + } + }; + const unassignTask = task => { + if (task) { + task = washTask(task); + delete task.owner; + client.update(task).then(() => { fetchTasks(); }); } }; const deleteTask = () => { if (taskToDelete) { - props.client.delete(`${taskToDelete.resourceType}/${taskToDelete.id}`).then(() => { + client.delete(`${taskToDelete.resourceType}/${taskToDelete.id}`).then(() => { console.log('Deleted Task'); fetchTasks(); }); @@ -138,7 +191,7 @@ const TasksSection = props => { if (state.patient && state.patient.id) { identifier = `Task?patient=${state.patient.id}`; } - props.client.request(identifier, { resolveReferences: ['for', 'owner'] }).then(request => { + client.request(identifier, { resolveReferences: ['for', 'owner', 'requester'] }).then(request => { console.log(request); if (request && request.entry) { setTasks(request.entry.map(e => e.resource)); @@ -152,9 +205,9 @@ const TasksSection = props => { }, []); useEffect(() => { - props.client.request(`Practitioner/${state.defaultUser}`).then(practitioner => { + client.request(`Practitioner/${state.defaultUser}`).then(practitioner => { if (practitioner) { - setPractitioner(practitioner); + setPractitionerDefault(practitioner); } }); }, [state.defaultUser]); @@ -166,7 +219,7 @@ const TasksSection = props => { const updateTaskStatus = (task, status) => { task.status = status; const updatedTask = structuredClone(task); // structured clone may not work on older browsers - props.client.update(washTask(updatedTask)).then(() => { + client.update(washTask(updatedTask)).then(() => { fetchTasks(); }); }; @@ -187,12 +240,8 @@ const TasksSection = props => { url: link }; const patient = lTask.for.id; - retrieveLaunchContext(smartLink, patient, props.client.state).then(result => { + retrieveLaunchContext(smartLink, patient, client.state).then(result => { updateTaskStatus(lTask, 'in-progress'); - lTask.status = 'in-progress'; - props.client.update(washTask(lTask)).then(() => { - fetchTasks(); - }); window.open(result.url, '_blank'); }); }; @@ -216,27 +265,46 @@ const TasksSection = props => { }; const renderAssignMenu = () => { const patient = anchorAssign?.task?.for; + const requester = anchorAssign?.task?.requester; const assignOptions = [ { id: 'me', - display: `provider${practitioner ? ' (' + getPractitionerFirstAndLastName(practitioner) + ')' : ''}` + display: 'Assign to me (' + userName + ')' + }, + { + id: 'requester', + display: `Assign to requester${requester ? ' (' + getPractitionerFirstAndLastName(requester) + ')' : ''}` + }, + { + id: 'defaultPractitioner', + display: `Assign to practitioner${practitionerDefault ? ' (' + getPractitionerFirstAndLastName(practitionerDefault) + ')' : ''}` }, { id: 'patient', - display: `patient${patient ? ' (' + getPatientFullName(patient) + ')' : ''}` + display: `Assign to patient${patient ? ' (' + getPatientFullName(patient) + ')' : ''}` + }, + { + id: 'unassign', + display: 'Unassign' } ]; return ( {assignOptions.map(({ id, display }) => { + // only give the 'Assign to requester if the requester is available' + if (((id === 'me') && userId && userName) + || ((id === 'requester') && (anchorAssign?.task?.requester)) + || ((id === 'defaultPractitioner') && (!anchorAssign?.task?.requester)) + || (id != 'me') && (id != 'requester') && (id != 'defaultPractitioner')) { return ( { handleChangeAssign(anchorAssign?.task, id); }} - >{`Assign to ${display}`} + >{`${display}`} ); + } })} ); @@ -384,16 +452,21 @@ const TasksSection = props => { }; const renderMainView = () => { + // only use the defaultUser if configured to, otherwise use the one passed in + let user = state.defaultUser; + if (!state.useDefaultUser && userId) { + user = userId; + } const unassignedTasks = tasks.filter(t => !t.owner); - const assignedTasks = tasks.filter(t => t.owner?.id === state.defaultUser); // should check current user, not default + const assignedTasks = tasks.filter(t => t.owner?.id === user); // should check current user, not default return (
- } label={`ALL TASKS (${tasks.length})`} /> } label={`MY TASKS (${assignedTasks.length})`} /> } label={`UNASSIGNED TASKS (${unassignedTasks.length})`} /> + } label={`ALL TASKS (${tasks.length})`} /> @@ -409,17 +482,17 @@ const TasksSection = props => { - {/* all tasks */} - {renderTasks(tasks)} - - {/* my tasks */} {renderTasks(assignedTasks)} - + {/* unassigned tasks */} {renderTasks(unassignedTasks)} + + {/* all tasks */} + {renderTasks(tasks)} +
); }; diff --git a/src/components/RequestDashboard/styles.jsx b/src/components/RequestDashboard/styles.jsx index 4a85cf57..2eab4379 100644 --- a/src/components/RequestDashboard/styles.jsx +++ b/src/components/RequestDashboard/styles.jsx @@ -135,6 +135,29 @@ export default makeStyles( }, taskTabOwner: { color: '#777' + }, + titleIcon: { + color: 'white', + fontSize: '19px', + fontFamily: 'Verdana', + float: 'left', + marginLeft: '20px', + verticalAlign: 'middle' + }, + loginIcon: { + color: 'white', + fontSize: '19px', + marginLeft: 'auto', + fontFamily: 'Verdana', + float: 'right', + marginRight: '20px', + verticalAlign: 'middle' + }, + whiteButton: { + color: 'white !important', + borderColor: 'white !important', + marginRight: '5px !important', + marginLeft: '20px !important', } }), diff --git a/src/components/SMARTBox/PatientBox.jsx b/src/components/SMARTBox/PatientBox.jsx index 01ff274d..9b4e4edc 100644 --- a/src/components/SMARTBox/PatientBox.jsx +++ b/src/components/SMARTBox/PatientBox.jsx @@ -43,13 +43,14 @@ const PatientBox = props => { patient, callback, clearCallback, - defaultUser, + user, client, callbackMap, updatePrefetchCallback, responseExpirationDays, request, - launchUrl + launchUrl, + showButtons, } = props; const medicationColumns = [ @@ -69,8 +70,10 @@ const PatientBox = props => { useEffect(() => { // get requests and responses on open of patients - getRequests(); - getResponses(); // TODO: PatientBox should not be rendering itself, needs to receive its state from parent + if (props.showButtons) { + getRequests(); + getResponses(); // TODO: PatientBox should not be rendering itself, needs to receive its state from parent + } getPatientInfo(); }, []); @@ -135,12 +138,12 @@ const PatientBox = props => { request.resourceType === 'MedicationRequest' || request.resourceType === 'MedicationDispense' ) { - updatePrefetchRequest(request, patient, defaultUser); + updatePrefetchRequest(request, patient, user); } else { clearCallback(); } } else { - updatePrefetchRequest(null, patient, defaultUser); + updatePrefetchRequest(null, patient, user); callback('request', {}); callback('code', null); callback('codeSystem', null); @@ -291,7 +294,7 @@ const PatientBox = props => { request.resourceType === 'MedicationRequest' || request.resourceType === 'MedicationDispense' ) { - updatePrefetchRequest(request, patient, defaultUser); + updatePrefetchRequest(request, patient, user); } else { clearCallback(); } @@ -578,68 +581,72 @@ const PatientBox = props => {
- {state.showMedications ? ( - - ) : ( - - - - - - )} - {state.showQuestionnaires ? ( - - ) : ( - - - - - - )} + {props.showButtons ? ( + state.showMedications ? ( + + ) : ( + + + + + + ) + ) : ""} + {props.showButtons ? ( + state.showQuestionnaires ? ( + + ) : ( + + + + + + ) + ) : ""} diff --git a/src/containers/BackOffice/BackOffice.jsx b/src/containers/BackOffice/BackOffice.jsx new file mode 100644 index 00000000..facd8b3f --- /dev/null +++ b/src/containers/BackOffice/BackOffice.jsx @@ -0,0 +1,58 @@ +import React, { memo, useEffect, useContext } from 'react'; +import BusinessIcon from '@mui/icons-material/Business'; +import AccountBoxIcon from '@mui/icons-material/AccountBox'; +import Box from '@mui/material/Box'; +import { Button } from '@mui/material'; +import { Container } from '@mui/system'; +import Dashboard from './Dashboard'; +import Index from '../Index'; +import useStyles from './styles'; + +import { logout } from '../../util/auth'; + +const BackOffice = (props) => { + const classes = useStyles(); + const { client, token } = props; + + useEffect(() => { + document.title = 'EHR | Back Office'; + }, []); + + return ( + + + { token && client ? ( + + +
+ +
+
+ +

EHR Back Office

+
+ + {token.name} + + +
+
+
+ + +
+ ) : ( + + ) } + +
+ + + ); +}; + +export default memo(BackOffice); diff --git a/src/containers/BackOffice/Dashboard.jsx b/src/containers/BackOffice/Dashboard.jsx new file mode 100644 index 00000000..86cb9da5 --- /dev/null +++ b/src/containers/BackOffice/Dashboard.jsx @@ -0,0 +1,81 @@ +import React, { useState, useEffect, useContext } from 'react'; + +import { Box, Tab, Tabs, Button } from '@mui/material'; +import { Container } from '@mui/system'; +import SettingsSection from '../../components/RequestDashboard/SettingsSection'; +import { SettingsContext } from '../ContextProvider/SettingsProvider'; +import TaskTab from './TaskTab'; + +function a11yProps(index) { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}` + }; + } + +export default function Dashboard(props) { + const { client, token } = props; + const [headerStyle, setHeaderStyle] = useState(undefined); + const [globalState, dispatch, updateSetting, readSettings] = React.useContext(SettingsContext); + console.log('global state patient -- > ', globalState.patient); + + + useEffect(() => { + readSettings(); + const updateScrollState = () => { + var threshold = 10; + if (window.scrollY > threshold) { + setHeaderStyle("true"); + } else { + setHeaderStyle(undefined); + } + } + document.addEventListener("scroll", updateScrollState); + return () => document.removeEventListener("scroll", updateScrollState); + }, []); + + const [tabIndex, setValue] = useState(0); + + const handleChange = (event, newValue) => { + setValue(newValue); + }; + + return ( +
+ + + + + + + + + + + + {tabIndex === 0 && ( + + + + )} + {tabIndex === 1 && ( + + + + )} + + + + +
+ ); +} \ No newline at end of file diff --git a/src/containers/BackOffice/TaskTab.jsx b/src/containers/BackOffice/TaskTab.jsx new file mode 100644 index 00000000..5b8ef194 --- /dev/null +++ b/src/containers/BackOffice/TaskTab.jsx @@ -0,0 +1,164 @@ +import React from 'react'; +import { useContext, useState, useEffect } from 'react'; + +import { Button, Box, Container, Grid, IconButton } from '@mui/material'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import PersonIcon from '@mui/icons-material/Person'; +import RefreshIcon from '@mui/icons-material/Refresh'; + +import { actionTypes } from '../ContextProvider/reducer'; + +import PatientSearchBar from '../../components/RequestBox/PatientSearchBar/PatientSearchBar.jsx'; +import TasksSection from '../../components/RequestDashboard/TasksSection'; +import { SettingsContext } from '../ContextProvider/SettingsProvider.jsx'; +import useStyles from './styles'; + +const TaskTab = props => { + const { client, token } = props; + const [globalState, dispatch] = useContext(SettingsContext); + const [state, setState] = useState({ + loading: false, + patient: {}, + user: null, + expanded: false, + patientList: [], + response: {}, + code: null, + codeSystem: null, + display: null, + request: {}, + showSettings: false, + token: null, + client: client, + medicationDispense: null, + lastCheckedMedicationTime: null, + prefetchCompleted: false, + medicationRequests: {} + }); + const classes = useStyles(); + + const getPatients = () => { + if (globalState.patientFhirQuery) { + client + .request(globalState.patientFhirQuery, { flat: true }) + .then(result => { + setState(prevState => ({ ...prevState, patientList: result })); + }) + .catch(e => { + setState(prevState => ({ ...prevState, patientList: e })); + console.log(e); + }); + } + }; + + useEffect(() => { + if (state.client) { + // Call patients on load of page + getPatients(); + } + // if use default user is set, use default user otherwise use logged in user if set + let currentUser = globalState.useDefaultUser ? globalState.defaultUser : (token.userId ? token.userId : globalState.defaultUser); + setState(prevState => ({...prevState, user: currentUser})); + }, []); + + const updateStateElement = (elementName, text) => { + if (elementName === 'patient') { + setState(prevState => ({ ...prevState, patient: text })); + dispatch({ + type: actionTypes.updatePatient, + value: text + }); + } else { + setState(prevState => ({ + ...prevState, + [elementName]: text + })); + } + }; + + const updateStateMap = (elementName, key, text) => { + setState(prevState => ({ + ...prevState, + [elementName]: { ...prevState[elementName], [key]: text } + })); + }; + + const clearState = () => { + setState(prevState => ({ + ...prevState, + response: {} + })); + }; + + const handleChange = () => (event, isExpanded) => { + setState(prevState => ({ + ...prevState, + expanded: isExpanded ? true : false + })); + }; + + return ( + + + + + } + aria-controls="panel1a-content" + id="panel1a-header" + > + + + {state.patient?.name ? ( + // Display the first name +

{state.patient?.name?.[0]?.given?.[0] + ' ' + state.patient?.name?.[0]?.family}

+ ) : ( +

All Patients

+ )} +
+ + {state.patientList.length > 0 && state.expanded && ( +
+ + {state.patientList instanceof Error ? ( + renderError() + ) : ( + + )} + +
+ )} +
+
+
+ + getPatients()} size="large"> + + + +
+ + + +
+ ); +} + +export default TaskTab; diff --git a/src/containers/BackOffice/styles.jsx b/src/containers/BackOffice/styles.jsx new file mode 100644 index 00000000..558b9866 --- /dev/null +++ b/src/containers/BackOffice/styles.jsx @@ -0,0 +1,79 @@ +import {styled} from '@mui/system'; +import { AppBar, Stack } from '@mui/material' +import { makeStyles } from '@mui/styles'; + +export default makeStyles( + theme => ({ + loginIcon: { + color: theme.palette.common.white, + fontSize: '19px', + marginLeft: 'auto', + fontFamily: 'Verdana', + float: 'right', + marginRight: '20px', + verticalAlign: 'middle' + }, + whiteButton: { + color: 'white !important', + borderColor: 'white !important', + marginRight: '5px !important', + marginLeft: '20px !important', + }, + patientButton: { + padding: '10px', + 'padding-left': '20px', + 'padding-right': '20px' + } + }) +); + + +export const StyledStack = styled(Stack)(({ theme, selected, disabled, isscrolled, highlight }) => ({ + position: 'relative', + // width: '200px', + margin: '0 5px', + padding: '8px 20px 8px 20px', + fontSize: '16px', + borderRadius: '8px', + cursor: disabled ? 'default' : 'pointer', + color: disabled ? theme.palette.text.gray : theme.palette.text.primary, + backgroundColor: highlight ? theme.palette.background.primary : (selected && !isscrolled) ? theme.palette.common.offWhite : 'inherit', + transition: `border 0.5s ease`, + border: '1px solid transparent', + borderBottomColor: (selected && isscrolled) ? theme.palette.primary.main : 'transparent', + boxShadow: (selected && !isscrolled) ? 'rgba(0,0,0,0.2) 8px -2px 12px 2px' : 'none', + '&:hover': { + border: disabled ? '' : `1px solid ${theme.palette.common.gray}` + } +})); + +export const GlossaryDiv = styled('div')(({ theme, isscrolled }) => ({ + backgroundColor: 'white', + zIndex: 1200, + padding: '30px', +})); + +export const StyledAppBarAlt = styled(AppBar, { + shouldForwardProp: (prop) => prop !== 'open', +})(({ theme, open, isscrolled, drawerwidth }) => { + console.log(theme); + console.log(theme.palette); + return { + marginTop: '15px', + marginBottom: '15px', + marginLeft: '2%', + marginRight: '2%', + width: '96%', + left: 0, + backgroundColor: isscrolled ? theme.palette.common.white : theme.palette.common.offWhite, + opacity: isscrolled ? 0.99 : 1, + color: theme.palette.common.black, + boxShadow: isscrolled ? 'rgba(0,0,0,0.2)' : 'none', + borderRadius: '8px', + border: isscrolled ? '1px solid #c1c1c1' : 'none', + ...(open && { + marginRight: `calc(2% + ${drawerwidth})`, + width: `calc(96% - ${drawerwidth}px)`, + + }), +}}); \ No newline at end of file diff --git a/src/containers/ContextProvider/SettingsProvider.jsx b/src/containers/ContextProvider/SettingsProvider.jsx index 4350a5ff..caaa2be0 100644 --- a/src/containers/ContextProvider/SettingsProvider.jsx +++ b/src/containers/ContextProvider/SettingsProvider.jsx @@ -1,13 +1,48 @@ import React from 'react'; import { reducer, initialState } from './reducer'; +import { actionTypes } from '../../containers/ContextProvider/reducer'; export const SettingsContext = React.createContext({ state: initialState, - dispatch: () => null + dispatch: () => null, + updateSetting: () => null, + readSettings: () => null, + saveSettings: () => null, }); + export const SettingsProvider = ({ children }) => { const [state, dispatch] = React.useReducer(reducer, initialState); - return {children}; + const updateSetting = (key, value) => { + dispatch({ + type: actionTypes.updateSetting, + settingId: key, + value: value + }); + }; + + const readSettings = () => { + JSON.parse(localStorage.getItem('reqgenSettings') || '[]').forEach(([key, value]) => { + try { + updateSetting(key, value); + } catch { + if (!key) { + console.log('Could not load setting:' + key); + } + } + }); + + // indicate to the rest of the app that the settings have been loaded + dispatch({ + type: actionTypes.flagStartup + }); + }; + + const saveSettings = () => { + const headers = Object.keys(state).map(key => [key, state[key]]); + localStorage.setItem('reqgenSettings', JSON.stringify(headers)); + }; + + return {children}; }; diff --git a/src/containers/Gateway/Gateway.jsx b/src/containers/Gateway/Gateway.jsx index 53a6e967..16f3e09b 100644 --- a/src/containers/Gateway/Gateway.jsx +++ b/src/containers/Gateway/Gateway.jsx @@ -1,7 +1,7 @@ import { memo, useState } from 'react'; import FHIR from 'fhirclient'; import env from 'env-var'; -import { Button, FormControl, TextField } from '@mui/material'; +import { Button, Checkbox, FormControl, FormControlLabel, TextField } from '@mui/material'; import Stack from '@mui/material/Stack'; import Autocomplete from '@mui/material/Autocomplete'; import useStyles from './styles'; @@ -14,6 +14,8 @@ const Gateway = props => { const envScope = env.get('VITE_CLIENT_SCOPES').asString().split(' '); const [clientId, setClientId] = useState(envClient || ''); const [fhirUrl, setFhirUrl] = useState(envFhir || ''); + const [backOffice, setBackOffice] = useState(false); + const [scope, _setScope] = useState(envScope || []); const setScope = value => { // split by space to facilitate copy/pasting strings of scopes into the input @@ -29,7 +31,7 @@ const Gateway = props => { FHIR.oauth2.authorize({ clientId: clientId, scope: scope.join(' '), - redirectUri: props.redirect, + redirectUri: props.redirect + (backOffice ? "/backoffice": ""), iss: fhirUrl }); }; @@ -99,6 +101,9 @@ const Gateway = props => { )} /> + + {setBackOffice(!backOffice)}} />} label="Back Office" /> + diff --git a/src/containers/Index.jsx b/src/containers/Index.jsx index a94b327f..069086b7 100644 --- a/src/containers/Index.jsx +++ b/src/containers/Index.jsx @@ -1,20 +1,48 @@ import { useState, useEffect } from 'react'; import FHIR from 'fhirclient'; import Home from '../components/RequestDashboard/Home'; +import BackOffice from './BackOffice/BackOffice'; -const Index = () => { - const [client, setClient] = useState(null); +const Index = (props) => { + const {backoffice} = props + const [client, setClient] = useState(null); + const [authToken, setAuthToken] = useState(null); + console.log(backoffice); + const [isBackOffice, setBackOffice] = useState(backoffice || null); + const parseJwt = (token) => { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + + const jsonToken = JSON.parse(jsonPayload); + setAuthToken(jsonToken); + if (jsonToken.realm_access) { + const roles = jsonToken.realm_access.roles; + console.log(roles); + if (roles.includes('BackOffice')) { + setBackOffice(true); + } else { + setBackOffice(false); + } + } + } useEffect(() => { FHIR.oauth2.ready().then(client => { + if(!isBackOffice) { + parseJwt(client.state.tokenResponse.access_token) + } setClient(client); }); }, []); return (
- {client ? ( - + {client && (isBackOffice !== null) ? ( + isBackOffice ? : + ) : (

Getting Client...

diff --git a/src/containers/PatientPortal.jsx b/src/containers/PatientPortal.jsx index cc253785..d79a4ada 100644 --- a/src/containers/PatientPortal.jsx +++ b/src/containers/PatientPortal.jsx @@ -4,9 +4,12 @@ import FHIR from 'fhirclient'; import Login from '../components/Auth/Login'; import Dashboard from '../components/Dashboard/Dashboard'; import AccountBoxIcon from '@mui/icons-material/AccountBox'; +import PersonIcon from '@mui/icons-material/Person'; import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; +import MuiAlert from '@mui/material/Alert'; +import Snackbar from '@mui/material/Snackbar'; import env from 'env-var'; import { actionTypes } from './ContextProvider/reducer'; import { SettingsContext } from './ContextProvider/SettingsProvider'; @@ -15,6 +18,7 @@ import { getPatientFirstAndLastName } from '../util/util'; const PatientPortal = () => { const classes = useStyles(); const [token, setToken] = useState(null); + const [data, setData] = useState(null); const [client, setClient] = useState(null); const [patientName, setPatientName] = useState(null); const [, dispatch] = React.useContext(SettingsContext); @@ -22,6 +26,7 @@ const PatientPortal = () => { useEffect(() => { if (token) { const data = JSON.parse(Buffer.from(token.split('.')[1], 'base64')); + setData(data); const client = FHIR.client({ serverUrl: env.get('VITE_EHR_BASE').asString(), tokenResponse: { @@ -30,13 +35,15 @@ const PatientPortal = () => { patient: data.patientId } }); - client.request(`Patient/${client.patient.id}`).then(patient => { - setPatientName(getPatientFirstAndLastName(patient)); - dispatch({ - type: actionTypes.updatePatient, - value: patient + if (client?.patient?.id) { + client.request(`Patient/${client.patient.id}`).then(patient => { + setPatientName(getPatientFirstAndLastName(patient)); + dispatch({ + type: actionTypes.updatePatient, + value: patient + }); }); - }); + } setClient(client); document.title = 'EHR | Patient Portal'; } @@ -49,9 +56,12 @@ const PatientPortal = () => { return (
- + - + + EHR Patient Portal {patientName ? ( @@ -61,10 +71,25 @@ const PatientPortal = () => { ) : null} - {token && client ? ( + {token && client && patientName ? ( ) : ( - +
+ { patientName ? ( '' ) : ( + + + Error! {data?.name} is not a Patient + + + ) } + +
)}
); diff --git a/src/containers/RequestBuilder.jsx b/src/containers/RequestBuilder.jsx index 6dee72d0..d58d8e54 100644 --- a/src/containers/RequestBuilder.jsx +++ b/src/containers/RequestBuilder.jsx @@ -28,6 +28,7 @@ const RequestBuilder = props => { const [state, setState] = useState({ loading: false, patient: {}, + user: null, expanded: true, patientList: [], response: {}, @@ -37,7 +38,7 @@ const RequestBuilder = props => { prefetchedResources: new Map(), request: {}, showSettings: false, - token: null, + token: props.token, client: client, medicationDispense: null, lastCheckedMedicationTime: null, @@ -46,6 +47,10 @@ const RequestBuilder = props => { }); const displayRequestBox = !!globalState.patient?.id; + useEffect(() => { + console.log(state.prefetchedResources); + }, [state.prefetchedResources]); + const isOrderNotSelected = () => { return Object.keys(state.request).length === 0; }; @@ -87,10 +92,15 @@ const RequestBuilder = props => { value: state.client.state.serverUrl }); } + + // if use default user is set, use default user otherwise use logged in user if set + let currentUser = globalState.useDefaultUser ? globalState.defaultUser : (state.userId ? state.userId : globalState.defaultUser); + setState(prevState => ({...prevState, user: currentUser})); }, []); const updateStateElement = (elementName, text) => { if (elementName === 'patient') { + setState(prevState => ({ ...prevState, patient: text })); dispatch({ type: actionTypes.updatePatient, value: text @@ -161,7 +171,7 @@ const RequestBuilder = props => { includeConfig: globalState.includeConfig, alternativeTherapy: globalState.alternativeTherapy }; - let user = globalState.defaultUser; + let user = state.user; let json_request = buildRequest( request, user, @@ -281,11 +291,17 @@ const RequestBuilder = props => { expandIcon={} aria-controls="panel1a-content" id="panel1a-header" - style={{ marginLeft: '45%' }} > - + + {state.patient?.name ? ( + // Display the first name +

{state.patient?.name?.[0]?.given?.[0] + ' ' + state.patient?.name?.[0]?.family}

+ ) : ( +

All Patients

+ )} {state.patientList.length > 0 && state.expanded && ( @@ -303,7 +319,8 @@ const RequestBuilder = props => { callbackMap={updateStateMap} clearCallback={clearState} responseExpirationDays={globalState.responseExpirationDays} - defaultUser={globalState.defaultUser} + user={state.user} + showButtons={true} /> )} @@ -339,7 +356,7 @@ const RequestBuilder = props => { responseExpirationDays={globalState.responseExpirationDays} pimsUrl={globalState.pimsUrl} smartAppUrl={globalState.smartAppUrl} - defaultUser={globalState.defaultUser} + user={state.user} loading={state.loading} patientFhirQuery={globalState.patientFhirQuery} prefetchCompleted={state.prefetchCompleted} diff --git a/src/index.css b/src/index.css index caf53ce6..3b6fcf7b 100644 --- a/src/index.css +++ b/src/index.css @@ -278,3 +278,30 @@ input:not(:focus):not([value='']):valid ~ .floating-label { overflow-y: auto; box-shadow: 10px 10px 20px black; } + +.backoffice-app { + text-align: center; + background-color: #A2025C; + padding: 20px; + margin-bottom: 20px; +} + + +.backoffice-app h1 { + color: white; + line-height: 1.3; + letter-spacing: 0.00938em; + font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; +} +.containerg { + display: grid; + grid-template-areas: ' main right'; +} + +.logo { + grid-area: main; + text-align: left; + margin-right: auto; + display: inline-flex; + box-sizing: unset; +} \ No newline at end of file diff --git a/src/util/auth.js b/src/util/auth.js index 6c2df049..5465731c 100644 --- a/src/util/auth.js +++ b/src/util/auth.js @@ -33,6 +33,12 @@ function login() { }); } +function logout() { + window.location.replace(`${env.get('VITE_AUTH').asString()}/realms/${env + .get('VITE_REALM') + .asString()}/protocol/openid-connect/logout`); +} + /** * Generates a JWT for a CDS service call, given the audience (the URL endpoint). The JWT is signed using a private key stored on the repository. * @@ -59,4 +65,4 @@ function createJwt(baseUrl, audience) { return KJUR.jws.JWS.sign(null, jwtHeader, jwtPayload, privKey); } -export { createJwt, login }; +export { createJwt, login, logout }; diff --git a/src/util/data.js b/src/util/data.js index 1eaa3201..f2f44460 100644 --- a/src/util/data.js +++ b/src/util/data.js @@ -26,6 +26,11 @@ const headerDefinitions = { type: 'input', default: env.get('VITE_DEFAULT_USER').asString() }, + useDefaultUser: { + display: 'Use Default User', + type: 'check', + default: env.get('VITE_USE_DEFAULT_USER').asBool() + }, ehrUrl: { display: 'EHR Server', type: 'input', @@ -81,6 +86,7 @@ const headerDefinitions = { type: 'input', default: env.get('VITE_INTERMEDIARY').asString() }, + hookToSend: { display: 'Send hook on patient select', type: 'dropdown', diff --git a/vite.config.ts b/vite.config.ts index 75d461a6..58b9fa78 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,6 +12,9 @@ export default defineConfig({ define: { 'process.env': process.env }, + optimizeDeps: { + include: ['@mui/material/Tooltip', '@emotion/styled'], + }, server: { // this sets a default port to 3000 port: 3000, From 542645b4b48cdc5f325591f9446ba00797ed3c03 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra <88040167+smalho01@users.noreply.github.com> Date: Tue, 3 Dec 2024 10:31:51 -0500 Subject: [PATCH 07/11] remove redundant copy (#141) --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9f417013..fa05dda5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,5 @@ RUN npm install EXPOSE 3000 HEALTHCHECK --interval=30s --start-period=15s --timeout=10m --retries=10 CMD wget --no-verbose --tries=1 --spider ${VITE_URL} || exit 1 -COPY --chown=node:node . . CMD npm run start From 081f222fd3871200585b65c21dbe2942376801ee Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Thu, 12 Dec 2024 14:55:54 -0500 Subject: [PATCH 08/11] fix smart launch from MELD sandbox --- src/containers/Index.jsx | 4 ++++ src/containers/Launch.jsx | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/containers/Index.jsx b/src/containers/Index.jsx index 069086b7..903ac44e 100644 --- a/src/containers/Index.jsx +++ b/src/containers/Index.jsx @@ -27,7 +27,11 @@ const Index = (props) => { } else { setBackOffice(false); } + } else { + // if no realm_access set, default to not using the back office + setBackOffice(false); } + console.log('isBackOffice: ' + isBackOffice); } useEffect(() => { FHIR.oauth2.ready().then(client => { diff --git a/src/containers/Launch.jsx b/src/containers/Launch.jsx index 5e8db748..dc2d865a 100644 --- a/src/containers/Launch.jsx +++ b/src/containers/Launch.jsx @@ -25,7 +25,14 @@ const Launch = props => { localStorage.setItem('clients', JSON.stringify(clients)); } } - const params = queryString.parse((window.location.hash || '').replace(/\/?#\/?launch\?/, '')); + + let params = queryString.parse((window.location.hash || '').replace(/\/?#\/?launch\?/, '')); + + // if no hash # value, pull the parameters out of the search + if (!params.iss) { + params = queryString.parse((window.location.search || '').replace(/\/?#?\/?[launch]?\?/, '')); + } + const iss = params.iss; console.log('iss: ' + iss); const launch = params.launch; From e4c6f3a730c68871d90b46ff2e9c62a9dfe62533 Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Tue, 31 Dec 2024 16:29:50 -0500 Subject: [PATCH 09/11] Updates for deploying to github pages. --- package.json | 2 +- src/components/App.jsx | 2 +- vite.config.ts | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1d1e7e90..c1f2c27f 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "scripts": { "start": "vite", "production": "json-server --watch src/db.json -p 3000 --host 0.0.0.0 --static build", - "build": "node build", + "build": "vite build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject", "lint": "eslint \"**/*.{js,jsx}\"", diff --git a/src/components/App.jsx b/src/components/App.jsx index ca55ec36..94e020e0 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -11,7 +11,7 @@ import theme from '../containers/styles/theme'; import { SettingsContext } from '../containers/ContextProvider/SettingsProvider'; import { actionTypes } from '../containers/ContextProvider/reducer'; -const isGhPages = process.env.VITE_GH_PAGES === 'true'; +const isGhPages = process.env.VITE_GH_PAGES.trim() === 'true'; const Router = isGhPages ? HashRouter : BrowserRouter; const redirect = isGhPages ? '/request-generator/#/index' : '/index'; const App = () => { diff --git a/vite.config.ts b/vite.config.ts index 58b9fa78..eee687ad 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -21,4 +21,8 @@ export default defineConfig({ open: false, host: true }, -}) \ No newline at end of file + build: { + outDir: 'build', + emptyOutDir: true, // also necessary + }, +}) From 9ff37ae67014e6012569024a195d89d4965223f2 Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Tue, 31 Dec 2024 16:35:15 -0500 Subject: [PATCH 10/11] small change to logging --- src/components/App.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/App.jsx b/src/components/App.jsx index 94e020e0..de99d55f 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -14,6 +14,7 @@ import { actionTypes } from '../containers/ContextProvider/reducer'; const isGhPages = process.env.VITE_GH_PAGES.trim() === 'true'; const Router = isGhPages ? HashRouter : BrowserRouter; const redirect = isGhPages ? '/request-generator/#/index' : '/index'; +console.log('redirect: ' + redirect); const App = () => { const [, dispatch] = React.useContext(SettingsContext); useEffect(() => { From eee269071a58359b06d1e07d0aa4866b9da25d49 Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Fri, 10 Jan 2025 16:06:07 -0500 Subject: [PATCH 11/11] show the card details --- src/components/DisplayBox/DisplayBox.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/DisplayBox/DisplayBox.jsx b/src/components/DisplayBox/DisplayBox.jsx index c5df3fe0..c2a3da81 100644 --- a/src/components/DisplayBox/DisplayBox.jsx +++ b/src/components/DisplayBox/DisplayBox.jsx @@ -242,7 +242,7 @@ const DisplayBox = props => { // -- Detail (ReactMarkdown supports Github-flavored markdown) -- const detailSection = card.detail ? (
- + {card.detail}
) : (

None

@@ -358,8 +358,8 @@ const DisplayBox = props => { {/* Forms */} {linksSection.length !== 0 ? (
- Required Forms {detailSection} + Required Forms {linksSection}
) : (