UNPKG

terra-clinical-result

Version:

The Terra Clinical Result package is a collection of standardized views for presenting clinical results documented to a Patient's Medical Chart, such as Vital Signs, Laboratory Results, and Discretely Documented Values

450 lines (399 loc) 17.5 kB
import React, { useRef, useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import classNamesBind from 'classnames/bind'; import ThemeContext from 'terra-theme-context'; import IconComment from 'terra-icon/lib/icon/IconComment'; import IconModified from 'terra-icon/lib/icon/IconModified'; import IconUnverified from 'terra-icon/lib/icon/IconDiamond'; import VisuallyHiddenText from 'terra-visually-hidden-text'; import { injectIntl } from 'react-intl'; import ClinicalResult from '../ClinicalResult'; import ClinicalResultBloodPressure from '../ClinicalResultBloodPressure'; import observationPropShape from '../proptypes/observationPropTypes'; import EnteredInError from '../common/other/_EnteredInError'; import ResultError from '../common/other/_ResultError'; import NoData from '../common/other/_KnownNoData'; import NumericOverflow from '../common/other/_NumericOverflow'; import { isEmpty, checkIsStatusInError, checkTypeNumeric } from '../common/utils'; import styles from './FlowsheetResultCell.module.scss'; const cx = classNamesBind.bind(styles); const propTypes = { /** * A set of clinical results. Example object structure listed above. . */ resultDataSet: PropTypes.arrayOf(PropTypes.shape({ /** * A single clinical result or blood pressure result id. */ id: PropTypes.string, /** * A single clinical result or blood pressure result. */ resultData: observationPropShape, })), /** * Visually hides the unit of measure when presented in a series of side-by-side columns of the same unit. */ hideUnit: PropTypes.bool, /** * The padding styling to apply to the Time Column Header Cell. * One of `'none'`, `'standard'`, `'compact'`. */ paddingStyle: PropTypes.oneOf(['none', 'standard', 'compact']), /** * Override that shows an Error display. Used when there is a known error or problem when retrieving or assembling the clinical result data. */ hasResultError: PropTypes.bool, /** * Override that shows a known "No Data" display. Used when there is known to be no value for a given clinical result concept at a specific datetime. */ hasResultNoData: PropTypes.bool, /** * @private * The intl object to be injected for translations. */ intl: PropTypes.shape({ formatMessage: PropTypes.func }), }; const defaultProps = { resultDataSet: [], paddingStyle: 'compact', }; const interpretationsWithIcons = [ 'critical', 'critical-high', 'critical-low', 'positive', 'abnormal', 'high', 'low', ]; const createEndIcons = (hasCommentIcon, hasModifiedIcon, hasUnverifiedIcon, resultKeyID, intl) => { if (!hasCommentIcon && !hasModifiedIcon && !hasUnverifiedIcon) { return null; } let iconElements; if (hasUnverifiedIcon) { iconElements = <IconUnverified className={cx('icon-unverified')} a11yLabel={intl.formatMessage({ id: 'Terra.clinicalResult.resultUnverified' })} role="img" focusable="true" />; } else { iconElements = ( <React.Fragment> {hasCommentIcon ? (<IconComment className={cx('icon-comment')} a11yLabel={intl.formatMessage({ id: 'Terra.clinicalResult.resultComment' })} role="img" focusable="true" />) : null} {hasModifiedIcon ? (<IconModified className={cx('icon-modified')} a11yLabel={intl.formatMessage({ id: 'Terra.clinicalResult.resultModified' })} role="img" focusable="true" />) : null} </React.Fragment> ); } return ( <div key={(`EndAccessoryIcons-${resultKeyID}`)} className={cx('end-accessory-icons')}> <div className={cx('end-accessory-stack')}> {iconElements} </div> </div> ); }; const createEndAdditionalResultsStack = (count, interpretationsArr, hasAccessoryIcons, resultKeyID, intl) => { const displayCount = count; if (displayCount < 1) { return null; } let additionalResultInterpretationIndicator; let criticality; if ([ 'critical', 'critical-high', 'critical-low', 'positive', ].some(r => interpretationsArr.indexOf(r) >= 0)) { additionalResultInterpretationIndicator = 'critical'; criticality = 'critical'; } else if ([ 'abnormal', 'high', 'low', ].some(r => interpretationsArr.indexOf(r) >= 0)) { additionalResultInterpretationIndicator = 'high'; criticality = 'out of range'; } else { criticality = 'normal'; } const additionalResultClassNames = cx([ 'additional-end-display', { 'no-accessory-icons': !hasAccessoryIcons }, { 'interpretation-critical': additionalResultInterpretationIndicator === 'critical' }, { 'interpretation-high': additionalResultInterpretationIndicator === 'high' }, ]); const additionalCountDisplayValue = (displayCount > 99) ? (<span className={cx(['additional-results-value', 'additional-results-max-value'])}>99+</span>) : (<span className={cx('additional-results-value')}>{displayCount}</span>); const text = (criticality === 'normal') ? intl.formatMessage({ id: 'Terra.clinicalResult.multipleNormalResults' }, { count }) : intl.formatMessage({ id: 'Terra.clinicalResult.multipleResults' }, { count, criticality }); return ( <div key={(`AdditionalResultsDisplay-${resultKeyID}`)} className={additionalResultClassNames}> <div className={cx('additional-results-stack')} aria-hidden="true"> {additionalCountDisplayValue} {additionalCountDisplayValue} </div> <VisuallyHiddenText text={text} /> </div> ); }; const createClinicalResultDisplay = (children, hasUnverifiedIcon, hasInterpretationIcon, containerDivRef, resultKeyID) => { const primaryResultClassnames = cx([ 'primary-display', { interpretation: hasInterpretationIcon && !hasUnverifiedIcon }, ]); return (<div key={(`ClinicalResultDisplay-${resultKeyID}`)} className={primaryResultClassnames} ref={containerDivRef}>{children}</div>); }; const createStandardResultDisplay = (resultDataItem, resultAttributes, hideUnit, resultKeyID, numericOverflow, containerDivRef) => { let resultsInnerDisplay; if (resultAttributes.statusInError) { resultsInnerDisplay = <EnteredInError />; } else if (numericOverflow) { resultsInnerDisplay = <NumericOverflow />; } else { resultsInnerDisplay = <ClinicalResult key={(`ClinicalResult-${resultKeyID}`)} {...resultDataItem} hideUnit={hideUnit} isTruncated isUnverified={resultAttributes.unverified} hideAccessoryDisplays />; } const clinicalResultDisplay = createClinicalResultDisplay(resultsInnerDisplay, resultAttributes.unverified, resultAttributes.interpretationIcon, containerDivRef, resultKeyID); return clinicalResultDisplay; }; const createBloodPressureResultDisplay = (resultDataItem, resultAttributes, hideUnit, resultKeyID, containerDivRef) => { const { systolic, diastolic, } = resultDataItem; let resultsInnerDisplay; if (resultAttributes.statusInError) { resultsInnerDisplay = <EnteredInError />; } else { resultsInnerDisplay = (<ClinicalResultBloodPressure key={(`ClinicalResultBloodPressure-${resultKeyID}`)} systolic={systolic} diastolic={diastolic} hideUnit={hideUnit} isTruncated hideAccessoryDisplays />); } const clinicalResultDisplay = createClinicalResultDisplay(resultsInnerDisplay, resultAttributes.unverified, resultAttributes.interpretationIcon, containerDivRef, resultKeyID); return clinicalResultDisplay; }; const setResultKeyID = (isBloodPressureResult, resultData) => { if (isBloodPressureResult) { if (resultData.id) { return resultData.id; } if (!isEmpty(resultData.systolic) && resultData.systolic.eventId) { return resultData.systolic.eventId; } if (!isEmpty(resultData.diastolic) && resultData.diastolic.eventId) { return resultData.diastolic.eventId; } } else { if (resultData.id) { return resultData.id; } if (resultData.eventId) { return resultData.eventId; } } return null; }; const checkIfSingleOrPairedResult = (resultDataItem) => { const isSingleResult = !!resultDataItem.result || false; if (isSingleResult) { return { isSingleResult, isPairedResult: false }; } const hasSystolicData = !isEmpty(resultDataItem.systolic) ? resultDataItem.systolic.result : false; const hasDiastolicData = !isEmpty(resultDataItem.diastolic) ? resultDataItem.diastolic.result : false; const isPairedResult = (hasSystolicData || hasDiastolicData) || false; return { isSingleResult, isPairedResult }; }; const AttributesTemplate = (statusInError = false, interpretationValue = false, commentBool = false, modifiedBool = false, unverifiedBool = false) => ({ statusInError, interpretationIcon: !!interpretationValue, comment: commentBool, modified: modifiedBool, unverified: unverifiedBool, }); const unpackResultAttributes = (resultDataItem) => { const { status, interpretation, hasComment, isModified, isUnverified, } = resultDataItem; const itemAttributes = new AttributesTemplate(); itemAttributes.statusInError = !isEmpty(status) ? checkIsStatusInError(status) : false; itemAttributes.interpretationIcon = !itemAttributes.statusInError && interpretationsWithIcons.includes(interpretation); itemAttributes.comment = hasComment; itemAttributes.modified = isModified; itemAttributes.unverified = isUnverified; return itemAttributes; }; const unpackResultDataSet = (resultDataSet) => { const firstResultData = resultDataSet[0]; let firstResultAttributes = {}; const { isSingleResult, isPairedResult } = checkIfSingleOrPairedResult(firstResultData); if (isSingleResult) { firstResultAttributes = unpackResultAttributes(firstResultData); } else if (isPairedResult) { const bpAttribute = { systolic: null, diastolic: null, }; const systolicData = firstResultData.systolic; const diastolicData = firstResultData.diastolic; bpAttribute.systolic = !isEmpty(systolicData) ? unpackResultAttributes(systolicData) : new AttributesTemplate(); bpAttribute.diastolic = !isEmpty(diastolicData) ? unpackResultAttributes(diastolicData) : new AttributesTemplate(); firstResultAttributes = new AttributesTemplate( (bpAttribute.systolic.statusInError || bpAttribute.diastolic.statusInError), (bpAttribute.systolic.interpretationIcon), (bpAttribute.systolic.comment || bpAttribute.diastolic.comment), (bpAttribute.systolic.modified || bpAttribute.diastolic.modified), (bpAttribute.systolic.unverified || bpAttribute.diastolic.unverified), ); } const isfirstSingleResult = isSingleResult; const isfirstPairedResult = isPairedResult; const resultKeyID = setResultKeyID(isfirstPairedResult, firstResultData); return { isfirstSingleResult, isfirstPairedResult, firstResultAttributes, firstResultData, resultKeyID, }; }; const createFlowsheetResultCellDisplay = (resultDataSet, hideUnit, numericOverflow, containerDivRef, intl) => { const { isfirstSingleResult, isfirstPairedResult, firstResultAttributes, firstResultData, resultKeyID, } = unpackResultDataSet(resultDataSet); const hasAccessoryIcons = (firstResultAttributes.comment || firstResultAttributes.modified || firstResultAttributes.unverified); const compositeCell = []; if (!isfirstSingleResult && !isfirstPairedResult) { compositeCell.push(<ResultError />); } else if (isfirstSingleResult) { const firstResultDisplay = createStandardResultDisplay(firstResultData, firstResultAttributes, hideUnit, resultKeyID, numericOverflow, containerDivRef); compositeCell.push(firstResultDisplay); } else { const firstResultDisplay = createBloodPressureResultDisplay(firstResultData, firstResultAttributes, hideUnit, resultKeyID, containerDivRef); compositeCell.push(firstResultDisplay); } const additionalResultCount = resultDataSet.length - 1; if (additionalResultCount > 0) { const additionalResultInterpretations = []; const additionalResultList = resultDataSet.slice(1, resultDataSet.length); additionalResultList.forEach((result) => { const { isSingleResult, isPairedResult } = checkIfSingleOrPairedResult(result); if (isSingleResult) { const isStatusInError = !isEmpty(result.status) ? checkIsStatusInError(result.status) : false; if (!isStatusInError) { const resultInterpretation = !isEmpty(result.interpretation) && !result.isUnverified ? result.interpretation : null; additionalResultInterpretations.push(resultInterpretation); } } else if (isPairedResult) { const isStatusInError = { systolic: !isEmpty(result.systolic) ? checkIsStatusInError(result.systolic.status) : false, diastolic: !isEmpty(result.diastolic) ? checkIsStatusInError(result.diastolic.status) : false, }; if (!isStatusInError.systolic && !isStatusInError.diastolic) { const sysInterpretation = !isEmpty(result.systolic.interpretation) && !result.systolic.isUnverified ? result.systolic.interpretation : null; const diaInterpretation = !isEmpty(result.diastolic.interpretation) && !result.diastolic.isUnverified ? result.diastolic.interpretation : null; additionalResultInterpretations.push(sysInterpretation); additionalResultInterpretations.push(diaInterpretation); } } }); const displayCount = additionalResultCount + 1; const additionalResultsStack = createEndAdditionalResultsStack(displayCount, additionalResultInterpretations, hasAccessoryIcons, resultKeyID, intl); // This handles the case for when additional results exist and accessory icons exist if (hasAccessoryIcons) { const endAccessoryIcons = createEndIcons(firstResultAttributes.comment, firstResultAttributes.modified, firstResultAttributes.unverified, resultKeyID, intl); // Here the additional results stack and accessory icons are being wrapped in a parent container // They need to be grouped together for styling purposes, otherwise the order they appear in will be flipped // To keep them in the proper order this parent container gets floated in the css to the right instead of the additional results stack and accessory icons individually const endDisplay = ( <div key="EndDisplay-AdditionalResultsAndIcons" className={cx('end-display')}> {additionalResultsStack} {endAccessoryIcons} </div> ); compositeCell.push(endDisplay); return <div className={cx('combined-display')}>{compositeCell}</div>; } const additionalResultsStackDisplay = ( <div key="EndDisplay-AdditionalResults" className={cx('end-display')}> {additionalResultsStack} </div> ); compositeCell.push(additionalResultsStackDisplay); return <div className={cx('combined-display')}>{compositeCell}</div>; } if (hasAccessoryIcons) { const endAccessoryIcons = createEndIcons(firstResultAttributes.comment, firstResultAttributes.modified, firstResultAttributes.unverified, resultKeyID, intl); const endAccessoryIconsDisplay = ( <div key="EndDisplay-Icons" className={cx('end-display')}> {endAccessoryIcons} </div> ); compositeCell.push(endAccessoryIconsDisplay); } return <div className={cx('combined-display')}>{compositeCell}</div>; }; const FlowsheetResultCell = (props) => { const { resultDataSet, hideUnit, paddingStyle, hasResultError, hasResultNoData, intl, ...customProps } = props; const containerDiv = useRef(null); const [numericOverflow, setNumericOverflow] = useState(false); useEffect(() => { if (!containerDiv.current || !resultDataSet[0]) { return; } if (checkTypeNumeric(resultDataSet[0])) { const contentWidth = containerDiv.current.children[0].getBoundingClientRect().width; const containerWidth = containerDiv.current.getBoundingClientRect().width; if (containerWidth <= contentWidth) { setNumericOverflow(true); } else if (containerWidth > contentWidth) { setNumericOverflow(false); } } }, [resultDataSet]); let flowsheetResultCellDisplay; if (hasResultError) { flowsheetResultCellDisplay = <div key="ClinicalResultDisplay-Error" className={cx(['primary-display', 'error'])}><ResultError /></div>; } else if (hasResultNoData) { flowsheetResultCellDisplay = <div key="ClinicalResultDisplay-NoData" className={cx('primary-display')}><NoData /></div>; } else if (!resultDataSet || !resultDataSet.length) { flowsheetResultCellDisplay = <div key="ClinicalResultDisplay-Error" className={cx(['primary-display', 'error'])}><ResultError /></div>; } else { flowsheetResultCellDisplay = createFlowsheetResultCellDisplay(resultDataSet, hideUnit, numericOverflow, containerDiv, intl); } const theme = React.useContext(ThemeContext); const flowsheetCellClassNames = classNames( cx( 'flowsheet-result-cell', { 'padding-standard': paddingStyle === 'standard' }, { 'padding-compact': paddingStyle === 'compact' }, theme.className, ), customProps.className, ); return ( <td {...customProps} className={flowsheetCellClassNames} > {flowsheetResultCellDisplay} </td> ); }; FlowsheetResultCell.propTypes = propTypes; FlowsheetResultCell.defaultProps = defaultProps; export default injectIntl(FlowsheetResultCell);