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..e2809e8 --- /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, + }; +} 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/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/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.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..1d599a3 --- /dev/null +++ b/src/components/RxSign/rx-sign.jsx @@ -0,0 +1,447 @@ +/* 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, + /** + * 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, + /** + * 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: '', + /** + * 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.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 + */ + 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); + } + + // 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: 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.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..de7214b 100644 --- a/src/components/RxView/rx-view.jsx +++ b/src/components/RxView/rx-view.jsx @@ -6,13 +6,13 @@ 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 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 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  { }; } + // 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 {