diff --git a/src/components/App.js b/src/components/App.js index dd43cadf..f22c2a08 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,5 +1,5 @@ import { ThemeProvider } from '@mui/styles'; -import React from 'react'; +import React, { useEffect } from 'react'; import { BrowserRouter, HashRouter, Route, Routes } from 'react-router-dom'; import Gateway from '../containers/Gateway/Gateway'; import Index from '../containers/Index'; @@ -7,10 +7,21 @@ import Launch from '../containers/Launch'; import PatientPortal from '../containers/PatientPortal'; import RegisterPage from '../containers/register/RegisterPage'; import theme from '../containers/styles/theme'; +import { SettingsContext } from '../containers/ContextProvider/SettingsProvider'; +import { actionTypes } from '../containers/ContextProvider/reducer'; + const isGhPages = process.env.REACT_APP_GH_PAGES === 'true'; const Router = isGhPages ? HashRouter : BrowserRouter; const redirect = isGhPages ? '/request-generator/#/index' : '/index'; const App = () => { + const [state, dispatch] = React.useContext(SettingsContext); + useEffect(() => { + dispatch({ + type: actionTypes.updateSetting, + settingId: 'redirect', + value: redirect + }); + }, []); return ( diff --git a/src/components/Auth/Login.jsx b/src/components/Auth/Login.jsx index e047db7e..5000d4b2 100644 --- a/src/components/Auth/Login.jsx +++ b/src/components/Auth/Login.jsx @@ -11,6 +11,7 @@ const Login = props => { const [username, _setUsername] = useState(''); const [password, _setPassword] = useState(''); const handleClose = () => setMessage(null); + document.title = 'EHR | Patient Portal'; const onSubmit = useCallback(() => { if (username && password) { diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index c5304a3a..95aae834 100644 --- a/src/components/Dashboard/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -1,8 +1,5 @@ import React, { memo, useState, useEffect } from 'react'; import useStyles from './styles'; -import DashboardElement from './DashboardElement'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import Checkbox from '@mui/material/Checkbox'; import Box from '@mui/material/Box'; import Drawer from '@mui/material/Drawer'; import CssBaseline from '@mui/material/CssBaseline'; @@ -10,6 +7,7 @@ import Toolbar from '@mui/material/Toolbar'; import List from '@mui/material/List'; import Divider from '@mui/material/Divider'; import AssignmentIcon from '@mui/icons-material/Assignment'; +import ListAltIcon from '@mui/icons-material/ListAlt'; import MedicationIcon from '@mui/icons-material/Medication'; import BiotechIcon from '@mui/icons-material/Biotech'; import LogoutIcon from '@mui/icons-material/Logout'; @@ -21,34 +19,28 @@ import NotificationsIcon from '@mui/icons-material/Notifications'; import AlarmIcon from '@mui/icons-material/Alarm'; import SettingsIcon from '@mui/icons-material/Settings'; import MedicalInformationIcon from '@mui/icons-material/MedicalInformation'; -import { Paper } from '@mui/material'; +import FormsSection from './ListSelections/FormsSection'; +import EmptySection from './ListSelections/EmptySection'; +import PatientTaskSection from './ListSelections/PatientTaskSection'; const Dashboard = props => { const classes = useStyles(); - const [resources, setResources] = useState([]); - const [message, setMessage] = useState('Loading...'); - const [checked, setChecked] = useState(true); - const drawerWidth = '340px'; - const handleChange = event => { - setChecked(event.target.checked); - }; + const [selectedIndex, setSelectedIndex] = useState(3); - const addResources = bundle => { - if (bundle.entry) { - bundle.entry.forEach(e => { - const resource = e.resource; - setResources(resources => [...resources, resource]); - }); - } + const handleListItemClick = (event, index) => { + setSelectedIndex(index); }; + + const drawerWidth = '340px'; + const createIcons = () => { const icons = []; const style = { fontSize: '40px' }; const itemStyle = { height: '80px' }; - const qStyle = { height: '80px', backgroundColor: '#f5f5fa' }; icons.push(['Notifications', , itemStyle]); icons.push(['Appointments', , itemStyle]); - icons.push(['Questionnaire Forms', , qStyle]); + icons.push(['Tasks', , itemStyle]); + icons.push(['Questionnaire Forms', , itemStyle]); icons.push(['Health Data', , itemStyle]); icons.push(['Medications', , itemStyle]); icons.push(['Tests and Results', , itemStyle]); @@ -57,32 +49,25 @@ const Dashboard = props => { return icons; }; + useEffect(() => { - if (props.client.patient.id) { - props.client.patient - .request('QuestionnaireResponse', { pageLimit: 0, onPage: addResources }) - .then(() => { - setMessage( - 'No QuestionnaireResponses Found for user with patientId: ' + props.client.patient.id - ); - }); - } else { - setMessage('Invalid patient: No patientId provided'); + if (selectedIndex === 8) { + // logout - set client to null to display login page + props.logout(); } - }, [props.client.patient]); + }, [selectedIndex]); - const renderElements = () => { - let resourcesToRender = []; - if (checked) { - resourcesToRender = resources.filter(e => { - return e.status === 'in-progress'; - }); - } else { - resourcesToRender = resources; + const renderBody = () => { + switch (selectedIndex) { + case 2: + return ; + case 3: + return ; + default: + return ; } - resourcesToRender.reverse(); - return resourcesToRender; }; + return (
@@ -102,8 +87,13 @@ const Dashboard = props => { {createIcons().map((option, index) => (
- - + + handleListItemClick(event, index)}> {option[1]} { -
-

Available Forms

- } - label="Only show in-progress forms" - /> - {resources.length > 0 ? ( - renderElements().map(e => { - return ( - - ); - }) - ) : ( - {message} - )} -
+ {renderBody()}
diff --git a/src/components/Dashboard/ListSelections/EmptySection.jsx b/src/components/Dashboard/ListSelections/EmptySection.jsx new file mode 100644 index 00000000..b9dd66ca --- /dev/null +++ b/src/components/Dashboard/ListSelections/EmptySection.jsx @@ -0,0 +1,13 @@ +import React, { memo } from 'react'; +import useStyles from '../styles'; + +const EmptySection = () => { + const classes = useStyles(); + return ( +
+

Not available

+
+ ); +}; + +export default memo(EmptySection); diff --git a/src/components/Dashboard/ListSelections/FormsSection.jsx b/src/components/Dashboard/ListSelections/FormsSection.jsx new file mode 100644 index 00000000..34491555 --- /dev/null +++ b/src/components/Dashboard/ListSelections/FormsSection.jsx @@ -0,0 +1,75 @@ +import React, { memo, useState, useEffect } from 'react'; +import { Paper } from '@mui/material'; +import DashboardElement from '../DashboardElement'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import useStyles from '../styles'; + +const FormsSection = props => { + const classes = useStyles(); + const [resources, setResources] = useState([]); + const [message, setMessage] = useState('Loading...'); + const [checked, setChecked] = useState(true); + + useEffect(() => { + if (props.client.patient.id) { + props.client.patient + .request('QuestionnaireResponse', { pageLimit: 0, onPage: addResources }) + .then(() => { + setMessage( + 'No QuestionnaireResponses Found for user with patientId: ' + props.client.patient.id + ); + }); + } else { + setMessage('Invalid patient: No patientId provided'); + } + }, [props.client.patient]); + + const handleChange = event => { + setChecked(event.target.checked); + }; + + const addResources = bundle => { + if (bundle.entry) { + bundle.entry.forEach(e => { + const resource = e.resource; + setResources(resources => [...resources, resource]); + }); + } + }; + + const renderElements = () => { + let resourcesToRender = []; + if (checked) { + resourcesToRender = resources.filter(e => { + return e.status === 'in-progress'; + }); + } else { + resourcesToRender = resources; + } + resourcesToRender.reverse(); + return resourcesToRender; + }; + + return ( +
+

Available Forms

+ } + label="Only show in-progress forms" + /> + {resources.length > 0 ? ( + renderElements().map(e => { + return ( + + ); + }) + ) : ( + {message} + )} +
+ ); +}; + +export default memo(FormsSection); diff --git a/src/components/Dashboard/ListSelections/PatientTaskSection.jsx b/src/components/Dashboard/ListSelections/PatientTaskSection.jsx new file mode 100644 index 00000000..6900f73f --- /dev/null +++ b/src/components/Dashboard/ListSelections/PatientTaskSection.jsx @@ -0,0 +1,205 @@ +import React, { memo, useState, useEffect, Fragment } from 'react'; +import useStyles from '../styles'; +import { Button, Grid, Stack, Modal, Box } from '@mui/material'; +import AssignmentTurnedInIcon from '@mui/icons-material/AssignmentTurnedIn'; +import AssignmentLateIcon from '@mui/icons-material/AssignmentLate'; +import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; +import { Info, Refresh } from '@mui/icons-material'; +import PersonIcon from '@mui/icons-material/Person'; +import PersonAddIcon from '@mui/icons-material/PersonAdd'; +import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import DeleteIcon from '@mui/icons-material/Delete'; + +const PatientTaskSection = props => { + const classes = useStyles(); + const [tasks, setTasks] = useState([]); + const [taskToDelete, setTaskToDelete] = useState(''); + const [open, setOpen] = useState(false); + const currUser = props.client.patient.id; + + const tryDelete = task => { + setTaskToDelete(task); + setOpen(true); + }; + const handleClose = () => { + setOpen(false); + }; + const fetchTasks = () => { + let identifier = 'Task'; + if (props.client.patient && props.client.patient.id) { + identifier = `Task?patient=${props.client.patient.id}`; + } + props.client.request(identifier, { resolveReferences: ['for', 'owner'] }).then(request => { + console.log(request); + if (request && request.entry) { + const allTasks = request.entry.map(e => e.resource); + const myTasks = allTasks.filter(t => t.owner?.id === currUser); + setTasks(myTasks); + } else { + setTasks([]); + } + }); + }; + + useEffect(() => { + fetchTasks(); + }, []); + + const deleteTask = () => { + if (taskToDelete) { + props.client.delete(`${taskToDelete.resourceType}/${taskToDelete.id}`).then(e => { + console.log('Deleted Task'); + fetchTasks(); + }); + setOpen(false); + setTaskToDelete(''); + } + }; + + const renderTasks = taskSubset => { + if (taskSubset.length > 0) { + return ( + + {taskSubset.map(t => renderTask(t))} + + ); + } else { + return

No Tasks Found

; + } + }; + + const renderTask = task => { + let statusColor = '#555'; + if (task.status.toLowerCase() === 'ready') { + statusColor = '#198754'; + } + + let taskForName = 'N/A'; + let taskOwnerName = 'N/A'; + if (task.for.resourceType.toLowerCase() === 'patient') { + const patient = task.for; + if (patient.name) { + taskForName = `${patient.name[0].given[0]} ${patient.name[0].family}`; + } + } + if (task.owner && task.owner.resourceType.toLowerCase() === 'patient') { + const patient = task.owner; + if (patient.name) { + taskOwnerName = `${patient.name[0].given[0]} ${patient.name[0].family}`; + } else { + taskOwnerName = task.owner.id; + } + } + let ownerText = ( + + + Unassigned + + ); + if (task.owner) { + ownerText = ( + + + {`Assigned to ${taskOwnerName}`} + + ); + } + return ( + + + + + {`Task ID: ${task.id}`} + + + {`Timestamp: ${task.authoredOn}`} + + + {`Beneficiary: ${task.for ? task.for.id : 'None'}`} + + + {`STATUS: ${task.status.toUpperCase()}`} + + + {task.description} + + + + {taskForName} + + + + {ownerText} + + + + + + {/*spacer*/} + + + + + + + + + + + ); + }; + + return ( +
+

Tasks

+ {renderTasks(tasks)} + + + + + {taskToDelete ? `Are you sure you want to delete Task ${taskToDelete.id}` : ''} + + + {/*spacer*/} + + + + + + + + + + +
+ ); +}; + +export default memo(PatientTaskSection); diff --git a/src/components/Dashboard/styles.jsx b/src/components/Dashboard/styles.jsx index e2a2437f..14d02edf 100644 --- a/src/components/Dashboard/styles.jsx +++ b/src/components/Dashboard/styles.jsx @@ -47,7 +47,64 @@ export default makeStyles( borderRadius: '12px', float: 'right' }, - spacer: {} + spacer: {}, + taskDeleteHeader: { + padding: '15px 0 15px 0' + }, + taskDeleteModal: { + border: '1px solid black', + width: '400px', + + backgroundColor: 'white', + position: 'fixed', + top: '50%', + left: '50%', + padding: '15px', + transform: 'translate(-50%, -50%)', + overflowY: 'auto', + fontSize: '18px', + boxShadow: '10px 10px 20px black' + }, + tabDivView: { + '&.MuiGrid-root': { + // padding: '0 15px 0 15px', + marginTop: '0vh', + alignItems: 'flex-start', + justifyContent: 'flex-start' + } + }, + taskHeaderTabs: { + margin: '15px 15px 5px 15px', + backgroundColor: '#F5F5F7' + }, + taskRefreshButton: { + padding: '35px 0 0 0' + }, + taskTabButton: { + padding: '10px 0px 5px 0px' + }, + taskTabMain: { + border: '0px solid black', + boxShadow: '2px 2px', + borderRadius: '5px', + padding: '8px', + background: 'linear-gradient(to right bottom, #F5F5F7, #eaeaef)', + '&:hover': { + background: 'linear-gradient(to right bottom, #FFFFFF, #efefff)' + } + }, + taskTabHeader: { + fontSize: '8px', + color: '#777', + borderBottom: '1px solid #e3e3ef' + }, + taskTabDescription: { + fontSize: '18px', + padding: '8px 0px 10px 2px' + }, + taskTabOwner: { + color: '#777' + } }), { name: 'Dashboard', index: 1 } diff --git a/src/components/DisplayBox/DisplayBox.js b/src/components/DisplayBox/DisplayBox.js index f0f6f953..7a510481 100644 --- a/src/components/DisplayBox/DisplayBox.js +++ b/src/components/DisplayBox/DisplayBox.js @@ -75,7 +75,7 @@ export default class DisplayBox extends Component { } } - supportedRequesType(resource) { + supportedRequestType(resource) { let resourceType = resource.resourceType.toUpperCase(); if ( resourceType === 'DEVICEREQUEST' || @@ -102,7 +102,9 @@ export default class DisplayBox extends Component { } } else { // disable this suggestion button if any are allowed - document.getElementById(buttonId).setAttribute('disabled', 'true'); + const element = document.getElementById(buttonId); + element.setAttribute('disabled', 'true'); + element.setAttribute('style', 'background-color:#4BB543;'); } if (suggestion.label) { @@ -131,7 +133,7 @@ export default class DisplayBox extends Component { console.log('suggested action CREATE result:'); console.log(result); - if (this.supportedRequesType(result)) { + if (this.supportedRequestType(result)) { // call into the request builder to resubmit the CRD request with the suggested request this.props.takeSuggestion(result); } @@ -377,6 +379,7 @@ export default class DisplayBox extends Component { {linksSection} + {suggestionsSection} ); @@ -389,19 +392,11 @@ export default class DisplayBox extends Component { } return (
- Notification Cards ({renderedCards.length})
{renderedCards}
+
{renderedCards}
); } else { return
; } } - - componentDidUpdate() { - // clear the suggestion buttons - console.log(this.buttonList); - this.buttonList.forEach((requestButton, id) => { - document.getElementById(requestButton).removeAttribute('disabled'); - }); - } } diff --git a/src/components/DisplayBox/card-list.css b/src/components/DisplayBox/card-list.css index 229f5a8c..3adcffe7 100644 --- a/src/components/DisplayBox/card-list.css +++ b/src/components/DisplayBox/card-list.css @@ -1,6 +1,6 @@ .decision-card { padding: 15px; - margin: 10px; + margin: 0 0 10px 10px; background: #fcfcfc; border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 4px; diff --git a/src/components/MedicationStatus/MedicationStatus.jsx b/src/components/MedicationStatus/MedicationStatus.jsx new file mode 100644 index 00000000..d023bdc8 --- /dev/null +++ b/src/components/MedicationStatus/MedicationStatus.jsx @@ -0,0 +1,72 @@ +import { MedicationStatusButton } from './MedicationStatusButton.jsx'; +import { MedicationStatusModal } from './MedicationStatusModal.jsx'; +import { useState, useEffect } from 'react'; +import { Card } from '@mui/material'; + +export const MedicationStatus = props => { + const { ehrUrl, request, medicationDispense, getMedicationStatus, lastCheckedMedicationTime } = + props; + const [showMedicationStatus, setShowMedicationStatus] = useState(false); + + useEffect(() => getMedicationStatus(), [request.id, ehrUrl]); + + const handleCloseMedicationStatus = () => { + setShowMedicationStatus(false); + }; + + const handleOpenMedicationStatus = () => { + setShowMedicationStatus(true); + }; + + return ( + + + + + ); +}; + +export const getStatusColor = status => { + switch (status) { + case 'completed': + return 'green'; + case 'preparation': + case 'in-progress': + case 'cancelled': + case 'on-hold': + case 'entered-in-error': + case 'stopped': + case 'declined': + case 'unknown': + default: + return '#0c0c0c'; + } +}; + +export const getStatusText = status => { + switch (status) { + case 'completed': + return 'Picked Up'; + case 'unknown': + return 'Not Started'; + case 'preparation': + case 'in-progress': + case 'cancelled': + case 'on-hold': + case 'entered-in-error': + case 'stopped': + case 'declined': + default: + return 'N/A'; + } +}; diff --git a/src/components/MedicationStatus/MedicationStatusButton.css b/src/components/MedicationStatus/MedicationStatusButton.css new file mode 100644 index 00000000..572495d8 --- /dev/null +++ b/src/components/MedicationStatus/MedicationStatusButton.css @@ -0,0 +1,16 @@ +.etasuButtonText { + margin-bottom: 0px; + font-weight: bold; + font-size: 14px; +} + +.etasuButtonTimestamp { + margin: 0 auto; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-size: 0.8rem; +} + +.timestampString { + font-size: 0.7rem; + opacity: 0.8; +} diff --git a/src/components/MedicationStatus/MedicationStatusButton.jsx b/src/components/MedicationStatus/MedicationStatusButton.jsx new file mode 100644 index 00000000..6c9db545 --- /dev/null +++ b/src/components/MedicationStatus/MedicationStatusButton.jsx @@ -0,0 +1,65 @@ +import { Button, Grid, Typography } from '@mui/material'; +import LocalPharmacyIcon from '@mui/icons-material/LocalPharmacy'; +import './MedicationStatusButton.css'; +import { getStatusText } from './MedicationStatus'; + +export const MedicationStatusButton = props => { + const { baseColor, medicationDispense, handleOpenMedicationStatus, lastCheckedMedicationTime } = + props; + return ( + + + {renderTimestamp(lastCheckedMedicationTime)} + + ); +}; + +const buttonSx = baseColor => ({ + backgroundColor: baseColor, + ':hover': { filter: 'brightness(110%)', backgroundColor: baseColor }, + flexDirection: 'column' +}); + +const renderTimestamp = checkedTime => { + return checkedTime ? ( + <> + + {convertTimeDifference(checkedTime)} + + + {new Date(checkedTime).toLocaleString()} + + + ) : ( + No medication selected + ); +}; + +const convertTimeDifference = start => { + const end = Date.now(); + const difference = end - start; + const diffMin = difference / 60000; + let prefix = 'a long time'; + if (diffMin < 1) { + prefix = 'a few seconds'; + } else if (diffMin > 1 && diffMin < 2) { + prefix = 'a minute'; + } else if (diffMin > 2 && diffMin < 60) { + prefix = `${Math.round(diffMin)} minutes`; + } else if (diffMin > 60 && diffMin < 120) { + prefix = 'an hour'; + } else if (diffMin > 120 && diffMin < 1440) { + prefix = `${Math.round(diffMin / 60)} hours`; + } else if (diffMin > 1440 && diffMin < 2880) { + prefix = 'a day'; + } else if (diffMin > 2880) { + prefix = `${Math.round(diffMin / 1440)} days`; + } + return `Last checked ${prefix} ago`; +}; diff --git a/src/components/MedicationStatus/MedicationStatusModal.css b/src/components/MedicationStatus/MedicationStatusModal.css new file mode 100644 index 00000000..616a79bb --- /dev/null +++ b/src/components/MedicationStatus/MedicationStatusModal.css @@ -0,0 +1,33 @@ +.renew-icon{ + cursor: pointer; + margin-left: 15px; + margin-right: 15px; + } + +.refresh{ + cursor: pointer; + margin-left: 15px; + margin-right: 15px; + animation-name: spin; + animation-duration: 500ms; + animation-iteration-count: 2; + animation-timing-function: ease-in-out; + } + + .status-icon{ + width: 100%; + height:5px; + } + +.bundle-entry{ + margin: 5px; + } + +@keyframes spin { + from { + transform:rotate(0deg); + } + to { + transform:rotate(360deg); + } + } \ No newline at end of file diff --git a/src/components/MedicationStatus/MedicationStatusModal.jsx b/src/components/MedicationStatus/MedicationStatusModal.jsx new file mode 100644 index 00000000..93b4dfd3 --- /dev/null +++ b/src/components/MedicationStatus/MedicationStatusModal.jsx @@ -0,0 +1,62 @@ +import { Box, Grid, IconButton, Modal, Tooltip } from '@mui/material'; +import AutorenewIcon from '@mui/icons-material/Autorenew'; +import { useState, useEffect } from 'react'; +import { getStatusColor, getStatusText } from './MedicationStatus'; +import './MedicationStatusModal.css'; + +const getIdText = medicationDispense => medicationDispense?.id || 'N/A'; + +export const MedicationStatusModal = props => { + const { callback, onClose, medicationDispense, update } = props; + const [spin, setSpin] = useState(false); + const color = getStatusColor(medicationDispense?.status); + const status = getStatusText(medicationDispense?.status); + + useEffect(() => { + if (update) { + setSpin(true); + callback(); + } + }, [update]); + + return ( + + +
+

Medication Status

+
+ + +
ID: {getIdText(medicationDispense)}
+
Status: {status}
+
+ +
+ + + setSpin(false)} + /> + + +
+
+
+
+
+
+ ); +}; + +const modalStyle = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + border: '1px solid #000', + boxShadow: 24, + p: 4 +}; diff --git a/src/components/RequestBox/InProgressFormBox/InProgressFormBox.js b/src/components/RequestBox/InProgressFormBox/InProgressFormBox.js index a9d90891..aba49800 100644 --- a/src/components/RequestBox/InProgressFormBox/InProgressFormBox.js +++ b/src/components/RequestBox/InProgressFormBox/InProgressFormBox.js @@ -3,27 +3,35 @@ import React from 'react'; import './InProgressFormBoxStyle.css'; export default function InProgressFormBox(props) { - return ( - props.qrResponse?.questionnaire ? ( - - - In Progress Form - - - Practitioner: {props.qrResponse.author ? props.qrResponse.author.reference : 'empty'} - - - Last Edited: {props.qrResponse.authored ? props.qrResponse.authored : 'empty'} - - - Form Link: {props.qrResponse.questionnaire ? props.qrResponse.questionnaire : 'empty'} - - - - - - ) : '' - ); -} \ No newline at end of file + return props.qrResponse?.questionnaire ? ( + + + In Progress Form + + + Practitioner: + {props.qrResponse.author ? props.qrResponse.author.reference : 'empty'} + + + Last Edited: {' '} + {props.qrResponse.authored ? props.qrResponse.authored : 'empty'} + + + Form Link: + {props.qrResponse.questionnaire ? props.qrResponse.questionnaire : 'empty'} + + + + + + ) : ( + '' + ); +} diff --git a/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js b/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js index 250428f4..34aa1e43 100644 --- a/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js +++ b/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js @@ -1,103 +1,106 @@ -import { Autocomplete, Box, TextField } from '@mui/material'; +import { Autocomplete, Box, TextField, IconButton } from '@mui/material'; import React, { useEffect, useState } from 'react'; import { PrefetchTemplate } from '../../../PrefetchTemplate'; import { defaultValues } from '../../../util/data'; +import RefreshIcon from '@mui/icons-material/Refresh'; + import PatientBox from '../../SMARTBox/PatientBox'; import './PatientSearchBarStyle.css'; export default function PatientSearchBar(props) { - const [options] = useState(defaultValues); - const [input, setInput] = useState(''); - const [listOfPatients, setListOfPatients] = useState([]); + const [options] = useState(defaultValues); + const [input, setInput] = useState(''); + const [listOfPatients, setListOfPatients] = useState([]); - useEffect(() => { - const newList = props.searchablePatients.map((patient) => ({ - id: patient.id, - name: getName(patient), - })); - setListOfPatients([newList]); - }, [props.searchablePatients]); + useEffect(() => { + const newList = props.searchablePatients.map(patient => ({ + id: patient.id, + name: getName(patient) + })); + setListOfPatients([newList]); + }, [props.searchablePatients]); - function getName(patient) { - if (patient.name) { - return (patient.name[0].given[0]) + ' ' + (patient.name[0].family); - } - return ''; + function getName(patient) { + if (patient.name) { + return patient.name[0].given[0] + ' ' + patient.name[0].family; } - - function getFilteredLength(searchstring, listOfPatients) { - const filteredListOfPatients = listOfPatients[0].filter((element) => { - if (searchstring === '') { - return element; - } - else { - return element.name.toLowerCase().includes(searchstring); - } - }); + return ''; + } - return filteredListOfPatients.length; - } + function getFilteredLength(searchstring, listOfPatients) { + const filteredListOfPatients = listOfPatients[0].filter(element => { + if (searchstring === '') { + return element; + } else { + return element.name.toLowerCase().includes(searchstring); + } + }); - function patientSearchBar() { - return ( - - -

Filter patient list

- { - setInput(newInputValue.toLowerCase()); - }} - options={listOfPatients[0].map(item => item.name)} - renderInput={(params) => } /> -

Showing {getFilteredLength(input, listOfPatients)} of {props.searchablePatients.length} records

-
- {displayFilteredPatientList(input, listOfPatients[0])} -
- ); - } - - function displayFilteredPatientList(searchstring, listOfPatients) { - const filteredListOfPatients = listOfPatients.filter((element) => { - if (searchstring === '') { - return element; - } - else { - return element.name.toLowerCase().includes(searchstring); - } - }); - return ( - - {filteredListOfPatients.map(patient => { - return ( - - item.id === patient.id)} - client={props.client} - callback={props.callback} - callbackList={props.callbackList} - callbackMap={props.callbackMap} - updatePrefetchCallback={PrefetchTemplate.generateQueries} - clearCallback={props.clearCallback} - ehrUrl={props.ehrUrl} - options={options} - responseExpirationDays={props.responseExpirationDays} - defaultUser={props.defaultUser} - /> - - ); - })} - - ); - } + return filteredListOfPatients.length; + } + function patientSearchBar() { return ( - - {listOfPatients[0] ? patientSearchBar() : 'loading...'} + + +

Filter patient list

+ { + setInput(newInputValue.toLowerCase()); + }} + options={listOfPatients[0].map(item => item.name)} + renderInput={params => } + /> +

+ Showing {getFilteredLength(input, listOfPatients)} of {props.searchablePatients.length}{' '} + records +

+ props.getPatients()} size="large"> + +
+ {displayFilteredPatientList(input, listOfPatients[0])} +
); -} \ No newline at end of file + } + + function displayFilteredPatientList(searchstring, listOfPatients) { + const filteredListOfPatients = listOfPatients.filter(element => { + if (searchstring === '') { + return element; + } else { + return element.name.toLowerCase().includes(searchstring); + } + }); + return ( + + {filteredListOfPatients.map(patient => { + return ( + + item.id === patient.id)} + client={props.client} + request={props.request} + launchUrl={props.launchUrl} + callback={props.callback} + callbackList={props.callbackList} + callbackMap={props.callbackMap} + updatePrefetchCallback={PrefetchTemplate.generateQueries} + clearCallback={props.clearCallback} + options={options} + responseExpirationDays={props.responseExpirationDays} + defaultUser={props.defaultUser} + /> + + ); + })} + + ); + } + + return {listOfPatients[0] ? patientSearchBar() : 'loading...'}; +} diff --git a/src/components/RequestBox/PatientSearchBar/PatientSearchBarStyle.css b/src/components/RequestBox/PatientSearchBar/PatientSearchBarStyle.css index 4908c658..010c5dd5 100644 --- a/src/components/RequestBox/PatientSearchBar/PatientSearchBarStyle.css +++ b/src/components/RequestBox/PatientSearchBar/PatientSearchBarStyle.css @@ -17,5 +17,6 @@ .search-header { display: flex; + padding: 12px; align-items: center; } \ No newline at end of file diff --git a/src/components/RequestBox/RequestBox.js b/src/components/RequestBox/RequestBox.js index 408367a2..36ecab22 100644 --- a/src/components/RequestBox/RequestBox.js +++ b/src/components/RequestBox/RequestBox.js @@ -1,14 +1,12 @@ -import { Button, ButtonGroup } from '@mui/material'; +import { Button, ButtonGroup, Grid } from '@mui/material'; import _ from 'lodash'; import React, { Component } from 'react'; import buildNewRxRequest from '../../util/buildScript.2017071.js'; -import PersonIcon from '@mui/icons-material/Person'; import MuiAlert from '@mui/material/Alert'; import Snackbar from '@mui/material/Snackbar'; -import { defaultValues, shortNameMap } from '../../util/data'; -import { getAge } from '../../util/fhir'; +import { shortNameMap } from '../../util/data'; +import { getAge, createMedicationDispenseFromMedicationRequest } from '../../util/fhir'; import { retrieveLaunchContext } from '../../util/util'; -import InProgressFormBox from './InProgressFormBox/InProgressFormBox.js'; import './request.css'; export default class RequestBox extends Component { @@ -17,7 +15,7 @@ export default class RequestBox extends Component { this.state = { gatherCount: 0, response: {}, - open: false + submittedRx: false }; this.renderRequestResources = this.renderRequestResources.bind(this); @@ -31,12 +29,10 @@ export default class RequestBox extends Component { // TODO - see how to submit response for alternative therapy replaceRequestAndSubmit(request) { - this.props.callback(request,request); // Submit the cds hook request. + this.props.callback(request, request); // Submit the cds hook request. this.submitOrderSign(request); } - componentDidMount() { } - prepPrefetch() { const preppedResources = new Map(); Object.keys(this.props.prefetchedResources).forEach(resourceKey => { @@ -97,10 +93,6 @@ export default class RequestBox extends Component { } } - updateStateElement = (elementName, text) => { - this.setState({ [elementName]: text }); - }; - emptyField = (empty); renderPatientInfo() { @@ -158,6 +150,7 @@ export default class RequestBox extends Component { if (prefetchMap.size > 0) { return this.renderRequestResources(prefetchMap); } + return
; } renderRequestResources(requestResources) { @@ -234,7 +227,7 @@ export default class RequestBox extends Component { if (!userId) { console.log( 'Practitioner not populated from prefetch, using default from config: ' + - this.props.defaultUser + this.props.defaultUser ); userId = this.props.defaultUser; } @@ -256,9 +249,8 @@ export default class RequestBox extends Component { /** * Relaunch DTR using the available context */ - relaunch = e => { + relaunch = () => { this.buildLaunchLink().then(link => { - //e.preventDefault(); window.open(link.url, '_blank'); }); }; @@ -316,7 +308,7 @@ export default class RequestBox extends Component { /** * Send NewRx for new Medication to the Pharmacy Information System (PIMS) */ - sendRx = e => { + sendRx = () => { console.log('Sending NewRx to: ' + this.props.pimsUrl); // build the NewRx Message @@ -343,7 +335,19 @@ export default class RequestBox extends Component { }) .then(response => { console.log('Successfully sent NewRx to PIMS'); - console.log(response); + + // create the MedicationDispense + var medicationDispense = createMedicationDispenseFromMedicationRequest(this.props.request); + console.log('Create MedicationDispense:'); + console.log(medicationDispense); + + // store the MedicationDispense in the EHR + console.log(medicationDispense); + this.props.client.update(medicationDispense).then(result => { + console.log('Update MedicationDispense result:'); + console.log(result); + }); + this.handleRxResponse(); }) .catch(error => { @@ -360,72 +364,62 @@ export default class RequestBox extends Component { return Object.keys(this.props.patient).length === 0; } - // SnackBar - handleRxResponse = () => this.setState({ open: true }); - - handleClose = () => this.setState({ open: false }); + // SnackBar + handleRxResponse = () => this.setState({ submittedRx: true }); + handleClose = () => this.setState({ submittedRx: false }); render() { const disableSendToCRD = this.isOrderNotSelected() || this.props.loading; const disableSendRx = this.isOrderNotSelected() || this.props.loading; const disableLaunchSmartOnFhir = this.isPatientNotSelected(); - const { open } = this.state; + return ( -
- { this.props.patient.id ? ( -
-
-
- Patient ID: {this.props.patient.id} -
-
+ <> +
+
+
+ Patient ID: {this.props.patient.id} +
+
+ + {this.renderPatientInfo()} + + {this.renderPrefetchedResources()} -
-
-
- {Object.keys(this.props.response).length ? - - : } - - - - - -
+ + +
- ) : ( - - )} +
+ + + + + +
+
- - Success! NewRx Recieved By Pharmacy - - -
+ anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left' + }} + open={this.state.submittedRx} + onClose={this.handleClose} + autoHideDuration={6000} + > + + Success! NewRx Received By Pharmacy + + + ); } } diff --git a/src/components/RequestBox/request.css b/src/components/RequestBox/request.css index 14b6aedd..be3c7e79 100644 --- a/src/components/RequestBox/request.css +++ b/src/components/RequestBox/request.css @@ -12,7 +12,6 @@ .request { border: 1px solid black; - height: auto; padding: 10px; border-radius: 5px; background-color: rgb(248, 248, 248); @@ -32,8 +31,6 @@ } .demographics { - width: 50%; - float: left; padding:10px 10px 10px 0px; } @@ -54,8 +51,6 @@ } .prefetched { - width: 50%; - float:left; padding:10px 10px 10px 0px; margin-top: 5px; } diff --git a/src/components/RequestDashboard/Home.jsx b/src/components/RequestDashboard/Home.jsx new file mode 100644 index 00000000..a1b0a231 --- /dev/null +++ b/src/components/RequestDashboard/Home.jsx @@ -0,0 +1,115 @@ +import React, { memo, useState } from 'react'; +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 useStyles from './styles'; +import PatientSection from './PatientSection'; +import { SettingsContext } from '../../containers/ContextProvider/SettingsProvider'; +import SettingsSection from './SettingsSection'; +import TasksSection from './TasksSection'; + +const Home = props => { + const classes = useStyles(); + const patientButton = 'Select a Patient'; + const taskButton = 'View Tasks'; + const settingsButton = 'Settings'; + const [section, setSection] = useState(''); + const [state, dispatch] = React.useContext(SettingsContext); + + const openSection = buttonId => { + setSection(buttonId); + }; + + // renders top menu tab buttons + const renderMainButton = (buttonId, icon) => { + let buttonClass = `${classes.mainButton} ${classes.mainButtonView}`; + let gridWidth = 2; + let tooltip = ''; + if (section) { + // section active, switch button view + buttonClass = `${classes.mainButton} ${classes.tabButtonView}`; + if (buttonId === section) { + buttonClass += ` ${classes.selectedTabView}`; + } + gridWidth = 0; + tooltip =
{buttonId}
; + } + return ( + + + + + + ); + }; + // render view depending on which tab button is selected + const renderMainView = () => { + let gridClass = `${classes.mainDiv} ${classes.mainDivView}`; + if (section) { + gridClass = `${classes.mainDiv} ${classes.tabDivView}`; + } + return ( + + {section ? '' : } {/* spacer */} + {renderMainButton(patientButton, )} + {renderMainButton(taskButton, )} + {renderMainButton(settingsButton, )} + {section ? ( + +
+
+ ) : ( + + )} + {/* spacer */} +
+ ); + }; + + // render content of each view, makes other content invisible so it doesn't rerender every time + const renderSectionView = () => { + let renderSection =
Loading...
; + + if (section) { + let patientRenderClass = section === patientButton ? '' : classes.disappear; + let taskRenderClass = section === taskButton ? '' : classes.disappear; + let settingsRenderClass = section === settingsButton ? '' : classes.disappear; + + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); + } else { + return ''; + } + }; + + return ( +
+ {renderMainView()} + {renderSectionView()} +
+ ); +}; + +export default memo(Home); diff --git a/src/components/RequestDashboard/PatientSection.jsx b/src/components/RequestDashboard/PatientSection.jsx new file mode 100644 index 00000000..a871bb69 --- /dev/null +++ b/src/components/RequestDashboard/PatientSection.jsx @@ -0,0 +1,22 @@ +import React, { memo } from 'react'; + +import useStyles from './styles'; +import RequestBuilder from '../../containers/RequestBuilder'; +import { SettingsContext } from '../../containers/ContextProvider/SettingsProvider'; + +const PatientSection = props => { + const classes = useStyles(); + const [state, dispatch] = React.useContext(SettingsContext); + // TODO: Make request builder use react-hooks + return ( +
+ {state.startup ? ( + + ) : ( + <>Loading... + )} +
+ ); +}; + +export default memo(PatientSection); diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx new file mode 100644 index 00000000..9c704046 --- /dev/null +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -0,0 +1,283 @@ +import React, { memo, useState, useEffect } from 'react'; +import { Button, Box, FormControlLabel, Grid, Checkbox, TextField } from '@mui/material'; + +import useStyles from './styles'; +import env from 'env-var'; +import FHIR from 'fhirclient'; + +import { headerDefinitions, types } from '../../util/data'; +import { actionTypes } from '../../containers/ContextProvider/reducer'; +import { SettingsContext } from '../../containers/ContextProvider/SettingsProvider'; + +const SettingsSection = props => { + const classes = useStyles(); + const [state, dispatch] = React.useContext(SettingsContext); + const [headers, setHeaders] = useState([]); + const [originalValues, setOriginalValues] = useState([]); + + useEffect(() => { + const headers = Object.keys(headerDefinitions) + .map(key => ({ ...headerDefinitions[key], key })) + // Display the fields in descending order of type. If two fields are the same type, then sort by ascending order of display text. + .sort( + (self, other) => + -self.type.localeCompare(other.type) || self.display.localeCompare(other.display) + ); + setHeaders(headers); + const originals = Object.keys(headerDefinitions).map(key => [key, state[key]]); + setOriginalValues(originals); + JSON.parse(localStorage.getItem('reqgenSettings') || '[]').forEach(element => { + try { + updateSetting(element[0], element[1]); + } catch { + if (element[0]) { + console.log('Could not load setting:' + element[0]); + } + } + }); + + // indicate to the rest of the app that the settings have been loaded + dispatch({ + type: actionTypes.flagStartup + }); + }, []); + const updateSetting = (key, value) => { + dispatch({ + type: actionTypes.updateSetting, + settingId: key, + value: value + }); + }; + const saveSettings = () => { + const headers = Object.keys(headerDefinitions).map(key => [key, state[key]]); + localStorage.setItem('reqgenSettings', JSON.stringify(headers)); + }; + + const resetSettings = () => { + originalValues.forEach(e => { + try { + updateSetting(e[0], e[1]); + } catch { + console.log('Failed to reset setting value'); + } + }); + }; + 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 }) => + () => { + let url = new URL(pimsUrl); + const resetUrl = url.origin + '/doctorOrders/api/deleteAll'; + console.log('reset pims: ' + resetUrl); + + fetch(resetUrl, { + method: 'DELETE' + }) + .then(response => { + console.log('Reset pims: '); + console.log(response); + }) + .catch(error => { + console.log('Reset pims error: '); + console.log(error); + }); + }; + const resetRemsAdmin = + ({ cdsUrl }) => + () => { + let url = new URL(cdsUrl); + const resetUrl = url.origin + '/etasu/reset'; + + fetch(resetUrl, { + method: 'POST' + }) + .then(response => { + console.log('Reset rems admin etasu: '); + console.log(response); + }) + .catch(error => { + console.log('Reset rems admin error: '); + console.log(error); + }); + }; + const clearMedicationDispenses = + ({ ehrUrl, access_token }) => + () => { + console.log('Clear MedicationDispenses from the EHR: ' + ehrUrl); + const client = FHIR.client({ + serverUrl: ehrUrl, + ...(access_token ? { tokenResponse: access_token } : {}) + }); + client + .request('MedicationDispense', { flat: true }) + .then(result => { + console.log(result); + result.forEach(resource => { + console.log(resource.id); + client + .delete('MedicationDispense/' + resource.id) + .then(result => { + console.log(result); + }) + .catch(e => { + console.log('Failed to delete MedicationDispense ' + resource.id); + console.log(e); + }); + }); + }) + .catch(e => { + console.log('Failed to retrieve list of MedicationDispense'); + console.log(e); + }); + }; + const reconnectEhr = + ({ baseUrl, redirect }) => + () => { + FHIR.oauth2.authorize({ + clientId: env.get('REACT_APP_CLIENT').asString(), + iss: baseUrl, + redirectUri: redirect, + scope: env.get('REACT_APP_CLIENT_SCOPES').asString() + }); + }; + const resetHeaderDefinitions = [ + { + display: 'Reset PIMS Database', + 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', + key: 'clearMedicationDispenses', + reset: clearMedicationDispenses + }, + { + display: 'Reconnect EHR', + key: 'reconnectEHR', + reset: reconnectEhr, + variant: 'contained' + } + ]; + + let firstCheckbox = true; + let showBreak = true; + return ( + + + {headers.map(({ key, type, display }) => { + switch (type) { + case 'input': + return ( + +
+ { + updateSetting(key, event.target.value); + }} + sx={{ width: '100%' }} + /> +
+
+ ); + case 'check': + if (firstCheckbox) { + firstCheckbox = false; + showBreak = true; + } else { + showBreak = false; + } + return ( + + {showBreak ? : ''} + + { + updateSetting(key, event.target.checked); + }} + /> + } + label={display} + /> + + + ); + default: + return ( +
+

{display}

+
+ ); + } + })} + {resetHeaderDefinitions.map(({ key, display, reset, variant }) => { + return ( + + + + ); + })} + {/* spacer */} +
+ + + + + + + + + +
+ ); +}; + +export default memo(SettingsSection); diff --git a/src/components/RequestDashboard/TabPanel.jsx b/src/components/RequestDashboard/TabPanel.jsx new file mode 100644 index 00000000..5857a316 --- /dev/null +++ b/src/components/RequestDashboard/TabPanel.jsx @@ -0,0 +1,23 @@ +import { Box } from '@mui/material'; +import React from 'react'; + +function TabPanel(props) { + const { children, value, index, name, ...other } = props; + + return ( + + ); +} + +export const MemoizedTabPanel = React.memo(TabPanel); diff --git a/src/components/RequestDashboard/TasksSection.jsx b/src/components/RequestDashboard/TasksSection.jsx new file mode 100644 index 00000000..e8c424d5 --- /dev/null +++ b/src/components/RequestDashboard/TasksSection.jsx @@ -0,0 +1,330 @@ +import React, { memo, useState, useEffect, Fragment } from 'react'; +import { + Button, + Box, + Modal, + Grid, + Tabs, + Tab, + Stack, + Select, + FormControl, + InputLabel +} from '@mui/material'; +import AssignmentIcon from '@mui/icons-material/Assignment'; +import PersonIcon from '@mui/icons-material/Person'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditNoteIcon from '@mui/icons-material/EditNote'; +import AssignmentLateIcon from '@mui/icons-material/AssignmentLate'; +import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; +import AssignmentTurnedInIcon from '@mui/icons-material/AssignmentTurnedIn'; +import PersonAddIcon from '@mui/icons-material/PersonAdd'; +import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import useStyles from './styles'; +import { SettingsContext } from '../../containers/ContextProvider/SettingsProvider'; +import { MemoizedTabPanel } from './TabPanel'; +import { Info, Refresh } from '@mui/icons-material'; +import MenuItem from '@mui/material/MenuItem'; + +const TasksSection = props => { + const classes = useStyles(); + const [tasks, setTasks] = useState([]); + const [state, dispatch] = React.useContext(SettingsContext); + const [value, setValue] = useState(0); + const [open, setOpen] = useState(false); + const [taskToDelete, setTaskToDelete] = useState(''); + + const handleChangeAssign = (event, task) => { + if (event.target.value === 'me') { + assignTaskToMe(task); + } else { + assignTaskToPatient(task); + } + }; + + const handleChange = (event, newValue) => { + setValue(newValue); + }; + const handleClose = () => { + setOpen(false); + }; + + const tryDelete = task => { + setTaskToDelete(task); + setOpen(true); + }; + const assignTaskToMe = task => { + if (task) { + let user = props.client.user.id; + if (!user) { + user = `Practitioner/${state.defaultUser}`; + } + task.owner = { + reference: user + }; + if (task.for) { + // reset 'for' element before updating + task.for = { + reference: `${task.for.resourceType}/${task.for.id}` + }; + } + props.client.update(task).then(e => { + fetchTasks(); + }); + } + }; + const assignTaskToPatient = task => { + if (task) { + let patientId = task.for.id; + let user = `Patient/${patientId}`; + task.owner = { + reference: user + }; + if (task.for) { + // reset 'for' element before updating + task.for = { + reference: `${task.for.resourceType}/${task.for.id}` + }; + } + props.client.update(task).then(e => { + fetchTasks(); + }); + } + }; + const deleteTask = () => { + if (taskToDelete) { + props.client.delete(`${taskToDelete.resourceType}/${taskToDelete.id}`).then(e => { + console.log('Deleted Task'); + fetchTasks(); + }); + setOpen(false); + setTaskToDelete(''); + } + }; + const fetchTasks = () => { + let identifier = 'Task'; + if (state.patient && state.patient.id) { + identifier = `Task?patient=${state.patient.id}`; + } + props.client.request(identifier, { resolveReferences: ['for', 'owner'] }).then(request => { + console.log(request); + if (request && request.entry) { + setTasks(request.entry.map(e => e.resource)); + } else { + setTasks([]); + } + }); + }; + useEffect(() => { + fetchTasks(); + }, []); + + useEffect(() => { + fetchTasks(); + }, [state.patient]); + const renderTasks = taskSubset => { + if (taskSubset.length > 0) { + return ( + + {taskSubset.map(t => renderTask(t))} + + ); + } else { + return

No Tasks Found

; + } + }; + const renderTask = task => { + let statusColor = '#555'; + if (task.status.toLowerCase() === 'ready') { + statusColor = '#198754'; + } + + let taskForName = 'N/A'; + let taskOwnerName = 'N/A'; + if (task.for?.resourceType?.toLowerCase() === 'patient') { + const patient = task.for; + if (patient.name) { + taskForName = `${patient.name[0].given[0]} ${patient.name[0].family}`; + } + } + if (task.owner && task.owner?.resourceType?.toLowerCase() === 'practitioner') { + const practitioner = task.owner; + if (practitioner.name) { + taskOwnerName = `${practitioner.name[0].given[0]} ${practitioner.name[0].family}`; + } else { + taskOwnerName = task.owner.id; + } + } + if (task.owner && task.owner?.resourceType?.toLowerCase() === 'patient') { + const patient = task.owner; + if (patient.name) { + taskOwnerName = `${patient.name[0].given[0]} ${patient.name[0].family}`; + } else { + taskOwnerName = task.owner.id; + } + } + let ownerText = ( + + + Unassigned + + ); + if (task.owner) { + ownerText = ( + + + {`Assigned to ${taskOwnerName}`} + + ); + } + return ( + + + + + {`Task ID: ${task.id}`} + + + {`Timestamp: ${task.authoredOn}`} + + + {`Beneficiary: ${task.for ? task.for.id : 'None'}`} + + + {`STATUS: ${task.status.toUpperCase()}`} + + + {task.description} + + + + {taskForName} + + + + {ownerText} + + + + + + + Assign + + + + + {/*spacer*/} + + + + + + + + + + + ); + }; + + const unassignedTasks = tasks.filter(t => !t.owner); + const assignedTasks = tasks.filter(t => t.owner?.id === state.defaultUser); // should check current user, not default + return ( + <> + + + + + {taskToDelete ? `Are you sure you want to delete Task ${taskToDelete.id}` : ''} + + + {/*spacer*/} + + + + + + + + + + +
+ + + + } label={`ALL TASKS (${tasks.length})`} /> + } label={`MY TASKS (${assignedTasks.length})`} /> + } label={`UNASSIGNED TASKS (${unassignedTasks.length})`} /> + + + + + + + + {/* all tasks */} + {renderTasks(tasks)} + + + {/* my tasks */} + {renderTasks(assignedTasks)} + + + {/* unassigned tasks */} + {renderTasks(unassignedTasks)} + +
+ + ); +}; + +export default memo(TasksSection); diff --git a/src/components/RequestDashboard/styles.jsx b/src/components/RequestDashboard/styles.jsx new file mode 100644 index 00000000..00c52a63 --- /dev/null +++ b/src/components/RequestDashboard/styles.jsx @@ -0,0 +1,142 @@ +import { makeStyles } from '@mui/styles'; +export default makeStyles( + theme => ({ + disappear: { + display: 'none' + }, + spacer: { + height: '50px', // must be same as buttons + borderBottom: '1px solid black', + flexGrow: 1, + backgroundColor: '#005B94' + }, + mainButtonView: { + '&.MuiButtonBase-root': { + width: '600px', + maxWidth: '90%', + height: '150px', + backgroundColor: '#0d6efd', + opacity: '75%', + color: '#fcfcfc', + fontSize: '1.5rem' + } + }, + mainDiv: {}, + mainDivView: { + '&.MuiGrid-root': { + padding: '0 50px 0 50px', + marginTop: '35vh' + } + }, + mainIcon: { + '&.MuiSvgIcon-root': { + '&.MuiSvgIcon-fontSizeMedium': { + fontSize: '3rem' + } + } + }, + mainSectionView: { + width: 'auto', + height: 'auto', + borderLeft: '1px solid black', + borderRight: '1px solid black' + }, + noTasks: { + backgroundColor: '#e4e4e4', + padding: '10px', + fontSize: '18px' + }, + // DO NOT ALPHABETIZE + // if you must alphabetize this file to have classes + // sorted, rename tabButtonView and selectedTabView such + // that tabButtonView occurs earlier in the list. + // Otherwise, the styles will override incorrectly. + tabButtonView: { + '&.MuiButtonBase-root': { + width: '75px', + height: '50px', + opacity: '75%', + fontSize: '1.5rem', + border: '1px solid black', + boxShadow: 'none', + borderRadius: '0' + }, + '& > *': { + // generic child selector + '&.MuiButton-iconSizeMedium': { + // specificity + marginRight: 0 + } + } + }, + selectedTabView: { + '&.MuiButtonBase-root': { + color: 'black', + borderBottom: 'none', + backgroundColor: '#F5F5F7', + '&:hover': { + backgroundColor: '#F5F5F7', + boxShadow: 'none' + } + } + }, + taskDeleteHeader: { + padding: '15px 0 15px 0' + }, + taskDeleteModal: { + border: '1px solid black', + width: '400px', + + backgroundColor: 'white', + position: 'fixed', + top: '50%', + left: '50%', + padding: '15px', + transform: 'translate(-50%, -50%)', + overflowY: 'auto', + fontSize: '18px', + boxShadow: '10px 10px 20px black' + }, + tabDivView: { + '&.MuiGrid-root': { + marginTop: '0vh', + alignItems: 'flex-start', + justifyContent: 'flex-start' + } + }, + taskHeaderTabs: { + margin: '15px 15px 5px 15px', + backgroundColor: '#F5F5F7' + }, + taskRefreshButton: { + padding: '35px 0 0 0' + }, + taskTabButton: { + padding: '10px 0px 5px 0px' + }, + taskTabMain: { + border: '0px solid black', + boxShadow: '-2px -2px 3px 1px', + borderRadius: '5px', + padding: '8px', + background: 'linear-gradient(to right bottom, #F5F5F7, #eaeaef)', + '&:hover': { + background: 'linear-gradient(to right bottom, #FFFFFF, #efefff)' + } + }, + taskTabHeader: { + fontSize: '9px', + color: '#777', + borderBottom: '1px solid #e3e3ef' + }, + taskTabDescription: { + fontSize: '18px', + padding: '8px 0px 10px 2px' + }, + taskTabOwner: { + color: '#777' + } + }), + + { name: 'RequestDashboard', index: 1 } +); diff --git a/src/components/SMARTBox/PatientBox.js b/src/components/SMARTBox/PatientBox.js index ea337028..11a71519 100644 --- a/src/components/SMARTBox/PatientBox.js +++ b/src/components/SMARTBox/PatientBox.js @@ -1,13 +1,17 @@ import React, { Component } from 'react'; import { getAge, getDrugCodeFromMedicationRequest } from '../../util/fhir'; import './smart.css'; -import { Button, IconButton } from '@mui/material'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import Box from '@mui/material/Box'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import FormControl from '@mui/material/FormControl'; -import Select from '@mui/material/Select'; +import { Button } from '@mui/material'; +import Tooltip from '@mui/material/Tooltip'; +import MedicationIcon from '@mui/icons-material/Medication'; +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import { retrieveLaunchContext } from '../../util/util'; export default class PatientBox extends Component { constructor(props) { @@ -22,7 +26,10 @@ export default class PatientBox extends Component { questionnaireResponses: {}, openRequests: false, openQuestionnaires: false, - questionnaireTitles: {} + questionnaireTitles: {}, + showMedications: false, + showQuestionnaires: false, + numInProgressForms: 0 }; this.handleRequestChange = this.handleRequestChange.bind(this); @@ -37,13 +44,12 @@ export default class PatientBox extends Component { this.getQuestionnaireTitles = this.getQuestionnaireTitles.bind(this); this.makeQROption = this.makeQROption.bind(this); this.handleResponseChange = this.handleResponseChange.bind(this); - this.makeDropdown = this.makeDropdown.bind(this); } componentDidMount() { // get requests and responses on open of patients this.getRequests(); - this.getResponses(); // TODO: PatientBox should not be rendering itself, needs to recieve its state from parent + this.getResponses(); // TODO: PatientBox should not be rendering itself, needs to receive its state from parent } getCoding(resource) { @@ -80,37 +86,14 @@ export default class PatientBox extends Component { return code; } - makeDropdown(options, label, stateVar, stateChange) { - return ( - - - {label} - - - - ); - } - makeOption(request, options) { const code = this.getCoding(request); let option = { key: request.id, text: code.display + ' (Medication request: ' + code.code + ')', + code: code.code, + name: code.display, value: JSON.stringify(request) }; options.push(option); @@ -142,16 +125,19 @@ export default class PatientBox extends Component { if (this.state.response) { const response = JSON.parse(this.state.response); - this.updateQRResponse(patient, response); + this.updateQRResponse(response); } } - updateQRResponse(patient, response) { + updatePatient(patient) { + this.props.callback('patient', patient); + } + + updateQRResponse(response) { this.props.callback('response', response); } fetchResources(queries) { - console.log(queries); var requests = []; this.props.callback('prefetchCompleted', false); queries.forEach((query, queryKey) => { @@ -245,7 +231,6 @@ export default class PatientBox extends Component { flat: true }) .then(result => { - // add the medicationReference as a contained resource result?.data.forEach(e => { if (e?.medicationReference) { @@ -278,7 +263,6 @@ export default class PatientBox extends Component { } }); - this.setState({ medicationRequests: result }); }); } @@ -295,8 +279,7 @@ export default class PatientBox extends Component { }); } - handleRequestChange(e) { - const data = e.target.value; + handleRequestChange(data, patient) { if (data) { let coding = this.getCoding(JSON.parse(data)); this.setState({ @@ -306,6 +289,21 @@ export default class PatientBox extends Component { display: coding.display, response: '' }); + this.props.callback('response', ''); + // update prefetch request for the medication + const request = JSON.parse(data); + if ( + request.resourceType === 'DeviceRequest' || + request.resourceType === 'ServiceRequest' || + request.resourceType === 'MedicationRequest' || + request.resourceType === 'MedicationDispense' + ) { + this.updatePrefetchRequest(request, patient, this.props.defaultUser); + } else { + this.props.clearCallback(); + } + // close the accordian after selecting a medication, can change if we want to keep open + this.props.callback('expanded', false); } else { this.setState({ request: '' @@ -313,12 +311,13 @@ export default class PatientBox extends Component { } } - handleResponseChange(e) { - const data = e.target.value; + handleResponseChange(data) { if (data) { this.setState({ response: data }); + const response = JSON.parse(data); + this.updateQRResponse(response); } else { this.setState({ response: '' @@ -356,6 +355,7 @@ export default class PatientBox extends Component { }) .then(result => { this.setState({ questionnaireResponses: result }); + this.setState({ numInProgressForms: result.data.length }); }) .then(() => this.getQuestionnaireTitles()); } @@ -382,46 +382,198 @@ export default class PatientBox extends Component { makeQROption(qr) { const questionnaireTitle = this.state.questionnaireTitles[qr.questionnaire]; - const display = `${questionnaireTitle}: created at ${qr.authored}`; + // const display = `${questionnaireTitle}: created at ${qr.authored}`; return { key: qr.id, - text: display, + text: questionnaireTitle, + time: qr.authored, value: JSON.stringify(qr) }; } + isOrderNotSelected() { + return Object.keys(this.props.request).length === 0; + } + + /** + * Launch In progress Form + */ + + relaunch = data => { + this.handleResponseChange(data); + this.props.callback('expanded', false); + this.buildLaunchLink(data).then(link => { + //e.preventDefault(); + window.open(link.url, '_blank'); + }); + }; + + async buildLaunchLink(data) { + // build appContext and URL encode it + let appContext = ''; + let order = undefined, + coverage = undefined, + response = undefined; + + if (!this.isOrderNotSelected()) { + if (Object.keys(this.props.request).length > 0) { + order = `${this.props.request.resourceType}/${this.props.request.id}`; + if (this.props.request.insurance && this.props.request.insurance.length > 0) { + coverage = `${this.props.request.insurance[0].reference}`; + } + } + } + + if (order) { + appContext += `order=${order}`; + + if (coverage) { + appContext += `&coverage=${coverage}`; + } + } + + // using data passed in instead of waiting for state/props variables to be updated + const resp = JSON.parse(data); + if (Object.keys(resp).length > 0) { + response = `QuestionnaireResponse/${resp.id}`; + } + + if (order && response) { + appContext += `&response=${response}`; + } else if (!order && response) { + appContext += `response=${response}`; + } + + const link = { + appContext: encodeURIComponent(appContext), + type: 'smart', + url: this.props.launchUrl + }; + + let linkCopy = Object.assign({}, link); + + const result = await retrieveLaunchContext( + linkCopy, + this.props.patient.id, + this.props.client.state + ); + linkCopy = result; + return linkCopy; + } + + makeResponseTable(columns, options, type, patient) { + return ( + + + + + {columns.map(column => ( + + {column.label} + + ))} + + + + {options.map(row => ( + + this.handleRequestChange(row.value, patient)} + > + + {row.name} + + {row.code} + + + ))} + +
+
+ ); + } + + makeQuestionnaireTable(columns, options, type, patient) { + return ( + + + + + {columns.map(column => ( + + {column.label} + + ))} + + + + {options.map(row => ( + + this.relaunch(row.value)} + className="hover-row" + > + + {row.text} + + {row.time} + + + ))} + +
+
+ ); + } + render() { const patient = this.props.patient; + if (!patient) { + return <>; + } let name = ''; + let fullName = ''; + let formatBirthdate = ''; if (patient.name) { name = {`${patient.name[0].given[0]} ${patient.name[0].family}`} ; + fullName = {`${patient.name[0].given.join(' ')} ${patient.name[0].family}`} ; + } + if (patient.birthDate) { + formatBirthdate = new Date(patient.birthDate).toDateString(); } // add all of the requests to the list of options let options = []; let responseOptions = []; - let returned = false; if (this.state.deviceRequests.data) { - returned = true; this.state.deviceRequests.data.forEach(e => { this.makeOption(e, options); }); } if (this.state.serviceRequests.data) { - returned = true; this.state.serviceRequests.data.forEach(e => { this.makeOption(e, options); }); } if (this.state.medicationRequests.data) { - returned = true; this.state.medicationRequests.data.forEach(e => { this.makeOption(e, options); }); } if (this.state.medicationDispenses.data) { - returned = true; this.state.medicationDispenses.data.forEach(e => { this.makeOption(e, options); }); @@ -429,13 +581,22 @@ export default class PatientBox extends Component { if (this.state.questionnaireResponses.data) { responseOptions = this.state.questionnaireResponses.data.map(qr => this.makeQROption(qr)); - returned = true; } - let noResults = 'No results found.'; - if (!returned) { - noResults = 'Loading...'; - } + const medicationColumns = [ + { id: 'name', label: 'Medication' }, + { id: 'code', label: 'Request #' } + ]; + + const questionnaireColumns = [ + { id: 'name', label: 'Title' }, + { id: 'time', label: 'Created' } + ]; + + const medicationTooltip = + options.length === 0 ? 'No medications found.' : `${options.length} medications available`; + const formTooltip = + this.state.numInProgressForms === 0 ? 'No In-Progress Forms' : 'Open In-Progress Forms'; return (
@@ -446,56 +607,100 @@ export default class PatientBox extends Component {
- Gender: {patient.gender} + Full Name: {fullName} +
+
+ Gender:{' '} + {patient.gender.charAt(0).toUpperCase() + patient.gender.slice(1)}
- Age: {getAge(patient.birthDate)} + DoB/Age: {formatBirthdate} ( + {getAge(patient.birthDate)} years old)
-
- Request: - {!options.length && returned ? ( - No requests +
+ {this.state.showMedications ? ( + ) : ( - this.makeDropdown( - options, - 'Select a medication request', - this.state.request, - this.handleRequestChange - ) + + + + + )} -
-
- - In Progress Form: - } + onClick={() => this.setState({ showQuestionnaires: false })} > - - - - {!responseOptions.length && returned ? ( - No in progress forms + Close In Progress Forms + ) : ( - this.makeDropdown( - responseOptions, - 'Choose an in-progress form', - this.state.response, - this.handleResponseChange - ) + + + + + )} +
-
+ {this.state.showMedications ? ( +
+ {this.makeResponseTable(medicationColumns, options, 'medication', patient)} +
+ ) : ( + + )} + {this.state.showQuestionnaires ? ( +
+ {this.makeQuestionnaireTable( + questionnaireColumns, + responseOptions, + 'questionnaire', + patient + )} +
+ ) : ( + + )}
); } diff --git a/src/components/SMARTBox/smart.css b/src/components/SMARTBox/smart.css index 243de77e..f95cd6dd 100644 --- a/src/components/SMARTBox/smart.css +++ b/src/components/SMARTBox/smart.css @@ -11,9 +11,28 @@ html{ padding:10px 10px 15px 10px; flex:1; background-color: #ddd; - display: grid; - grid-template-columns: 15% 35% 35% 10%; - column-gap: 5px; + display: flex; + justify-content: space-between; +} +.button-options { + display: flex; + column-gap: 12px; +} + +.patient-table-info { + /* display: flex; */ + margin-bottom: 10px; +} +tr:nth-child(odd) { + background-color: #ddd !important; +} +.hover-row:hover { + background-color: #E7F1FF !important; +} + +.big-button { + display: flex !important; + flex-direction: column !important; } .patient-box{ @@ -63,12 +82,9 @@ html{ float:right; } -.patient-info { - display:inline-block; -} - .request-info { - display: inherit; + display: flex; + flex-direction: column; } @@ -84,9 +100,7 @@ html{ } .select-btn { - height: 40px; - align-self: center; - margin-top: 25px !important; + height: 52px; } .emptyForm { diff --git a/src/components/SettingsBox/SettingsBox.css b/src/components/SettingsBox/SettingsBox.css deleted file mode 100644 index b2527c3c..00000000 --- a/src/components/SettingsBox/SettingsBox.css +++ /dev/null @@ -1,41 +0,0 @@ - -.setting-input { - margin: 1px 0 10px 0; - border-color: #999; - border-width: 1px; - border-style:solid; - height: 25px; -} - - -.setting-checkbox { - padding: 2px 4px; -} - -.setting-inner-checkbox { - margin-left: 1px; - margin-right: 1px; -} - -.setting-header{ - font-size:12px; - margin:0; -} - -.setting-btn { - margin-top: 6px; - } - - -.setting-btn:hover{ - border-width:1px 3px 1px 1px; - margin-left:2px; - margin-top:8px; -} - -.setting-btn:disabled, -.setting-btn[disabled] { - border: 1px solid #999999; - background-color: #333232; - color: #858282; -} diff --git a/src/components/SettingsBox/SettingsBox.js b/src/components/SettingsBox/SettingsBox.js deleted file mode 100644 index e3760c88..00000000 --- a/src/components/SettingsBox/SettingsBox.js +++ /dev/null @@ -1,178 +0,0 @@ -import React, { Component } from 'react'; -import './SettingsBox.css'; -import InputBox from '../Inputs/InputBox'; -import CheckBox from '../Inputs/CheckBox'; -import { headerDefinitions, types } from '../../util/data'; -import FHIR from 'fhirclient'; - -const clearQuestionnaireResponses = - ({ ehrUrl, defaultUser, access_token }, consoleLog) => - _event => { - console.log( - 'Clear QuestionnaireResponses from the EHR: ' + ehrUrl + ' for author ' + defaultUser - ); - const client = FHIR.client({ - serverUrl: ehrUrl, - ...(access_token ? { tokenResponse: access_token } : {}) - }); - client - .request('QuestionnaireResponse?author=' + defaultUser, { flat: true }) - .then(result => { - console.log(result); - result.forEach(resource => { - console.log(resource.id); - client - .delete('QuestionnaireResponse/' + resource.id) - .then(result => { - consoleLog( - 'Successfully deleted QuestionnaireResponse ' + resource.id + ' from EHR', - types.info - ); - 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 }, consoleLog) => - _event => { - let url = new URL(pimsUrl); - const resetUrl = url.origin + '/doctorOrders/api/deleteAll'; - console.log('reset pims: ' + resetUrl); - - fetch(resetUrl, { - method: 'DELETE' - }) - .then(response => { - console.log('Reset pims: '); - console.log(response); - consoleLog('Successfully reset pims database', types.info); - }) - .catch(error => { - console.log('Reset pims error: '); - consoleLog('Server returned error when resetting pims: ', types.error); - consoleLog(error.message); - console.log(error); - }); - }; - -const resetRemsAdmin = - ({ cdsUrl }, consoleLog) => - _event => { - let url = new URL(cdsUrl); - const resetUrl = url.origin + '/etasu/reset'; - - fetch(resetUrl, { - method: 'POST' - }) - .then(response => { - console.log('Reset rems admin etasu: '); - console.log(response); - consoleLog('Successfully reset rems admin etasu', types.info); - }) - .catch(error => { - console.log('Reset rems admin error: '); - consoleLog('Server returned error when resetting rems admin etasu: ', types.error); - consoleLog(error.message); - console.log(error); - }); - }; - -const resetHeaderDefinitions = [ - { - display: 'Clear EHR QuestionnaireResponses', - key: 'clearQuestionnaireResponses', - reset: clearQuestionnaireResponses - }, - { - display: 'Reset PIMS Database', - key: 'resetPims', - reset: resetPims - }, - { - display: 'Reset REMS-Admin Database', - key: 'resetRemsAdmin', - reset: resetRemsAdmin - } -]; - -export default class SettingsBox extends Component { - constructor(props) { - super(props); - } - - componentDidMount() {} - - render() { - const { state, consoleLog, updateCB } = this.props; - - const headers = Object.keys(headerDefinitions) - .map(key => ({ ...headerDefinitions[key], key })) - // Display the fields in descending order of type. If two fields are the same type, then sort by ascending order of display text. - .sort( - (self, other) => - -self.type.localeCompare(other.type) || self.display.localeCompare(other.display) - ); - - return ( -
- {headers.map(({ key, type, display }) => { - switch (type) { - case 'input': - return ( -
-

{display}

- -
- ); - case 'check': - return ( -
-

- {display} - -

-

 

-
- ); - default: - return ( -
-

{display}

-
- ); - } - })} - {resetHeaderDefinitions.map(({ key, display, reset }) => { - return ( -
- -
- ); - })} -
- ); - } -} diff --git a/src/containers/ContextProvider/SettingsProvider.js b/src/containers/ContextProvider/SettingsProvider.js new file mode 100644 index 00000000..4350a5ff --- /dev/null +++ b/src/containers/ContextProvider/SettingsProvider.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { reducer, initialState } from './reducer'; + +export const SettingsContext = React.createContext({ + state: initialState, + dispatch: () => null +}); + +export const SettingsProvider = ({ children }) => { + const [state, dispatch] = React.useReducer(reducer, initialState); + + return {children}; +}; diff --git a/src/containers/ContextProvider/reducer.js b/src/containers/ContextProvider/reducer.js new file mode 100644 index 00000000..5c856693 --- /dev/null +++ b/src/containers/ContextProvider/reducer.js @@ -0,0 +1,39 @@ +import { headerDefinitions } from '../../util/data'; +export const actionTypes = Object.freeze({ + updatePatient: 'update_patient', // {type, value} + updateSetting: 'update_setting', // {type, settingId, value} + flagStartup: 'flag_startup' // {type} +}); +// todo: add an enum that defines possible settings +export const reducer = (state, action) => { + switch (action.type) { + case actionTypes.updateSetting: + return { + ...state, + [action.settingId]: action.value + }; + case actionTypes.updatePatient: + return { + ...state, + patient: action.value + }; + case actionTypes.flagStartup: + return { + ...state, + startup: true + }; + default: + return state; + } +}; + +const initialState = { + patient: null, + startup: false, + redirect: '' +}; +Object.keys(headerDefinitions).forEach(e => { + initialState[e] = headerDefinitions[e].default; // fill default settings values +}); + +export { initialState }; diff --git a/src/containers/Index.jsx b/src/containers/Index.jsx index 3b74544b..0bf71379 100644 --- a/src/containers/Index.jsx +++ b/src/containers/Index.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import FHIR from 'fhirclient'; import env from 'env-var'; -import RequestBuilder from '../containers/RequestBuilder'; +import Home from '../components/RequestDashboard/Home'; const Index = props => { const [client, setClient] = useState(null); @@ -15,7 +15,7 @@ const Index = props => { return (
{client ? ( - + ) : (

Getting Client...

diff --git a/src/containers/PatientPortal.jsx b/src/containers/PatientPortal.jsx index d8bf2bc1..fe61fc0e 100644 --- a/src/containers/PatientPortal.jsx +++ b/src/containers/PatientPortal.jsx @@ -30,9 +30,15 @@ const PatientPortal = () => { setPatientName(getName(patient)); }); setClient(client); + document.title = 'EHR | Patient Portal'; } }, [token]); + const logout = () => { + setClient(null); + setPatientName(null); + }; + const getName = patient => { const name = []; if (patient.name) { @@ -60,7 +66,7 @@ const PatientPortal = () => { {token && client ? ( - + ) : ( )} diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index eba3a64c..7904d594 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -1,14 +1,14 @@ import React, { Component } from 'react'; -import { Button, Box, IconButton } from '@mui/material'; +import { Button, Box, Grid, IconButton, Modal, DialogTitle } from '@mui/material'; import PersonIcon from '@mui/icons-material/Person'; import RefreshIcon from '@mui/icons-material/Refresh'; +import PersonSearchIcon from '@mui/icons-material/PersonSearch'; import DisplayBox from '../components/DisplayBox/DisplayBox'; import '../index.css'; -import SettingsBox from '../components/SettingsBox/SettingsBox'; import RequestBox from '../components/RequestBox/RequestBox'; import buildRequest from '../util/buildRequest.js'; -import { types, defaultValues } from '../util/data.js'; -import { createJwt, setupKeys } from '../util/auth'; +import { types, defaultValues as codeValues, headerDefinitions } from '../util/data.js'; +import { createJwt } from '../util/auth'; import env from 'env-var'; import FHIR from 'fhirclient'; @@ -16,8 +16,11 @@ 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 SettingsIcon from '@mui/icons-material/Settings'; import PatientSearchBar from '../components/RequestBox/PatientSearchBar/PatientSearchBar'; +import { MedicationStatus } from '../components/MedicationStatus/MedicationStatus.jsx'; +import { actionTypes } from './ContextProvider/reducer.js'; +import axios from 'axios'; export default class RequestBuilder extends Component { constructor(props) { @@ -25,10 +28,10 @@ export default class RequestBuilder extends Component { this.state = { loading: false, logs: [], - patient: {}, - expanded: false, + patient: {}, + expanded: true, patientList: [], - response: null, + response: {}, code: null, codeSystem: null, display: null, @@ -37,57 +40,54 @@ export default class RequestBuilder extends Component { showSettings: false, token: null, client: this.props.client, - codeValues: defaultValues, - // Configurable values - alternativeTherapy: env.get('REACT_APP_ALT_DRUG').asBool(), - baseUrl: env.get('REACT_APP_EHR_BASE').asString(), - cdsUrl: env.get('REACT_APP_CDS_SERVICE').asString(), - defaultUser: env.get('REACT_APP_DEFAULT_USER').asString(), - ehrUrl: env.get('REACT_APP_EHR_SERVER').asString(), - ehrUrlSentToRemsAdminForPreFetch: env - .get('REACT_APP_EHR_SERVER_TO_BE_SENT_TO_REMS_ADMIN_FOR_PREFETCH') - .asString(), - generateJsonToken: env.get('REACT_APP_GENERATE_JWT').asBool(), - includeConfig: true, - launchUrl: env.get('REACT_APP_LAUNCH_URL').asString(), - orderSelect: env.get('REACT_APP_ORDER_SELECT').asString(), - orderSign: env.get('REACT_APP_ORDER_SIGN').asString(), - patientFhirQuery: env.get('REACT_APP_PATIENT_FHIR_QUERY').asString(), - patientView: env.get('REACT_APP_PATIENT_VIEW').asString(), - pimsUrl: env.get('REACT_APP_PIMS_SERVER').asString(), - responseExpirationDays: env.get('REACT_APP_RESPONSE_EXPIRATION_DAYS').asInt(), - sendPrefetch: true, - smartAppUrl: env.get('REACT_APP_SMART_LAUNCH_URL').asString() + medicationDispense: null, + lastCheckedMedicationTime: null }; this.updateStateElement = this.updateStateElement.bind(this); - this.submit_info = this.submit_info.bind(this); + this.submitInfo = this.submitInfo.bind(this); this.consoleLog = this.consoleLog.bind(this); this.takeSuggestion = this.takeSuggestion.bind(this); - this.reconnectEhr = this.reconnectEhr.bind(this); this.requestBox = React.createRef(); } + getMedicationStatus = () => { + this.setState({ lastCheckedMedicationTime: Date.now() }); + + axios + .get( + `${this.props.globalState.ehrUrl}/MedicationDispense?prescription=${this.state.request.id}` + ) + .then( + response => { + const bundle = response.data; + this.setState({ medicationDispense: bundle.entry?.[0].resource }); + }, + error => { + console.log('Was not able to get medication status', error); + } + ); + }; + componentDidMount() { if (!this.state.client) { this.reconnectEhr(); } else { // Call patients on load of page this.getPatients(); - this.setState({ baseUrl: this.state.client.state.serverUrl }); - this.setState({ ehrUrl: this.state.client.state.serverUrl }); + this.props.dispatch({ + type: actionTypes.updateSetting, + settingId: 'baseUrl', + value: this.state.client.state.serverUrl + }); + this.props.dispatch({ + type: actionTypes.updateSetting, + settingId: 'ehrUrl', + value: this.state.client.state.serverUrl + }); } } - reconnectEhr() { - FHIR.oauth2.authorize({ - clientId: env.get('REACT_APP_CLIENT').asString(), - iss: this.state.baseUrl, - redirectUri: this.props.redirect, - scope: env.get('REACT_APP_CLIENT_SCOPES').asString() - }); - } - consoleLog(content, type) { console.log(content); let jsonContent = { @@ -100,7 +100,14 @@ export default class RequestBuilder extends Component { } updateStateElement = (elementName, text) => { - this.setState({ [elementName]: text }); + if (elementName === 'patient') { + this.props.dispatch({ + type: actionTypes.updatePatient, + value: text + }); + } else { + this.setState({ [elementName]: text }); + } // if the patientFhirQuery is updated, make a call to get the patients if (elementName === 'patientFhirQuery') { setTimeout(() => { @@ -109,51 +116,50 @@ export default class RequestBuilder extends Component { } }; - timeout = time => { let controller = new AbortController(); setTimeout(() => controller.abort(), time * 1000); return controller; }; - submit_info(prefetch, request, patient, hook) { + submitInfo(prefetch, request, patient, hook) { this.setState({ loading: true }); this.consoleLog('Initiating form submission', types.info); this.setState({ patient }); const hookConfig = { - includeConfig: this.state.includeConfig, - alternativeTherapy: this.state.alternativeTherapy + includeConfig: this.props.globalState.includeConfig, + alternativeTherapy: this.props.globalState.alternativeTherapy }; - let user = this.state.defaultUser; + let user = this.props.globalState.defaultUser; let json_request = buildRequest( request, user, patient, - this.state.ehrUrlSentToRemsAdminForPreFetch, + this.props.globalState.ehrUrlSentToRemsAdminForPreFetch, this.state.client.state.tokenResponse, prefetch, - this.state.sendPrefetch, + this.props.globalState.sendPrefetch, hook, hookConfig ); - let cdsUrl = this.state.cdsUrl; + let cdsUrl = this.props.globalState.cdsUrl; if (hook === 'order-sign') { - cdsUrl = cdsUrl + '/' + this.state.orderSign; + cdsUrl = cdsUrl + '/' + this.props.globalState.orderSign; } else if (hook === 'order-select') { - cdsUrl = cdsUrl + '/' + this.state.orderSelect; + cdsUrl = cdsUrl + '/' + this.props.globalState.orderSelect; } else if (hook === 'patient-view') { - cdsUrl = cdsUrl + '/' + this.state.patientView; + cdsUrl = cdsUrl + '/' + this.props.globalState.patientView; } else { this.consoleLog("ERROR: unknown hook type: '", hook, "'"); return; } - let baseUrl = this.state.baseUrl; + let baseUrl = this.props.globalState.baseUrl; const headers = { 'Content-Type': 'application/json' }; - if (this.state.generateJsonToken) { + if (this.props.globalState.generateJsonToken) { const jwt = 'Bearer ' + createJwt(baseUrl, cdsUrl); headers.authorization = jwt; } @@ -202,18 +208,20 @@ export default class RequestBuilder extends Component { } getPatients = () => { - this.props.client - .request(this.state.patientFhirQuery, { flat: true }) - .then(result => { - this.setState({ - patientList: result, - }); - }) - .catch(e => { - this.setState({ - patientList: e + if (this.props.globalState.patientFhirQuery) { + this.props.client + .request(this.props.globalState.patientFhirQuery, { flat: true }) + .then(result => { + this.setState({ + patientList: result + }); + }) + .catch(e => { + this.setState({ + patientList: e + }); }); - }); + } }; updateStateList = (elementName, text) => { @@ -239,131 +247,119 @@ export default class RequestBuilder extends Component { response: {} }); }; + handleChange = () => (event, isExpanded) => { - this.setState({ expanded: isExpanded ? true: false}); + this.setState({ expanded: isExpanded ? true : false }); }; + isOrderNotSelected() { + return Object.keys(this.state.request).length === 0; + } + render() { + const displayRequestBox = !!this.props.globalState.patient?.id; + const disableGetMedicationStatus = this.isOrderNotSelected() || this.state.loading; + return ( -
-
- - -
-
- {/*
*/} - {this.state.showSettings && ( -
- -
- )} -
-
- + <> + + + } - aria-controls="panel1a-content" - id="panel1a-header" - style={{marginLeft: '45%'}} - > - - - - {this.state.patientList.length > 0 && this.state.expanded ? -
+ expandIcon={} + aria-controls="panel1a-content" + id="panel1a-header" + style={{ marginLeft: '45%' }} + > + + + + {this.state.patientList.length > 0 && this.state.expanded && ( - {this.state.patientList instanceof Error - ? this.renderError() - : } + {this.state.patientList instanceof Error ? ( + this.renderError() + ) : ( + + )} -
- : - } - -
+ )} +
- this.getPatients()} - size="large" - > +
+ + this.getPatients()} size="large"> -
-
-
- {/*for the ehr launch */} - + + + {displayRequestBox && ( + + + + )} + {!disableGetMedicationStatus && ( + + + + )} + + + + -
-
- -
- -
-
+ + + ); } } diff --git a/src/index.css b/src/index.css index 19229868..5c0b331e 100644 --- a/src/index.css +++ b/src/index.css @@ -2,15 +2,10 @@ body { margin: 0; padding: 0; font-family: sans-serif; - } -.left-form { - width: 50%; - float: left; - margin-top: 25px; -} -.btn:focus,.btn:active { +.btn:focus, +.btn:active { outline: none !important; box-shadow: none; } @@ -22,298 +17,247 @@ body { left: 13px; top: 17px; transition: 0.2s ease all; - font-size: 14px; + font-size: 14px; } -input:focus ~ .floating-label{ +input:focus ~ .floating-label { top: -14px; opacity: 1; - } -input:not(:focus):not([value=""]):valid ~ .floating-label{ +input:not(:focus):not([value='']):valid ~ .floating-label { top: -14px; opacity: 1; - } -.error-border{ - border-color:#E34531 !important; +.error-border { + border-color: #e34531 !important; } -.input-text:not(:focus):not([value=""]):valid{ +.input-text:not(:focus):not([value='']):valid { border-color: #000000; - } .form-control:focus { border-color: inherit; -webkit-box-shadow: none; box-shadow: none; } -.input-text{ - border-width:1px 0px 0px 9px; +.input-text { + border-width: 1px 0px 0px 9px; border-style: solid none solid solid; } -.input-text:hover{ +.input-text:hover { border-color: #999999; } -.input-text:focus{ +.input-text:focus { border-color: #555555; } -.btn-class{ - border-width:1px 5px 3px 1px; +.btn-class { + border-width: 1px 5px 3px 1px; border-style: solid solid solid solid; border-color: black; - background: linear-gradient(white,white) -} - - -.btn-class-correct{ - border-color: #3145C3; + background: linear-gradient(white, white); } -.right-form{ - - float:right; - width:50%; - /* gotta account for the header on the left form */ - margin-top:52px; +.btn-class-correct { + border-color: #3145c3; } -.button-empty-fields{ - opacity:0.5 !important; +.button-empty-fields { + opacity: 0.5 !important; } -.button-error{ - border-color: #E34531; +.button-error { + border-color: #e34531; } -.genderBlockMaleUnselected{ +.genderBlockMaleUnselected { border-width: 1px 1px 3px 5px; - opacity:0.5; - + opacity: 0.5; } -.genderBlockFemaleUnselected{ +.genderBlockFemaleUnselected { border-width: 1px 5px 3px 1px; - opacity:0.5; + opacity: 0.5; } -.genderBlockMaleSelected{ +.genderBlockMaleSelected { border-width: 1px 1px 1px 3px; - background:#DDDDDD; - + background: #dddddd; } -.genderBlockFemaleSelected{ - +.genderBlockFemaleSelected { border-width: 1px 3px 1px 1px; - background:#DDDDDD; - - + background: #dddddd; } -.genderBlockMale{ - width:50%; +.genderBlockMale { + width: 50%; border-top-right-radius: 0cm; border-bottom-right-radius: 0cm; - - } -.genderBlockFemale{ - width:50%; +.genderBlockFemale { + width: 50%; border-top-left-radius: 0cm; border-bottom-left-radius: 0cm; - } -.genderBlockFemaleUnselected:hover{ - border-width:1px 3px 1px 1px; - padding-bottom:-2px; - margin-bottom:2px; +.genderBlockFemaleUnselected:hover { + border-width: 1px 3px 1px 1px; + padding-bottom: -2px; + margin-bottom: 2px; } -.genderBlockMaleUnselected:hover{ - border-width:1px 1px 1px 3px; - padding-bottom:-2px; - margin-bottom:2px; +.genderBlockMaleUnselected:hover { + border-width: 1px 1px 1px 3px; + padding-bottom: -2px; + margin-bottom: 2px; } -.dropdownCode{ +.dropdownCode { border-width: 1px 1px 0px 9px !important; } -.dropdownCode:focus{ +.dropdownCode:focus { border-color: #555555 !important; } -.header{ +.header { text-align: center; font-size: 18px; - padding:10px; + padding: 10px; } -.checkBox{ - float:right; +.checkBox { + float: right; border-style: solid; - border-width:1px 5px 3px 1px; + border-width: 1px 5px 3px 1px; } -.checkBoxClicked{ +.checkBoxClicked { border-width: 1px 3px 1px 1px; - float:right; - border-color:gray black black gray; - + float: right; + border-color: gray black black gray; } -.checkBox:hover{ - border-width:1px 3px 1px 1px; - border-color:gray black black gray; +.checkBox:hover { + border-width: 1px 3px 1px 1px; + border-color: gray black black gray; } -.onOffState{ +.onOffState { border-style: solid; - border-radius:50px; + border-radius: 50px; border-width: 0px 5px 0px 5px; margin-left: 5px; } -.onOff{ +.onOff { /* border-color: #E34531; */ - border-color: #C5C5C5; + border-color: #c5c5c5; } -.onOffActive{ +.onOffActive { /* border-color: #5CB85C; */ border-color: #222222; - } -.ui.selection.active.dropdown:hover{ - border-color:black; +.ui.selection.active.dropdown:hover { + border-color: black; } -.blackBorder{ - border-color:black !important; +.blackBorder { + border-color: black !important; } -.ui.selection.active.dropdown{ - border-color:#333333; +.ui.selection.active.dropdown { + border-color: #333333; } -.ui{ +.ui { transition: all 2s; } - -.visible{ - opacity:1; +.visible { + opacity: 1; } -.invisible{ +.invisible { /* display:none; */ - opacity:0; + opacity: 0; } -.spinner{ - display:inline-block; - transition:all 0.5s; - margin-left:15px; - line-height:3em; +.spinner { + display: inline-block; + transition: all 0.5s; + margin-left: 15px; + line-height: 3em; } -.rightStateInput{ - width:50%; - float:right; - margin-bottom:25px; +.rightStateInput { + width: 50%; + float: right; + margin-bottom: 25px; border-left: 5px solid transparent; } -.leftStateInput{ - width:50%; - border-right:5px solid transparent; - float:left; - margin-bottom:25px; +.leftStateInput { + width: 50%; + border-right: 5px solid transparent; + float: left; + margin-bottom: 25px; } .version-button { - width: 60px; - border-style: solid solid solid solid; - border-color: black; + width: 60px; + border-style: solid solid solid solid; + border-color: black; } .launch-button { - border-style: solid solid solid solid; - border-color: black; - width:120px; + border-style: solid solid solid solid; + border-color: black; + width: 120px; } .right-button { - border-width:1px 3px 3px 1px; - border-radius: 0 10% 10% 0%; - margin-right: 10px; + border-width: 1px 3px 3px 1px; + border-radius: 0 10% 10% 0%; + margin-right: 10px; } .left-button { - border-width:1px 1px 3px 3px; - border-radius: 10% 0% 0% 10%; -} - -.version-button:hover{ - background:black; - color:#AAA; -} - -.launch-button:hover{ - background:black; - color:#AAA; -} - -.launch-button.not-active{ - color:#666; + border-width: 1px 1px 3px 3px; + border-radius: 10% 0% 0% 10%; } -.launch-button.active{ - background:black; - color:white; +.version-button:hover { + background: black; + color: #aaa; } -.version-button.not-active{ - color:#666; +.launch-button:hover { + background: black; + color: #aaa; } -.version-button.active{ - background:black; - color:white; +.launch-button.not-active { + color: #666; } -.btn-class.settings{ - border-width:1px; - font-size:22px; - height:36px; - float:right; - -} -.settings:hover{ - background:black; - color:white; +.launch-button.active { + background: black; + color: white; } -.settings.active{ - background:black; - color:white; +.version-button.not-active { + color: #666; } -.settings-icon{ - vertical-align:1px; -} -#settings-header{ - margin-bottom: 10px; +.version-button.active { + background: black; + color: white; } -.nav-header{ - margin-bottom: 10px; - display: flow; - height: 55px; - padding:10px; +.nav-header { border-bottom: 1px solid black; - background-color: #005B94; + background-color: #005b94; } -.loading{ +.loading { width: 100vw; height: 100vh; display: flex; @@ -325,8 +269,15 @@ input:not(:focus):not([value=""]):valid ~ .floating-label{ .title { margin-bottom: 65px; } - .settings-box { - width: 50%; - margin-left: 20px; -} \ No newline at end of file + border: 1px solid black; + width: 75%; + height: 75%; + background-color: white; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + overflow-y: auto; + box-shadow: 10px 10px 20px black; +} diff --git a/src/index.js b/src/index.js index 4c4b3f6c..2eea30c3 100644 --- a/src/index.js +++ b/src/index.js @@ -2,5 +2,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './components/App'; +import { SettingsProvider } from './containers/ContextProvider/SettingsProvider'; -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render( + + + , + document.getElementById('root') +); diff --git a/src/util/buildScript.2017071.js b/src/util/buildScript.2017071.js index 0b0f8f91..653b60e4 100644 --- a/src/util/buildScript.2017071.js +++ b/src/util/buildScript.2017071.js @@ -2,6 +2,8 @@ import { getDrugCodeableConceptFromMedicationRequest } from './fhir'; +var SCRIPT_VERSION = '20170715'; + function xmlAddTextNode(xmlDoc, parent, sectionName, value) { var section = xmlDoc.createElement(sectionName); var textNode = xmlDoc.createTextNode(value); @@ -9,6 +11,14 @@ function xmlAddTextNode(xmlDoc, parent, sectionName, value) { parent.appendChild(section); } +function xmlAddTextNodeWithAttribute(xmlDoc, parent, sectionName, value, attrName, attrValue) { + var section = xmlDoc.createElement(sectionName); + section.setAttribute(attrName, attrValue); + var textNode = xmlDoc.createTextNode(value); + section.appendChild(textNode); + parent.appendChild(section); +} + function buildNewRxName(doc, nameResource) { var name = doc.createElement('Name'); xmlAddTextNode(doc, name, 'LastName', nameResource.family); @@ -59,19 +69,26 @@ function buildNewRxPatient(doc, patientResource) { return patient; } -function buildNewRxPrescriber(doc, practitionerResource) { - var prescriber = doc.createElement('Prescriber'); - var nonVeterinarian = doc.createElement('NonVeterinarian'); - - // Prescriber Identifier +function getPractitionerNpi(practitionerResource) { for (let i = 0; i < practitionerResource.identifier.length; i++) { let id = practitionerResource.identifier[i]; if (id.system && id.system.includes('us-npi')) { - var identification = doc.createElement('Identification'); - xmlAddTextNode(doc, identification, 'NPI', id.value); - nonVeterinarian.appendChild(identification); + return id.value; } } + return null; +} + +function buildNewRxPrescriber(doc, practitionerResource, npi) { + const prescriber = doc.createElement('Prescriber'); + const nonVeterinarian = doc.createElement('NonVeterinarian'); + + // Prescriber Identifier + if (npi) { + const identification = doc.createElement('Identification'); + xmlAddTextNode(doc, identification, 'NPI', npi); + nonVeterinarian.appendChild(identification); + } // Prescriber Name const practitionerNameResource = practitionerResource.name[0]; @@ -82,11 +99,11 @@ function buildNewRxPrescriber(doc, practitionerResource) { nonVeterinarian.appendChild(buildNewRxAddress(doc, practitionerAddressResource)); // Prescriber Phone Number and Email - var communicationNumbers = doc.createElement('CommunicationNumbers'); + const communicationNumbers = doc.createElement('CommunicationNumbers'); for (let i = 0; i < practitionerResource.telecom.length; i++) { const telecom = practitionerResource.telecom[i]; if (telecom.system === 'phone') { - var primaryTelephone = doc.createElement('PrimaryTelephone'); + const primaryTelephone = doc.createElement('PrimaryTelephone'); xmlAddTextNode(doc, primaryTelephone, 'Number', telecom.value); communicationNumbers.appendChild(primaryTelephone); } else if (telecom.system === 'email') { @@ -208,7 +225,8 @@ function buildNewRxMedication(doc, medicationRequestResource) { var drugCoded = doc.createElement('DrugCoded'); // loop through the coding values and find the ndc code and the rxnorm code - let medicationCodingList = getDrugCodeableConceptFromMedicationRequest(medicationRequestResource)?.coding; + let medicationCodingList = + getDrugCodeableConceptFromMedicationRequest(medicationRequestResource)?.coding; for (let i = 0; i < medicationCodingList.length; i++) { const coding = medicationCodingList[i]; const system = coding.system.toLowerCase(); @@ -279,6 +297,15 @@ export default function buildNewRxRequest( ) { var doc = document.implementation.createDocument('', '', null); var message = doc.createElement('Message'); + // set the message attributes + message.setAttribute('DatatypesVersion', SCRIPT_VERSION); + message.setAttribute('TransportVersion', SCRIPT_VERSION); + message.setAttribute('TransactionDomain', 'SCRIPT'); + message.setAttribute('TransactionVersion', SCRIPT_VERSION); + message.setAttribute('StructuresVersion', SCRIPT_VERSION); + message.setAttribute('ECLVersion', SCRIPT_VERSION); + message.setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + message.setAttribute('xsi:noNamespaceSchemaLocation', 'transport.xsd'); // Header var header = doc.createElement('Header'); @@ -286,6 +313,16 @@ export default function buildNewRxRequest( const d1 = new Date(); const messageIdValue = d1.getTime(); xmlAddTextNode(doc, header, 'MessageID', messageIdValue); + + // SentTime + xmlAddTextNode(doc, header, 'SentTime', d1.toISOString()); + + // PrescriberOrderNumber + xmlAddTextNode(doc, header, 'PrescriberOrderNumber', medicationRequestResource?.id); + + // To + xmlAddTextNodeWithAttribute(doc, header, 'To', 'Pharmacy 123', 'Qualifier', 'P'); + message.appendChild(header); // Body @@ -296,7 +333,16 @@ export default function buildNewRxRequest( newRx.appendChild(buildNewRxPatient(doc, patientResource)); // Prescriber - newRx.appendChild(buildNewRxPrescriber(doc, practitionerResource)); + const npi = getPractitionerNpi(practitionerResource); + const prescriber = buildNewRxPrescriber(doc, practitionerResource, npi); + newRx.appendChild(prescriber); + if (npi) { + // set the prescriber NPI in the header.from + xmlAddTextNodeWithAttribute(doc, header, 'From', npi, 'Qualifier', 'C'); + } else { + // just set it to the request generator + xmlAddTextNodeWithAttribute(doc, header, 'From', 'Request Generator', 'Qualifier', 'C'); + } // Medication newRx.appendChild(buildNewRxMedication(doc, medicationRequestResource)); diff --git a/src/util/buildScript.2022071.js b/src/util/buildScript.2022071.js index 58bb9016..85e1a250 100644 --- a/src/util/buildScript.2022071.js +++ b/src/util/buildScript.2022071.js @@ -217,7 +217,8 @@ function buildNewRxMedication(doc, medicationRequestResource) { var drugCoded = doc.createElement('DrugCoded'); // loop through the coding values and find the ndc code and the rxnorm code - let medicationCodingList = getDrugCodeableConceptFromMedicationRequest(medicationRequestResource)?.coding; + let medicationCodingList = + getDrugCodeableConceptFromMedicationRequest(medicationRequestResource)?.coding; for (let i = 0; i < medicationCodingList.length; i++) { const coding = medicationCodingList[i]; const system = coding.system.toLowerCase(); diff --git a/src/util/data.js b/src/util/data.js index d49ec438..b1f0b2e2 100644 --- a/src/util/data.js +++ b/src/util/data.js @@ -1,71 +1,90 @@ +import env from 'env-var'; + const headerDefinitions = { alternativeTherapy: { display: 'Alternative Therapy Cards Allowed', - type: 'check' + type: 'check', + default: env.get('REACT_APP_ALT_DRUG').asBool() }, baseUrl: { display: 'Base Server', - type: 'input' + type: 'input', + default: env.get('REACT_APP_EHR_BASE').asString() }, cdsUrl: { display: 'REMS Admin', - type: 'input' + type: 'input', + default: env.get('REACT_APP_CDS_SERVICE').asString() }, defaultUser: { display: 'Default User', - type: 'input' + type: 'input', + default: env.get('REACT_APP_DEFAULT_USER').asString() }, ehrUrl: { display: 'EHR Server', - type: 'input' + type: 'input', + default: env.get('REACT_APP_EHR_SERVER').asString() }, ehrUrlSentToRemsAdminForPreFetch: { display: 'EHR Server Sent to REMS Admin for Prefetch', - type: 'input' + type: 'input', + default: env.get('REACT_APP_EHR_SERVER_TO_BE_SENT_TO_REMS_ADMIN_FOR_PREFETCH').asString() }, generateJsonToken: { display: 'Generate JSON Web Token', - type: 'check' + type: 'check', + default: env.get('REACT_APP_GENERATE_JWT').asBool() }, includeConfig: { display: 'Include Configuration in CRD Request', - type: 'check' + type: 'check', + default: true }, launchUrl: { display: 'DTR Launch URL (QuestionnaireForm)', - type: 'input' + type: 'input', + default: env.get('REACT_APP_LAUNCH_URL').asString() }, orderSelect: { display: 'Order Select Rest End Point', - type: 'input' + type: 'input', + default: env.get('REACT_APP_ORDER_SELECT').asString() }, orderSign: { display: 'Order Sign Rest End Point', - type: 'input' + type: 'input', + default: env.get('REACT_APP_ORDER_SIGN').asString() }, patientFhirQuery: { display: 'Patient FHIR Query', - type: 'input' + type: 'input', + default: env.get('REACT_APP_PATIENT_FHIR_QUERY').asString() }, patientView: { display: 'Patient View Rest End Point', - type: 'input' + type: 'input', + default: env.get('REACT_APP_PATIENT_VIEW').asString() }, pimsUrl: { display: 'PIMS Server', - type: 'input' + type: 'input', + default: env.get('REACT_APP_PIMS_SERVER').asString() }, responseExpirationDays: { display: 'In Progress Form Expiration Days', - type: 'input' + type: 'input', + default: env.get('REACT_APP_RESPONSE_EXPIRATION_DAYS').asInt() }, sendPrefetch: { display: 'Send Prefetch', - type: 'check' + type: 'check', + default: true }, smartAppUrl: { display: 'SMART App', - type: 'input' + type: 'input', + default: env.get('REACT_APP_SMART_LAUNCH_URL').asString() } }; diff --git a/src/util/fhir.js b/src/util/fhir.js index d0f5eb0b..0cbb845e 100644 --- a/src/util/fhir.js +++ b/src/util/fhir.js @@ -28,9 +28,9 @@ function getAge(dateString) { } /* -* Retrieve the CodeableConcept for the medication from the medicationCodeableConcept if available. -* Read CodeableConcept from contained Medication matching the medicationReference otherwise. -*/ + * Retrieve the CodeableConcept for the medication from the medicationCodeableConcept if available. + * Read CodeableConcept from contained Medication matching the medicationReference otherwise. + */ function getDrugCodeableConceptFromMedicationRequest(medicationRequest) { if (medicationRequest) { if (medicationRequest?.medicationCodeableConcept) { @@ -47,19 +47,43 @@ function getDrugCodeableConceptFromMedicationRequest(medicationRequest) { } } }); - return coding; + return coding; } } return undefined; - } - - /* +} + +/* * Retrieve the coding for the medication from the medicationCodeableConcept if available. * Read coding from contained Medication matching the medicationReference otherwise. */ function getDrugCodeFromMedicationRequest(medicationRequest) { const codeableConcept = getDrugCodeableConceptFromMedicationRequest(medicationRequest); return codeableConcept?.coding?.[0]; - } +} + +function createMedicationDispenseFromMedicationRequest(medicationRequest) { + console.log('createMedicationDispenseFromMedicationRequest'); + var medicationDispense = {}; + medicationDispense.resourceType = 'MedicationDispense'; + medicationDispense.id = medicationRequest?.id + '-dispense'; + medicationDispense.status = 'unknown'; + if (medicationRequest.medicationCodeableConcept) { + medicationDispense.medicationCodeableConcept = medicationRequest.medicationCodeableConcept; + } else if (medicationRequest.medicationReference) { + medicationDispense.medicationReference = medicationRequest.medicationReference; + } + medicationDispense.subject = medicationRequest.subject; + medicationDispense.authorizingPrescription = [ + { reference: 'MedicationRequest/' + medicationRequest.id } + ]; + return medicationDispense; +} -export { fhir, getAge, getDrugCodeableConceptFromMedicationRequest, getDrugCodeFromMedicationRequest }; +export { + fhir, + getAge, + getDrugCodeableConceptFromMedicationRequest, + getDrugCodeFromMedicationRequest, + createMedicationDispenseFromMedicationRequest +};