From 1b589e2336c238fe4a8720c2f19fe6076ee82fa0 Mon Sep 17 00:00:00 2001 From: Joyce Quach <33106214+jtquach1@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:46:19 -0400 Subject: [PATCH 1/2] REMS-747 Patient portal task assignee string changes (#135) * Refactor redundant code into utility functions * Add null check * Remove unused imports * Make function for practitioner name string similar to patient and provider * Add second client.request to get R4 Practitioner * Fix non-React-component eslint warnings about unused variables/imports and run Prettier --- .../ListSelections/NotificationsSection.jsx | 2 +- src/components/EtasuStatus/EtasuStatus.jsx | 2 +- .../EtasuStatus/EtasuStatusComponent.jsx | 2 +- .../PatientSearchBar/PatientSearchBar.jsx | 10 +- src/components/RequestBox/RequestBox.jsx | 5 +- .../RequestDashboard/SettingsSection.jsx | 265 +++++++++--------- .../RequestDashboard/TasksSection.jsx | 75 ++--- src/components/SMARTBox/PatientBox.jsx | 10 +- src/containers/PatientPortal.jsx | 15 +- src/util/buildRequest.js | 2 +- src/util/util.js | 15 + 11 files changed, 212 insertions(+), 191 deletions(-) diff --git a/src/components/Dashboard/ListSelections/NotificationsSection.jsx b/src/components/Dashboard/ListSelections/NotificationsSection.jsx index e1a3cf5..65db123 100644 --- a/src/components/Dashboard/ListSelections/NotificationsSection.jsx +++ b/src/components/Dashboard/ListSelections/NotificationsSection.jsx @@ -7,7 +7,7 @@ import { createMedicationFromMedicationRequest } from '../../../util/fhir'; import { standardsBasedGetEtasu, getMedicationSpecificEtasuUrl } from '../../../util/util'; const NotificationsSection = () => { - const [globalState, _] = useContext(SettingsContext); + const [globalState] = useContext(SettingsContext); const classes = useStyles(); const [etasu, setEtasu] = useState([]); const [medications, setMedications] = useState([]); diff --git a/src/components/EtasuStatus/EtasuStatus.jsx b/src/components/EtasuStatus/EtasuStatus.jsx index a9277e5..e49a6cd 100644 --- a/src/components/EtasuStatus/EtasuStatus.jsx +++ b/src/components/EtasuStatus/EtasuStatus.jsx @@ -7,7 +7,7 @@ import { createMedicationFromMedicationRequest } from '../../util/fhir.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 [globalState] = useContext(SettingsContext); const { code, request } = props; const [remsAdminResponse, setRemsAdminResponse] = useState({}); diff --git a/src/components/EtasuStatus/EtasuStatusComponent.jsx b/src/components/EtasuStatus/EtasuStatusComponent.jsx index 7bc0792..74bb1dd 100644 --- a/src/components/EtasuStatus/EtasuStatusComponent.jsx +++ b/src/components/EtasuStatus/EtasuStatusComponent.jsx @@ -6,7 +6,7 @@ import { SettingsContext } from '../../containers/ContextProvider/SettingsProvid import { standardsBasedGetEtasu, getMedicationSpecificEtasuUrl } from '../../util/util.js'; export const EtasuStatusComponent = props => { - const [globalState, _] = useContext(SettingsContext); + const [globalState] = useContext(SettingsContext); const { remsAdminResponseInit, data, display, medication } = props; diff --git a/src/components/RequestBox/PatientSearchBar/PatientSearchBar.jsx b/src/components/RequestBox/PatientSearchBar/PatientSearchBar.jsx index 37a1d88..54e2362 100644 --- a/src/components/RequestBox/PatientSearchBar/PatientSearchBar.jsx +++ b/src/components/RequestBox/PatientSearchBar/PatientSearchBar.jsx @@ -5,6 +5,7 @@ import { defaultValues } from '../../../util/data'; import PatientBox from '../../SMARTBox/PatientBox'; import './PatientSearchBarStyle.css'; +import { getPatientFirstAndLastName } from '../../../util/util'; const PatientSearchBar = props => { const [options] = useState(defaultValues); @@ -14,18 +15,11 @@ const PatientSearchBar = props => { useEffect(() => { const newList = props.searchablePatients.map(patient => ({ id: patient.id, - name: getName(patient) + name: getPatientFirstAndLastName(patient) })); setListOfPatients([newList]); }, [props.searchablePatients]); - function getName(patient) { - if (patient.name) { - return patient.name[0].given[0] + ' ' + patient.name[0].family; - } - return ''; - } - function getFilteredLength(searchString, listOfPatients) { const filteredListOfPatients = listOfPatients[0].filter(element => { if (searchString === '') { diff --git a/src/components/RequestBox/RequestBox.jsx b/src/components/RequestBox/RequestBox.jsx index 6365398..6416a26 100644 --- a/src/components/RequestBox/RequestBox.jsx +++ b/src/components/RequestBox/RequestBox.jsx @@ -15,7 +15,8 @@ import { import { retrieveLaunchContext, prepPrefetch, - getMedicationSpecificEtasuUrl + getMedicationSpecificEtasuUrl, + getPatientFirstAndLastName } from '../../util/util.js'; import './request.css'; import axios from 'axios'; @@ -71,7 +72,7 @@ const RequestBox = props => { } let name; if (patient.name) { - name = {`${patient.name[0].given[0]} ${patient.name[0].family}`} ; + name = {getPatientFirstAndLastName(patient)}; } else { name = emptyField; } diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index 2f59da0..c368a28 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -25,8 +25,15 @@ import AddIcon from '@mui/icons-material/Add'; import env from 'env-var'; import FHIR from 'fhirclient'; -import { headerDefinitions, medicationRequestToRemsAdmins, ORDER_SIGN, ORDER_SELECT, PATIENT_VIEW, ENCOUNTER_START, REMS_ETASU } from '../../util/data'; -import { actionTypes, initialState } from '../../containers/ContextProvider/reducer'; +import { + headerDefinitions, + ORDER_SIGN, + ORDER_SELECT, + PATIENT_VIEW, + ENCOUNTER_START, + REMS_ETASU +} from '../../util/data'; +import { actionTypes } from '../../containers/ContextProvider/reducer'; import { SettingsContext } from '../../containers/ContextProvider/SettingsProvider'; const ENDPOINT = [ORDER_SIGN, ORDER_SELECT, PATIENT_VIEW, ENCOUNTER_START, REMS_ETASU]; @@ -255,30 +262,30 @@ const SettingsSection = props => { case 'dropdown': return ( - - - Hook to send when selecting a patient - - - - + + + + Hook to send when selecting a patient + + + + - - ) + ); default: return (
@@ -300,115 +307,115 @@ const SettingsSection = props => { }} > {!state['useIntermediary'] && ( - - - - Medication Display - Medication RxNorm Code - Hook / Endpoint - REMS Admin URL - {/* This empty TableCell corresponds to the add and delete +
+ + + Medication Display + Medication RxNorm Code + Hook / Endpoint + REMS Admin URL + {/* This empty TableCell corresponds to the add and delete buttons. It is used to fill up the sticky header which will appear over the gray/white table rows. */} - - - - - {Object.entries(state.medicationRequestToRemsAdmins).map(([key, row]) => { - return ( - - - - dispatch({ - type: actionTypes.updateCdsHookSetting, - settingId: key, - value: { display: event.target.value } - }) - } - sx={{ width: '100%' }} - /> - - - - dispatch({ - type: actionTypes.updateCdsHookSetting, - settingId: key, - value: { rxnorm: event.target.value } - }) - } - sx={{ width: '100%' }} - /> - - - - - - - dispatch({ - type: actionTypes.updateCdsHookSetting, - settingId: key, - value: { remsAdmin: event.target.value } - }) - } - sx={{ width: '100%' }} - /> - - - - - dispatch({ type: actionTypes.addCdsHookSetting, settingId: key }) + + + + + {Object.entries(state.medicationRequestToRemsAdmins).map(([key, row]) => { + return ( + + + + dispatch({ + type: actionTypes.updateCdsHookSetting, + settingId: key, + value: { display: event.target.value } + }) } - size="large" - > - - - - - - dispatch({ type: actionTypes.deleteCdsHookSetting, settingId: key }) + sx={{ width: '100%' }} + /> + + + + dispatch({ + type: actionTypes.updateCdsHookSetting, + settingId: key, + value: { rxnorm: event.target.value } + }) } - size="large" + sx={{ width: '100%' }} + /> + + +
+ {ENDPOINT.map(endpointType => ( + + {endpointType} + + ))} + + + + + dispatch({ + type: actionTypes.updateCdsHookSetting, + settingId: key, + value: { remsAdmin: event.target.value } + }) + } + sx={{ width: '100%' }} + /> + + + + + dispatch({ type: actionTypes.addCdsHookSetting, settingId: key }) + } + size="large" + > + + + + + + dispatch({ type: actionTypes.deleteCdsHookSetting, settingId: key }) + } + size="large" + > + + + + + + ); + })} + + )} diff --git a/src/components/RequestDashboard/TasksSection.jsx b/src/components/RequestDashboard/TasksSection.jsx index 317a437..b712b87 100644 --- a/src/components/RequestDashboard/TasksSection.jsx +++ b/src/components/RequestDashboard/TasksSection.jsx @@ -1,18 +1,5 @@ import React, { memo, useState, useEffect, Fragment } from 'react'; -import { - Button, - Box, - Modal, - Grid, - Tabs, - Tab, - Stack, - Select, - FormControl, - InputLabel, - MenuItem, - Menu -} from '@mui/material'; +import { Button, Box, Modal, Grid, Tabs, Tab, Stack, MenuItem, Menu } from '@mui/material'; import AssignmentIcon from '@mui/icons-material/Assignment'; import PersonIcon from '@mui/icons-material/Person'; import DeleteIcon from '@mui/icons-material/Delete'; @@ -20,15 +7,18 @@ 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 ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import CheckIcon from '@mui/icons-material/Check'; 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 { retrieveLaunchContext } from '../../util/util'; +import { Refresh } from '@mui/icons-material'; +import { + getPatientFirstAndLastName, + getPatientFullName, + getPractitionerFirstAndLastName, + retrieveLaunchContext +} from '../../util/util'; const taskStatus = Object.freeze({ inProgress: 'in-progress', @@ -45,7 +35,8 @@ const TasksSection = props => { const [open, setOpen] = useState(false); const [taskToDelete, setTaskToDelete] = useState(''); const [anchorStatus, setAnchorStatus] = useState(null); - const [anchorAssign, setAnchorAssign] = useState(null); + const [anchorAssign, setAnchorAssign] = useState(null); // R4 Task + const [practitioner, setPractitioner] = useState(null); // R4 Practitioner const menuOpen = Boolean(anchorStatus); const assignMenuOpen = Boolean(anchorAssign); @@ -116,7 +107,7 @@ const TasksSection = props => { reference: user }; - props.client.update(task).then(e => { + props.client.update(task).then(() => { fetchTasks(); }); } @@ -127,14 +118,14 @@ const TasksSection = props => { task.owner = { reference: task.for.reference }; - props.client.update(task).then(e => { + props.client.update(task).then(() => { fetchTasks(); }); } }; const deleteTask = () => { if (taskToDelete) { - props.client.delete(`${taskToDelete.resourceType}/${taskToDelete.id}`).then(e => { + props.client.delete(`${taskToDelete.resourceType}/${taskToDelete.id}`).then(() => { console.log('Deleted Task'); fetchTasks(); }); @@ -160,6 +151,14 @@ const TasksSection = props => { fetchTasks(); }, []); + useEffect(() => { + props.client.request(`Practitioner/${state.defaultUser}`).then(practitioner => { + if (practitioner) { + setPractitioner(practitioner); + } + }); + }, [state.defaultUser]); + useEffect(() => { fetchTasks(); }, [state.patient]); @@ -167,7 +166,7 @@ const TasksSection = props => { const updateTaskStatus = (task, status) => { task.status = status; const updatedTask = structuredClone(task); // structured clone may not work on older browsers - props.client.update(washTask(updatedTask)).then(e => { + props.client.update(washTask(updatedTask)).then(() => { fetchTasks(); }); }; @@ -191,7 +190,7 @@ const TasksSection = props => { retrieveLaunchContext(smartLink, patient, props.client.state).then(result => { updateTaskStatus(lTask, 'in-progress'); lTask.status = 'in-progress'; - props.client.update(washTask(lTask)).then(_e => { + props.client.update(washTask(lTask)).then(() => { fetchTasks(); }); window.open(result.url, '_blank'); @@ -216,22 +215,33 @@ const TasksSection = props => { ); }; const renderAssignMenu = () => { - const assignOptions = ['me', 'patient']; + const patient = anchorAssign?.task?.for; + const assignOptions = [ + { + id: 'me', + display: `provider${practitioner ? ' (' + getPractitionerFirstAndLastName(practitioner) + ')' : ''}` + }, + { + id: 'patient', + display: `patient${patient ? ' (' + getPatientFullName(patient) + ')' : ''}` + } + ]; return ( - {assignOptions.map(op => { + {assignOptions.map(({ id, display }) => { return ( { - handleChangeAssign(anchorAssign?.task, op); + handleChangeAssign(anchorAssign?.task, id); }} - >{`Assign to ${op}`} + >{`Assign to ${display}`} ); })} ); }; + const renderTasks = taskSubset => { if (taskSubset.length > 0) { return ( @@ -259,13 +269,13 @@ const TasksSection = props => { if (task.for?.resourceType?.toLowerCase() === 'patient') { const patient = task.for; if (patient.name) { - taskForName = `${patient.name[0].given[0]} ${patient.name[0].family}`; + taskForName = getPatientFirstAndLastName(patient); } } 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}`; + taskOwnerName = getPractitionerFirstAndLastName(practitioner); } else { taskOwnerName = task.owner.id; } @@ -273,7 +283,7 @@ const TasksSection = props => { 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}`; + taskOwnerName = getPatientFirstAndLastName(patient); } else { taskOwnerName = task.owner.id; } @@ -454,6 +464,7 @@ const TasksSection = props => { {renderStatusMenu()} + {/* edit this function so it is like renderPortalView or renderMainView where it refers to each and every task */} {renderAssignMenu()} {props.portalView ? renderPortalView() : renderMainView()} diff --git a/src/components/SMARTBox/PatientBox.jsx b/src/components/SMARTBox/PatientBox.jsx index 885252c..01ff274 100644 --- a/src/components/SMARTBox/PatientBox.jsx +++ b/src/components/SMARTBox/PatientBox.jsx @@ -11,7 +11,11 @@ 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'; +import { + getPatientFirstAndLastName, + getPatientFullName, + retrieveLaunchContext +} from '../../util/util'; const PatientBox = props => { const [state, setState] = useState({ @@ -539,11 +543,11 @@ const PatientBox = props => { if (patient.name) { setState(prevState => ({ ...prevState, - name: `${patient.name[0].given[0]} ${patient.name[0].family}` + name: getPatientFirstAndLastName(patient) })); setState(prevState => ({ ...prevState, - fullName: `${patient.name[0].given.join(' ')} ${patient.name[0].family}` + fullName: getPatientFullName(patient) })); } if (patient.birthDate) { diff --git a/src/containers/PatientPortal.jsx b/src/containers/PatientPortal.jsx index e940d56..cc25378 100644 --- a/src/containers/PatientPortal.jsx +++ b/src/containers/PatientPortal.jsx @@ -10,6 +10,7 @@ import Typography from '@mui/material/Typography'; import env from 'env-var'; import { actionTypes } from './ContextProvider/reducer'; import { SettingsContext } from './ContextProvider/SettingsProvider'; +import { getPatientFirstAndLastName } from '../util/util'; const PatientPortal = () => { const classes = useStyles(); @@ -30,7 +31,7 @@ const PatientPortal = () => { } }); client.request(`Patient/${client.patient.id}`).then(patient => { - setPatientName(getName(patient)); + setPatientName(getPatientFirstAndLastName(patient)); dispatch({ type: actionTypes.updatePatient, value: patient @@ -46,18 +47,6 @@ const PatientPortal = () => { setPatientName(null); }; - const getName = patient => { - const name = []; - if (patient.name) { - if (patient.name[0].given) { - name.push(patient.name[0].given[0]); - } - if (patient.name[0].family) { - name.push(patient.name[0].family); - } - } - return name.join(' '); - }; return (
diff --git a/src/util/buildRequest.js b/src/util/buildRequest.js index 7cabe93..c0397a0 100644 --- a/src/util/buildRequest.js +++ b/src/util/buildRequest.js @@ -1,4 +1,4 @@ -import { ORDER_SIGN, ORDER_SELECT } from "./data"; +import { ORDER_SIGN, ORDER_SELECT } from './data'; export default function buildRequest( request, diff --git a/src/util/util.js b/src/util/util.js index 1e5a5d1..821d2f2 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -182,6 +182,21 @@ const prepPrefetch = prefetchedResources => { return preppedResources; }; +// FHIR R4 Patient +export const getPatientFirstAndLastName = patient => { + return patient ? `${patient.name[0].given[0]} ${patient.name[0].family}` : ''; +}; + +// FHIR R4 Patient +export const getPatientFullName = patient => { + return patient ? `${patient.name[0].given.join(' ')} ${patient.name[0].family}` : ''; +}; + +// FHIR R4 Practitioner +export const getPractitionerFirstAndLastName = practitioner => { + return practitioner ? `${practitioner.name[0].given[0]} ${practitioner.name[0].family}` : ''; +}; + export { retrieveLaunchContext, standardsBasedGetEtasu, From 039dd60dcd15bfbee597ba10e383f4efb36456b8 Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Mon, 9 Sep 2024 19:15:25 -0400 Subject: [PATCH 2/2] Update environment variable to point to the new NCPDP SCRIPT endpoint on PIMS --- .env | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env b/.env index fe4de04..39c1fd4 100644 --- a/.env +++ b/.env @@ -15,7 +15,7 @@ VITE_GH_PAGES=false VITE_LAUNCH_URL = http://localhost:4040/launch VITE_PASSWORD = alice VITE_PATIENT_FHIR_QUERY = Patient?_sort=identifier&_count=12 -VITE_PIMS_SERVER = http://localhost:5051/doctorOrders/api/addRx +VITE_PIMS_SERVER = http://localhost:5051/ncpdp/script VITE_PUBLIC_KEYS = http://localhost:3000/request-generator/.well-known/jwks.json VITE_REALM = ClientFhirServer VITE_RESPONSE_EXPIRATION_DAYS = 30 diff --git a/README.md b/README.md index a8e0e52..4c7dd50 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ Following are a list of modifiable paths: | VITE_LAUNCH_URL | `http://localhost:4040/launch` | The launch URL of the SMART app the request generator should use for standalone launches. Note that this URL is only used outside of the context of the CDS Hooks workflow. Normally, the SMART app launch URL will come from a link inside a card that is returned by the REMS Admin. | | VITE_PASSWORD | `alice` | The default password for logging in as the default user, defined by VITE_USER. This should be changed if using a different default user. | | VITE_PATIENT_FHIR_QUERY | `Patient?_sort=identifier&_count=12` | The FHIR query the app makes when searching for patients in the EHR. This should be modified if a different behavior is desired by the apps patient selection popup. This can also be modified directly in the app's settings. | -| VITE_PIMS_SERVER | `http://localhost:5051/doctorOrders/api/addRx` | The Pharmacy System endpoint for submitting medications. This should be changed depending on which pharmacy system you want to connect with. | +| VITE_PIMS_SERVER | `http://localhost:5051/ncpdp/script` | The Pharmacy System endpoint for submitting medications. This should be changed depending on which pharmacy system you want to connect with. | | VITE_PUBLIC_KEYS | `http://localhost:3000/request-generator/.well-known/jwks.json` | The endpoint which contains the public keys for authentication with the REMS admin. Should be changed if the keys are moved elsewhere. | | VITE_REALM | `ClientFhirServer` | The Keycloak realm to use. Only relevant is using Keycloak as an authentication server. This only affects direct logins like through the Patient Portal, not SMART launches like opening the app normally. | | VITE_RESPONSE_EXPIRATION_DAYS | `30` | The number of days old a Questionnaire Response can be before it is ignored and filtered out. This ensures the patient search excludes outdated or obsolete prior sessions from creating clutter. |