diff --git a/package-lock.json b/package-lock.json index 85a25a3..b98d3f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8136,6 +8136,162 @@ "win32" ] }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", + "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", + "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", + "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", + "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", + "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", + "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", + "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", + "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", + "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", + "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", + "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@segment/loosely-validate-event": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz", diff --git a/package.json b/package.json index 1d1e7e9..b9a1807 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "react-dom": "^17.0.0", "react-markdown": "^8.0.7", "react-router-dom": "^6.17.0", - "uuid": "^9.0.1", "vite": "^5.1.6", "vite-tsconfig-paths": "^4.3.2" }, diff --git a/src/components/Auth/Login.jsx b/src/components/Auth/Login.jsx index 77decaa..9fa13be 100644 --- a/src/components/Auth/Login.jsx +++ b/src/components/Auth/Login.jsx @@ -22,7 +22,7 @@ const Login = props => { params.append('client_id', env.get('VITE_CLIENT').asString()); axios .post( - `${env.get('VITE_AUTH').asString()}/auth/realms/${env + `${env.get('VITE_AUTH').asString()}/realms/${env .get('VITE_REALM') .asString()}/protocol/openid-connect/token`, params, diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index 10f1ea0..8d7d43c 100644 --- a/src/components/Dashboard/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -23,6 +23,7 @@ import FormsSection from './ListSelections/FormsSection'; import EmptySection from './ListSelections/EmptySection'; import PatientTaskSection from './ListSelections/PatientTaskSection'; import MedicationsSection from './ListSelections/MedicationsSection'; +import NotificationsSection from './ListSelections/NotificationsSection'; // Since we're using JS and can't use TS enums const Section = Object.freeze({ @@ -79,6 +80,8 @@ const Dashboard = props => { return ; case Section.MEDICATIONS: return ; + case Section.NOTIFICATIONS: + return default: return ; } diff --git a/src/components/Dashboard/ListSelections/NotificationsSection.jsx b/src/components/Dashboard/ListSelections/NotificationsSection.jsx new file mode 100644 index 0000000..4e17d25 --- /dev/null +++ b/src/components/Dashboard/ListSelections/NotificationsSection.jsx @@ -0,0 +1,36 @@ +import React, { memo, useContext, useEffect, useState } from 'react'; +import useStyles from '../styles'; +import { SettingsContext } from '../../../containers/ContextProvider/SettingsProvider'; +import { EtasuStatusComponent } from '../../EtasuStatus/EtasuStatusComponent'; +import axios from 'axios'; + +const NotificationsSection = () => { + const [globalState, _] = useContext(SettingsContext); + const classes = useStyles(); + const [etasu, setEtasu] = useState([]); + useEffect(() => { + const patientFirstName = globalState.patient?.name?.at(0)?.given?.at(0); + const patientLastName = globalState.patient?.name?.at(0)?.family; + const patientDOB = globalState.patient?.birthDate; + + const etasuUrl = `${globalState.remsAdminServer}/etasu/met/patient/${patientFirstName}/${patientLastName}/${patientDOB}`; + axios({ + method: 'get', + url: etasuUrl + }).then((response) => { + setEtasu(response.data); + }, (error) =>{ + console.error(error); + }) + }, []); + return ( +
+

Notifications

+ {etasu.map((remsCase) => { + return + })} +
+ ); +}; + +export default memo(NotificationsSection); diff --git a/src/components/EtasuStatus/EtasuStatus.jsx b/src/components/EtasuStatus/EtasuStatus.jsx new file mode 100644 index 0000000..2b0f996 --- /dev/null +++ b/src/components/EtasuStatus/EtasuStatus.jsx @@ -0,0 +1,44 @@ +import { EtasuStatusButton } from './EtasuStatusButton.jsx'; +import { EtasuStatusModal } from './EtasuStatusModal.jsx'; +import { useState, useEffect, useContext } from 'react'; +import { Card, Typography } from '@mui/material'; +import { SettingsContext } from '../../containers/ContextProvider/SettingsProvider.jsx'; +import axios from 'axios'; +import { EtasuStatusComponent } from './EtasuStatusComponent.jsx'; +import { getEtasu } from '../../util/util.js'; + +// converts code into etasu for the component to render +// simplifies usage for applications that only know the code, not the case they want to display +export const EtasuStatus = props => { + const [globalState, _] = useContext(SettingsContext); + + const { code } = + props; + const [remsAdminResponse, setRemsAdminResponse] = useState({}); + useEffect(() => getEtasuStatus(), [code]); + const getEtasuStatus = () => { + const patientFirstName = globalState.patient?.name?.at(0)?.given?.at(0); + const patientLastName = globalState.patient?.name?.at(0)?.family; + const patientDOB = globalState.patient?.birthDate; + + console.log( + 'get Etastu Status: ' + + patientFirstName + + ' ' + + patientLastName + + ' - ' + + patientDOB + + ' - ' + + code + ); + const etasuUrl = `${globalState.remsAdminServer}/etasu/met/patient/${patientFirstName}/${patientLastName}/${patientDOB}/drugCode/${code}`; + getEtasu(etasuUrl, setRemsAdminResponse); + }; + + return ( + <> + {remsAdminResponse.case_number ? : ""} + + ); +}; + diff --git a/src/components/EtasuStatus/EtasuStatusButton.css b/src/components/EtasuStatus/EtasuStatusButton.css new file mode 100644 index 0000000..006c066 --- /dev/null +++ b/src/components/EtasuStatus/EtasuStatusButton.css @@ -0,0 +1,17 @@ +.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; + } + \ No newline at end of file diff --git a/src/components/EtasuStatus/EtasuStatusButton.jsx b/src/components/EtasuStatus/EtasuStatusButton.jsx new file mode 100644 index 0000000..06aa189 --- /dev/null +++ b/src/components/EtasuStatus/EtasuStatusButton.jsx @@ -0,0 +1,64 @@ +import { Button, Grid, Typography } from '@mui/material'; +import ListIcon from '@mui/icons-material/List'; +import './EtasuStatusButton.css'; + +export const EtasuStatusButton = props => { + const { baseColor, remsAdminResponse, handleOpenEtasuStatus, lastCheckedEtasuTime } = + props; + return ( + + + {renderTimestamp(lastCheckedEtasuTime)} + + ); +}; + +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/EtasuStatus/EtasuStatusComponent.jsx b/src/components/EtasuStatus/EtasuStatusComponent.jsx new file mode 100644 index 0000000..2e863b9 --- /dev/null +++ b/src/components/EtasuStatus/EtasuStatusComponent.jsx @@ -0,0 +1,68 @@ +import { EtasuStatusButton } from './EtasuStatusButton.jsx'; +import { EtasuStatusModal } from './EtasuStatusModal.jsx'; +import { useState, useEffect, useContext } from 'react'; +import { Card, Typography } from '@mui/material'; +import { SettingsContext } from '../../containers/ContextProvider/SettingsProvider.jsx'; +import axios from 'axios'; +import { getEtasu } from '../../util/util.js'; + +export const EtasuStatusComponent = props => { + const [globalState, _] = useContext(SettingsContext); + + const { remsAdminResponseInit } = + props; + + const [remsAdminResponse, setRemsAdminResponse] = useState(remsAdminResponseInit); + const [lastCheckedEtasuTime, setLastCheckedEtasuTime] = useState(0); + + const [showEtasuStatus, setShowEtasuStatus] = useState(false); + + useEffect(() => { + setLastCheckedEtasuTime(Date.now()); + }, []); + const handleCloseEtasuStatus = () => { + setShowEtasuStatus(false); + }; + + const handleOpenEtasuStatus = () => { + setShowEtasuStatus(true); + }; + + const refreshEtasu = () => { + if(remsAdminResponse) { + const etasuUrl = `${globalState.remsAdminServer}/etasu/met/patient/${remsAdminResponse.patientFirstName}/${remsAdminResponse.patientLastName}/${remsAdminResponse.patientDOB}/drugCode/${remsAdminResponse.drugCode}`; + getEtasu(etasuUrl, setRemsAdminResponse); + setLastCheckedEtasuTime(Date.now()); + } + } + return ( + + + {remsAdminResponse?.drugName} + + + + + ); +}; + +export const getStatusColor = status => { + switch (status) { + case 'Approved': + return 'green'; + case 'Pending': + return '#f0ad4e'; + default: + return '#0c0c0c'; + } +}; diff --git a/src/components/EtasuStatus/EtasuStatusModal.css b/src/components/EtasuStatus/EtasuStatusModal.css new file mode 100644 index 0000000..a5a47e2 --- /dev/null +++ b/src/components/EtasuStatus/EtasuStatusModal.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/EtasuStatus/EtasuStatusModal.jsx b/src/components/EtasuStatus/EtasuStatusModal.jsx new file mode 100644 index 0000000..713b5af --- /dev/null +++ b/src/components/EtasuStatus/EtasuStatusModal.jsx @@ -0,0 +1,101 @@ +import { Box, Grid, IconButton, Modal, Tooltip, List, ListItem, ListItemIcon, ListItemText } from '@mui/material'; +import AutorenewIcon from '@mui/icons-material/Autorenew'; +import { useState, useEffect } from 'react'; +import { getStatusColor } from './EtasuStatusComponent'; +import './EtasuStatusModal.css'; +import CheckCircle from '@mui/icons-material/CheckCircle'; +import Close from '@mui/icons-material/Close'; + +const getIdText = remsAdminResponse => remsAdminResponse?.case_number || 'N/A'; + +export const EtasuStatusModal = props => { + const { callback, onClose, remsAdminResponse, update } = props; + const [spin, setSpin] = useState(false); + const color = getStatusColor(remsAdminResponse?.status); + + useEffect(() => { + if (update) { + setSpin(true); + callback(); + } + }, [update]); + + return ( + + +
+

REMS Status

+
+ + +
+ Case Number: {getIdText(remsAdminResponse)} +
+
Status: {remsAdminResponse.status || 'N/A'}
+
+ +
+ + + setSpin(false)} + /> + + +
+
+
+
+

+

ETASU

+ + {remsAdminResponse ? ( + + {remsAdminResponse?.metRequirements?.map((metRequirements) => ( + + + {metRequirements.completed ? ( + + ) : ( + + )} + + {metRequirements.completed ? ( + + ) : ( + + )} + + ))} + + ) : ( + 'Not Available' + )} + +
+
+
+
+ ); +}; + +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/containers/RequestBuilder.jsx b/src/containers/RequestBuilder.jsx index a4fb4f5..5bb65cf 100644 --- a/src/containers/RequestBuilder.jsx +++ b/src/containers/RequestBuilder.jsx @@ -16,10 +16,14 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import PatientSearchBar from '../components/RequestBox/PatientSearchBar/PatientSearchBar.jsx'; import { MedicationStatus } from '../components/MedicationStatus/MedicationStatus.jsx'; import { actionTypes } from './ContextProvider/reducer.js'; + import axios from 'axios'; +import { EtasuStatus } from '../components/EtasuStatus/EtasuStatus'; +import { SettingsContext } from './ContextProvider/SettingsProvider.jsx'; const RequestBuilder = props => { - const { globalState, dispatch, client } = props; + const { client } = props; + const [globalState, dispatch] = React.useContext(SettingsContext); const [state, setState] = useState({ loading: false, patient: {}, @@ -35,7 +39,7 @@ const RequestBuilder = props => { token: null, client: client, medicationDispense: null, - lastCheckedMedicationTime: null + lastCheckedMedicationTime: null, }); const displayRequestBox = !!globalState.patient?.id; @@ -43,7 +47,9 @@ const RequestBuilder = props => { return Object.keys(state.request).length === 0; }; + const disableGetMedicationStatus = isOrderNotSelected() || state.loading; + const disableGetEtasu = isOrderNotSelected() || state.loading; const getMedicationStatus = () => { setState(prevState => ({ ...prevState, @@ -64,6 +70,8 @@ const RequestBuilder = props => { ); }; + + useEffect(() => { if (state.client) { // Call patients on load of page @@ -293,17 +301,26 @@ const RequestBuilder = props => { /> )} - {!disableGetMedicationStatus && ( - - - - )} + + {!disableGetEtasu && ( + + + + )} + {!disableGetMedicationStatus && ( + + + + )} + diff --git a/src/util/data.js b/src/util/data.js index bd3712a..49e8428 100644 --- a/src/util/data.js +++ b/src/util/data.js @@ -85,6 +85,11 @@ const headerDefinitions = { display: 'SMART App', type: 'input', default: env.get('VITE_SMART_LAUNCH_URL').asString() + }, + remsAdminServer: { + display: 'REMS Admin Server', + type: 'input', + default: env.get('VITE_SERVER').asString() } }; diff --git a/src/util/util.js b/src/util/util.js index b63e36e..244e805 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -66,4 +66,30 @@ function retrieveLaunchContext(link, patientId, clientState) { }); } -export { retrieveLaunchContext }; +function getEtasu(etasuUrl, responseCallback) { + axios({ + method: 'get', + url: etasuUrl + }).then( + response => { + // Sorting an array mutates the data in place. + const remsMetRes = response.data; + if (remsMetRes.metRequirements) { + remsMetRes.metRequirements.sort((first, second) => { + // Keep the other forms unsorted. + if (second.requirementName.includes('Patient Status Update')) { + // Sort the Patient Status Update forms in descending order of timestamp. + return second.requirementName.localeCompare(first.requirementName); + } + return 0; + }); + } + responseCallback(response.data); + }, + error => { + console.log(error); + } + ); +} + +export { retrieveLaunchContext, getEtasu };