From fe4279bcb430277a4b2138a04d1b1374f6c79e03 Mon Sep 17 00:00:00 2001 From: c-schuler Date: Fri, 7 Jun 2024 13:20:20 -0600 Subject: [PATCH 1/2] Adding Rx Sign Tab and Support - Added Rx Sign tab - Added actions to support workflow - Updated reducers to account for supply duration, intent and category (needed for opioid content) --- src/actions/action-types.js | 4 + src/actions/medication-sign-actions.js | 94 ++++++ src/components/Header/header.jsx | 3 +- src/components/MainView/main-view.jsx | 4 +- src/components/RxSign/rx-sign.css | 47 +++ src/components/RxSign/rx-sign.jsx | 427 +++++++++++++++++++++++++ src/components/RxView/rx-view.jsx | 2 +- src/reducers/hook-reducers.js | 8 + src/reducers/medication-reducers.js | 39 +++ 9 files changed, 625 insertions(+), 3 deletions(-) create mode 100644 src/actions/medication-sign-actions.js create mode 100644 src/components/RxSign/rx-sign.css create mode 100644 src/components/RxSign/rx-sign.jsx diff --git a/src/actions/action-types.js b/src/actions/action-types.js index a758f25..02e2de6 100644 --- a/src/actions/action-types.js +++ b/src/actions/action-types.js @@ -58,6 +58,10 @@ export const STORE_MED_DOSAGE_AMOUNT = 'STORE_MED_DOSAGE_AMOUNT'; export const STORE_DATE = 'STORE_DATE'; export const TOGGLE_DATE = 'TOGGLE_DATE'; +// Medication Order on RxSign +export const STORE_DISPENSE_REQUEST = 'STORE_DISPENSE_REQUEST'; +export const ORDER_SIGN_BUTTON_PRESS = 'ORDER_SIGN_BUTTON_PRESS'; + // Order Imaging export const APPLY_PAMA_RATING = 'APPLY_PAMA_RATING'; export const UPDATE_STUDY = 'UPDATED_STUDY'; diff --git a/src/actions/medication-sign-actions.js b/src/actions/medication-sign-actions.js new file mode 100644 index 0000000..c7e7f9d --- /dev/null +++ b/src/actions/medication-sign-actions.js @@ -0,0 +1,94 @@ +import * as types from './action-types'; + +/** + * Sets the user input from the medication select input box + * @param {*} input - User input string + */ +export function storeUserMedInput(input) { + return { + type: types.STORE_USER_MED_INPUT, + input, + }; +} + +/** + * Sets the specific medication from the medication select input box + * @param {*} medication - String of the medication ID + */ +export function storeUserChosenMedication(medication) { + return { + type: types.STORE_USER_CHOSEN_MEDICATION, + medication, + }; +} + +/** + * Sets the medication amount and frequency set on the UI in the store + * @param {*} amount - Dosage amount of the medication to take + * @param {*} frequency - String dosage frequency of the medication + */ +export function storeMedDosageAmount(amount, frequency) { + return { + type: types.STORE_MED_DOSAGE_AMOUNT, + amount, + frequency, + }; +} + +/** + * Sets the dispense request on the UI in the store + * @param {*} supplyDuration - Duration of the expected supply dispense + */ +export function storeDispenseRequest(supplyDuration) { + return { + type: types.STORE_DISPENSE_REQUEST, + supplyDuration, + }; +} + +/** + * Sets the date for the medication to be taken at a specific time (range) + * @param {*} range - String stating the date is the 'start' or 'end' date + * @param {*} date - String of the date + */ +export function storeDate(range, date) { + return { + type: types.STORE_DATE, + range, + date, + }; +} + +/** + * Toggle the start or end date so that it is either included or excluded from the MedicationOrder FHIR object in the request + * @param {*} range - String stating the date is the 'start' or 'end' date + */ +export function toggleDate(range) { + return { + type: types.TOGGLE_DATE, + range, + }; +} + +/** + * Call service when sign order button is selected + */ +export function signOrder(event) { + return { + type: types.ORDER_SIGN_BUTTON_PRESS, + event, + } +} + +/** + * Takes action on the user-clicked suggestion from a card. The suggestion will be the suggestion chosen + * from the CDS service response (exact format from specification). + * + * @param {*} suggestion - Object containing the suggestion chosen from the user (see format here: https://cds-hooks.org/specification/current/#suggestion) + */ +export function takeSuggestion(suggestion) { + return { + type: types.TAKE_SUGGESTION, + suggestion, + }; +} \ No newline at end of file diff --git a/src/components/Header/header.jsx b/src/components/Header/header.jsx index f41d4da..b4e86f3 100644 --- a/src/components/Header/header.jsx +++ b/src/components/Header/header.jsx @@ -159,7 +159,7 @@ export class Header extends Component { // If the tab is clicked again, make sure the Sandbox is qualified to call out to EHR's based // on current context (i.e. for the Rx View, ensure a medication has been prescribed before // re-invoking the services on that hook if the Rx View tab is clicked multiple times) - if (hook === 'order-select') { + if (hook === 'order-select' || hook === 'order-sign') { const medicationPrescribed = state.medicationState.decisions.prescribable && state.medicationState.medListPhase === 'done'; if (medicationPrescribed) { @@ -293,6 +293,7 @@ export class Header extends Component {
+
diff --git a/src/components/MainView/main-view.jsx b/src/components/MainView/main-view.jsx index 072f6f8..28b5ac1 100644 --- a/src/components/MainView/main-view.jsx +++ b/src/components/MainView/main-view.jsx @@ -13,6 +13,7 @@ import styles from './main-view.css'; import Header from '../Header/header'; import PatientView from '../PatientView/patient-view'; import RxView from '../RxView/rx-view'; +import RxSign from '../RxSign/rx-sign'; import Pama from '../Pama/pama'; import ContextView from '../ContextView/context-view'; import FhirServerEntry from '../FhirServerEntry/fhir-server-entry'; @@ -111,7 +112,7 @@ export class MainView extends Component { async componentDidMount() { // Set the loading spinner face-up this.props.setLoadingStatus(true); - const validHooks = ['patient-view', 'order-select']; + const validHooks = ['patient-view', 'order-select', 'order-sign']; let parsedHook = this.getQueryParam('hook'); const parsedScreen = this.getQueryParam('screen'); if (validHooks.indexOf(parsedHook) < 0) { @@ -215,6 +216,7 @@ export class MainView extends Component { const hookView = { 'patient-view': , 'rx-view': , + 'rx-sign': , pama: , }[this.props.screen]; diff --git a/src/components/RxSign/rx-sign.css b/src/components/RxSign/rx-sign.css new file mode 100644 index 0000000..d064e38 --- /dev/null +++ b/src/components/RxSign/rx-sign.css @@ -0,0 +1,47 @@ +.rx-sign { + height: auto; + display: inline-block; + padding: 30px; + margin: 0 0 20px; + vertical-align: top; + width: 100%; +} + +.half-view { + width: 50%; +} + +.view-title { + padding: 0 0 10px; + margin: 0 0 10px; + font-size: 1.5em; + letter-spacing: -0.025em; + color: #384E77; /* $color-primary */ + border-bottom: 2px solid #eee; +} + +.dose-instruction { + border: 1px solid #ddd; + padding: 5px 5px 0 10px; +} + +.dosage-amount { + margin-right: 3em; +} + +.dosage-timing { + margin-top: 1em; +} + +@media (max-width: 975px) { + .rx-sign { + padding: 10px; + display: inline-block; + position: relative; + } + + .half-view { + width: 100%; + display: inline-block; + } +} diff --git a/src/components/RxSign/rx-sign.jsx b/src/components/RxSign/rx-sign.jsx new file mode 100644 index 0000000..0467464 --- /dev/null +++ b/src/components/RxSign/rx-sign.jsx @@ -0,0 +1,427 @@ +/* eslint-disable react/forbid-prop-types */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import forIn from 'lodash/forIn'; +import cx from 'classnames'; +import Field from 'terra-form-field'; +// import Checkbox from 'terra-form-checkbox'; +import Select from 'react-select' +import SelectField from 'terra-form-select'; +import Text from 'terra-text'; +import Input, { InputField } from 'terra-form-input'; +import DatePicker from 'terra-date-picker'; +import List, { Item } from 'terra-list'; +import Button from 'terra-button'; + +import debounce from 'debounce'; + +import cdsExecution from '../../middleware/cds-execution'; +import CardList from '../CardList/card-list'; +import PatientBanner from '../PatientBanner/patient-banner'; +import styles from './rx-sign.css'; +import { createFhirResource } from '../../reducers/medication-reducers'; + +import { + storeUserMedInput, storeUserChosenMedication, + storeMedDosageAmount, storeDispenseRequest, storeDate, toggleDate, + takeSuggestion, signOrder +} from '../../actions/medication-sign-actions'; + +import * as types from '../../actions/action-types'; + +cdsExecution.registerTriggerHandler('rx-sign/order-sign', { + needExplicitTrigger: types.ORDER_SIGN_BUTTON_PRESS, + onSystemActions: () => { }, + onMessage: () => { }, + generateContext: (state) => { + const { fhirVersion } = state.fhirServerState; + const resource = createFhirResource(fhirVersion, state.patientState.currentPatient.id, state.medicationState); + + return { + draftOrders: { + resourceType: 'Bundle', + entry: [{ resource }], + }, + }; + }, +}); + +const propTypes = { + /** + * Flag to determine if the CDS Developer Panel view is visible + */ + isContextVisible: PropTypes.bool.isRequired, + /** + * Patient resource in context + */ + patient: PropTypes.object, + /** + * Array of medications a user may choose from at a given moment + */ + medications: PropTypes.arrayOf(PropTypes.object), + /** + * Prescribed medicine chosen by the user for the patient + */ + prescription: PropTypes.object, + /** + * Hash detailing the dosage and frequency of the prescribed medicine + */ + medicationInstructions: PropTypes.object, + /** + * Hash detailing the supply duration of the prescribed medicine + */ + dispenseRequest: PropTypes.object, + /** + * Hash detailing the start/end dates of the prescribed medication + */ + prescriptionDates: PropTypes.object, + /** + * Function for storing user input when the medication field changes + */ + onMedicationChangeInput: PropTypes.func.isRequired, + /** + * Function to signal a chosen medication + */ + chooseMedication: PropTypes.func.isRequired, + /** + * Function to signal a change in the dosage instructions (amount or frequency) + */ + updateDosageInstructions: PropTypes.func.isRequired, + /** + * Function to signal a change in the dispense request (supplyDuration) + */ + updateDispenseRequest: PropTypes.func.isRequired, + /** + * Function to signal a change in a date (start or end) + */ + updateDate: PropTypes.func.isRequired, + /** + * Function to signal a change in the toggled status of the date (start or end) + */ + toggleEnabledDate: PropTypes.func.isRequired, + /** + * Function to signal the selected service to be called + */ + signOrder: PropTypes.func.isRequired, + /** + * Function callback to take a specific suggestion from a card + */ + takeSuggestion: PropTypes.func.isRequired, +}; + +/** + * Left-hand side on the mock-EHR view that displays the cards and relevant UI for the order-select hook. + * The services are not called until a medication is chosen, or a change in prescription is made + */ +export class RxSign extends Component { + constructor(props) { + super(props); + + this.state = { + /** + * Value of the input box for medication + */ + value: '', + /** + * Tracks the dosage amount chosen from the form field + */ + dosageAmount: 1, + /** + * Tracks the dosage frequency chosen from the form field + */ + dosageFrequency: 'daily', + /** + * Tracks the supply duration chosen from the form field + */ + supplyDuration: 1, + /** + * Tracks the start date value and toggle of the prescription + */ + startRange: { + enabled: true, + value: undefined, + }, + /** + * Tracks the end date value and toggle of the prescription + */ + endRange: { + enabled: true, + value: undefined, + }, + }; + + this.changeMedicationInput = this.changeMedicationInput.bind(this); + this.changeDosageAmount = this.changeDosageAmount.bind(this); + this.changeDosageFrequency = this.changeDosageFrequency.bind(this); + this.changeSupplyDuration = this.changeSupplyDuration.bind(this); + this.selectStartDate = this.selectStartDate.bind(this); + this.selectEndDate = this.selectEndDate.bind(this); + this.toggleEnabledDate = this.toggleEnabledDate.bind(this); + this.signOrder = this.signOrder.bind(this); + } + + /** + * Update any incoming values that change for state + */ + componentWillReceiveProps(nextProps) { + if (nextProps.medicationInstructions.number !== this.state.dosageAmount + || nextProps.medicationInstructions.frequency !== this.state.dosageFrequency + || nextProps.medicationInstructions.supplyDuration !== this.state.supplyDuration + || nextProps.prescriptionDates.start.value !== this.state.startRange.value + || nextProps.prescriptionDates.end.value !== this.state.endRange.value) { + this.setState({ + dosageAmount: nextProps.medicationInstructions.number, + dosageFrequency: nextProps.medicationInstructions.frequency, + supplyDuration: nextProps.dispenseRequest.supplyDuration, + startRange: { + enabled: this.state.startRange.enabled, + value: nextProps.prescriptionDates.start.value, + }, + endRange: { + enabled: this.state.endRange.enabled, + value: nextProps.prescriptionDates.end.value, + }, + }); + } + } + + changeMedicationInput(event) { + this.setState({ value: event.target.value }); + debounce(this.props.onMedicationChangeInput(event.target.value), 50); + } + + // Note: Bound the dosage amount to a value between 1 and 5 + changeDosageAmount(event) { + let transformedNumber = Number(event.target.value) || 1; + if (transformedNumber > 5) { transformedNumber = 5; } + if (transformedNumber < 1) { transformedNumber = 1; } + this.setState({ dosageAmount: transformedNumber }); + this.props.updateDosageInstructions(transformedNumber, this.state.dosageFrequency); + } + + // Note: A second parameter (selected value) is supplied automatically by the Terra onChange function for the Form Select component + changeDosageFrequency(event, value) { + this.setState({ dosageFrequency: value }); + this.props.updateDosageInstructions(this.state.dosageAmount, value); + } + + changeSupplyDuration(event) { + let transformedNumber = Number(event.target.value) || 1; + if (transformedNumber > 90) { transformedNumber = 90; } + if (transformedNumber < 1) { transformedNumber = 1; } + this.setState( { supplyDuration: transformedNumber } ); + this.props.updateDispenseRequest(transformedNumber, this.state.supplyDuration); + } + + // Note: A second parameter (date value) is supplied automatically by the Terra onChange function for the DatePicker component + selectStartDate(event, value) { + const newStartRange = { + enabled: this.state.startRange.enabled, + value, + }; + this.setState({ + startRange: newStartRange, + }); + this.props.updateDate('start', newStartRange); + } + + // Note: A second parameter (date value) is supplied automatically by the Terra onChange function for the DatePicker component + selectEndDate(event, value) { + const newEndRange = { + enabled: this.state.endRange.enabled, + value, + }; + this.setState({ + endRange: newEndRange, + }); + this.props.updateDate('end', newEndRange); + } + + toggleEnabledDate(event, range) { + this.setState({ [`${range}Range`]: event.target.value }); + this.props.toggleEnabledDate(range); + } + + signOrder(event) { + this.setState({ + medicationState: 'done' + }); + this.props.signOrder(event) + } + + /** + * Create an array of key-value pair objects that React Select component understands + * given the Conditions present for the patient + */ + createDropdownConditions() { + const conditions = []; + forIn(this.props.patient.conditionsResources, (c) => { + const { code } = c.resource.code.coding[0]; + conditions.push({ + value: code, + label: c.resource.code.text, + }); + }); + return conditions; + } + + render() { + const isHalfView = this.props.isContextVisible ? styles['half-view'] : ''; + const medicationArray = this.props.medications; + + return ( +
+

Rx Sign

+ +
+ + + + {medicationArray.map((med) => ( + { this.props.chooseMedication(med); }} + > + {

{med.name}

} +
+ ))} +
+
+ {this.props.prescription ? {this.props.prescription.name} : null} +
+ + + + + + + + + + +
+
+ + + + + + +
+
+ +
+
+ +
+ ); + } +} + +RxSign.propTypes = propTypes; + +const mapStateToProps = (state) => ({ + isContextVisible: state.hookState.isContextVisible, + patient: state.patientState.currentPatient, + medications: state.medicationState.options[state.medicationState.medListPhase] || [], + prescription: state.medicationState.decisions.prescribable, + medicationInstructions: state.medicationState.medicationInstructions, + dispenseRequest: state.medicationState.dispenseRequest, + prescriptionDates: state.medicationState.prescriptionDates, +}); + +const mapDispatchToProps = (dispatch) => ( + { + onMedicationChangeInput: (input) => { + dispatch(storeUserMedInput(input)); + }, + chooseMedication: (medication) => { + dispatch(storeUserChosenMedication(medication)); + }, + updateDosageInstructions: (amount, frequency) => { + dispatch(storeMedDosageAmount(amount, frequency)); + }, + updateDispenseRequest: (supplyDuration) => { + dispatch(storeDispenseRequest(supplyDuration)) + }, + updateDate: (range, date) => { + dispatch(storeDate(range, date)); + }, + toggleEnabledDate: (range) => { + dispatch(toggleDate(range)); + }, + signOrder: (event) => { + dispatch(signOrder(event)); + }, + takeSuggestion: (suggestion) => { + dispatch(takeSuggestion(suggestion)); + }, + } +); + +export default connect(mapStateToProps, mapDispatchToProps)(RxSign); diff --git a/src/components/RxView/rx-view.jsx b/src/components/RxView/rx-view.jsx index ec8903d..1afa452 100644 --- a/src/components/RxView/rx-view.jsx +++ b/src/components/RxView/rx-view.jsx @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; import forIn from 'lodash/forIn'; import cx from 'classnames'; import Field from 'terra-form-field'; -import Checkbox from 'terra-form-checkbox'; +// import Checkbox from 'terra-form-checkbox'; import Select from 'react-select' import SelectField from 'terra-form-select'; import Text from 'terra-text'; diff --git a/src/reducers/hook-reducers.js b/src/reducers/hook-reducers.js index 5800425..0e75051 100644 --- a/src/reducers/hook-reducers.js +++ b/src/reducers/hook-reducers.js @@ -26,6 +26,14 @@ const initialState = { }, }, }, + 'rx-sign': { + triggerPoints: { + 'rx-sign/order-sign': { + hook: 'order-sign', + lastExchangeRound: 0, + }, + }, + }, pama: { triggerPoints: { 'pama/order-select': { diff --git a/src/reducers/medication-reducers.js b/src/reducers/medication-reducers.js index 2237146..e762c08 100644 --- a/src/reducers/medication-reducers.js +++ b/src/reducers/medication-reducers.js @@ -96,6 +96,29 @@ export const createFhirResource = (fhirVersion, patientId, state, patientConditi } } + if (state.dispenseRequest) { + const { dispenseRequest } = state; + + resource.dispenseRequest = { + expectedSupplyDuration: { + value: dispenseRequest.supplyDuration, + unit: 'days', + system: 'http://unitsofmeasure.org', + code: 'd' + }, + }; + + resource.intent = 'order'; + resource.category = { + coding: [ + { + system: 'http://terminology.hl7.org/CodeSystem/medicationrequest-category', + code: 'community' + } + ] + } + } + const med = state.decisions.prescribable; resource.medicationCodeableConcept = { @@ -166,6 +189,12 @@ const initialState = { number: parseInt(getQueryParam('prescribedInstructionNumber'), 10) || 1, frequency: getQueryParam('prescribedInstructionFrequency') || 'daily', }, + /** + * The dispense request information + */ + dispenseRequest: { + supplyDuration: parseInt(getQueryParam('prescribedSupplyDuration'), 10) || 1, + }, /** * The dates and enabled status of the medication start and end dates */ @@ -336,6 +365,16 @@ const medicationReducers = (state = initialState, action) => { }; } + // Stores the user-defined dispense request + case types.STORE_DISPENSE_REQUEST: { + return { + ...state, + dispenseRequest: { + supplyDuration: action.supplyDuration, + }, + }; + } + // Stores the user-defined dates for the prescription (start and end) case types.STORE_DATE: { return { From 1a4c1ccf6c639b7431a7f3214fb07e9ff7237c23 Mon Sep 17 00:00:00 2001 From: c-schuler Date: Fri, 7 Jun 2024 17:13:13 -0600 Subject: [PATCH 2/2] Cleanup - Migrating componentWillReceiveProps to use getDerivedStateFromProps - Removing unused state property --- src/actions/medication-sign-actions.js | 70 +- src/components/CardDemo/card-demo.jsx | 2 +- .../ConfigureServices/configure-services.jsx | 7 +- .../FhirServerEntry/fhir-server-entry.jsx | 7 +- src/components/PatientEntry/patient-entry.jsx | 7 +- src/components/RxSign/rx-sign.jsx | 670 +++++++++--------- src/components/RxView/rx-view.jsx | 28 +- .../ServicesEntry/services-entry.jsx | 9 +- src/reducers/helpers/services-filter.js | 1 - src/reducers/medication-reducers.js | 10 +- 10 files changed, 417 insertions(+), 394 deletions(-) diff --git a/src/actions/medication-sign-actions.js b/src/actions/medication-sign-actions.js index c7e7f9d..e2809e8 100644 --- a/src/actions/medication-sign-actions.js +++ b/src/actions/medication-sign-actions.js @@ -5,10 +5,10 @@ import * as types from './action-types'; * @param {*} input - User input string */ export function storeUserMedInput(input) { - return { - type: types.STORE_USER_MED_INPUT, - input, - }; + return { + type: types.STORE_USER_MED_INPUT, + input, + }; } /** @@ -16,10 +16,10 @@ export function storeUserMedInput(input) { * @param {*} medication - String of the medication ID */ export function storeUserChosenMedication(medication) { - return { - type: types.STORE_USER_CHOSEN_MEDICATION, - medication, - }; + return { + type: types.STORE_USER_CHOSEN_MEDICATION, + medication, + }; } /** @@ -28,11 +28,11 @@ export function storeUserChosenMedication(medication) { * @param {*} frequency - String dosage frequency of the medication */ export function storeMedDosageAmount(amount, frequency) { - return { - type: types.STORE_MED_DOSAGE_AMOUNT, - amount, - frequency, - }; + return { + type: types.STORE_MED_DOSAGE_AMOUNT, + amount, + frequency, + }; } /** @@ -40,10 +40,10 @@ export function storeMedDosageAmount(amount, frequency) { * @param {*} supplyDuration - Duration of the expected supply dispense */ export function storeDispenseRequest(supplyDuration) { - return { - type: types.STORE_DISPENSE_REQUEST, - supplyDuration, - }; + return { + type: types.STORE_DISPENSE_REQUEST, + supplyDuration, + }; } /** @@ -52,11 +52,11 @@ export function storeDispenseRequest(supplyDuration) { * @param {*} date - String of the date */ export function storeDate(range, date) { - return { - type: types.STORE_DATE, - range, - date, - }; + return { + type: types.STORE_DATE, + range, + date, + }; } /** @@ -64,20 +64,20 @@ export function storeDate(range, date) { * @param {*} range - String stating the date is the 'start' or 'end' date */ export function toggleDate(range) { - return { - type: types.TOGGLE_DATE, - range, - }; + return { + type: types.TOGGLE_DATE, + range, + }; } /** * Call service when sign order button is selected */ export function signOrder(event) { - return { - type: types.ORDER_SIGN_BUTTON_PRESS, - event, - } + return { + type: types.ORDER_SIGN_BUTTON_PRESS, + event, + }; } /** @@ -87,8 +87,8 @@ export function signOrder(event) { * @param {*} suggestion - Object containing the suggestion chosen from the user (see format here: https://cds-hooks.org/specification/current/#suggestion) */ export function takeSuggestion(suggestion) { - return { - type: types.TAKE_SUGGESTION, - suggestion, - }; -} \ No newline at end of file + return { + type: types.TAKE_SUGGESTION, + suggestion, + }; +} diff --git a/src/components/CardDemo/card-demo.jsx b/src/components/CardDemo/card-demo.jsx index 12e00dd..452c203 100644 --- a/src/components/CardDemo/card-demo.jsx +++ b/src/components/CardDemo/card-demo.jsx @@ -175,7 +175,7 @@ export class CardDemo extends Component { value={this.props.tempUserJson || exampleCode} ref={(el) => { this.cm = el; }} onChange={this.updateCard} - style={{ 'fontFamily': 'Inconsolata, Menlo, Consolas, monospace !important' }} + style={{ fontFamily: 'Inconsolata, Menlo, Consolas, monospace !important' }} options={options} /> diff --git a/src/components/ConfigureServices/configure-services.jsx b/src/components/ConfigureServices/configure-services.jsx index 7472bbc..eaf88ae 100644 --- a/src/components/ConfigureServices/configure-services.jsx +++ b/src/components/ConfigureServices/configure-services.jsx @@ -39,10 +39,11 @@ export class ConfigureServices extends Component { this.handleCloseModal = this.handleCloseModal.bind(this); } - componentWillReceiveProps(nextProps) { - if (this.props.isOpen !== nextProps.isOpen) { - this.setState({ isOpen: nextProps.isOpen }); + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.isOpen !== prevState.isOpen) { + return ({ isOpen: nextProps.isOpen }); } + return null; } handleCloseModal() { diff --git a/src/components/FhirServerEntry/fhir-server-entry.jsx b/src/components/FhirServerEntry/fhir-server-entry.jsx index b91c350..ec8111e 100644 --- a/src/components/FhirServerEntry/fhir-server-entry.jsx +++ b/src/components/FhirServerEntry/fhir-server-entry.jsx @@ -77,10 +77,11 @@ export class FhirServerEntry extends Component { this.handleResetDefaultServer = this.handleResetDefaultServer.bind(this); } - componentWillReceiveProps(nextProps) { - if (this.props.isOpen !== nextProps.isOpen) { - this.setState({ isOpen: nextProps.isOpen }); + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.isOpen !== prevState.isOpen) { + return ({ isOpen: nextProps.isOpen }); } + return null; } handleCloseModal() { diff --git a/src/components/PatientEntry/patient-entry.jsx b/src/components/PatientEntry/patient-entry.jsx index e1be94c..5396f0d 100644 --- a/src/components/PatientEntry/patient-entry.jsx +++ b/src/components/PatientEntry/patient-entry.jsx @@ -70,10 +70,11 @@ export class PatientEntry extends Component { this.handleChange = this.handleChange.bind(this); } - componentWillReceiveProps(nextProps) { - if (this.props.isOpen !== nextProps.isOpen) { - this.setState({ isOpen: nextProps.isOpen }); + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.isOpen !== prevState.isOpen) { + return ({ isOpen: nextProps.isOpen }); } + return null; } handleCloseModal() { diff --git a/src/components/RxSign/rx-sign.jsx b/src/components/RxSign/rx-sign.jsx index 0467464..1d599a3 100644 --- a/src/components/RxSign/rx-sign.jsx +++ b/src/components/RxSign/rx-sign.jsx @@ -7,7 +7,7 @@ import forIn from 'lodash/forIn'; import cx from 'classnames'; import Field from 'terra-form-field'; // import Checkbox from 'terra-form-checkbox'; -import Select from 'react-select' +import Select from 'react-select'; import SelectField from 'terra-form-select'; import Text from 'terra-text'; import Input, { InputField } from 'terra-form-input'; @@ -24,91 +24,95 @@ import styles from './rx-sign.css'; import { createFhirResource } from '../../reducers/medication-reducers'; import { - storeUserMedInput, storeUserChosenMedication, - storeMedDosageAmount, storeDispenseRequest, storeDate, toggleDate, - takeSuggestion, signOrder + storeUserMedInput, storeUserChosenMedication, + storeMedDosageAmount, storeDispenseRequest, storeDate, toggleDate, + takeSuggestion, signOrder, } from '../../actions/medication-sign-actions'; import * as types from '../../actions/action-types'; cdsExecution.registerTriggerHandler('rx-sign/order-sign', { - needExplicitTrigger: types.ORDER_SIGN_BUTTON_PRESS, - onSystemActions: () => { }, - onMessage: () => { }, - generateContext: (state) => { - const { fhirVersion } = state.fhirServerState; - const resource = createFhirResource(fhirVersion, state.patientState.currentPatient.id, state.medicationState); + needExplicitTrigger: types.ORDER_SIGN_BUTTON_PRESS, + onSystemActions: () => { }, + onMessage: () => { }, + generateContext: (state) => { + const { fhirVersion } = state.fhirServerState; + const resource = createFhirResource(fhirVersion, state.patientState.currentPatient.id, state.medicationState); - return { - draftOrders: { - resourceType: 'Bundle', - entry: [{ resource }], - }, - }; - }, + return { + draftOrders: { + resourceType: 'Bundle', + entry: [{ resource }], + }, + }; + }, }); const propTypes = { - /** + /** * Flag to determine if the CDS Developer Panel view is visible */ - isContextVisible: PropTypes.bool.isRequired, - /** + isContextVisible: PropTypes.bool.isRequired, + /** * Patient resource in context */ - patient: PropTypes.object, - /** + patient: PropTypes.object, + /** * Array of medications a user may choose from at a given moment */ - medications: PropTypes.arrayOf(PropTypes.object), - /** + medications: PropTypes.arrayOf(PropTypes.object), + /** * Prescribed medicine chosen by the user for the patient */ - prescription: PropTypes.object, - /** + prescription: PropTypes.object, + /** * Hash detailing the dosage and frequency of the prescribed medicine */ - medicationInstructions: PropTypes.object, - /** + medicationInstructions: PropTypes.object, + /** * Hash detailing the supply duration of the prescribed medicine */ - dispenseRequest: PropTypes.object, - /** + dispenseRequest: PropTypes.object, + /** * Hash detailing the start/end dates of the prescribed medication */ - prescriptionDates: PropTypes.object, - /** + prescriptionDates: PropTypes.object, + /** + * Coding code from the selected Condition resource in context + */ + selectedConditionCode: PropTypes.string, + /** * Function for storing user input when the medication field changes */ - onMedicationChangeInput: PropTypes.func.isRequired, - /** + onMedicationChangeInput: PropTypes.func.isRequired, + /** * Function to signal a chosen medication */ - chooseMedication: PropTypes.func.isRequired, - /** + chooseMedication: PropTypes.func.isRequired, + /** * Function to signal a change in the dosage instructions (amount or frequency) */ - updateDosageInstructions: PropTypes.func.isRequired, - /** + updateDosageInstructions: PropTypes.func.isRequired, + /** * Function to signal a change in the dispense request (supplyDuration) */ - updateDispenseRequest: PropTypes.func.isRequired, - /** + updateDispenseRequest: PropTypes.func.isRequired, + /** * Function to signal a change in a date (start or end) */ - updateDate: PropTypes.func.isRequired, - /** + updateDate: PropTypes.func.isRequired, + /** * Function to signal a change in the toggled status of the date (start or end) */ - toggleEnabledDate: PropTypes.func.isRequired, - /** + toggleEnabledDate: PropTypes.func.isRequired, + /** * Function to signal the selected service to be called */ - signOrder: PropTypes.func.isRequired, - /** + signOrder: PropTypes.func.isRequired, + /** * Function callback to take a specific suggestion from a card */ - takeSuggestion: PropTypes.func.isRequired, + takeSuggestion: PropTypes.func.isRequired, }; /** @@ -116,312 +120,328 @@ const propTypes = { * The services are not called until a medication is chosen, or a change in prescription is made */ export class RxSign extends Component { - constructor(props) { - super(props); + constructor(props) { + super(props); - this.state = { - /** - * Value of the input box for medication - */ - value: '', - /** - * Tracks the dosage amount chosen from the form field - */ - dosageAmount: 1, - /** - * Tracks the dosage frequency chosen from the form field - */ - dosageFrequency: 'daily', - /** - * Tracks the supply duration chosen from the form field - */ - supplyDuration: 1, - /** - * Tracks the start date value and toggle of the prescription - */ - startRange: { - enabled: true, - value: undefined, - }, - /** - * Tracks the end date value and toggle of the prescription - */ - endRange: { - enabled: true, - value: undefined, - }, - }; + this.state = { + /** + * Value of the input box for medication + */ + value: '', + /** + * Coding code of the Condition chosen from a dropdown list for the patient + */ + conditionCode: '', + /** + * Coding display of the Condition chosen from a dropdown list for the patient + */ + conditionDisplay: '', + /** + * Tracks the dosage amount chosen from the form field + */ + dosageAmount: 1, + /** + * Tracks the dosage frequency chosen from the form field + */ + dosageFrequency: 'daily', + /** + * Tracks the supply duration chosen from the form field + */ + supplyDuration: 1, + /** + * Tracks the start date value and toggle of the prescription + */ + startRange: { + enabled: true, + value: undefined, + }, + /** + * Tracks the end date value and toggle of the prescription + */ + endRange: { + enabled: true, + value: undefined, + }, + }; - this.changeMedicationInput = this.changeMedicationInput.bind(this); - this.changeDosageAmount = this.changeDosageAmount.bind(this); - this.changeDosageFrequency = this.changeDosageFrequency.bind(this); - this.changeSupplyDuration = this.changeSupplyDuration.bind(this); - this.selectStartDate = this.selectStartDate.bind(this); - this.selectEndDate = this.selectEndDate.bind(this); - this.toggleEnabledDate = this.toggleEnabledDate.bind(this); - this.signOrder = this.signOrder.bind(this); - } + this.changeMedicationInput = this.changeMedicationInput.bind(this); + this.selectCondition = this.selectCondition.bind(this); + this.changeDosageAmount = this.changeDosageAmount.bind(this); + this.changeDosageFrequency = this.changeDosageFrequency.bind(this); + this.changeSupplyDuration = this.changeSupplyDuration.bind(this); + this.selectStartDate = this.selectStartDate.bind(this); + this.selectEndDate = this.selectEndDate.bind(this); + this.toggleEnabledDate = this.toggleEnabledDate.bind(this); + this.signOrder = this.signOrder.bind(this); + } - /** + /** * Update any incoming values that change for state */ - componentWillReceiveProps(nextProps) { - if (nextProps.medicationInstructions.number !== this.state.dosageAmount - || nextProps.medicationInstructions.frequency !== this.state.dosageFrequency - || nextProps.medicationInstructions.supplyDuration !== this.state.supplyDuration - || nextProps.prescriptionDates.start.value !== this.state.startRange.value - || nextProps.prescriptionDates.end.value !== this.state.endRange.value) { - this.setState({ - dosageAmount: nextProps.medicationInstructions.number, - dosageFrequency: nextProps.medicationInstructions.frequency, - supplyDuration: nextProps.dispenseRequest.supplyDuration, - startRange: { - enabled: this.state.startRange.enabled, - value: nextProps.prescriptionDates.start.value, - }, - endRange: { - enabled: this.state.endRange.enabled, - value: nextProps.prescriptionDates.end.value, - }, - }); - } + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.medicationInstructions.number !== prevState.dosageAmount + || nextProps.medicationInstructions.frequency !== prevState.dosageFrequency + || nextProps.medicationInstructions.supplyDuration !== prevState.supplyDuration + || nextProps.selectedConditionCode !== prevState.conditionCode + || nextProps.prescriptionDates.start.value !== prevState.startRange.value + || nextProps.prescriptionDates.end.value !== prevState.endRange.value) { + return ({ + conditionCode: nextProps.selectedConditionCode, + dosageAmount: nextProps.medicationInstructions.number, + dosageFrequency: nextProps.medicationInstructions.frequency, + supplyDuration: nextProps.medicationInstructions.supplyDuration, + startRange: { + // enabled: nextProps.startRange.enabled, + value: nextProps.prescriptionDates.start.value, + }, + endRange: { + // enabled: nextProps.endRange.enabled, + value: nextProps.prescriptionDates.end.value, + }, + }); } + return null; + } - changeMedicationInput(event) { - this.setState({ value: event.target.value }); - debounce(this.props.onMedicationChangeInput(event.target.value), 50); - } + changeMedicationInput(event) { + this.setState({ value: event.target.value }); + debounce(this.props.onMedicationChangeInput(event.target.value), 50); + } - // Note: Bound the dosage amount to a value between 1 and 5 - changeDosageAmount(event) { - let transformedNumber = Number(event.target.value) || 1; - if (transformedNumber > 5) { transformedNumber = 5; } - if (transformedNumber < 1) { transformedNumber = 1; } - this.setState({ dosageAmount: transformedNumber }); - this.props.updateDosageInstructions(transformedNumber, this.state.dosageFrequency); - } + // Note: A second parameter (selected value) is supplied automatically by the Terra onChange function for the Form Select component + selectCondition(event) { + this.props.chooseCondition(event.value); + this.setState({ conditionCode: event.value }); + this.setState({ conditionDisplay: event.label }); + } - // Note: A second parameter (selected value) is supplied automatically by the Terra onChange function for the Form Select component - changeDosageFrequency(event, value) { - this.setState({ dosageFrequency: value }); - this.props.updateDosageInstructions(this.state.dosageAmount, value); - } + // Note: Bound the dosage amount to a value between 1 and 5 + changeDosageAmount(event) { + let transformedNumber = Number(event.target.value) || 1; + if (transformedNumber > 5) { transformedNumber = 5; } + if (transformedNumber < 1) { transformedNumber = 1; } + this.setState({ dosageAmount: transformedNumber }); + this.props.updateDosageInstructions(transformedNumber, this.state.dosageFrequency); + } - changeSupplyDuration(event) { - let transformedNumber = Number(event.target.value) || 1; - if (transformedNumber > 90) { transformedNumber = 90; } - if (transformedNumber < 1) { transformedNumber = 1; } - this.setState( { supplyDuration: transformedNumber } ); - this.props.updateDispenseRequest(transformedNumber, this.state.supplyDuration); - } + // Note: A second parameter (selected value) is supplied automatically by the Terra onChange function for the Form Select component + changeDosageFrequency(event, value) { + this.setState({ dosageFrequency: value }); + this.props.updateDosageInstructions(this.state.dosageAmount, value); + } - // Note: A second parameter (date value) is supplied automatically by the Terra onChange function for the DatePicker component - selectStartDate(event, value) { - const newStartRange = { - enabled: this.state.startRange.enabled, - value, - }; - this.setState({ - startRange: newStartRange, - }); - this.props.updateDate('start', newStartRange); - } + changeSupplyDuration(event) { + let transformedNumber = Number(event.target.value) || 1; + if (transformedNumber > 90) { transformedNumber = 90; } + if (transformedNumber < 1) { transformedNumber = 1; } + this.setState({ supplyDuration: transformedNumber }); + this.props.updateDispenseRequest(transformedNumber, this.state.supplyDuration); + } - // Note: A second parameter (date value) is supplied automatically by the Terra onChange function for the DatePicker component - selectEndDate(event, value) { - const newEndRange = { - enabled: this.state.endRange.enabled, - value, - }; - this.setState({ - endRange: newEndRange, - }); - this.props.updateDate('end', newEndRange); - } + // Note: A second parameter (date value) is supplied automatically by the Terra onChange function for the DatePicker component + selectStartDate(event, value) { + const newStartRange = { + enabled: this.state.startRange.enabled, + value, + }; + this.setState({ + startRange: newStartRange, + }); + this.props.updateDate('start', newStartRange); + } - toggleEnabledDate(event, range) { - this.setState({ [`${range}Range`]: event.target.value }); - this.props.toggleEnabledDate(range); - } + // Note: A second parameter (date value) is supplied automatically by the Terra onChange function for the DatePicker component + selectEndDate(event, value) { + const newEndRange = { + enabled: this.state.endRange.enabled, + value, + }; + this.setState({ + endRange: newEndRange, + }); + this.props.updateDate('end', newEndRange); + } - signOrder(event) { - this.setState({ - medicationState: 'done' - }); - this.props.signOrder(event) - } + toggleEnabledDate(event, range) { + this.setState({ [`${range}Range`]: event.target.value }); + this.props.toggleEnabledDate(range); + } - /** + signOrder(event) { + this.props.signOrder(event); + } + + /** * Create an array of key-value pair objects that React Select component understands * given the Conditions present for the patient */ - createDropdownConditions() { - const conditions = []; - forIn(this.props.patient.conditionsResources, (c) => { - const { code } = c.resource.code.coding[0]; - conditions.push({ - value: code, - label: c.resource.code.text, - }); - }); - return conditions; - } + createDropdownConditions() { + const conditions = []; + forIn(this.props.patient.conditionsResources, (c) => { + const { code } = c.resource.code.coding[0]; + conditions.push({ + value: code, + label: c.resource.code.text, + }); + }); + return conditions; + } - render() { - const isHalfView = this.props.isContextVisible ? styles['half-view'] : ''; - const medicationArray = this.props.medications; + render() { + const isHalfView = this.props.isContextVisible ? styles['half-view'] : ''; + const medicationArray = this.props.medications; - return ( -
-

Rx Sign

- -
- - - - {medicationArray.map((med) => ( - { this.props.chooseMedication(med); }} - > - {

{med.name}

} -
- ))} -
-
- {this.props.prescription ? {this.props.prescription.name} : null} -
- - - - - - - - - - -
-
- - - - - - -
-
- -
-
- -
- ); - } + return ( +
+

Rx Sign

+ +
+ + + + {medicationArray.map((med) => ( + { this.props.chooseMedication(med); }} + > +

{med.name}

+
+ ))} +
+
+ {this.props.prescription ? {this.props.prescription.name} : null} +
+ + + + + + + + + + +
+
+ + + + + + +
+
+ +
+
+ +
+ ); + } } RxSign.propTypes = propTypes; const mapStateToProps = (state) => ({ - isContextVisible: state.hookState.isContextVisible, - patient: state.patientState.currentPatient, - medications: state.medicationState.options[state.medicationState.medListPhase] || [], - prescription: state.medicationState.decisions.prescribable, - medicationInstructions: state.medicationState.medicationInstructions, - dispenseRequest: state.medicationState.dispenseRequest, - prescriptionDates: state.medicationState.prescriptionDates, + isContextVisible: state.hookState.isContextVisible, + patient: state.patientState.currentPatient, + medications: state.medicationState.options[state.medicationState.medListPhase] || [], + prescription: state.medicationState.decisions.prescribable, + medicationInstructions: state.medicationState.medicationInstructions, + dispenseRequest: state.medicationState.dispenseRequest, + prescriptionDates: state.medicationState.prescriptionDates, }); const mapDispatchToProps = (dispatch) => ( - { - onMedicationChangeInput: (input) => { - dispatch(storeUserMedInput(input)); - }, - chooseMedication: (medication) => { - dispatch(storeUserChosenMedication(medication)); - }, - updateDosageInstructions: (amount, frequency) => { - dispatch(storeMedDosageAmount(amount, frequency)); - }, - updateDispenseRequest: (supplyDuration) => { - dispatch(storeDispenseRequest(supplyDuration)) - }, - updateDate: (range, date) => { - dispatch(storeDate(range, date)); - }, - toggleEnabledDate: (range) => { - dispatch(toggleDate(range)); - }, - signOrder: (event) => { - dispatch(signOrder(event)); - }, - takeSuggestion: (suggestion) => { - dispatch(takeSuggestion(suggestion)); - }, - } + { + onMedicationChangeInput: (input) => { + dispatch(storeUserMedInput(input)); + }, + chooseMedication: (medication) => { + dispatch(storeUserChosenMedication(medication)); + }, + updateDosageInstructions: (amount, frequency) => { + dispatch(storeMedDosageAmount(amount, frequency)); + }, + updateDispenseRequest: (supplyDuration) => { + dispatch(storeDispenseRequest(supplyDuration)); + }, + updateDate: (range, date) => { + dispatch(storeDate(range, date)); + }, + toggleEnabledDate: (range) => { + dispatch(toggleDate(range)); + }, + signOrder: (event) => { + dispatch(signOrder(event)); + }, + takeSuggestion: (suggestion) => { + dispatch(takeSuggestion(suggestion)); + }, + } ); export default connect(mapStateToProps, mapDispatchToProps)(RxSign); diff --git a/src/components/RxView/rx-view.jsx b/src/components/RxView/rx-view.jsx index 1afa452..de7214b 100644 --- a/src/components/RxView/rx-view.jsx +++ b/src/components/RxView/rx-view.jsx @@ -7,12 +7,12 @@ import forIn from 'lodash/forIn'; import cx from 'classnames'; import Field from 'terra-form-field'; // import Checkbox from 'terra-form-checkbox'; -import Select from 'react-select' +import Select from 'react-select'; import SelectField from 'terra-form-select'; import Text from 'terra-text'; -import Input, {InputField} from 'terra-form-input'; +import Input, { InputField } from 'terra-form-input'; import DatePicker from 'terra-date-picker'; -import List, {Item} from 'terra-list'; +import List, { Item } from 'terra-list'; import debounce from 'debounce'; @@ -164,29 +164,29 @@ export class RxView extends Component { /** * Update any incoming values that change for state */ - componentWillReceiveProps(nextProps) { - if (nextProps.medicationInstructions.number !== this.state.dosageAmount - || nextProps.medicationInstructions.frequency !== this.state.dosageFrequency - || nextProps.selectedConditionCode !== this.state.conditionCode - || nextProps.prescriptionDates.start.value !== this.state.startRange.value - || nextProps.prescriptionDates.end.value !== this.state.endRange.value) { - this.setState({ + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.medicationInstructions.number !== prevState.dosageAmount + || nextProps.medicationInstructions.frequency !== prevState.dosageFrequency + || nextProps.selectedConditionCode !== prevState.conditionCode + || nextProps.prescriptionDates.start.value !== prevState.startRange.value + || nextProps.prescriptionDates.end.value !== prevState.endRange.value) { + return ({ conditionCode: nextProps.selectedConditionCode, dosageAmount: nextProps.medicationInstructions.number, dosageFrequency: nextProps.medicationInstructions.frequency, startRange: { - enabled: this.state.startRange.enabled, + // enabled: nextProps.startRange.enabled, value: nextProps.prescriptionDates.start.value, }, endRange: { - enabled: this.state.endRange.enabled, + // enabled: nextProps.endRange.enabled, value: nextProps.prescriptionDates.end.value, }, }); } + return null; } - // Note: A second parameter (selected value) is supplied automatically by the Terra onChange function for the Form Select component selectCondition(event) { this.props.chooseCondition(event.value); @@ -296,7 +296,7 @@ export class RxView extends Component { isSelectable onSelect={() => { this.props.chooseMedication(med); }} > - {

{med.name}

} +

{med.name}

))} diff --git a/src/components/ServicesEntry/services-entry.jsx b/src/components/ServicesEntry/services-entry.jsx index c98438c..c647506 100644 --- a/src/components/ServicesEntry/services-entry.jsx +++ b/src/components/ServicesEntry/services-entry.jsx @@ -50,10 +50,11 @@ export class ServicesEntry extends Component { this.handleChange = this.handleChange.bind(this); } - componentWillReceiveProps(nextProps) { - if (this.props.isOpen !== nextProps.isOpen) { - this.setState({ isOpen: nextProps.isOpen }); + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.isOpen !== prevState.isOpen) { + return ({ isOpen: nextProps.isOpen }); } + return null; } handleCloseModal() { @@ -131,7 +132,7 @@ export class ServicesEntry extends Component { inputName="discovery-endpoint-input" /> -Note: See  + Note: See