From cca81074bfbb0a7c9d4aaf5b275811feb0180329 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Thu, 18 Jan 2024 14:51:13 -0500 Subject: [PATCH 01/30] Fix typo --- src/components/RequestBox/RequestBox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RequestBox/RequestBox.js b/src/components/RequestBox/RequestBox.js index 408367a2..913b4d3f 100644 --- a/src/components/RequestBox/RequestBox.js +++ b/src/components/RequestBox/RequestBox.js @@ -422,7 +422,7 @@ export default class RequestBox extends Component { elevation={6} variant="filled" > - Success! NewRx Recieved By Pharmacy + Success! NewRx Received By Pharmacy From d2d970c0a4c4d490a640b6bd8f476067239591f3 Mon Sep 17 00:00:00 2001 From: Ariel Virgulto <31778437+avirgulto@users.noreply.github.com> Date: Tue, 30 Jan 2024 18:55:06 -0500 Subject: [PATCH 02/30] Feature/576 update workflow (#95) * Update initial table add and workflow * Update workflow * Clean up code base * Add hover and click on full rows of table instead of button and patient select button at high level --------- Co-authored-by: Ariel Virgulto --- .../PatientSearchBar/PatientSearchBar.js | 2 + src/components/RequestBox/RequestBox.js | 7 +- src/components/SMARTBox/PatientBox.js | 341 +++++++++++++----- src/components/SMARTBox/smart.css | 36 +- src/containers/RequestBuilder.js | 4 +- 5 files changed, 280 insertions(+), 110 deletions(-) diff --git a/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js b/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js index 250428f4..338b366f 100644 --- a/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js +++ b/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js @@ -78,6 +78,8 @@ export default function PatientSearchBar(props) { key={patient.id} patient={props.searchablePatients.find(item => item.id === patient.id)} client={props.client} + request={props.request} + launchUrl={props.launchUrl} callback={props.callback} callbackList={props.callbackList} callbackMap={props.callbackMap} diff --git a/src/components/RequestBox/RequestBox.js b/src/components/RequestBox/RequestBox.js index 913b4d3f..53627c31 100644 --- a/src/components/RequestBox/RequestBox.js +++ b/src/components/RequestBox/RequestBox.js @@ -158,6 +158,7 @@ export default class RequestBox extends Component { if (prefetchMap.size > 0) { return this.renderRequestResources(prefetchMap); } + return
; } renderRequestResources(requestResources) { @@ -385,12 +386,6 @@ export default class RequestBox extends Component {
- {Object.keys(this.props.response).length ? - - : } + : + + - {!responseOptions.length && returned ? ( - No in progress forms - ) : ( - this.makeDropdown( - responseOptions, - 'Choose an in-progress form', - this.state.response, - this.handleResponseChange - ) - )} -
+ } + { this.state.showQuestionnaires ? + + : + + + + + + } + + { 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/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index eba3a64c..a75d266c 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -28,7 +28,7 @@ export default class RequestBuilder extends Component { patient: {}, expanded: false, patientList: [], - response: null, + response: {}, code: null, codeSystem: null, display: null, @@ -298,6 +298,8 @@ export default class RequestBuilder extends Component { getPatients = {this.getPatients} searchablePatients={this.state.patientList} client={this.props.client} + request={this.state.request} + launchUrl={this.state.launchUrl} callback={this.updateStateElement} callbackList={this.updateStateList} callbackMap={this.updateStateMap} From efd4e78be2223573155ba957b8871892b751e23d Mon Sep 17 00:00:00 2001 From: Patrick LaRocque <41444457+plarocque4@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:43:12 -0500 Subject: [PATCH 03/30] Update NewRx (#98) * Update NewRx message to include all the required header information. * updates from review * Only get npi once * Change use of var to const --------- Co-authored-by: Joyce Quach --- src/util/buildScript.2017071.js | 70 +++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 12 deletions(-) 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)); From 77c8477e7c5a9675d2bcf5e0f9109048b9b56d72 Mon Sep 17 00:00:00 2001 From: Keeyan Date: Thu, 8 Feb 2024 15:49:13 -0500 Subject: [PATCH 04/30] make settings a modal and add save button (#96) * make settings a modal and add save button * remove potential for uncaught mismatch (#97) * fix checkboxes * fix issue with patient load --- src/components/SettingsBox/SettingsBox.css | 7 +- src/components/SettingsBox/SettingsBox.js | 122 +++++++++++++++------ src/containers/RequestBuilder.js | 76 ++++++------- src/index.css | 14 ++- src/util/data.js | 55 +++++++--- 5 files changed, 181 insertions(+), 93 deletions(-) diff --git a/src/components/SettingsBox/SettingsBox.css b/src/components/SettingsBox/SettingsBox.css index b2527c3c..9f473389 100644 --- a/src/components/SettingsBox/SettingsBox.css +++ b/src/components/SettingsBox/SettingsBox.css @@ -18,8 +18,12 @@ } .setting-header{ - font-size:12px; + font-weight:bold; margin:0; + margin-bottom: 10px; + padding:15px; + color:white; + background-color:#005B94; } .setting-btn { @@ -39,3 +43,4 @@ background-color: #333232; color: #858282; } + diff --git a/src/components/SettingsBox/SettingsBox.js b/src/components/SettingsBox/SettingsBox.js index e3760c88..29192dd9 100644 --- a/src/components/SettingsBox/SettingsBox.js +++ b/src/components/SettingsBox/SettingsBox.js @@ -1,9 +1,12 @@ import React, { Component } from 'react'; import './SettingsBox.css'; import InputBox from '../Inputs/InputBox'; -import CheckBox from '../Inputs/CheckBox'; +import Checkbox from '@mui/material/Checkbox'; + import { headerDefinitions, types } from '../../util/data'; import FHIR from 'fhirclient'; +import { Box, Button, FormControlLabel, Grid, TextField } from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; const clearQuestionnaireResponses = ({ ehrUrl, defaultUser, access_token }, consoleLog) => @@ -89,7 +92,7 @@ const resetRemsAdmin = const resetHeaderDefinitions = [ { - display: 'Clear EHR QuestionnaireResponses', + display: 'Clear In-Progress Forms', key: 'clearQuestionnaireResponses', reset: clearQuestionnaireResponses }, @@ -108,12 +111,44 @@ const resetHeaderDefinitions = [ export default class SettingsBox extends Component { constructor(props) { super(props); + this.state = { + originalValues: [] + }; + this.cancelSettings = this.cancelSettings.bind(this); + this.closeSettings = this.closeSettings.bind(this); + this.saveSettings = this.saveSettings.bind(this); + } + + componentDidMount() { + const headers = Object.keys(headerDefinitions).map(key => ([key, this.props.state[key]])); + this.setState({originalValues: headers}); + } + + closeSettings() { + this.props.updateCB('showSettings', false); + } + saveSettings() { + const headers = Object.keys(headerDefinitions).map(key => ([key, this.props.state[key]])); + console.log(headers); + localStorage.setItem('reqgenSettings', JSON.stringify(headers)); + this.closeSettings(); } - componentDidMount() {} + cancelSettings() { + this.state.originalValues.forEach((e) => { + try{ + this.props.updateCB(e[0], e[1]); + } catch { + console.log('Failed to reset setting value'); + } + }); + this.closeSettings(); + } render() { const { state, consoleLog, updateCB } = this.props; + let firstCheckbox = true; + let showBreak = true; const headers = Object.keys(headerDefinitions) .map(key => ({ ...headerDefinitions[key], key })) @@ -125,35 +160,46 @@ export default class SettingsBox extends Component { return (
+ +

Settings

+ {headers.map(({ key, type, display }) => { + switch (type) { case 'input': return ( -
-

{display}

- +
+ {updateCB(key, event.target.value);}} + sx = {{width:'100%'}} />
+ ); case 'check': + if(firstCheckbox) { + firstCheckbox = false; + showBreak = true; + } else { + showBreak = false; + } return ( -
-

- {display} - + {showBreak ? :''} + + {updateCB(key, event.target.checked);}}/> + } + label = {display} /> -

-

 

-
+ + ); default: return ( @@ -163,15 +209,29 @@ export default class SettingsBox extends Component { ); } })} - {resetHeaderDefinitions.map(({ key, display, reset }) => { - return ( -
- -
- ); - })} + {resetHeaderDefinitions.map(({ key, display, reset }) => { + return ( + + + + ); + })} + {/* spacer */} +
+ + + + + + + + + +
); } diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index a75d266c..00018b58 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { Button, Box, IconButton } from '@mui/material'; +import { Button, Box, IconButton, Modal, DialogTitle } from '@mui/material'; import PersonIcon from '@mui/icons-material/Person'; import RefreshIcon from '@mui/icons-material/Refresh'; import DisplayBox from '../components/DisplayBox/DisplayBox'; @@ -7,7 +7,7 @@ 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 { types, defaultValues, headerDefinitions } from '../util/data.js'; import { createJwt, setupKeys } from '../util/auth'; import env from 'env-var'; @@ -25,7 +25,7 @@ export default class RequestBuilder extends Component { this.state = { loading: false, logs: [], - patient: {}, + patient: {}, expanded: false, patientList: [], response: {}, @@ -38,26 +38,6 @@ export default class RequestBuilder extends Component { 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() }; this.updateStateElement = this.updateStateElement.bind(this); @@ -69,6 +49,21 @@ export default class RequestBuilder extends Component { } componentDidMount() { + // init settings + Object.keys(headerDefinitions).map((key) => { + this.setState({ [key]: headerDefinitions[key].default }); + }); + // load settings + JSON.parse(localStorage.getItem('reqgenSettings') || '[]').forEach((element) => { + try { + this.updateStateElement(element[0], element[1]); + } catch { + if (element[0]) { + console.log('Could not load setting:' + element[0]); + } + } + }); + if (!this.state.client) { this.reconnectEhr(); } else { @@ -240,7 +235,7 @@ export default class RequestBuilder extends Component { }); }; handleChange = () => (event, isExpanded) => { - this.setState({ expanded: isExpanded ? true: false}); + this.setState({ expanded: isExpanded ? true : false }); }; render() { @@ -265,8 +260,7 @@ export default class RequestBuilder extends Component {
- {/*
*/} - {this.state.showSettings && ( + { this.setState({ showSettings: false }); }} >
- )} +
-
- - + + } aria-controls="panel1a-content" id="panel1a-header" - style={{marginLeft: '45%'}} + style={{ marginLeft: '45%' }} >
: } - + - - this.getPatients()} - size="large" - > - - - + + this.getPatients()} + size="large" + > + + +
{/*for the ehr launch */} diff --git a/src/index.css b/src/index.css index 19229868..410e3449 100644 --- a/src/index.css +++ b/src/index.css @@ -325,8 +325,16 @@ input:not(:focus):not([value=""]):valid ~ .floating-label{ .title { margin-bottom: 65px; } - .settings-box { - width: 50%; - margin-left: 20px; + 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 + } \ No newline at end of file diff --git a/src/util/data.js b/src/util/data.js index d49ec438..f135947b 100644 --- a/src/util/data.js +++ b/src/util/data.js @@ -1,71 +1,92 @@ +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() } }; From f9249843bd828ebd4c0bf5bb1d7b50a8e972de82 Mon Sep 17 00:00:00 2001 From: Keeyan Date: Fri, 9 Feb 2024 15:30:52 -0500 Subject: [PATCH 05/30] prevent suggestions crash (#99) * don't error on suggestions * fix unrelated warning * remove unnecessary key * disable on success --- src/components/DisplayBox/DisplayBox.js | 16 ++++++---------- src/components/SMARTBox/PatientBox.js | 3 +-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/components/DisplayBox/DisplayBox.js b/src/components/DisplayBox/DisplayBox.js index f0f6f953..cd8c1374 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} ); @@ -397,11 +400,4 @@ export default class DisplayBox extends Component { } } - componentDidUpdate() { - // clear the suggestion buttons - console.log(this.buttonList); - this.buttonList.forEach((requestButton, id) => { - document.getElementById(requestButton).removeAttribute('disabled'); - }); - } } diff --git a/src/components/SMARTBox/PatientBox.js b/src/components/SMARTBox/PatientBox.js index 2e225b07..22f54f5b 100644 --- a/src/components/SMARTBox/PatientBox.js +++ b/src/components/SMARTBox/PatientBox.js @@ -479,9 +479,8 @@ export default class PatientBox extends Component { {options.map((row) => ( - + this.handleRequestChange(row.value, patient)} From 5631214d404b6f445a88e8b1060ff46b30cf45ba Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Thu, 15 Feb 2024 00:46:12 -0500 Subject: [PATCH 06/30] Create the MedicationDispense when sending NewRx. Add button to clean up existing MedicationDispenses. --- src/components/RequestBox/RequestBox.js | 27 +++++++----- src/components/SettingsBox/SettingsBox.js | 52 ++++++++++++++++++++--- src/util/fhir.js | 19 ++++++++- 3 files changed, 81 insertions(+), 17 deletions(-) diff --git a/src/components/RequestBox/RequestBox.js b/src/components/RequestBox/RequestBox.js index 53627c31..482a284c 100644 --- a/src/components/RequestBox/RequestBox.js +++ b/src/components/RequestBox/RequestBox.js @@ -2,13 +2,11 @@ import { Button, ButtonGroup } 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 { @@ -35,8 +33,6 @@ export default class RequestBox extends Component { this.submitOrderSign(request); } - componentDidMount() { } - prepPrefetch() { const preppedResources = new Map(); Object.keys(this.props.prefetchedResources).forEach(resourceKey => { @@ -257,9 +253,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'); }); }; @@ -317,7 +312,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 @@ -344,7 +339,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 => { diff --git a/src/components/SettingsBox/SettingsBox.js b/src/components/SettingsBox/SettingsBox.js index 29192dd9..f12a3487 100644 --- a/src/components/SettingsBox/SettingsBox.js +++ b/src/components/SettingsBox/SettingsBox.js @@ -1,16 +1,51 @@ import React, { Component } from 'react'; import './SettingsBox.css'; -import InputBox from '../Inputs/InputBox'; import Checkbox from '@mui/material/Checkbox'; import { headerDefinitions, types } from '../../util/data'; import FHIR from 'fhirclient'; import { Box, Button, FormControlLabel, Grid, TextField } from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; + +const clearMedicationDispenses = + ({ ehrUrl, access_token }, consoleLog) => + () => { + 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 => { + consoleLog( + 'Successfully deleted MedicationDispense ' + resource.id + ' from EHR', + types.info + ); + 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 clearQuestionnaireResponses = ({ ehrUrl, defaultUser, access_token }, consoleLog) => - _event => { + () => { console.log( 'Clear QuestionnaireResponses from the EHR: ' + ehrUrl + ' for author ' + defaultUser ); @@ -47,7 +82,7 @@ const clearQuestionnaireResponses = const resetPims = ({ pimsUrl }, consoleLog) => - _event => { + () => { let url = new URL(pimsUrl); const resetUrl = url.origin + '/doctorOrders/api/deleteAll'; console.log('reset pims: ' + resetUrl); @@ -70,7 +105,7 @@ const resetPims = const resetRemsAdmin = ({ cdsUrl }, consoleLog) => - _event => { + () => { let url = new URL(cdsUrl); const resetUrl = url.origin + '/etasu/reset'; @@ -96,6 +131,11 @@ const resetHeaderDefinitions = [ key: 'clearQuestionnaireResponses', reset: clearQuestionnaireResponses }, + { + display: 'Clear EHR MedicationDispenses', + key: 'clearMedicationDispenses', + reset: clearMedicationDispenses + }, { display: 'Reset PIMS Database', key: 'resetPims', @@ -211,7 +251,7 @@ export default class SettingsBox extends Component { })} {resetHeaderDefinitions.map(({ key, display, reset }) => { return ( - + diff --git a/src/util/fhir.js b/src/util/fhir.js index d0f5eb0b..6c2ae3c9 100644 --- a/src/util/fhir.js +++ b/src/util/fhir.js @@ -1,3 +1,4 @@ + function fhir(resource, ehrUrl, patient, auth) { const headers = { 'Content-Type': 'application/json' @@ -62,4 +63,20 @@ function getDrugCodeFromMedicationRequest(medicationRequest) { return codeableConcept?.coding?.[0]; } -export { fhir, getAge, getDrugCodeableConceptFromMedicationRequest, getDrugCodeFromMedicationRequest }; +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, createMedicationDispenseFromMedicationRequest }; From 5e31c9dbde6892f837cbbd92633031b736de07ae Mon Sep 17 00:00:00 2001 From: Joyce Quach <33106214+jtquach1@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:42:55 -0500 Subject: [PATCH 07/30] Read dispensed status from ehr (#100) * Prettier * Remove unused function * Rename variable * Remove unused prop/variable * Port over MedicationDispense components from mcode/rems-smart-on-fhir * Styling changes --- .../MedicationStatus/MedicationStatus.jsx | 88 ++++++ .../MedicationStatusButton.css | 16 + .../MedicationStatusButton.jsx | 65 ++++ .../MedicationStatusModal.css | 33 ++ .../MedicationStatusModal.jsx | 62 ++++ .../PatientSearchBar/PatientSearchBar.js | 172 +++++------ src/components/RequestBox/RequestBox.js | 98 +++--- src/components/SettingsBox/SettingsBox.js | 153 ++++----- src/containers/RequestBuilder.js | 258 +++++++++------- src/index.css | 291 +++++++----------- 10 files changed, 735 insertions(+), 501 deletions(-) create mode 100644 src/components/MedicationStatus/MedicationStatus.jsx create mode 100644 src/components/MedicationStatus/MedicationStatusButton.css create mode 100644 src/components/MedicationStatus/MedicationStatusButton.jsx create mode 100644 src/components/MedicationStatus/MedicationStatusModal.css create mode 100644 src/components/MedicationStatus/MedicationStatusModal.jsx diff --git a/src/components/MedicationStatus/MedicationStatus.jsx b/src/components/MedicationStatus/MedicationStatus.jsx new file mode 100644 index 00000000..745148de --- /dev/null +++ b/src/components/MedicationStatus/MedicationStatus.jsx @@ -0,0 +1,88 @@ +import axios from 'axios'; +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 } = props; + const [medicationDispense, setMedicationDispense] = useState(null); + const [showMedicationStatus, setShowMedicationStatus] = useState(false); + const [lastCheckedMedicationTime, setLastCheckedMedicationTime] = useState(null); + + useEffect(() => getMedicationStatus(), [request.id]); + + const getMedicationStatus = () => { + setLastCheckedMedicationTime(Date.now()); + + axios.get(`${ehrUrl}/MedicationDispense?prescription=${request.id}`).then( + response => { + const bundle = response.data; + setMedicationDispense(bundle.entry?.[0].resource); + }, + error => { + console.log('Was not able to get medication status', error); + } + ); + }; + + 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/PatientSearchBar/PatientSearchBar.js b/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js index 338b366f..881d1bc7 100644 --- a/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js +++ b/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js @@ -6,100 +6,96 @@ 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 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 getFilteredLength(searchstring, listOfPatients) { + const filteredListOfPatients = listOfPatients[0].filter(element => { + if (searchstring === '') { + return element; + } else { + return element.name.toLowerCase().includes(searchstring); + } + }); - 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} - 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 +

+ {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/RequestBox.js b/src/components/RequestBox/RequestBox.js index 482a284c..eed4ac43 100644 --- a/src/components/RequestBox/RequestBox.js +++ b/src/components/RequestBox/RequestBox.js @@ -15,7 +15,7 @@ export default class RequestBox extends Component { this.state = { gatherCount: 0, response: {}, - open: false + submittedRx: false }; this.renderRequestResources = this.renderRequestResources.bind(this); @@ -29,7 +29,7 @@ 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); } @@ -93,10 +93,6 @@ export default class RequestBox extends Component { } } - updateStateElement = (elementName, text) => { - this.setState({ [elementName]: text }); - }; - emptyField = (empty); renderPatientInfo() { @@ -231,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; } @@ -368,66 +364,56 @@ export default class RequestBox extends Component { return Object.keys(this.props.patient).length === 0; } - // SnackBar + // SnackBar handleRxResponse = () => this.setState({ open: true }); handleClose = () => this.setState({ open: 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} -
-
- {this.renderPatientInfo()} - {this.renderPrefetchedResources()} -
-
-
- - - - - -
+ <> +
+
+
+ Patient ID: {this.props.patient.id} +
+
+ {this.renderPatientInfo()} + {this.renderPrefetchedResources()} +
+
+
+ + + + +
- ) : ( - - )} +
- - Success! NewRx Received 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/SettingsBox/SettingsBox.js b/src/components/SettingsBox/SettingsBox.js index f12a3487..684188ec 100644 --- a/src/components/SettingsBox/SettingsBox.js +++ b/src/components/SettingsBox/SettingsBox.js @@ -6,12 +6,10 @@ import { headerDefinitions, types } from '../../util/data'; import FHIR from 'fhirclient'; import { Box, Button, FormControlLabel, Grid, TextField } from '@mui/material'; -const clearMedicationDispenses = +const clearMedicationDispenses = ({ ehrUrl, access_token }, consoleLog) => () => { - console.log( - 'Clear MedicationDispenses from the EHR: ' + ehrUrl - ); + console.log('Clear MedicationDispenses from the EHR: ' + ehrUrl); const client = FHIR.client({ serverUrl: ehrUrl, ...(access_token ? { tokenResponse: access_token } : {}) @@ -160,23 +158,23 @@ export default class SettingsBox extends Component { } componentDidMount() { - const headers = Object.keys(headerDefinitions).map(key => ([key, this.props.state[key]])); - this.setState({originalValues: headers}); + const headers = Object.keys(headerDefinitions).map(key => [key, this.props.state[key]]); + this.setState({ originalValues: headers }); } closeSettings() { this.props.updateCB('showSettings', false); } saveSettings() { - const headers = Object.keys(headerDefinitions).map(key => ([key, this.props.state[key]])); + const headers = Object.keys(headerDefinitions).map(key => [key, this.props.state[key]]); console.log(headers); localStorage.setItem('reqgenSettings', JSON.stringify(headers)); this.closeSettings(); } cancelSettings() { - this.state.originalValues.forEach((e) => { - try{ + this.state.originalValues.forEach(e => { + try { this.props.updateCB(e[0], e[1]); } catch { console.log('Failed to reset setting value'); @@ -199,80 +197,89 @@ export default class SettingsBox extends Component { ); return ( -
- -

Settings

- - {headers.map(({ key, type, display }) => { - - switch (type) { - case 'input': - return ( - -
- {updateCB(key, event.target.value);}} - sx = {{width:'100%'}} - /> -
-
- ); - case 'check': - if(firstCheckbox) { - firstCheckbox = false; - showBreak = true; - } else { - showBreak = false; - } - return ( - - {showBreak ? :''} - - {updateCB(key, event.target.checked);}}/> - } - label = {display} - /> + +

Settings

+ + {headers.map(({ key, type, display }) => { + switch (type) { + case 'input': + return ( + +
+ { + updateCB(key, event.target.value); + }} + sx={{ width: '100%' }} + /> +
-
- ); - default: - return ( -
-

{display}

-
- ); - } - })} - {resetHeaderDefinitions.map(({ key, display, reset }) => { - return ( - - - - ); - })} + ); + case 'check': + if (firstCheckbox) { + firstCheckbox = false; + showBreak = true; + } else { + showBreak = false; + } + return ( + + {showBreak ? : ''} + + { + updateCB(key, event.target.checked); + }} + /> + } + label={display} + /> + + + ); + default: + return ( +
+

{display}

+
+ ); + } + })} + {resetHeaderDefinitions.map(({ key, display, reset }) => { + return ( + + + + ); + })} {/* spacer */} -
+
- + - +
-
); } } diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index 00018b58..e4845d42 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { Button, Box, IconButton, Modal, DialogTitle } 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 DisplayBox from '../components/DisplayBox/DisplayBox'; @@ -16,8 +16,9 @@ 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'; export default class RequestBuilder extends Component { constructor(props) { @@ -37,7 +38,7 @@ export default class RequestBuilder extends Component { showSettings: false, token: null, client: this.props.client, - codeValues: defaultValues, + codeValues: defaultValues }; this.updateStateElement = this.updateStateElement.bind(this); @@ -50,11 +51,11 @@ export default class RequestBuilder extends Component { componentDidMount() { // init settings - Object.keys(headerDefinitions).map((key) => { + Object.keys(headerDefinitions).map(key => { this.setState({ [key]: headerDefinitions[key].default }); }); // load settings - JSON.parse(localStorage.getItem('reqgenSettings') || '[]').forEach((element) => { + JSON.parse(localStorage.getItem('reqgenSettings') || '[]').forEach(element => { try { this.updateStateElement(element[0], element[1]); } catch { @@ -104,7 +105,6 @@ export default class RequestBuilder extends Component { } }; - timeout = time => { let controller = new AbortController(); setTimeout(() => controller.abort(), time * 1000); @@ -201,7 +201,7 @@ export default class RequestBuilder extends Component { .request(this.state.patientFhirQuery, { flat: true }) .then(result => { this.setState({ - patientList: result, + patientList: result }); }) .catch(e => { @@ -234,34 +234,49 @@ export default class RequestBuilder extends Component { response: {} }); }; + handleChange = () => (event, isExpanded) => { this.setState({ expanded: isExpanded ? true : false }); }; + isOrderNotSelected() { + return Object.keys(this.state.request).length === 0; + } + render() { + const displayRequestBox = !!this.state.patient.id; + const disableGetMedicationStatus = this.isOrderNotSelected() || this.state.loading; + return ( -
-
- + - + { + this.setState({ showSettings: false }); }} > - Reconnect EHR - -
-
- { this.setState({ showSettings: false }); }} > -
+
-
-
- - } - aria-controls="panel1a-content" - id="panel1a-header" - style={{ marginLeft: '45%' }} - > - - - - {this.state.patientList.length > 0 && this.state.expanded ? -
+ + + + + } + 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 && ( + + + + )} + + + + -
-
- -
- -
-
+ + + ); } } + +const navigationBarButtonStyle = { + backgroundColor: 'white', + color: 'black', + borderColor: 'black', + display: 'flex', + marginX: 2, + marginY: 1, + '&:hover': { + color: 'white', + backgroundColor: 'black', + borderColor: 'black' + }, + '&:active': { + color: 'white', + backgroundColor: 'black', + borderColor: 'black' + } +}; diff --git a/src/index.css b/src/index.css index 410e3449..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; @@ -329,12 +273,11 @@ input:not(:focus):not([value=""]):valid ~ .floating-label{ border: 1px solid black; width: 75%; height: 75%; - background-color:white; + background-color: white; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); overflow-y: auto; - box-shadow: 10px 10px 20px black - -} \ No newline at end of file + box-shadow: 10px 10px 20px black; +} From 063808f20dd8ccf6c4ef3ab726bfbd8cdd16865a Mon Sep 17 00:00:00 2001 From: Ariel Virgulto <31778437+avirgulto@users.noreply.github.com> Date: Thu, 22 Feb 2024 22:43:57 -0500 Subject: [PATCH 08/30] Update new state variable to open the snackbar as expected (#103) Co-authored-by: Ariel Virgulto --- src/components/RequestBox/RequestBox.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/RequestBox/RequestBox.js b/src/components/RequestBox/RequestBox.js index eed4ac43..85dcd7e9 100644 --- a/src/components/RequestBox/RequestBox.js +++ b/src/components/RequestBox/RequestBox.js @@ -365,9 +365,9 @@ export default class RequestBox extends Component { } // SnackBar - handleRxResponse = () => this.setState({ open: true }); + handleRxResponse = () => this.setState({ submittedRx: true }); - handleClose = () => this.setState({ open: false }); + handleClose = () => this.setState({ submittedRx: false }); render() { const disableSendToCRD = this.isOrderNotSelected() || this.props.loading; From cefa780aa74d67b067897d189d4f8840f3047c14 Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Thu, 1 Feb 2024 17:10:37 -0500 Subject: [PATCH 09/30] fix unrelated warning --- src/components/SMARTBox/PatientBox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SMARTBox/PatientBox.js b/src/components/SMARTBox/PatientBox.js index 22f54f5b..13926cbb 100644 --- a/src/components/SMARTBox/PatientBox.js +++ b/src/components/SMARTBox/PatientBox.js @@ -465,7 +465,7 @@ export default class PatientBox extends Component { - + {columns.map((column) => ( Date: Thu, 1 Feb 2024 17:11:23 -0500 Subject: [PATCH 10/30] remove unnecessary key --- src/components/SMARTBox/PatientBox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SMARTBox/PatientBox.js b/src/components/SMARTBox/PatientBox.js index 13926cbb..22f54f5b 100644 --- a/src/components/SMARTBox/PatientBox.js +++ b/src/components/SMARTBox/PatientBox.js @@ -465,7 +465,7 @@ export default class PatientBox extends Component {
- + {columns.map((column) => ( Date: Thu, 15 Feb 2024 15:17:33 -0500 Subject: [PATCH 11/30] add task ui and update other ui --- src/components/DisplayBox/DisplayBox.js | 2 +- src/components/DisplayBox/card-list.css | 2 +- .../PatientSearchBar/PatientSearchBar.js | 11 +- .../PatientSearchBarStyle.css | 1 + src/components/RequestBox/RequestBox.js | 12 +- src/components/RequestBox/request.css | 5 - src/components/RequestDashboard/Home.jsx | 115 +++++++ .../RequestDashboard/PatientSection.jsx | 15 + .../RequestDashboard/SettingsSection.jsx | 215 +++++++++++++ src/components/RequestDashboard/TabPanel.jsx | 24 ++ .../RequestDashboard/TasksSection.jsx | 248 +++++++++++++++ src/components/RequestDashboard/styles.jsx | 151 ++++++++++ src/components/SettingsBox/SettingsBox.css | 46 --- src/components/SettingsBox/SettingsBox.js | 285 ------------------ .../ContextProvider/SettingsProvider.js | 17 ++ src/containers/ContextProvider/reducer.js | 31 ++ src/containers/Index.jsx | 7 +- src/containers/RequestBuilder.js | 74 +++-- 18 files changed, 885 insertions(+), 376 deletions(-) create mode 100644 src/components/RequestDashboard/Home.jsx create mode 100644 src/components/RequestDashboard/PatientSection.jsx create mode 100644 src/components/RequestDashboard/SettingsSection.jsx create mode 100644 src/components/RequestDashboard/TabPanel.jsx create mode 100644 src/components/RequestDashboard/TasksSection.jsx create mode 100644 src/components/RequestDashboard/styles.jsx delete mode 100644 src/components/SettingsBox/SettingsBox.css delete mode 100644 src/components/SettingsBox/SettingsBox.js create mode 100644 src/containers/ContextProvider/SettingsProvider.js create mode 100644 src/containers/ContextProvider/reducer.js diff --git a/src/components/DisplayBox/DisplayBox.js b/src/components/DisplayBox/DisplayBox.js index cd8c1374..4a018084 100644 --- a/src/components/DisplayBox/DisplayBox.js +++ b/src/components/DisplayBox/DisplayBox.js @@ -392,7 +392,7 @@ export default class DisplayBox extends Component { } return (
- Notification Cards ({renderedCards.length})
{renderedCards}
+
{renderedCards}
); } else { 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/RequestBox/PatientSearchBar/PatientSearchBar.js b/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js index 881d1bc7..8974331f 100644 --- a/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js +++ b/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js @@ -1,7 +1,9 @@ -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'; @@ -56,6 +58,13 @@ export default function PatientSearchBar(props) { Showing {getFilteredLength(input, listOfPatients)} of {props.searchablePatients.length}{' '} records

+ props.getPatients()} + size="large" + > + + {displayFilteredPatientList(input, listOfPatients[0])} 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 85dcd7e9..2c8577cf 100644 --- a/src/components/RequestBox/RequestBox.js +++ b/src/components/RequestBox/RequestBox.js @@ -1,4 +1,4 @@ -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'; @@ -382,8 +382,14 @@ export default class RequestBox extends Component { Patient ID: {this.props.patient.id}
- {this.renderPatientInfo()} - {this.renderPrefetchedResources()} + + + {this.renderPatientInfo()} + + + {this.renderPrefetchedResources()} + +
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..1b46899b --- /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..26c17810 --- /dev/null +++ b/src/components/RequestDashboard/PatientSection.jsx @@ -0,0 +1,15 @@ +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 so + // we can get rid of this hacky shim + return
; +}; + +export default memo(PatientSection); diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx new file mode 100644 index 00000000..7883dd20 --- /dev/null +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -0,0 +1,215 @@ +import React, { memo, useState, useEffect } from 'react'; +import { Button, Box, FormControlLabel, Grid, Checkbox, TextField } from '@mui/material'; + +import useStyles from './styles'; +import { headerDefinitions } from '../../util/data'; +import { stateActions } 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]); + } + } + }); + }, []); + const updateSetting = (key, value) => { + dispatch({ + type: stateActions.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 }) => + _event => { + 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 }) => + _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); + }) + .catch(error => { + console.log('Reset pims error: '); + console.log(error); + }); + }; + const resetRemsAdmin = + ({ cdsUrl }) => + _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); + }) + .catch(error => { + console.log('Reset rems admin error: '); + console.log(error); + }); + }; + const resetHeaderDefinitions = [ + { + display: 'Clear In-Progress Forms', + key: 'clearQuestionnaireResponses', + reset: clearQuestionnaireResponses + }, + { + display: 'Reset PIMS Database', + key: 'resetPims', + reset: resetPims + }, + { + display: 'Reset REMS-Admin Database', + key: 'resetRemsAdmin', + reset: resetRemsAdmin + } + ]; + + + 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 }) => { + 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..413badf7 --- /dev/null +++ b/src/components/RequestDashboard/TabPanel.jsx @@ -0,0 +1,24 @@ +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); + \ No newline at end of file diff --git a/src/components/RequestDashboard/TasksSection.jsx b/src/components/RequestDashboard/TasksSection.jsx new file mode 100644 index 00000000..d8b4a4e5 --- /dev/null +++ b/src/components/RequestDashboard/TasksSection.jsx @@ -0,0 +1,248 @@ +import React, { memo, useState, useEffect, Fragment } from 'react'; +import { Button, Box, Modal, Grid, Tabs, Tab, Stack } 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'; + +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 handleChange = (event, newValue) => { + setValue(newValue); + }; + const handleClose = () => { + setOpen(false); + }; + + const tryDelete = (task) => { + setTaskToDelete(task); + setOpen(true); + }; + const assignTask = (task) => { + // TODO: should allow assigning to anybody, not just self + 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 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; + } + } + 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*/} + + + + + + + + + + + ); + }; + + 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..02c033b1 --- /dev/null +++ b/src/components/RequestDashboard/styles.jsx @@ -0,0 +1,151 @@ +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' + }, + mainButton: { + '&.MuiButtonBase-root': { + // transition: 'all 250ms ease-in 0ms', + } + }, + 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', + // margin: '0 15px 0 15px', // must be same as tabDivView + 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': { + // specificty + 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': { + // 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: 'RequestDashboard', index: 1 } +); diff --git a/src/components/SettingsBox/SettingsBox.css b/src/components/SettingsBox/SettingsBox.css deleted file mode 100644 index 9f473389..00000000 --- a/src/components/SettingsBox/SettingsBox.css +++ /dev/null @@ -1,46 +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-weight:bold; - margin:0; - margin-bottom: 10px; - padding:15px; - color:white; - background-color:#005B94; -} - -.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 684188ec..00000000 --- a/src/components/SettingsBox/SettingsBox.js +++ /dev/null @@ -1,285 +0,0 @@ -import React, { Component } from 'react'; -import './SettingsBox.css'; -import Checkbox from '@mui/material/Checkbox'; - -import { headerDefinitions, types } from '../../util/data'; -import FHIR from 'fhirclient'; -import { Box, Button, FormControlLabel, Grid, TextField } from '@mui/material'; - -const clearMedicationDispenses = - ({ ehrUrl, access_token }, consoleLog) => - () => { - 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 => { - consoleLog( - 'Successfully deleted MedicationDispense ' + resource.id + ' from EHR', - types.info - ); - 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 clearQuestionnaireResponses = - ({ ehrUrl, defaultUser, access_token }, consoleLog) => - () => { - 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) => - () => { - 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) => - () => { - 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 In-Progress Forms', - key: 'clearQuestionnaireResponses', - reset: clearQuestionnaireResponses - }, - { - display: 'Clear EHR MedicationDispenses', - key: 'clearMedicationDispenses', - reset: clearMedicationDispenses - }, - { - 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); - this.state = { - originalValues: [] - }; - this.cancelSettings = this.cancelSettings.bind(this); - this.closeSettings = this.closeSettings.bind(this); - this.saveSettings = this.saveSettings.bind(this); - } - - componentDidMount() { - const headers = Object.keys(headerDefinitions).map(key => [key, this.props.state[key]]); - this.setState({ originalValues: headers }); - } - - closeSettings() { - this.props.updateCB('showSettings', false); - } - saveSettings() { - const headers = Object.keys(headerDefinitions).map(key => [key, this.props.state[key]]); - console.log(headers); - localStorage.setItem('reqgenSettings', JSON.stringify(headers)); - this.closeSettings(); - } - - cancelSettings() { - this.state.originalValues.forEach(e => { - try { - this.props.updateCB(e[0], e[1]); - } catch { - console.log('Failed to reset setting value'); - } - }); - this.closeSettings(); - } - - render() { - const { state, consoleLog, updateCB } = this.props; - let firstCheckbox = true; - let showBreak = true; - - 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 ( - -

Settings

- - {headers.map(({ key, type, display }) => { - switch (type) { - case 'input': - return ( - -
- { - updateCB(key, event.target.value); - }} - sx={{ width: '100%' }} - /> -
-
- ); - case 'check': - if (firstCheckbox) { - firstCheckbox = false; - showBreak = true; - } else { - showBreak = false; - } - return ( - - {showBreak ? : ''} - - { - updateCB(key, event.target.checked); - }} - /> - } - label={display} - /> - - - ); - default: - return ( -
-

{display}

-
- ); - } - })} - {resetHeaderDefinitions.map(({ key, display, reset }) => { - return ( - - - - ); - })} - {/* spacer */} -
- - - - - - - - - -
- ); - } -} diff --git a/src/containers/ContextProvider/SettingsProvider.js b/src/containers/ContextProvider/SettingsProvider.js new file mode 100644 index 00000000..56eaeb58 --- /dev/null +++ b/src/containers/ContextProvider/SettingsProvider.js @@ -0,0 +1,17 @@ +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} + + ); +}; \ No newline at end of file diff --git a/src/containers/ContextProvider/reducer.js b/src/containers/ContextProvider/reducer.js new file mode 100644 index 00000000..77f3e014 --- /dev/null +++ b/src/containers/ContextProvider/reducer.js @@ -0,0 +1,31 @@ +import { headerDefinitions } from '../../util/data'; +export const stateActions = Object.freeze({ + updatePatient: 'update_patient', + updateSetting: 'update_setting', // {type, settingId, value} +}); +// todo: add an enum that defines possible settings +export const reducer = (state, action) => { + switch (action.type) { + case stateActions.updateSetting: + return { + ...state, + [action.settingId]: action.value + }; + case stateActions.updatePatient: + return { + ...state, + 'patient': action.value + }; + default: + return state; + } + }; + +const initialState = { + patient: null +}; +Object.keys(headerDefinitions).forEach((e) => { + initialState[e] = headerDefinitions[e].default; // fill default settings values +}); + +export { initialState }; \ No newline at end of file diff --git a/src/containers/Index.jsx b/src/containers/Index.jsx index 3b74544b..f69d8245 100644 --- a/src/containers/Index.jsx +++ b/src/containers/Index.jsx @@ -1,7 +1,8 @@ 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'; +import { SettingsProvider } from './ContextProvider/SettingsProvider'; const Index = props => { const [client, setClient] = useState(null); @@ -15,7 +16,9 @@ const Index = props => { return (
{client ? ( - + + + ) : (

Getting Client...

diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index e4845d42..b392b26a 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -2,13 +2,13 @@ import React, { Component } from 'react'; 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, headerDefinitions } from '../util/data.js'; -import { createJwt, setupKeys } from '../util/auth'; +import { createJwt } from '../util/auth'; import env from 'env-var'; import FHIR from 'fhirclient'; @@ -19,6 +19,7 @@ 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 { stateActions } from './ContextProvider/reducer.js'; export default class RequestBuilder extends Component { constructor(props) { @@ -27,7 +28,7 @@ export default class RequestBuilder extends Component { loading: false, logs: [], patient: {}, - expanded: false, + expanded: true, patientList: [], response: {}, code: null, @@ -78,7 +79,7 @@ export default class RequestBuilder extends Component { reconnectEhr() { FHIR.oauth2.authorize({ clientId: env.get('REACT_APP_CLIENT').asString(), - iss: this.state.baseUrl, + iss: this.props.globalState.baseUrl, redirectUri: this.props.redirect, scope: env.get('REACT_APP_CLIENT_SCOPES').asString() }); @@ -96,7 +97,14 @@ export default class RequestBuilder extends Component { } updateStateElement = (elementName, text) => { - this.setState({ [elementName]: text }); + if(elementName === 'patient') { + this.props.dispatch({ + type: stateActions.updatePatient, + value: text + }); + } else { + this.setState({ [elementName]: text }); + } // if the patientFhirQuery is updated, make a call to get the patients if (elementName === 'patientFhirQuery') { setTimeout(() => { @@ -116,39 +124,39 @@ export default class RequestBuilder extends Component { 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; } @@ -197,8 +205,9 @@ export default class RequestBuilder extends Component { } getPatients = () => { - this.props.client - .request(this.state.patientFhirQuery, { flat: true }) + if(this.props.globalState.patientFhirQuery) { + this.props.client + .request(this.props.globalState.patientFhirQuery, { flat: true }) .then(result => { this.setState({ patientList: result @@ -209,6 +218,7 @@ export default class RequestBuilder extends Component { patientList: e }); }); + } }; updateStateList = (elementName, text) => { @@ -309,15 +319,15 @@ export default class RequestBuilder extends Component { searchablePatients={this.state.patientList} client={this.props.client} request={this.state.request} - launchUrl={this.state.launchUrl} + launchUrl={this.props.globalState.launchUrl} callback={this.updateStateElement} callbackList={this.updateStateList} callbackMap={this.updateStateMap} // updatePrefetchCallback={PrefetchTemplate.generateQueries} clearCallback={this.clearState} options={this.state.codeValues} - responseExpirationDays={this.state.responseExpirationDays} - defaultUser={this.state.defaultUser} + responseExpirationDays={this.props.globalState.responseExpirationDays} + defaultUser={this.props.globalState.defaultUser} /> )} @@ -335,29 +345,29 @@ export default class RequestBuilder extends Component { {displayRequestBox && ( )} @@ -372,7 +382,7 @@ export default class RequestBuilder extends Component { From 37561aae298858d8170736348e03d3d2ddf88857 Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Thu, 15 Feb 2024 15:35:29 -0500 Subject: [PATCH 12/30] prettier --- src/components/DisplayBox/DisplayBox.js | 1 - src/components/RequestDashboard/Home.jsx | 186 +++---- .../RequestDashboard/PatientSection.jsx | 8 +- .../RequestDashboard/SettingsSection.jsx | 245 ++++----- src/components/RequestDashboard/TabPanel.jsx | 39 +- .../RequestDashboard/TasksSection.jsx | 473 ++++++++++-------- src/components/RequestDashboard/styles.jsx | 92 ++-- .../ContextProvider/SettingsProvider.js | 14 +- src/containers/ContextProvider/reducer.js | 42 +- 9 files changed, 568 insertions(+), 532 deletions(-) diff --git a/src/components/DisplayBox/DisplayBox.js b/src/components/DisplayBox/DisplayBox.js index 4a018084..7a510481 100644 --- a/src/components/DisplayBox/DisplayBox.js +++ b/src/components/DisplayBox/DisplayBox.js @@ -399,5 +399,4 @@ export default class DisplayBox extends Component { return
; } } - } diff --git a/src/components/RequestDashboard/Home.jsx b/src/components/RequestDashboard/Home.jsx index 1b46899b..43206752 100644 --- a/src/components/RequestDashboard/Home.jsx +++ b/src/components/RequestDashboard/Home.jsx @@ -11,105 +11,105 @@ 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 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); - }; + 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; + // 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 */} +
+ ); + }; - return (
-
-
-
-
-
-
+ // render content of each view, makes other content invisible so it doesn't rerender every time + const renderSectionView = () => { + let renderSection =
Loading...
; -
); - } else { - return ''; - } - }; + if (section) { + let patientRenderClass = section === patientButton ? '' : classes.disappear; + let taskRenderClass = section === taskButton ? '' : classes.disappear; + let settingsRenderClass = section === settingsButton ? '' : classes.disappear; - return ( -
- {renderMainView()} - {renderSectionView()} + return ( +
+
+ +
+
+ +
+
+ +
- ); + ); + } else { + return ''; + } + }; + + return ( +
+ {renderMainView()} + {renderSectionView()} +
+ ); }; export default memo(Home); diff --git a/src/components/RequestDashboard/PatientSection.jsx b/src/components/RequestDashboard/PatientSection.jsx index 26c17810..5443462b 100644 --- a/src/components/RequestDashboard/PatientSection.jsx +++ b/src/components/RequestDashboard/PatientSection.jsx @@ -6,10 +6,14 @@ import { SettingsContext } from '../../containers/ContextProvider/SettingsProvid const PatientSection = props => { const classes = useStyles(); - const [ state, dispatch ] = React.useContext(SettingsContext); + const [state, dispatch] = React.useContext(SettingsContext); // TODO: Make request builder use react-hooks so // we can get rid of this hacky shim - return
; + return ( +
+ +
+ ); }; export default memo(PatientSection); diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index 7883dd20..f175ebaa 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -8,112 +8,113 @@ import { SettingsContext } from '../../containers/ContextProvider/SettingsProvid const SettingsSection = props => { const classes = useStyles(); - const [ state, dispatch ] = React.useContext(SettingsContext); + const [state, dispatch] = React.useContext(SettingsContext); const [headers, setHeaders] = useState([]); const [originalValues, setOriginalValues] = useState([]); - - useEffect(() => { + 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) - ); + .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]])); + 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]); - } + 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]); } + } }); }, []); const updateSetting = (key, value) => { dispatch({ - type: stateActions.updateSetting, - settingId: key, - value: value + type: stateActions.updateSetting, + settingId: key, + value: value }); }; const saveSettings = () => { - const headers = Object.keys(headerDefinitions).map(key => ([key, state[key]])); + const headers = Object.keys(headerDefinitions).map(key => [key, state[key]]); localStorage.setItem('reqgenSettings', JSON.stringify(headers)); }; const resetSettings = () => { - originalValues.forEach((e) => { - try{ + originalValues.forEach(e => { + try { updateSetting(e[0], e[1]); } catch { console.log('Failed to reset setting value'); } }); }; - const clearQuestionnaireResponses = ({ defaultUser }) => - _event => { - 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); - }); + const clearQuestionnaireResponses = + ({ defaultUser }) => + _event => { + 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); }); - }) - .catch(e => { - console.log('Failed to retrieve list of QuestionnaireResponses'); - console.log(e); - }); - }; + }; - const resetPims = ({ pimsUrl }) => - _event => { - let url = new URL(pimsUrl); - const resetUrl = url.origin + '/doctorOrders/api/deleteAll'; - console.log('reset pims: ' + resetUrl); + const resetPims = + ({ pimsUrl }) => + _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); + fetch(resetUrl, { + method: 'DELETE' }) - .catch(error => { - console.log('Reset pims error: '); - console.log(error); - }); - }; + .then(response => { + console.log('Reset pims: '); + console.log(response); + }) + .catch(error => { + console.log('Reset pims error: '); + console.log(error); + }); + }; const resetRemsAdmin = - ({ cdsUrl }) => - _event => { - let url = new URL(cdsUrl); - const resetUrl = url.origin + '/etasu/reset'; + ({ cdsUrl }) => + _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); + fetch(resetUrl, { + method: 'POST' }) - .catch(error => { - console.log('Reset rems admin error: '); - console.log(error); - }); - }; + .then(response => { + console.log('Reset rems admin etasu: '); + console.log(response); + }) + .catch(error => { + console.log('Reset rems admin error: '); + console.log(error); + }); + }; const resetHeaderDefinitions = [ { display: 'Clear In-Progress Forms', @@ -131,33 +132,33 @@ const SettingsSection = props => { reset: resetRemsAdmin } ]; - 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%'}} - /> -
+ +
+ { + updateSetting(key, event.target.value); + }} + sx={{ width: '100%' }} + /> +
); case 'check': - if(firstCheckbox) { + if (firstCheckbox) { firstCheckbox = false; showBreak = true; } else { @@ -165,14 +166,18 @@ const SettingsSection = props => { } return ( - {showBreak ? :''} + {showBreak ? : ''} - {updateSetting(key, event.target.checked);}}/> + { + updateSetting(key, event.target.checked); + }} + /> } - label = {display} + label={display} /> @@ -185,30 +190,36 @@ const SettingsSection = props => { ); } })} - {resetHeaderDefinitions.map(({ key, display, reset }) => { + {resetHeaderDefinitions.map(({ key, display, reset }) => { return ( - - - +
); - })} - {/* spacer */} -
- - - - - - - - + })} + {/* spacer */} +
+ + + + + + + -
-
+
+
+
); }; diff --git a/src/components/RequestDashboard/TabPanel.jsx b/src/components/RequestDashboard/TabPanel.jsx index 413badf7..5857a316 100644 --- a/src/components/RequestDashboard/TabPanel.jsx +++ b/src/components/RequestDashboard/TabPanel.jsx @@ -2,23 +2,22 @@ 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); - \ No newline at end of file + 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 index d8b4a4e5..431a09ec 100644 --- a/src/components/RequestDashboard/TasksSection.jsx +++ b/src/components/RequestDashboard/TasksSection.jsx @@ -15,234 +15,263 @@ import { MemoizedTabPanel } from './TabPanel'; import { Info, Refresh } from '@mui/icons-material'; 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 handleChange = (event, newValue) => { - setValue(newValue); - }; - const handleClose = () => { - setOpen(false); - }; - - const tryDelete = (task) => { - setTaskToDelete(task); - setOpen(true); - }; - const assignTask = (task) => { - // TODO: should allow assigning to anybody, not just self - 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 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(); - }, []); + 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 handleChange = (event, newValue) => { + setValue(newValue); + }; + const handleClose = () => { + setOpen(false); + }; - useEffect(() => { + const tryDelete = task => { + setTaskToDelete(task); + setOpen(true); + }; + const assignTask = task => { + // TODO: should allow assigning to anybody, not just self + 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(); - }, [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'; - } + }); + } + }; + 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(); + }, []); - 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; - } - } - 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*/} - - - - - - - - - - - ); - }; + 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'; + } - const unassignedTasks = tasks.filter((t) => !t.owner); - const assignedTasks = tasks.filter((t) => t.owner?.id === state.defaultUser); // should check current user, not default + 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; + } + } + let ownerText = ( + + + Unassigned + + ); + if (task.owner) { + ownerText = ( + + + {`Assigned to ${taskOwnerName}`} + + ); + } 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)} - -
- + + + + + {`Task ID: ${task.id}`} + + + {`Timestamp: ${task.authoredOn}`} + + + {`Beneficiary: ${task.for ? task.for.id : 'None'}`} + + + {`STATUS: ${task.status.toUpperCase()}`} + + + {task.description} + + + + {taskForName} + + + + {ownerText} + + + + + + + + + {/*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 index 02c033b1..a4bc9998 100644 --- a/src/components/RequestDashboard/styles.jsx +++ b/src/components/RequestDashboard/styles.jsx @@ -2,7 +2,7 @@ import { makeStyles } from '@mui/styles'; export default makeStyles( theme => ({ disappear: { - display: 'none' + display: 'none' }, spacer: { height: '50px', // must be same as buttons @@ -45,16 +45,16 @@ export default makeStyles( height: 'auto', // margin: '0 15px 0 15px', // must be same as tabDivView borderLeft: '1px solid black', - borderRight: '1px solid black', + borderRight: '1px solid black' }, noTasks: { - backgroundColor: '#e4e4e4', - padding: '10px', - fontSize: '18px' + backgroundColor: '#e4e4e4', + padding: '10px', + fontSize: '18px' }, // DO NOT ALPHABETIZE // if you must alphabetize this file to have classes - // sorted, rename tabButtonView and selectedTabView such + // sorted, rename tabButtonView and selectedTabView such // that tabButtonView occurs earlier in the list. // Otherwise, the styles will override incorrectly. tabButtonView: { @@ -65,8 +65,7 @@ export default makeStyles( fontSize: '1.5rem', border: '1px solid black', boxShadow: 'none', - borderRadius: '0', - + borderRadius: '0' }, '& > *': { // generic child selector @@ -77,32 +76,32 @@ export default makeStyles( } }, selectedTabView: { - '&.MuiButtonBase-root': { - color: 'black', - borderBottom: 'none', + '&.MuiButtonBase-root': { + color: 'black', + borderBottom: 'none', + backgroundColor: '#F5F5F7', + '&:hover': { backgroundColor: '#F5F5F7', - '&:hover': { - backgroundColor: '#F5F5F7', - boxShadow: 'none' - }, + boxShadow: 'none' } + } }, taskDeleteHeader: { - padding: '15px 0 15px 0', + padding: '15px 0 15px 0' }, taskDeleteModal: { - border: '1px solid black', - width: '400px', + 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' + 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': { @@ -113,39 +112,38 @@ export default makeStyles( } }, taskHeaderTabs: { - margin: '15px 15px 5px 15px', - backgroundColor: '#F5F5F7', + margin: '15px 15px 5px 15px', + backgroundColor: '#F5F5F7' }, taskRefreshButton: { - padding: '35px 0 0 0' + padding: '35px 0 0 0' }, taskTabButton: { - padding: '10px 0px 5px 0px' + 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)', - } + 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', + fontSize: '8px', + color: '#777', + borderBottom: '1px solid #e3e3ef' }, taskTabDescription: { - fontSize: '18px', - padding: '8px 0px 10px 2px', + fontSize: '18px', + padding: '8px 0px 10px 2px' }, taskTabOwner: { - color: '#777' - }, + color: '#777' + } }), - { name: 'RequestDashboard', index: 1 } ); diff --git a/src/containers/ContextProvider/SettingsProvider.js b/src/containers/ContextProvider/SettingsProvider.js index 56eaeb58..4350a5ff 100644 --- a/src/containers/ContextProvider/SettingsProvider.js +++ b/src/containers/ContextProvider/SettingsProvider.js @@ -2,16 +2,12 @@ import React from 'react'; import { reducer, initialState } from './reducer'; export const SettingsContext = React.createContext({ - state: initialState, - dispatch: () => null + state: initialState, + dispatch: () => null }); export const SettingsProvider = ({ children }) => { - const [state, dispatch] = React.useReducer(reducer, initialState); + const [state, dispatch] = React.useReducer(reducer, initialState); - return ( - - {children} - - ); -}; \ No newline at end of file + return {children}; +}; diff --git a/src/containers/ContextProvider/reducer.js b/src/containers/ContextProvider/reducer.js index 77f3e014..7fc15a16 100644 --- a/src/containers/ContextProvider/reducer.js +++ b/src/containers/ContextProvider/reducer.js @@ -1,31 +1,31 @@ import { headerDefinitions } from '../../util/data'; export const stateActions = Object.freeze({ - updatePatient: 'update_patient', - updateSetting: 'update_setting', // {type, settingId, value} + updatePatient: 'update_patient', + updateSetting: 'update_setting' // {type, settingId, value} }); // todo: add an enum that defines possible settings export const reducer = (state, action) => { - switch (action.type) { - case stateActions.updateSetting: - return { - ...state, - [action.settingId]: action.value - }; - case stateActions.updatePatient: - return { - ...state, - 'patient': action.value - }; - default: - return state; - } - }; + switch (action.type) { + case stateActions.updateSetting: + return { + ...state, + [action.settingId]: action.value + }; + case stateActions.updatePatient: + return { + ...state, + patient: action.value + }; + default: + return state; + } +}; const initialState = { - patient: null + patient: null }; -Object.keys(headerDefinitions).forEach((e) => { - initialState[e] = headerDefinitions[e].default; // fill default settings values +Object.keys(headerDefinitions).forEach(e => { + initialState[e] = headerDefinitions[e].default; // fill default settings values }); -export { initialState }; \ No newline at end of file +export { initialState }; From 4233b75ecc0d46ccca374f39815dd6755bfc0206 Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Tue, 20 Feb 2024 02:43:08 -0500 Subject: [PATCH 13/30] minor merge fixes --- src/containers/RequestBuilder.js | 42 +++----------------------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index b392b26a..fee487c6 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -254,47 +254,11 @@ export default class RequestBuilder extends Component { } render() { - const displayRequestBox = !!this.state.patient.id; + const displayRequestBox = !!this.props.globalState.patient?.id; const disableGetMedicationStatus = this.isOrderNotSelected() || this.state.loading; return ( <> - - - - { - this.setState({ showSettings: false }); - }} - > -
- -
-
-
@@ -373,7 +337,7 @@ export default class RequestBuilder extends Component { )} {!disableGetMedicationStatus && ( - + )} @@ -382,7 +346,7 @@ export default class RequestBuilder extends Component { From 5d341a2c62b74b09af277f1b22d6cd683bdd4cb9 Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Tue, 20 Feb 2024 03:08:49 -0500 Subject: [PATCH 14/30] fix settings related errors --- src/components/RequestDashboard/PatientSection.jsx | 4 +++- src/components/RequestDashboard/SettingsSection.jsx | 4 ++++ src/containers/ContextProvider/reducer.js | 12 ++++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/RequestDashboard/PatientSection.jsx b/src/components/RequestDashboard/PatientSection.jsx index 5443462b..97ef1fa3 100644 --- a/src/components/RequestDashboard/PatientSection.jsx +++ b/src/components/RequestDashboard/PatientSection.jsx @@ -11,7 +11,9 @@ const PatientSection = props => { // we can get rid of this hacky shim return (
- + {state.startup ? + : + <>Loading...}
); }; diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index f175ebaa..b599c597 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -31,6 +31,10 @@ const SettingsSection = props => { console.log('Could not load setting:' + element[0]); } } + // indicate to the rest of the app that the settings have been loaded + dispatch({ + type: stateActions.flagStartup + }); }); }, []); const updateSetting = (key, value) => { diff --git a/src/containers/ContextProvider/reducer.js b/src/containers/ContextProvider/reducer.js index 7fc15a16..1cde8e68 100644 --- a/src/containers/ContextProvider/reducer.js +++ b/src/containers/ContextProvider/reducer.js @@ -1,7 +1,9 @@ import { headerDefinitions } from '../../util/data'; export const stateActions = Object.freeze({ updatePatient: 'update_patient', - updateSetting: 'update_setting' // {type, settingId, value} + updateSetting: 'update_setting', // {type, settingId, value} + flagStartup: 'flag_startup' + }); // todo: add an enum that defines possible settings export const reducer = (state, action) => { @@ -16,13 +18,19 @@ export const reducer = (state, action) => { ...state, patient: action.value }; + case stateActions.flagStartup: + return { + ...state, + startup: true + }; default: return state; } }; const initialState = { - patient: null + patient: null, + startup: false }; Object.keys(headerDefinitions).forEach(e => { initialState[e] = headerDefinitions[e].default; // fill default settings values From 4afc32e27ce92f2efc4c5dbd1142a70b965f3bfb Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Tue, 20 Feb 2024 14:03:18 -0500 Subject: [PATCH 15/30] restore button in settings --- .../RequestDashboard/SettingsSection.jsx | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index b599c597..c4f9bb4b 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -2,7 +2,9 @@ import React, { memo, useState, useEffect } from 'react'; import { Button, Box, FormControlLabel, Grid, Checkbox, TextField } from '@mui/material'; import useStyles from './styles'; -import { headerDefinitions } from '../../util/data'; +import FHIR from 'fhirclient'; + +import { headerDefinitions, types } from '../../util/data'; import { stateActions } from '../../containers/ContextProvider/reducer'; import { SettingsContext } from '../../containers/ContextProvider/SettingsProvider'; @@ -60,7 +62,7 @@ const SettingsSection = props => { }; const clearQuestionnaireResponses = ({ defaultUser }) => - _event => { + () => { props.client .request('QuestionnaireResponse?author=' + defaultUser, { flat: true }) .then(result => { @@ -84,7 +86,7 @@ const SettingsSection = props => { const resetPims = ({ pimsUrl }) => - _event => { + () => { let url = new URL(pimsUrl); const resetUrl = url.origin + '/doctorOrders/api/deleteAll'; console.log('reset pims: ' + resetUrl); @@ -103,7 +105,7 @@ const SettingsSection = props => { }; const resetRemsAdmin = ({ cdsUrl }) => - _event => { + () => { let url = new URL(cdsUrl); const resetUrl = url.origin + '/etasu/reset'; @@ -119,6 +121,40 @@ const SettingsSection = props => { console.log(error); }); }; + const clearMedicationDispenses = + ({ ehrUrl, access_token }, consoleLog) => + () => { + 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 => { + consoleLog( + 'Successfully deleted MedicationDispense ' + resource.id + ' from EHR', + types.info + ); + 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 resetHeaderDefinitions = [ { display: 'Clear In-Progress Forms', @@ -134,6 +170,11 @@ const SettingsSection = props => { display: 'Reset REMS-Admin Database', key: 'resetRemsAdmin', reset: resetRemsAdmin + }, + { + display: 'Clear EHR MedicationDispenses', + key: 'clearMedicationDispenses', + reset: clearMedicationDispenses } ]; @@ -196,7 +237,7 @@ const SettingsSection = props => { })} {resetHeaderDefinitions.map(({ key, display, reset }) => { return ( - + From b8a33a2f1889afe398f62ebb9ba598c050b6166e Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Tue, 20 Feb 2024 14:03:32 -0500 Subject: [PATCH 16/30] prettier --- .../InProgressFormBox/InProgressFormBox.js | 56 ++--- .../PatientSearchBar/PatientSearchBar.js | 10 +- src/components/RequestBox/RequestBox.js | 12 +- .../RequestDashboard/PatientSection.jsx | 8 +- src/components/SMARTBox/PatientBox.js | 196 +++++++++++------- src/containers/ContextProvider/reducer.js | 9 +- src/containers/RequestBuilder.js | 29 +-- src/util/buildScript.2022071.js | 3 +- src/util/data.js | 6 +- src/util/fhir.js | 29 ++- 10 files changed, 209 insertions(+), 149 deletions(-) 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 8974331f..34aa1e43 100644 --- a/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js +++ b/src/components/RequestBox/PatientSearchBar/PatientSearchBar.js @@ -58,13 +58,9 @@ export default function PatientSearchBar(props) { Showing {getFilteredLength(input, listOfPatients)} of {props.searchablePatients.length}{' '} records

- props.getPatients()} - size="large" - > - - + props.getPatients()} size="large"> + + {displayFilteredPatientList(input, listOfPatients[0])} diff --git a/src/components/RequestBox/RequestBox.js b/src/components/RequestBox/RequestBox.js index 2c8577cf..36ecab22 100644 --- a/src/components/RequestBox/RequestBox.js +++ b/src/components/RequestBox/RequestBox.js @@ -383,13 +383,13 @@ export default class RequestBox extends Component {
- - {this.renderPatientInfo()} - - - {this.renderPrefetchedResources()} - + + {this.renderPatientInfo()} + + {this.renderPrefetchedResources()} + +
diff --git a/src/components/RequestDashboard/PatientSection.jsx b/src/components/RequestDashboard/PatientSection.jsx index 97ef1fa3..2ba78ee7 100644 --- a/src/components/RequestDashboard/PatientSection.jsx +++ b/src/components/RequestDashboard/PatientSection.jsx @@ -11,9 +11,11 @@ const PatientSection = props => { // we can get rid of this hacky shim return (
- {state.startup ? - : - <>Loading...} + {state.startup ? ( + + ) : ( + <>Loading... + )}
); }; diff --git a/src/components/SMARTBox/PatientBox.js b/src/components/SMARTBox/PatientBox.js index 22f54f5b..e7b189dd 100644 --- a/src/components/SMARTBox/PatientBox.js +++ b/src/components/SMARTBox/PatientBox.js @@ -29,7 +29,7 @@ export default class PatientBox extends Component { questionnaireTitles: {}, showMedications: false, showQuestionnaires: false, - numInProgressForms: 0, + numInProgressForms: 0 }; this.handleRequestChange = this.handleRequestChange.bind(this); @@ -49,7 +49,7 @@ export default class PatientBox extends Component { 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 recieve its state from parent } getCoding(resource) { @@ -86,7 +86,6 @@ export default class PatientBox extends Component { return code; } - makeOption(request, options) { const code = this.getCoding(request); @@ -232,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) { @@ -265,7 +263,6 @@ export default class PatientBox extends Component { } }); - this.setState({ medicationRequests: result }); }); } @@ -400,9 +397,9 @@ export default class PatientBox extends Component { /** * Launch In progress From - */ + */ - relaunch = (data) => { + relaunch = data => { this.handleResponseChange(data); this.props.callback('expanded', false); this.buildLaunchLink(data).then(link => { @@ -455,34 +452,38 @@ export default class PatientBox extends Component { let linkCopy = Object.assign({}, link); - const result = await retrieveLaunchContext(linkCopy, this.props.patient.id, this.props.client.state); + 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) => ( - + {columns.map(column => ( + {column.label} ))} - {options.map((row) => ( - + {options.map(row => ( + this.handleRequestChange(row.value, patient)} > @@ -500,29 +501,29 @@ export default class PatientBox extends Component { makeQuestionnaireTable(columns, options, type, patient) { return ( - +
- + - {columns.map((column) => ( - + {columns.map(column => ( + {column.label} ))} - {options.map((row) => ( - + {options.map(row => ( + this.relaunch(row.value)} - className='hover-row' + className="hover-row" > {row.text} @@ -580,17 +581,19 @@ export default class PatientBox extends Component { } const medicationColumns = [ - { id: 'name', label: 'Medication'}, - { id: 'code', label: 'Request #'}, + { id: 'name', label: 'Medication' }, + { id: 'code', label: 'Request #' } ]; const questionnaireColumns = [ - { id: 'name', label: 'Title'}, - { id: 'time', label: 'Created'} + { 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'; + 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 (
@@ -604,54 +607,97 @@ export default class PatientBox extends Component { Full Name: {fullName}
- Gender: {patient.gender.charAt(0).toUpperCase() + patient.gender.slice(1)} + Gender:{' '} + {patient.gender.charAt(0).toUpperCase() + patient.gender.slice(1)}
- DoB/Age: {formatBirthdate} ({getAge(patient.birthDate)} years old) + DoB/Age: {formatBirthdate} ( + {getAge(patient.birthDate)} years old)
- { this.state.showMedications ? - - : - - - - } - { this.state.showQuestionnaires ? - - : - - - - - - } - + {this.state.showMedications ? ( + + ) : ( + + + + + + )} + {this.state.showQuestionnaires ? ( + + ) : ( + + + + + + )} +
- { this.state.showMedications ? -
- { this.makeResponseTable(medicationColumns, options, 'medication', patient) } + {this.state.showMedications ? ( +
+ {this.makeResponseTable(medicationColumns, options, 'medication', patient)}
- : } - { this.state.showQuestionnaires ? -
- { this.makeQuestionnaireTable(questionnaireColumns, responseOptions, 'questionnaire', patient) } + ) : ( + + )} + {this.state.showQuestionnaires ? ( +
+ {this.makeQuestionnaireTable( + questionnaireColumns, + responseOptions, + 'questionnaire', + patient + )}
- : } + ) : ( + + )}
); } diff --git a/src/containers/ContextProvider/reducer.js b/src/containers/ContextProvider/reducer.js index 1cde8e68..38f0ded1 100644 --- a/src/containers/ContextProvider/reducer.js +++ b/src/containers/ContextProvider/reducer.js @@ -3,7 +3,6 @@ export const stateActions = Object.freeze({ updatePatient: 'update_patient', updateSetting: 'update_setting', // {type, settingId, value} flagStartup: 'flag_startup' - }); // todo: add an enum that defines possible settings export const reducer = (state, action) => { @@ -19,10 +18,10 @@ export const reducer = (state, action) => { patient: action.value }; case stateActions.flagStartup: - return { - ...state, - startup: true - }; + return { + ...state, + startup: true + }; default: return state; } diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index fee487c6..c2f5b46b 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -97,7 +97,7 @@ export default class RequestBuilder extends Component { } updateStateElement = (elementName, text) => { - if(elementName === 'patient') { + if (elementName === 'patient') { this.props.dispatch({ type: stateActions.updatePatient, value: text @@ -205,19 +205,19 @@ export default class RequestBuilder extends Component { } getPatients = () => { - if(this.props.globalState.patientFhirQuery) { + 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 + .request(this.props.globalState.patientFhirQuery, { flat: true }) + .then(result => { + this.setState({ + patientList: result + }); + }) + .catch(e => { + this.setState({ + patientList: e + }); }); - }); } }; @@ -337,7 +337,10 @@ export default class RequestBuilder extends Component { )} {!disableGetMedicationStatus && ( - + )} 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 f135947b..b1f0b2e2 100644 --- a/src/util/data.js +++ b/src/util/data.js @@ -29,9 +29,7 @@ const headerDefinitions = { ehrUrlSentToRemsAdminForPreFetch: { display: 'EHR Server Sent to REMS Admin for Prefetch', type: 'input', - default: env - .get('REACT_APP_EHR_SERVER_TO_BE_SENT_TO_REMS_ADMIN_FOR_PREFETCH') - .asString() + default: env.get('REACT_APP_EHR_SERVER_TO_BE_SENT_TO_REMS_ADMIN_FOR_PREFETCH').asString() }, generateJsonToken: { display: 'Generate JSON Web Token', @@ -86,7 +84,7 @@ const headerDefinitions = { smartAppUrl: { display: 'SMART App', type: 'input', - default: env.get('REACT_APP_SMART_LAUNCH_URL').asString() + default: env.get('REACT_APP_SMART_LAUNCH_URL').asString() } }; diff --git a/src/util/fhir.js b/src/util/fhir.js index 6c2ae3c9..0cbb845e 100644 --- a/src/util/fhir.js +++ b/src/util/fhir.js @@ -1,4 +1,3 @@ - function fhir(resource, ehrUrl, patient, auth) { const headers = { 'Content-Type': 'application/json' @@ -29,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) { @@ -48,20 +47,20 @@ 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'); @@ -75,8 +74,16 @@ function createMedicationDispenseFromMedicationRequest(medicationRequest) { medicationDispense.medicationReference = medicationRequest.medicationReference; } medicationDispense.subject = medicationRequest.subject; - medicationDispense.authorizingPrescription = [ { 'reference': 'MedicationRequest/' + medicationRequest.id } ]; + medicationDispense.authorizingPrescription = [ + { reference: 'MedicationRequest/' + medicationRequest.id } + ]; return medicationDispense; } -export { fhir, getAge, getDrugCodeableConceptFromMedicationRequest, getDrugCodeFromMedicationRequest, createMedicationDispenseFromMedicationRequest }; +export { + fhir, + getAge, + getDrugCodeableConceptFromMedicationRequest, + getDrugCodeFromMedicationRequest, + createMedicationDispenseFromMedicationRequest +}; From d3743c2bacb92f9db9c78adedb5855c8ae5dc9a6 Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Wed, 21 Feb 2024 16:24:34 -0500 Subject: [PATCH 17/30] fix bugs and reintroduce reconnect ehr button --- src/components/App.js | 13 +- .../RequestDashboard/SettingsSection.jsx | 193 +++++++++--------- src/components/SMARTBox/PatientBox.js | 3 + src/containers/ContextProvider/reducer.js | 3 +- src/containers/Index.jsx | 5 +- src/index.js | 8 +- 6 files changed, 127 insertions(+), 98 deletions(-) diff --git a/src/components/App.js b/src/components/App.js index dd43cadf..1f9e44cd 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 { stateActions } 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: stateActions.updateSetting, + settingId: 'redirect', + value: redirect + }); + }, []); return ( diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index c4f9bb4b..c2640422 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -2,6 +2,7 @@ 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'; @@ -122,7 +123,7 @@ const SettingsSection = props => { }); }; const clearMedicationDispenses = - ({ ehrUrl, access_token }, consoleLog) => + ({ ehrUrl, access_token }) => () => { console.log('Clear MedicationDispenses from the EHR: ' + ehrUrl); const client = FHIR.client({ @@ -138,10 +139,6 @@ const SettingsSection = props => { client .delete('MedicationDispense/' + resource.id) .then(result => { - consoleLog( - 'Successfully deleted MedicationDispense ' + resource.id + ' from EHR', - types.info - ); console.log(result); }) .catch(e => { @@ -155,17 +152,27 @@ const SettingsSection = props => { 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: 'Clear In-Progress Forms', - key: 'clearQuestionnaireResponses', - reset: clearQuestionnaireResponses - }, { display: 'Reset PIMS Database', key: 'resetPims', reset: resetPims }, + { + display: 'Clear In-Progress Forms', + key: 'clearQuestionnaireResponses', + reset: clearQuestionnaireResponses + }, { display: 'Reset REMS-Admin Database', key: 'resetRemsAdmin', @@ -175,96 +182,100 @@ const SettingsSection = props => { 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}

+ + + {headers.map(({ key, type, display }) => { + switch (type) { + case 'input': + return ( + +
+ { + updateSetting(key, event.target.value); + }} + sx={{ width: '100%' }} + />
- ); - } - })} - {resetHeaderDefinitions.map(({ key, display, reset }) => { - return ( - - - - ); - })} - {/* spacer */} -
- + + ); + 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 */} +
+ - - - - - - + + + + + -
-
+
+
); }; diff --git a/src/components/SMARTBox/PatientBox.js b/src/components/SMARTBox/PatientBox.js index e7b189dd..4a567107 100644 --- a/src/components/SMARTBox/PatientBox.js +++ b/src/components/SMARTBox/PatientBox.js @@ -540,6 +540,9 @@ export default class PatientBox extends Component { render() { const patient = this.props.patient; + if (!patient) { + return <>; + } let name = ''; let fullName = ''; let formatBirthdate = ''; diff --git a/src/containers/ContextProvider/reducer.js b/src/containers/ContextProvider/reducer.js index 38f0ded1..a475f6c1 100644 --- a/src/containers/ContextProvider/reducer.js +++ b/src/containers/ContextProvider/reducer.js @@ -29,7 +29,8 @@ export const reducer = (state, action) => { const initialState = { patient: null, - startup: false + startup: false, + redirect: '' }; Object.keys(headerDefinitions).forEach(e => { initialState[e] = headerDefinitions[e].default; // fill default settings values diff --git a/src/containers/Index.jsx b/src/containers/Index.jsx index f69d8245..0bf71379 100644 --- a/src/containers/Index.jsx +++ b/src/containers/Index.jsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'; import FHIR from 'fhirclient'; import env from 'env-var'; import Home from '../components/RequestDashboard/Home'; -import { SettingsProvider } from './ContextProvider/SettingsProvider'; const Index = props => { const [client, setClient] = useState(null); @@ -16,9 +15,7 @@ const Index = props => { return (
{client ? ( - - - + ) : (

Getting Client...

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') +); From fe17131599acdf43dc27c255679c8bdc52ac398c Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Thu, 22 Feb 2024 15:23:38 -0500 Subject: [PATCH 18/30] code cleanup and spelling --- src/components/RequestDashboard/Home.jsx | 2 +- .../RequestDashboard/PatientSection.jsx | 3 +- src/components/RequestDashboard/styles.jsx | 9 +-- src/components/SMARTBox/PatientBox.js | 2 +- src/containers/RequestBuilder.js | 56 ++++--------------- 5 files changed, 14 insertions(+), 58 deletions(-) diff --git a/src/components/RequestDashboard/Home.jsx b/src/components/RequestDashboard/Home.jsx index 43206752..a1b0a231 100644 --- a/src/components/RequestDashboard/Home.jsx +++ b/src/components/RequestDashboard/Home.jsx @@ -71,7 +71,7 @@ const Home = props => { ) : ( - )}{' '} + )} {/* spacer */} ); diff --git a/src/components/RequestDashboard/PatientSection.jsx b/src/components/RequestDashboard/PatientSection.jsx index 2ba78ee7..a871bb69 100644 --- a/src/components/RequestDashboard/PatientSection.jsx +++ b/src/components/RequestDashboard/PatientSection.jsx @@ -7,8 +7,7 @@ import { SettingsContext } from '../../containers/ContextProvider/SettingsProvid const PatientSection = props => { const classes = useStyles(); const [state, dispatch] = React.useContext(SettingsContext); - // TODO: Make request builder use react-hooks so - // we can get rid of this hacky shim + // TODO: Make request builder use react-hooks return (
{state.startup ? ( diff --git a/src/components/RequestDashboard/styles.jsx b/src/components/RequestDashboard/styles.jsx index a4bc9998..cb3da16b 100644 --- a/src/components/RequestDashboard/styles.jsx +++ b/src/components/RequestDashboard/styles.jsx @@ -10,11 +10,6 @@ export default makeStyles( flexGrow: 1, backgroundColor: '#005B94' }, - mainButton: { - '&.MuiButtonBase-root': { - // transition: 'all 250ms ease-in 0ms', - } - }, mainButtonView: { '&.MuiButtonBase-root': { width: '600px', @@ -43,7 +38,6 @@ export default makeStyles( mainSectionView: { width: 'auto', height: 'auto', - // margin: '0 15px 0 15px', // must be same as tabDivView borderLeft: '1px solid black', borderRight: '1px solid black' }, @@ -70,7 +64,7 @@ export default makeStyles( '& > *': { // generic child selector '&.MuiButton-iconSizeMedium': { - // specificty + // specificity marginRight: 0 } } @@ -105,7 +99,6 @@ export default makeStyles( }, tabDivView: { '&.MuiGrid-root': { - // padding: '0 15px 0 15px', marginTop: '0vh', alignItems: 'flex-start', justifyContent: 'flex-start' diff --git a/src/components/SMARTBox/PatientBox.js b/src/components/SMARTBox/PatientBox.js index 4a567107..9471e30c 100644 --- a/src/components/SMARTBox/PatientBox.js +++ b/src/components/SMARTBox/PatientBox.js @@ -396,7 +396,7 @@ export default class PatientBox extends Component { } /** - * Launch In progress From + * Launch In progress Form */ relaunch = data => { diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index c2f5b46b..5014e1a0 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -46,45 +46,28 @@ export default class RequestBuilder extends Component { this.submit_info = this.submit_info.bind(this); this.consoleLog = this.consoleLog.bind(this); this.takeSuggestion = this.takeSuggestion.bind(this); - this.reconnectEhr = this.reconnectEhr.bind(this); this.requestBox = React.createRef(); } componentDidMount() { - // init settings - Object.keys(headerDefinitions).map(key => { - this.setState({ [key]: headerDefinitions[key].default }); - }); - // load settings - JSON.parse(localStorage.getItem('reqgenSettings') || '[]').forEach(element => { - try { - this.updateStateElement(element[0], element[1]); - } catch { - if (element[0]) { - console.log('Could not load setting:' + element[0]); - } - } - }); - 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: stateActions.updateSetting, + settingId: 'baseUrl', + value: this.state.client.state.serverUrl + }); + this.props.dispatch({ + type: stateActions.updateSetting, + settingId: 'ehrUrl', + value: this.state.client.state.serverUrl + }); } } - reconnectEhr() { - FHIR.oauth2.authorize({ - clientId: env.get('REACT_APP_CLIENT').asString(), - iss: this.props.globalState.baseUrl, - redirectUri: this.props.redirect, - scope: env.get('REACT_APP_CLIENT_SCOPES').asString() - }); - } - consoleLog(content, type) { console.log(content); let jsonContent = { @@ -359,22 +342,3 @@ export default class RequestBuilder extends Component { ); } } - -const navigationBarButtonStyle = { - backgroundColor: 'white', - color: 'black', - borderColor: 'black', - display: 'flex', - marginX: 2, - marginY: 1, - '&:hover': { - color: 'white', - backgroundColor: 'black', - borderColor: 'black' - }, - '&:active': { - color: 'white', - backgroundColor: 'black', - borderColor: 'black' - } -}; From a3ffa6bf6850dd959bd13c26d0dbf57d81f59cfc Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Thu, 22 Feb 2024 15:33:41 -0500 Subject: [PATCH 19/30] spelling --- src/components/SMARTBox/PatientBox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SMARTBox/PatientBox.js b/src/components/SMARTBox/PatientBox.js index 9471e30c..11a71519 100644 --- a/src/components/SMARTBox/PatientBox.js +++ b/src/components/SMARTBox/PatientBox.js @@ -49,7 +49,7 @@ export default class PatientBox extends Component { 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) { From 95d81314795c8b80d7602186ec068db0de149e5f Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Thu, 22 Feb 2024 15:53:36 -0500 Subject: [PATCH 20/30] update reducer --- src/components/App.js | 4 ++-- src/components/RequestDashboard/SettingsSection.jsx | 6 +++--- src/containers/ContextProvider/reducer.js | 12 ++++++------ src/containers/RequestBuilder.js | 8 ++++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/App.js b/src/components/App.js index 1f9e44cd..f22c2a08 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -8,7 +8,7 @@ import PatientPortal from '../containers/PatientPortal'; import RegisterPage from '../containers/register/RegisterPage'; import theme from '../containers/styles/theme'; import { SettingsContext } from '../containers/ContextProvider/SettingsProvider'; -import { stateActions } from '../containers/ContextProvider/reducer'; +import { actionTypes } from '../containers/ContextProvider/reducer'; const isGhPages = process.env.REACT_APP_GH_PAGES === 'true'; const Router = isGhPages ? HashRouter : BrowserRouter; @@ -17,7 +17,7 @@ const App = () => { const [state, dispatch] = React.useContext(SettingsContext); useEffect(() => { dispatch({ - type: stateActions.updateSetting, + type: actionTypes.updateSetting, settingId: 'redirect', value: redirect }); diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index c2640422..ec5a97fd 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -6,7 +6,7 @@ import env from 'env-var'; import FHIR from 'fhirclient'; import { headerDefinitions, types } from '../../util/data'; -import { stateActions } from '../../containers/ContextProvider/reducer'; +import { actionTypes } from '../../containers/ContextProvider/reducer'; import { SettingsContext } from '../../containers/ContextProvider/SettingsProvider'; const SettingsSection = props => { @@ -36,13 +36,13 @@ const SettingsSection = props => { } // indicate to the rest of the app that the settings have been loaded dispatch({ - type: stateActions.flagStartup + type: actionTypes.flagStartup }); }); }, []); const updateSetting = (key, value) => { dispatch({ - type: stateActions.updateSetting, + type: actionTypes.updateSetting, settingId: key, value: value }); diff --git a/src/containers/ContextProvider/reducer.js b/src/containers/ContextProvider/reducer.js index a475f6c1..5c856693 100644 --- a/src/containers/ContextProvider/reducer.js +++ b/src/containers/ContextProvider/reducer.js @@ -1,23 +1,23 @@ import { headerDefinitions } from '../../util/data'; -export const stateActions = Object.freeze({ - updatePatient: 'update_patient', +export const actionTypes = Object.freeze({ + updatePatient: 'update_patient', // {type, value} updateSetting: 'update_setting', // {type, settingId, value} - flagStartup: 'flag_startup' + flagStartup: 'flag_startup' // {type} }); // todo: add an enum that defines possible settings export const reducer = (state, action) => { switch (action.type) { - case stateActions.updateSetting: + case actionTypes.updateSetting: return { ...state, [action.settingId]: action.value }; - case stateActions.updatePatient: + case actionTypes.updatePatient: return { ...state, patient: action.value }; - case stateActions.flagStartup: + case actionTypes.flagStartup: return { ...state, startup: true diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index 5014e1a0..7d4c3d5c 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -19,7 +19,7 @@ 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 { stateActions } from './ContextProvider/reducer.js'; +import { actionTypes } from './ContextProvider/reducer.js'; export default class RequestBuilder extends Component { constructor(props) { @@ -56,12 +56,12 @@ export default class RequestBuilder extends Component { // Call patients on load of page this.getPatients(); this.props.dispatch({ - type: stateActions.updateSetting, + type: actionTypes.updateSetting, settingId: 'baseUrl', value: this.state.client.state.serverUrl }); this.props.dispatch({ - type: stateActions.updateSetting, + type: actionTypes.updateSetting, settingId: 'ehrUrl', value: this.state.client.state.serverUrl }); @@ -82,7 +82,7 @@ export default class RequestBuilder extends Component { updateStateElement = (elementName, text) => { if (elementName === 'patient') { this.props.dispatch({ - type: stateActions.updatePatient, + type: actionTypes.updatePatient, value: text }); } else { From 772615f4ec117464c7649761c2579aae55bddc99 Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Thu, 22 Feb 2024 16:45:47 -0500 Subject: [PATCH 21/30] increase task card clarity --- src/components/RequestDashboard/styles.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/RequestDashboard/styles.jsx b/src/components/RequestDashboard/styles.jsx index cb3da16b..00c52a63 100644 --- a/src/components/RequestDashboard/styles.jsx +++ b/src/components/RequestDashboard/styles.jsx @@ -116,7 +116,7 @@ export default makeStyles( }, taskTabMain: { border: '0px solid black', - boxShadow: '2px 2px', + boxShadow: '-2px -2px 3px 1px', borderRadius: '5px', padding: '8px', background: 'linear-gradient(to right bottom, #F5F5F7, #eaeaef)', @@ -125,7 +125,7 @@ export default makeStyles( } }, taskTabHeader: { - fontSize: '8px', + fontSize: '9px', color: '#777', borderBottom: '1px solid #e3e3ef' }, From 114c4f45cd4958746c961bd1a85d4087b8020bf3 Mon Sep 17 00:00:00 2001 From: Ariel Virgulto Date: Fri, 23 Feb 2024 16:20:52 -0500 Subject: [PATCH 22/30] Updates to patient portal and add in assign to me and assign to patient dropdown --- src/components/Auth/Login.jsx | 3 +- src/components/Dashboard/Dashboard.jsx | 94 +++----- .../Dashboard/ListSelections/EmptySection.jsx | 14 ++ .../Dashboard/ListSelections/FormsSection.jsx | 82 +++++++ .../ListSelections/PatientTaskSection.jsx | 207 ++++++++++++++++++ src/components/Dashboard/styles.jsx | 59 ++++- .../RequestDashboard/TasksSection.jsx | 67 ++++-- src/containers/PatientPortal.jsx | 8 +- 8 files changed, 452 insertions(+), 82 deletions(-) create mode 100644 src/components/Dashboard/ListSelections/EmptySection.jsx create mode 100644 src/components/Dashboard/ListSelections/FormsSection.jsx create mode 100644 src/components/Dashboard/ListSelections/PatientTaskSection.jsx diff --git a/src/components/Auth/Login.jsx b/src/components/Auth/Login.jsx index e047db7e..f5ae2934 100644 --- a/src/components/Auth/Login.jsx +++ b/src/components/Auth/Login.jsx @@ -11,7 +11,8 @@ 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) { const params = new URLSearchParams(); diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index c5304a3a..a1f93e6f 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,8 @@ 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..a9cf5c78 --- /dev/null +++ b/src/components/Dashboard/ListSelections/EmptySection.jsx @@ -0,0 +1,14 @@ +import React, {memo } from 'react'; +import useStyles from '../styles'; + +const EmptySection = () => { + + const classes = useStyles(); + return ( +
+

Not available

+
+ ); +}; + +export default memo(EmptySection); \ No newline at end of file diff --git a/src/components/Dashboard/ListSelections/FormsSection.jsx b/src/components/Dashboard/ListSelections/FormsSection.jsx new file mode 100644 index 00000000..3acbd34b --- /dev/null +++ b/src/components/Dashboard/ListSelections/FormsSection.jsx @@ -0,0 +1,82 @@ +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); \ No newline at end of file diff --git a/src/components/Dashboard/ListSelections/PatientTaskSection.jsx b/src/components/Dashboard/ListSelections/PatientTaskSection.jsx new file mode 100644 index 00000000..2ec7eef2 --- /dev/null +++ b/src/components/Dashboard/ListSelections/PatientTaskSection.jsx @@ -0,0 +1,207 @@ +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); \ No newline at end of file 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/RequestDashboard/TasksSection.jsx b/src/components/RequestDashboard/TasksSection.jsx index 431a09ec..e1f68268 100644 --- a/src/components/RequestDashboard/TasksSection.jsx +++ b/src/components/RequestDashboard/TasksSection.jsx @@ -1,5 +1,5 @@ import React, { memo, useState, useEffect, Fragment } from 'react'; -import { Button, Box, Modal, Grid, Tabs, Tab, Stack } from '@mui/material'; +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'; @@ -13,6 +13,7 @@ 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(); @@ -21,6 +22,15 @@ const TasksSection = props => { 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); }; @@ -32,8 +42,7 @@ const TasksSection = props => { setTaskToDelete(task); setOpen(true); }; - const assignTask = task => { - // TODO: should allow assigning to anybody, not just self + const assignTaskToMe = task => { if (task) { let user = props.client.user.id; if (!user) { @@ -53,6 +62,25 @@ const TasksSection = props => { }); } }; + 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 => { @@ -103,13 +131,13 @@ const TasksSection = props => { let taskForName = 'N/A'; let taskOwnerName = 'N/A'; - if (task.for.resourceType.toLowerCase() === 'patient') { + 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') { + 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}`; @@ -117,6 +145,14 @@ const TasksSection = props => { 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 = ( @@ -164,15 +200,18 @@ const TasksSection = props => { - + + Assign + + {/*spacer*/} 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 ? ( - + ) : ( )} From 706d5631faac69912693c855e20cb9f9e9a8439c Mon Sep 17 00:00:00 2001 From: Ariel Virgulto Date: Fri, 23 Feb 2024 16:23:45 -0500 Subject: [PATCH 23/30] prettier updates --- src/components/Auth/Login.jsx | 2 +- src/components/Dashboard/Dashboard.jsx | 23 ++-- .../Dashboard/ListSelections/EmptySection.jsx | 17 ++- .../Dashboard/ListSelections/FormsSection.jsx | 103 ++++++++---------- .../ListSelections/PatientTaskSection.jsx | 70 ++++++------ .../RequestDashboard/TasksSection.jsx | 25 ++++- 6 files changed, 124 insertions(+), 116 deletions(-) diff --git a/src/components/Auth/Login.jsx b/src/components/Auth/Login.jsx index f5ae2934..5000d4b2 100644 --- a/src/components/Auth/Login.jsx +++ b/src/components/Auth/Login.jsx @@ -12,7 +12,7 @@ const Login = props => { const [password, _setPassword] = useState(''); const handleClose = () => setMessage(null); document.title = 'EHR | Patient Portal'; - + const onSubmit = useCallback(() => { if (username && password) { const params = new URLSearchParams(); diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index a1f93e6f..95aae834 100644 --- a/src/components/Dashboard/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -30,7 +30,7 @@ const Dashboard = props => { const handleListItemClick = (event, index) => { setSelectedIndex(index); }; - + const drawerWidth = '340px'; const createIcons = () => { @@ -58,13 +58,13 @@ const Dashboard = props => { }, [selectedIndex]); const renderBody = () => { - switch(selectedIndex) { + switch (selectedIndex) { case 2: - return (); + return ; case 3: - return (); - default: - return (); + return ; + default: + return ; } }; @@ -87,8 +87,13 @@ const Dashboard = props => { {createIcons().map((option, index) => (
- - handleListItemClick(event, index)}> + + handleListItemClick(event, index)}> {option[1]} { - { renderBody() } + {renderBody()}
diff --git a/src/components/Dashboard/ListSelections/EmptySection.jsx b/src/components/Dashboard/ListSelections/EmptySection.jsx index a9cf5c78..b9dd66ca 100644 --- a/src/components/Dashboard/ListSelections/EmptySection.jsx +++ b/src/components/Dashboard/ListSelections/EmptySection.jsx @@ -1,14 +1,13 @@ -import React, {memo } from 'react'; +import React, { memo } from 'react'; import useStyles from '../styles'; const EmptySection = () => { - - const classes = useStyles(); - return ( -
-

Not available

-
- ); + const classes = useStyles(); + return ( +
+

Not available

+
+ ); }; -export default memo(EmptySection); \ No newline at end of file +export default memo(EmptySection); diff --git a/src/components/Dashboard/ListSelections/FormsSection.jsx b/src/components/Dashboard/ListSelections/FormsSection.jsx index 3acbd34b..34491555 100644 --- a/src/components/Dashboard/ListSelections/FormsSection.jsx +++ b/src/components/Dashboard/ListSelections/FormsSection.jsx @@ -1,4 +1,4 @@ -import React, {memo, useState, useEffect } from 'react'; +import React, { memo, useState, useEffect } from 'react'; import { Paper } from '@mui/material'; import DashboardElement from '../DashboardElement'; import FormControlLabel from '@mui/material/FormControlLabel'; @@ -6,77 +6,70 @@ 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'); - } + 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); + setChecked(event.target.checked); }; const addResources = bundle => { - if (bundle.entry) { - bundle.entry.forEach(e => { - const resource = e.resource; - setResources(resources => [...resources, resource]); - }); - } + 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; - }; + 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} - )} -
+
+

Available Forms

+ } + label="Only show in-progress forms" + /> + {resources.length > 0 ? ( + renderElements().map(e => { + return ( + + ); + }) + ) : ( + {message} + )} +
); - }; -export default memo(FormsSection); \ No newline at end of file +export default memo(FormsSection); diff --git a/src/components/Dashboard/ListSelections/PatientTaskSection.jsx b/src/components/Dashboard/ListSelections/PatientTaskSection.jsx index 2ec7eef2..6900f73f 100644 --- a/src/components/Dashboard/ListSelections/PatientTaskSection.jsx +++ b/src/components/Dashboard/ListSelections/PatientTaskSection.jsx @@ -11,7 +11,6 @@ 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(''); @@ -165,43 +164,42 @@ const PatientTaskSection = props => { return (
-

Tasks

- {renderTasks(tasks)} - - - - - {taskToDelete ? `Are you sure you want to delete Task ${taskToDelete.id}` : ''} - - - {/*spacer*/} - - - - - - - +

Tasks

+ {renderTasks(tasks)} + + + + + {taskToDelete ? `Are you sure you want to delete Task ${taskToDelete.id}` : ''} + + + {/*spacer*/} + + + - - + + + +
+
+
); - }; -export default memo(PatientTaskSection); \ No newline at end of file +export default memo(PatientTaskSection); diff --git a/src/components/RequestDashboard/TasksSection.jsx b/src/components/RequestDashboard/TasksSection.jsx index e1f68268..3e127619 100644 --- a/src/components/RequestDashboard/TasksSection.jsx +++ b/src/components/RequestDashboard/TasksSection.jsx @@ -1,5 +1,16 @@ import React, { memo, useState, useEffect, Fragment } from 'react'; -import { Button, Box, Modal, Grid, Tabs, Tab, Stack, Select, FormControl, InputLabel } from '@mui/material'; +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'; @@ -79,7 +90,6 @@ const TasksSection = props => { fetchTasks(); }); } - }; const deleteTask = () => { if (taskToDelete) { @@ -200,16 +210,19 @@ const TasksSection = props => {
- + Assign From 505955196c78fab9ec891701111fde6fb9bda5da Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Tue, 20 Feb 2024 16:16:33 -0500 Subject: [PATCH 24/30] Rename method to camelcase --- src/containers/RequestBuilder.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index 7d4c3d5c..465663b3 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -43,7 +43,7 @@ export default class RequestBuilder extends Component { }; 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.requestBox = React.createRef(); @@ -102,7 +102,7 @@ export default class RequestBuilder extends Component { 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 }); @@ -293,7 +293,7 @@ export default class RequestBuilder extends Component { Date: Wed, 21 Feb 2024 23:37:23 -0500 Subject: [PATCH 25/30] Move MedicationDispense fetching up to parent component --- .../MedicationStatus/MedicationStatus.jsx | 22 +++------------- src/containers/RequestBuilder.js | 25 ++++++++++++++++--- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/components/MedicationStatus/MedicationStatus.jsx b/src/components/MedicationStatus/MedicationStatus.jsx index 745148de..d023bdc8 100644 --- a/src/components/MedicationStatus/MedicationStatus.jsx +++ b/src/components/MedicationStatus/MedicationStatus.jsx @@ -1,30 +1,14 @@ -import axios from 'axios'; 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 } = props; - const [medicationDispense, setMedicationDispense] = useState(null); + const { ehrUrl, request, medicationDispense, getMedicationStatus, lastCheckedMedicationTime } = + props; const [showMedicationStatus, setShowMedicationStatus] = useState(false); - const [lastCheckedMedicationTime, setLastCheckedMedicationTime] = useState(null); - useEffect(() => getMedicationStatus(), [request.id]); - - const getMedicationStatus = () => { - setLastCheckedMedicationTime(Date.now()); - - axios.get(`${ehrUrl}/MedicationDispense?prescription=${request.id}`).then( - response => { - const bundle = response.data; - setMedicationDispense(bundle.entry?.[0].resource); - }, - error => { - console.log('Was not able to get medication status', error); - } - ); - }; + useEffect(() => getMedicationStatus(), [request.id, ehrUrl]); const handleCloseMedicationStatus = () => { setShowMedicationStatus(false); diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index 465663b3..fdc6959d 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -7,7 +7,7 @@ import DisplayBox from '../components/DisplayBox/DisplayBox'; import '../index.css'; import RequestBox from '../components/RequestBox/RequestBox'; import buildRequest from '../util/buildRequest.js'; -import { types, defaultValues, headerDefinitions } from '../util/data.js'; +import { types, defaultValues as codeValues, headerDefinitions } from '../util/data.js'; import { createJwt } from '../util/auth'; import env from 'env-var'; @@ -20,6 +20,7 @@ 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) { @@ -39,7 +40,8 @@ export default class RequestBuilder extends Component { showSettings: false, token: null, client: this.props.client, - codeValues: defaultValues + medicationDispense: null, + lastCheckedMedicationTime: null }; this.updateStateElement = this.updateStateElement.bind(this); @@ -49,6 +51,20 @@ export default class RequestBuilder extends Component { this.requestBox = React.createRef(); } + getMedicationStatus = () => { + this.setState({ lastCheckedMedicationTime: Date.now() }); + + axios.get(`${this.state.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(); @@ -272,7 +288,7 @@ export default class RequestBuilder extends Component { callbackMap={this.updateStateMap} // updatePrefetchCallback={PrefetchTemplate.generateQueries} clearCallback={this.clearState} - options={this.state.codeValues} + options={codeValues} responseExpirationDays={this.props.globalState.responseExpirationDays} defaultUser={this.props.globalState.defaultUser} /> @@ -323,6 +339,9 @@ export default class RequestBuilder extends Component { )} From fac2edfc08be520a31adbc6da84c4318dcbda831 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Mon, 26 Feb 2024 11:01:52 -0500 Subject: [PATCH 26/30] Remove unused code --- src/containers/RequestBuilder.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index fdc6959d..60bc2b97 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -286,9 +286,7 @@ export default class RequestBuilder extends Component { callback={this.updateStateElement} callbackList={this.updateStateList} callbackMap={this.updateStateMap} - // updatePrefetchCallback={PrefetchTemplate.generateQueries} clearCallback={this.clearState} - options={codeValues} responseExpirationDays={this.props.globalState.responseExpirationDays} defaultUser={this.props.globalState.defaultUser} /> From 6b6940b6852c4f9ee4571f583f609e4f1eebd7f5 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Mon, 26 Feb 2024 11:24:43 -0500 Subject: [PATCH 27/30] Fix incorrect variable --- src/containers/RequestBuilder.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index 60bc2b97..e589d618 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -54,15 +54,17 @@ export default class RequestBuilder extends Component { getMedicationStatus = () => { this.setState({ lastCheckedMedicationTime: Date.now() }); - axios.get(`${this.state.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); - } - ); + axios + .get(`${this.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() { From 06b1ca11e8d8f0179c6fe23354717af1f5b9852e Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Mon, 26 Feb 2024 11:27:10 -0500 Subject: [PATCH 28/30] Fix typo again --- src/containers/RequestBuilder.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/containers/RequestBuilder.js b/src/containers/RequestBuilder.js index e589d618..7904d594 100644 --- a/src/containers/RequestBuilder.js +++ b/src/containers/RequestBuilder.js @@ -55,7 +55,9 @@ export default class RequestBuilder extends Component { this.setState({ lastCheckedMedicationTime: Date.now() }); axios - .get(`${this.globalState.ehrUrl}/MedicationDispense?prescription=${this.state.request.id}`) + .get( + `${this.props.globalState.ehrUrl}/MedicationDispense?prescription=${this.state.request.id}` + ) .then( response => { const bundle = response.data; From 461f596a0fe375b983be9ddc903cad7818814f18 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Mon, 26 Feb 2024 12:28:23 -0500 Subject: [PATCH 29/30] Move update startup dispatch call out of for loop since if local storage is empty, the dispatch will never happen --- src/components/RequestDashboard/SettingsSection.jsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index ec5a97fd..9c704046 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -34,10 +34,11 @@ const SettingsSection = props => { 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 - }); + }); + + // indicate to the rest of the app that the settings have been loaded + dispatch({ + type: actionTypes.flagStartup }); }, []); const updateSetting = (key, value) => { From 246e30d2debc73d034e24e40981071a8b46e63e2 Mon Sep 17 00:00:00 2001 From: Ariel Virgulto Date: Mon, 26 Feb 2024 12:57:12 -0500 Subject: [PATCH 30/30] update fill of select --- src/components/RequestDashboard/TasksSection.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/RequestDashboard/TasksSection.jsx b/src/components/RequestDashboard/TasksSection.jsx index 3e127619..e8c424d5 100644 --- a/src/components/RequestDashboard/TasksSection.jsx +++ b/src/components/RequestDashboard/TasksSection.jsx @@ -210,8 +210,9 @@ const TasksSection = props => { - Assign