From bad20ab44356070427ba1189b2f47f517c211acc Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Mon, 16 Sep 2024 10:35:57 -0400 Subject: [PATCH 1/4] add ui for backoffice --- src/components/App.jsx | 7 +- src/components/Auth/Login.jsx | 2 +- src/containers/BackOffice/BackOffice.jsx | 42 ++++ src/containers/BackOffice/BackOfficeHome.jsx | 55 +++++ src/containers/BackOffice/Dashboard.jsx | 198 ++++++++++++++++++ .../BackOffice/SimplePatientDetails.jsx | 107 ++++++++++ .../BackOffice/SimplePatientSelect.jsx | 35 ++++ src/containers/BackOffice/styles.jsx | 52 +++++ src/containers/Gateway/Gateway.jsx | 9 +- src/containers/Index.jsx | 32 ++- src/containers/RequestBuilder.jsx | 3 + vite.config.ts | 3 + 12 files changed, 537 insertions(+), 8 deletions(-) create mode 100644 src/containers/BackOffice/BackOffice.jsx create mode 100644 src/containers/BackOffice/BackOfficeHome.jsx create mode 100644 src/containers/BackOffice/Dashboard.jsx create mode 100644 src/containers/BackOffice/SimplePatientDetails.jsx create mode 100644 src/containers/BackOffice/SimplePatientSelect.jsx create mode 100644 src/containers/BackOffice/styles.jsx diff --git a/src/components/App.jsx b/src/components/App.jsx index f26f03d..ca55ec3 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( - `${env.get('VITE_AUTH').asString()}/realms/${env + `${env.get('VITE_AUTH').asString()}/auth/realms/${env .get('VITE_REALM') .asString()}/protocol/openid-connect/token`, params, diff --git a/src/containers/BackOffice/BackOffice.jsx b/src/containers/BackOffice/BackOffice.jsx new file mode 100644 index 0000000..20340bc --- /dev/null +++ b/src/containers/BackOffice/BackOffice.jsx @@ -0,0 +1,42 @@ +import React, { memo, useState, useEffect } from 'react'; +import FHIR from 'fhirclient'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import env from 'env-var'; +import { SettingsContext } from '../ContextProvider/SettingsProvider'; +import Dashboard from './Dashboard'; + +const BackOffice = (props) => { + const { client } = props; + const [, dispatch] = React.useContext(SettingsContext); + + useEffect(() => { + document.title = 'EHR | Back Office'; + }, []); + + const logout = () => { + setClient(null); + setPatientName(null); + }; + + const getName = patient => { + const name = []; + if (patient.name) { + if (patient.name[0].given) { + name.push(patient.name[0].given[0]); + } + if (patient.name[0].family) { + name.push(patient.name[0].family); + } + } + return name.join(' '); + }; + return ( +
+ +
+ ); +}; + +export default memo(BackOffice); diff --git a/src/containers/BackOffice/BackOfficeHome.jsx b/src/containers/BackOffice/BackOfficeHome.jsx new file mode 100644 index 0000000..664f3c1 --- /dev/null +++ b/src/containers/BackOffice/BackOfficeHome.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Grid, Card, CardContent, Typography, Container } from '@mui/material'; + +function BackOfficeHome(props) { + return ( + + + Home + + + + + + + + Questionnaires + + + Manage and view your questionnaires here. + + + + + + + + + + Forms + + + Access and fill out forms. + + + + + + + + + + Patients + + + View and manage patient data. + + + + + + + ); +} + +export default BackOfficeHome; diff --git a/src/containers/BackOffice/Dashboard.jsx b/src/containers/BackOffice/Dashboard.jsx new file mode 100644 index 0000000..9330c55 --- /dev/null +++ b/src/containers/BackOffice/Dashboard.jsx @@ -0,0 +1,198 @@ +import React, { useState, useEffect, useContext } from 'react'; +import CssBaseline from '@mui/material/CssBaseline'; + +import { styled, useTheme } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import Drawer from '@mui/material/Drawer'; +import LibraryBooksIcon from '@mui/icons-material/LibraryBooks'; + +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; +import IconButton from '@mui/material/IconButton'; +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import HomeIcon from '@mui/icons-material/Home'; +import { StyledAppBarAlt, StyledStack } from './styles'; +import SettingsSection from '../../components/RequestDashboard/SettingsSection'; +import TasksSection from '../../components/RequestDashboard/TasksSection'; +import { SettingsContext } from '../ContextProvider/SettingsProvider'; +import SimplePatientSelect from './SimplePatientSelect'; +import SimplePatientDetails from './SimplePatientDetails'; +import BackOfficeHome from './BackOfficeHome'; +import { Person, Settings } from '@mui/icons-material'; + + +const drawerWidth = 400; + +const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })( + ({ theme, open }) => ({ + flexGrow: 1, + padding: theme.spacing(3), + transition: theme.transitions.create('margin', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + marginRight: -drawerWidth, + ...(open && { + transition: theme.transitions.create('margin', { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + marginRight: 0, + }), + position: 'relative', + }), +); + + +const DrawerHeader = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(0, 1), + // necessary for content to be below app bar + ...theme.mixins.toolbar, + justifyContent: 'flex-start', +})); + +export default function Dashboard(props) { + const { client } = props; + const [headerStyle, setHeaderStyle] = useState(undefined); + const [globalState, dispatch] = useContext(SettingsContext); + console.log(globalState.patient); + const tabs = { + homeTab: { + id: 'home', + label: 'Home', + icon: , + content:
+ }, + selectTab: { + id: 'select', + label: 'Select Patients', + icon: , + content:
+ }, + tasksTab: { + id: 'task', + label: 'View Tasks', + icon: , + content:
+ }, + settingsTab: { + id: 'settings', + label: 'Settings', + icon: , + content:
+ } + }; + const [selected, setSelected] = useState(tabs.homeTab); + const [glossaryOpen, setGlossaryOpen] = useState(false); + + + useEffect(() => { + retrieveInProgress(); + const updateScrollState = () => { + var threshold = 10; + if (window.scrollY > threshold) { + setHeaderStyle("true"); + } else { + setHeaderStyle(undefined); + } + } + document.addEventListener("scroll", updateScrollState); + return () => document.removeEventListener("scroll", updateScrollState); + }, []); + + const theme = useTheme(); + + const handleDrawerOpen = () => { + setGlossaryOpen(true); + }; + + const handleDrawerClose = () => { + setGlossaryOpen(false); + }; + + const retrieveInProgress = () => { + + let updateDate = new Date(); + updateDate.setDate(updateDate.getDate() - globalState.responseExpirationDays); + const searchParameters = [ + `_lastUpdated=gt${updateDate.toISOString().split('T')[0]}`, + 'status=in-progress', + '_sort=-authored' + ]; + client + .request(`QuestionnaireResponse?${searchParameters.join('&')}`, { + resolveReferences: ['subject'], + graph: false, + flat: true + }) + .then(result => { + console.log(result); + }); + } + return ( + + + + + + + + {Object.values(tabs).map((tab) => { + return ( setSelected(tab)} + isscrolled={headerStyle} + direction="row" + alignItems="center" + gap={2} + key={tab.id} + selected={selected?.id === tab.id} + > + {tab.icon} + + {tab.label} + + ) + })} + + + + View Patient + + + + + +
+ + {Object.values(tabs).map((tab) => { + if(tab.id === selected?.id){ + return tab.content; + } + })} +
+ + + + {theme.direction === 'rtl' ? : } + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/containers/BackOffice/SimplePatientDetails.jsx b/src/containers/BackOffice/SimplePatientDetails.jsx new file mode 100644 index 0000000..19186e9 --- /dev/null +++ b/src/containers/BackOffice/SimplePatientDetails.jsx @@ -0,0 +1,107 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { Stack, Typography, Box, Button, Card, CardContent } from '@mui/material'; +import { SettingsContext } from '../ContextProvider/SettingsProvider'; +import { retrieveLaunchContext } from '../../util/util'; + +const SimplePatientDetails = ({ client }) => { + const [questionnaires, setQuestionnaires] = useState([]); + const [globalState, dispatch] = useContext(SettingsContext); + const patient = globalState.patient; + const searchInProgressQuestionnaires = async (patientId) => { + try { + const response = await client.request({ + url: `QuestionnaireResponse?subject=Patient/${patientId}&status=in-progress`, + method: 'GET', + }); + setQuestionnaires(response.entry || []); + } catch (error) { + console.error('Error fetching questionnaires:', error); + } + }; + + useEffect(() => { + if (patient?.id) { + searchInProgressQuestionnaires(patient.id); + } + }, [patient]); + + const launchResponse = (response) => { + const appContext = `response=QuestionnaireResponse/${response.id}`; + const link = { + appContext: encodeURIComponent(appContext), + type: 'smart', + url: globalState.launchUrl + }; + + let linkCopy = Object.assign({}, link); + + retrieveLaunchContext(linkCopy, patient.id, client.state).then((res) => { + window.open(res.url, '_blank'); + }); + } + if(patient) { + return ( + + + Patient Details + + + + + + Name: {patient.name?.[0]?.given?.join(' ')} {patient.name?.[0]?.family} + + + Date of Birth: {patient.birthDate} + + + Gender: {patient.gender} + + + Address: {patient.address?.[0]?.line?.join(', ')}, {patient.address?.[0]?.city}, {patient.address?.[0]?.state}, {patient.address?.[0]?.postalCode} + + + + + + + In-Progress Questionnaires + + + {questionnaires.length > 0 ? ( + + {questionnaires.map((q) => ( + + ))} + + ) : ( + No in-progress questionnaires found. + )} + + ); +} else { + return ( +
No Patient Selected
+ ) +} +}; + +export default SimplePatientDetails; diff --git a/src/containers/BackOffice/SimplePatientSelect.jsx b/src/containers/BackOffice/SimplePatientSelect.jsx new file mode 100644 index 0000000..fee0e24 --- /dev/null +++ b/src/containers/BackOffice/SimplePatientSelect.jsx @@ -0,0 +1,35 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { Stack, Paper, Typography } from '@mui/material'; +import { actionTypes } from '../ContextProvider/reducer'; +import { SettingsContext } from '../ContextProvider/SettingsProvider'; + +function SimplePatientSelect(props) { + const { client } = props; + const [patients, setPatients] = useState([]); + const [globalState, dispatch] = useContext(SettingsContext); + + const handlePatientClick = (patient) => { + dispatch({ type: actionTypes.updatePatient, value: patient }); + }; + useEffect(() => { + client.request("Patient").then(result => { + setPatients(result.entry.map(entry => entry.resource)); + }); + }, []); + + return ( + + {patients.map((patient, index) => ( + {handlePatientClick(patient)}}> + + {patient.name?.[0]?.given?.[0] || 'Unknown'} {patient.name?.[0]?.family || ''} + + DOB: {patient.birthDate || 'Unknown'} + Patient ID: {patient.id} + + ))} + + ); +} + +export default SimplePatientSelect; diff --git a/src/containers/BackOffice/styles.jsx b/src/containers/BackOffice/styles.jsx new file mode 100644 index 0000000..f504758 --- /dev/null +++ b/src/containers/BackOffice/styles.jsx @@ -0,0 +1,52 @@ +import {styled} from '@mui/system'; +import { AppBar, Stack } from '@mui/material' + +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/Gateway/Gateway.jsx b/src/containers/Gateway/Gateway.jsx index 53a6e96..16f3e09 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 a94b327..88d49ff 100644 --- a/src/containers/Index.jsx +++ b/src/containers/Index.jsx @@ -1,19 +1,45 @@ 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); + 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); + 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 ? : ) : (
diff --git a/src/containers/RequestBuilder.jsx b/src/containers/RequestBuilder.jsx index 6dee72d..8e22de7 100644 --- a/src/containers/RequestBuilder.jsx +++ b/src/containers/RequestBuilder.jsx @@ -46,6 +46,9 @@ const RequestBuilder = props => { }); const displayRequestBox = !!globalState.patient?.id; + useEffect(() => { + console.log(state.prefetchedResources); + }, [state.prefetchedResources]); const isOrderNotSelected = () => { return Object.keys(state.request).length === 0; }; diff --git a/vite.config.ts b/vite.config.ts index 75d461a..58b9fa7 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 ef5a9dcab22a478032023f0cb7a3a9e3f14c581e Mon Sep 17 00:00:00 2001 From: Ariel Virgulto Date: Mon, 30 Sep 2024 11:04:21 -0400 Subject: [PATCH 2/4] Some updated UI to match design of others --- src/containers/BackOffice/BackOffice.jsx | 48 +++--- src/containers/BackOffice/Dashboard.jsx | 207 +++++++++-------------- src/index.css | 27 +++ 3 files changed, 125 insertions(+), 157 deletions(-) diff --git a/src/containers/BackOffice/BackOffice.jsx b/src/containers/BackOffice/BackOffice.jsx index 20340bc..528648c 100644 --- a/src/containers/BackOffice/BackOffice.jsx +++ b/src/containers/BackOffice/BackOffice.jsx @@ -1,41 +1,35 @@ -import React, { memo, useState, useEffect } from 'react'; -import FHIR from 'fhirclient'; -import AppBar from '@mui/material/AppBar'; -import Toolbar from '@mui/material/Toolbar'; -import Typography from '@mui/material/Typography'; -import env from 'env-var'; +import React, { memo, useEffect, useContext } from 'react'; +import BusinessIcon from '@mui/icons-material/Business'; +import Box from '@mui/material/Box'; +import { Container } from '@mui/system'; import { SettingsContext } from '../ContextProvider/SettingsProvider'; import Dashboard from './Dashboard'; const BackOffice = (props) => { const { client } = props; - const [, dispatch] = React.useContext(SettingsContext); + const [, dispatch] = useContext(SettingsContext); useEffect(() => { document.title = 'EHR | Back Office'; }, []); - const logout = () => { - setClient(null); - setPatientName(null); - }; - - const getName = patient => { - const name = []; - if (patient.name) { - if (patient.name[0].given) { - name.push(patient.name[0].given[0]); - } - if (patient.name[0].family) { - name.push(patient.name[0].family); - } - } - return name.join(' '); - }; return ( -
- -
+ +
+ +
+
+ +

Back Office

+
+
+
+
+ +
+ ); }; diff --git a/src/containers/BackOffice/Dashboard.jsx b/src/containers/BackOffice/Dashboard.jsx index 9330c55..e24345c 100644 --- a/src/containers/BackOffice/Dashboard.jsx +++ b/src/containers/BackOffice/Dashboard.jsx @@ -1,17 +1,12 @@ import React, { useState, useEffect, useContext } from 'react'; -import CssBaseline from '@mui/material/CssBaseline'; import { styled, useTheme } from '@mui/material/styles'; -import Box from '@mui/material/Box'; -import Drawer from '@mui/material/Drawer'; import LibraryBooksIcon from '@mui/icons-material/LibraryBooks'; -import Toolbar from '@mui/material/Toolbar'; +import { Box, Tab, Tabs, Button } from '@mui/material'; +import { Container } from '@mui/system'; +import Dialog from '@mui/material/Dialog'; import Typography from '@mui/material/Typography'; -import Divider from '@mui/material/Divider'; -import IconButton from '@mui/material/IconButton'; -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import HomeIcon from '@mui/icons-material/Home'; import { StyledAppBarAlt, StyledStack } from './styles'; import SettingsSection from '../../components/RequestDashboard/SettingsSection'; @@ -20,73 +15,20 @@ import { SettingsContext } from '../ContextProvider/SettingsProvider'; import SimplePatientSelect from './SimplePatientSelect'; import SimplePatientDetails from './SimplePatientDetails'; import BackOfficeHome from './BackOfficeHome'; -import { Person, Settings } from '@mui/icons-material'; - -const drawerWidth = 400; - -const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })( - ({ theme, open }) => ({ - flexGrow: 1, - padding: theme.spacing(3), - transition: theme.transitions.create('margin', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - marginRight: -drawerWidth, - ...(open && { - transition: theme.transitions.create('margin', { - easing: theme.transitions.easing.easeOut, - duration: theme.transitions.duration.enteringScreen, - }), - marginRight: 0, - }), - position: 'relative', - }), -); - - -const DrawerHeader = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - padding: theme.spacing(0, 1), - // necessary for content to be below app bar - ...theme.mixins.toolbar, - justifyContent: 'flex-start', -})); +function a11yProps(index) { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}` + }; + } export default function Dashboard(props) { const { client } = props; const [headerStyle, setHeaderStyle] = useState(undefined); const [globalState, dispatch] = useContext(SettingsContext); - console.log(globalState.patient); - const tabs = { - homeTab: { - id: 'home', - label: 'Home', - icon: , - content:
- }, - selectTab: { - id: 'select', - label: 'Select Patients', - icon: , - content:
- }, - tasksTab: { - id: 'task', - label: 'View Tasks', - icon: , - content:
- }, - settingsTab: { - id: 'settings', - label: 'Settings', - icon: , - content:
- } - }; - const [selected, setSelected] = useState(tabs.homeTab); + console.log('global state patient -- > ', globalState.patient); + const [glossaryOpen, setGlossaryOpen] = useState(false); @@ -104,8 +46,6 @@ export default function Dashboard(props) { return () => document.removeEventListener("scroll", updateScrollState); }, []); - const theme = useTheme(); - const handleDrawerOpen = () => { setGlossaryOpen(true); }; @@ -133,66 +73,73 @@ export default function Dashboard(props) { console.log(result); }); } + + const [tabIndex, setValue] = useState(0); + + const handleChange = (event, newValue) => { + setValue(newValue); + }; + return ( - - - - - - - {Object.values(tabs).map((tab) => { - return ( setSelected(tab)} - isscrolled={headerStyle} - direction="row" - alignItems="center" - gap={2} - key={tab.id} - selected={selected?.id === tab.id} - > - {tab.icon} - - {tab.label} - - ) - })} - - - - View Patient - - - - - -
- - {Object.values(tabs).map((tab) => { - if(tab.id === selected?.id){ - return tab.content; - } - })} -
- - - - {theme.direction === 'rtl' ? : } - - - -
-
-
+
+ + + + + + + + + + + + + + + {tabIndex === 0 && ( + + + + )} + {tabIndex === 1 && ( + + + + )} + {tabIndex === 2 && ( + + + + )} + {tabIndex === 3 && ( + + + + )} + + + + +
+
+
+
); } \ No newline at end of file diff --git a/src/index.css b/src/index.css index caf53ce..3b6fcf7 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 From ea346cf4b12178a166b28323b390629ad2356f24 Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Tue, 15 Oct 2024 15:31:05 -0400 Subject: [PATCH 3/4] Back office staff workflow updates --- .env | 1 + README.md | 1 + src/components/Auth/Login.jsx | 3 +- 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 | 35 +--- .../RequestDashboard/TasksSection.jsx | 127 +++++++++++--- src/components/RequestDashboard/styles.jsx | 24 +++ src/components/SMARTBox/PatientBox.jsx | 145 ++++++++-------- src/containers/BackOffice/BackOffice.jsx | 52 ++++-- src/containers/BackOffice/BackOfficeHome.jsx | 55 ------ src/containers/BackOffice/Dashboard.jsx | 78 +-------- .../BackOffice/SimplePatientDetails.jsx | 107 ------------ .../BackOffice/SimplePatientSelect.jsx | 35 ---- src/containers/BackOffice/TaskTab.jsx | 164 ++++++++++++++++++ src/containers/BackOffice/styles.jsx | 27 +++ .../ContextProvider/SettingsProvider.jsx | 39 ++++- src/containers/Index.jsx | 6 +- src/containers/PatientPortal.jsx | 45 +++-- src/containers/RequestBuilder.jsx | 26 ++- src/util/auth.js | 8 +- src/util/data.js | 6 + 25 files changed, 605 insertions(+), 440 deletions(-) delete mode 100644 src/containers/BackOffice/BackOfficeHome.jsx delete mode 100644 src/containers/BackOffice/SimplePatientDetails.jsx delete mode 100644 src/containers/BackOffice/SimplePatientSelect.jsx create mode 100644 src/containers/BackOffice/TaskTab.jsx diff --git a/.env b/.env index 39c1fd4..e9b49c8 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 4c7dd50..0d76231 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/Auth/Login.jsx b/src/components/Auth/Login.jsx index 77decaa..72c1158 100644 --- a/src/components/Auth/Login.jsx +++ b/src/components/Auth/Login.jsx @@ -22,7 +22,8 @@ const Login = props => { params.append('client_id', env.get('VITE_CLIENT').asString()); axios .post( - `${env.get('VITE_AUTH').asString()}/auth/realms/${env + // this change breaks the patient portal login! + `${env.get('VITE_AUTH').asString()}/realms/${env .get('VITE_REALM') .asString()}/protocol/openid-connect/token`, params, diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index e6b3dda..ba7b505 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 54e2362..0f73050 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 6416a26..54ed70a 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 788cacd..9ce8648 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 2151272..0439e99 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 c368a28..e4a5e0f 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 }); }; @@ -88,7 +62,7 @@ const SettingsSection = props => { ({ defaultUser }) => () => { props.client - .request('QuestionnaireResponse?author=' + defaultUser, { flat: true }) + .request('QuestionnaireResponse', { flat: true }) .then(result => { result.forEach(resource => { props.client @@ -217,6 +191,7 @@ const SettingsSection = props => { let firstCheckbox = true; let showBreak = true; + return ( @@ -225,6 +200,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 b712b87..f627957 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 4a85cf5..263a198 100644 --- a/src/components/RequestDashboard/styles.jsx +++ b/src/components/RequestDashboard/styles.jsx @@ -135,6 +135,30 @@ export default makeStyles( }, taskTabOwner: { color: '#777' + }, + titleIcon: { + color: 'white', + fontSize: '19px', + marginLeft: 'auto', + 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 01ff274..9b4e4ed 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 index 528648c..facd8b3 100644 --- a/src/containers/BackOffice/BackOffice.jsx +++ b/src/containers/BackOffice/BackOffice.jsx @@ -1,34 +1,56 @@ 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 { SettingsContext } from '../ContextProvider/SettingsProvider'; import Dashboard from './Dashboard'; +import Index from '../Index'; +import useStyles from './styles'; + +import { logout } from '../../util/auth'; const BackOffice = (props) => { - const { client } = props; - const [, dispatch] = useContext(SettingsContext); + const classes = useStyles(); + const { client, token } = props; useEffect(() => { - document.title = 'EHR | Back Office'; + document.title = 'EHR | Back Office'; }, []); return ( + -
- -
-
- -

Back Office

+ { token && client ? ( + + +
+ +
+
+ +

EHR Back Office

+
+ + {token.name} + +
-
-
- +
+ + + + ) : ( + + ) } + + ); }; diff --git a/src/containers/BackOffice/BackOfficeHome.jsx b/src/containers/BackOffice/BackOfficeHome.jsx deleted file mode 100644 index 664f3c1..0000000 --- a/src/containers/BackOffice/BackOfficeHome.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import { Grid, Card, CardContent, Typography, Container } from '@mui/material'; - -function BackOfficeHome(props) { - return ( - - - Home - - - - - - - - Questionnaires - - - Manage and view your questionnaires here. - - - - - - - - - - Forms - - - Access and fill out forms. - - - - - - - - - - Patients - - - View and manage patient data. - - - - - - - ); -} - -export default BackOfficeHome; diff --git a/src/containers/BackOffice/Dashboard.jsx b/src/containers/BackOffice/Dashboard.jsx index e24345c..86cb9da 100644 --- a/src/containers/BackOffice/Dashboard.jsx +++ b/src/containers/BackOffice/Dashboard.jsx @@ -1,20 +1,10 @@ import React, { useState, useEffect, useContext } from 'react'; -import { styled, useTheme } from '@mui/material/styles'; -import LibraryBooksIcon from '@mui/icons-material/LibraryBooks'; - import { Box, Tab, Tabs, Button } from '@mui/material'; import { Container } from '@mui/system'; -import Dialog from '@mui/material/Dialog'; -import Typography from '@mui/material/Typography'; -import HomeIcon from '@mui/icons-material/Home'; -import { StyledAppBarAlt, StyledStack } from './styles'; import SettingsSection from '../../components/RequestDashboard/SettingsSection'; -import TasksSection from '../../components/RequestDashboard/TasksSection'; import { SettingsContext } from '../ContextProvider/SettingsProvider'; -import SimplePatientSelect from './SimplePatientSelect'; -import SimplePatientDetails from './SimplePatientDetails'; -import BackOfficeHome from './BackOfficeHome'; +import TaskTab from './TaskTab'; function a11yProps(index) { return { @@ -24,16 +14,14 @@ function a11yProps(index) { } export default function Dashboard(props) { - const { client } = props; + const { client, token } = props; const [headerStyle, setHeaderStyle] = useState(undefined); - const [globalState, dispatch] = useContext(SettingsContext); + const [globalState, dispatch, updateSetting, readSettings] = React.useContext(SettingsContext); console.log('global state patient -- > ', globalState.patient); - const [glossaryOpen, setGlossaryOpen] = useState(false); - useEffect(() => { - retrieveInProgress(); + readSettings(); const updateScrollState = () => { var threshold = 10; if (window.scrollY > threshold) { @@ -46,34 +34,6 @@ export default function Dashboard(props) { return () => document.removeEventListener("scroll", updateScrollState); }, []); - const handleDrawerOpen = () => { - setGlossaryOpen(true); - }; - - const handleDrawerClose = () => { - setGlossaryOpen(false); - }; - - const retrieveInProgress = () => { - - let updateDate = new Date(); - updateDate.setDate(updateDate.getDate() - globalState.responseExpirationDays); - const searchParameters = [ - `_lastUpdated=gt${updateDate.toISOString().split('T')[0]}`, - 'status=in-progress', - '_sort=-authored' - ]; - client - .request(`QuestionnaireResponse?${searchParameters.join('&')}`, { - resolveReferences: ['subject'], - graph: false, - flat: true - }) - .then(result => { - console.log(result); - }); - } - const [tabIndex, setValue] = useState(0); const handleChange = (event, newValue) => { @@ -81,10 +41,8 @@ export default function Dashboard(props) { }; return ( -
- - - - - + + @@ -108,20 +64,10 @@ export default function Dashboard(props) { {tabIndex === 0 && ( - + )} {tabIndex === 1 && ( - - - - )} - {tabIndex === 2 && ( - - - - )} - {tabIndex === 3 && ( @@ -129,16 +75,6 @@ export default function Dashboard(props) { - -
-
); diff --git a/src/containers/BackOffice/SimplePatientDetails.jsx b/src/containers/BackOffice/SimplePatientDetails.jsx deleted file mode 100644 index 19186e9..0000000 --- a/src/containers/BackOffice/SimplePatientDetails.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useState, useEffect, useContext } from 'react'; -import { Stack, Typography, Box, Button, Card, CardContent } from '@mui/material'; -import { SettingsContext } from '../ContextProvider/SettingsProvider'; -import { retrieveLaunchContext } from '../../util/util'; - -const SimplePatientDetails = ({ client }) => { - const [questionnaires, setQuestionnaires] = useState([]); - const [globalState, dispatch] = useContext(SettingsContext); - const patient = globalState.patient; - const searchInProgressQuestionnaires = async (patientId) => { - try { - const response = await client.request({ - url: `QuestionnaireResponse?subject=Patient/${patientId}&status=in-progress`, - method: 'GET', - }); - setQuestionnaires(response.entry || []); - } catch (error) { - console.error('Error fetching questionnaires:', error); - } - }; - - useEffect(() => { - if (patient?.id) { - searchInProgressQuestionnaires(patient.id); - } - }, [patient]); - - const launchResponse = (response) => { - const appContext = `response=QuestionnaireResponse/${response.id}`; - const link = { - appContext: encodeURIComponent(appContext), - type: 'smart', - url: globalState.launchUrl - }; - - let linkCopy = Object.assign({}, link); - - retrieveLaunchContext(linkCopy, patient.id, client.state).then((res) => { - window.open(res.url, '_blank'); - }); - } - if(patient) { - return ( - - - Patient Details - - - - - - Name: {patient.name?.[0]?.given?.join(' ')} {patient.name?.[0]?.family} - - - Date of Birth: {patient.birthDate} - - - Gender: {patient.gender} - - - Address: {patient.address?.[0]?.line?.join(', ')}, {patient.address?.[0]?.city}, {patient.address?.[0]?.state}, {patient.address?.[0]?.postalCode} - - - - - - - In-Progress Questionnaires - - - {questionnaires.length > 0 ? ( - - {questionnaires.map((q) => ( - - ))} - - ) : ( - No in-progress questionnaires found. - )} - - ); -} else { - return ( -
No Patient Selected
- ) -} -}; - -export default SimplePatientDetails; diff --git a/src/containers/BackOffice/SimplePatientSelect.jsx b/src/containers/BackOffice/SimplePatientSelect.jsx deleted file mode 100644 index fee0e24..0000000 --- a/src/containers/BackOffice/SimplePatientSelect.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useContext, useEffect, useState } from 'react'; -import { Stack, Paper, Typography } from '@mui/material'; -import { actionTypes } from '../ContextProvider/reducer'; -import { SettingsContext } from '../ContextProvider/SettingsProvider'; - -function SimplePatientSelect(props) { - const { client } = props; - const [patients, setPatients] = useState([]); - const [globalState, dispatch] = useContext(SettingsContext); - - const handlePatientClick = (patient) => { - dispatch({ type: actionTypes.updatePatient, value: patient }); - }; - useEffect(() => { - client.request("Patient").then(result => { - setPatients(result.entry.map(entry => entry.resource)); - }); - }, []); - - return ( - - {patients.map((patient, index) => ( - {handlePatientClick(patient)}}> - - {patient.name?.[0]?.given?.[0] || 'Unknown'} {patient.name?.[0]?.family || ''} - - DOB: {patient.birthDate || 'Unknown'} - Patient ID: {patient.id} - - ))} - - ); -} - -export default SimplePatientSelect; diff --git a/src/containers/BackOffice/TaskTab.jsx b/src/containers/BackOffice/TaskTab.jsx new file mode 100644 index 0000000..5b8ef19 --- /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 index f504758..558b986 100644 --- a/src/containers/BackOffice/styles.jsx +++ b/src/containers/BackOffice/styles.jsx @@ -1,5 +1,32 @@ 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', diff --git a/src/containers/ContextProvider/SettingsProvider.jsx b/src/containers/ContextProvider/SettingsProvider.jsx index 4350a5f..caaa2be 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/Index.jsx b/src/containers/Index.jsx index 88d49ff..069086b 100644 --- a/src/containers/Index.jsx +++ b/src/containers/Index.jsx @@ -7,6 +7,7 @@ import BackOffice from './BackOffice/BackOffice'; 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) => { @@ -17,6 +18,7 @@ const Index = (props) => { }).join('')); const jsonToken = JSON.parse(jsonPayload); + setAuthToken(jsonToken); if (jsonToken.realm_access) { const roles = jsonToken.realm_access.roles; console.log(roles); @@ -39,8 +41,8 @@ const Index = (props) => { return (
{client && (isBackOffice !== null) ? ( - isBackOffice ? : - + isBackOffice ? : + ) : (

Getting Client...

diff --git a/src/containers/PatientPortal.jsx b/src/containers/PatientPortal.jsx index cc25378..d79a4ad 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 8e22de7..d58d8e5 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, @@ -49,6 +50,7 @@ const RequestBuilder = props => { useEffect(() => { console.log(state.prefetchedResources); }, [state.prefetchedResources]); + const isOrderNotSelected = () => { return Object.keys(state.request).length === 0; }; @@ -90,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 @@ -164,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, @@ -284,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 && ( @@ -306,7 +319,8 @@ const RequestBuilder = props => { callbackMap={updateStateMap} clearCallback={clearState} responseExpirationDays={globalState.responseExpirationDays} - defaultUser={globalState.defaultUser} + user={state.user} + showButtons={true} /> )} @@ -342,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/util/auth.js b/src/util/auth.js index 6c2df04..5465731 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 1eaa320..f2f4446 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', From 369855cf866d3a94285c7f8be16ee320287e98bf Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Fri, 25 Oct 2024 16:28:50 -0400 Subject: [PATCH 4/4] fix styling --- src/components/RequestDashboard/styles.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/RequestDashboard/styles.jsx b/src/components/RequestDashboard/styles.jsx index 263a198..2eab437 100644 --- a/src/components/RequestDashboard/styles.jsx +++ b/src/components/RequestDashboard/styles.jsx @@ -139,7 +139,6 @@ export default makeStyles( titleIcon: { color: 'white', fontSize: '19px', - marginLeft: 'auto', fontFamily: 'Verdana', float: 'left', marginLeft: '20px',