diff --git a/app/pages/dashboard/PatientDrawer/CGMDeltaSummary/index.js b/app/components/PatientDrawer/CGMDeltaSummary/index.js similarity index 99% rename from app/pages/dashboard/PatientDrawer/CGMDeltaSummary/index.js rename to app/components/PatientDrawer/CGMDeltaSummary/index.js index 238b1ec451..ed38fbb8e1 100644 --- a/app/pages/dashboard/PatientDrawer/CGMDeltaSummary/index.js +++ b/app/components/PatientDrawer/CGMDeltaSummary/index.js @@ -6,7 +6,7 @@ import { utils as vizUtils, colors as vizColors } from '@tidepool/viz'; import styled from '@emotion/styled'; const { bankersRound } = vizUtils.stat; const { getTimezoneFromTimePrefs } = vizUtils.datetime; -import { MS_IN_HOUR } from '../../../../core/constants'; +import { MS_IN_HOUR } from '../../../core/constants'; import getReportDaysText from '../getReportDaysText'; diff --git a/app/pages/dashboard/PatientDrawer/CGMStatistics/index.js b/app/components/PatientDrawer/CGMStatistics/index.js similarity index 98% rename from app/pages/dashboard/PatientDrawer/CGMStatistics/index.js rename to app/components/PatientDrawer/CGMStatistics/index.js index 5645dbab2b..45e9826a72 100644 --- a/app/pages/dashboard/PatientDrawer/CGMStatistics/index.js +++ b/app/components/PatientDrawer/CGMStatistics/index.js @@ -5,7 +5,7 @@ import { Flex, Box, Text } from 'theme-ui'; import { utils as vizUtils } from '@tidepool/viz'; const { formatDatum, bankersRound } = vizUtils.stat; const { getTimezoneFromTimePrefs } = vizUtils.datetime; -import { MGDL_UNITS } from '../../../../core/constants'; +import { MGDL_UNITS } from '../../../core/constants'; import getReportDaysText from '../getReportDaysText'; const TableRow = ({ label, sublabel, value, units, id }) => { diff --git a/app/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.js b/app/components/PatientDrawer/MenuBar/CGMClipboardButton.js similarity index 94% rename from app/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.js rename to app/components/PatientDrawer/MenuBar/CGMClipboardButton.js index bc17372335..6dd7cf9060 100644 --- a/app/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.js +++ b/app/components/PatientDrawer/MenuBar/CGMClipboardButton.js @@ -1,8 +1,8 @@ import React, { useEffect, useState, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; -import Button from '../../../../components/elements/Button'; -import { MS_IN_HOUR } from '../../../../core/constants'; +import Button from '../../../components/elements/Button'; +import { MS_IN_HOUR } from '../../../core/constants'; import { Box, Flex } from 'theme-ui'; import { utils as vizUtils } from '@tidepool/viz'; const { agpCGMText } = vizUtils.text; diff --git a/app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js b/app/components/PatientDrawer/MenuBar/MenuBar.js similarity index 91% rename from app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js rename to app/components/PatientDrawer/MenuBar/MenuBar.js index 8cc65c6bf8..724221191a 100644 --- a/app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js +++ b/app/components/PatientDrawer/MenuBar/MenuBar.js @@ -1,20 +1,21 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; -import * as actions from '../../../../redux/actions'; +import * as actions from '../../../redux/actions'; import { useSelector, useDispatch } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { push } from 'connected-react-router'; import { Flex, Box, Text } from 'theme-ui'; import { colors as vizColors } from '@tidepool/viz'; -import Button from '../../../../components/elements/Button'; +import Button from '../../../components/elements/Button'; import moment from 'moment'; -import PatientLastReviewed from '../../../../components/clinic/PatientLastReviewed'; +import PatientLastReviewed from '../../../components/clinic/PatientLastReviewed'; import { useFlags } from 'launchdarkly-react-client-sdk'; import CGMClipboardButton from './CGMClipboardButton'; -import api from '../../../../core/api'; +import api from '../../../core/api'; import { map, keys } from 'lodash'; -import copyIcon from '../../../../core/icons/copyIcon.svg'; -import viewIcon from '../../../../core/icons/viewIcon.svg'; +import copyIcon from '../../../core/icons/copyIcon.svg'; +import viewIcon from '../../../core/icons/viewIcon.svg'; +import { trackMetric } from '../../../core/metricUtils'; export const OVERVIEW_TAB_INDEX = 0; export const STACKED_DAILY_TAB_INDEX = 1; @@ -32,7 +33,7 @@ const tabs = { }, }; -const MenuBar = ({ patientId, onClose, onSelectTab, selectedTab, trackMetric }) => { +const MenuBar = ({ patientId, onClose, onSelectTab, selectedTab }) => { const dispatch = useDispatch(); const { t } = useTranslation(); const { showTideDashboardLastReviewed } = useFlags(); @@ -157,7 +158,6 @@ MenuBar.propTypes = { onClose: PropTypes.func.isRequired, onSelectTab: PropTypes.func.isRequired, selectedTab: PropTypes.number.isRequired, - trackMetric: PropTypes.func.isRequired, }; export default MenuBar; diff --git a/app/pages/dashboard/PatientDrawer/MenuBar/index.js b/app/components/PatientDrawer/MenuBar/index.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/MenuBar/index.js rename to app/components/PatientDrawer/MenuBar/index.js diff --git a/app/pages/dashboard/PatientDrawer/Overview.js b/app/components/PatientDrawer/Overview.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/Overview.js rename to app/components/PatientDrawer/Overview.js diff --git a/app/pages/dashboard/PatientDrawer/PatientDrawer.js b/app/components/PatientDrawer/PatientDrawer.js similarity index 84% rename from app/pages/dashboard/PatientDrawer/PatientDrawer.js rename to app/components/PatientDrawer/PatientDrawer.js index 92df8f3673..aa0f829b39 100644 --- a/app/pages/dashboard/PatientDrawer/PatientDrawer.js +++ b/app/components/PatientDrawer/PatientDrawer.js @@ -1,19 +1,17 @@ import React from 'react'; -import Icon from '../../../components/elements/Icon'; +import Icon from '../../components/elements/Icon'; import { useLocation, useHistory } from 'react-router-dom'; import Drawer from '@material-ui/core/Drawer'; import styled from '@emotion/styled'; import { makeStyles } from '@material-ui/core/styles'; import CloseRoundedIcon from '@material-ui/icons/CloseRounded'; import { Box } from 'theme-ui'; -import { useFlags } from 'launchdarkly-react-client-sdk'; import Overview from './Overview'; import StackedDaily from './StackedDaily'; import MenuBar, { OVERVIEW_TAB_INDEX, STACKED_DAILY_TAB_INDEX } from './MenuBar'; import useAgpCGM from './useAgpCGM'; -import { shadows } from '../../../themes/baseTheme'; -import { useScrollToTop } from '../../../core/hooks'; +import { shadows } from '../../themes/baseTheme'; const StyledCloseButton = styled(Icon)` position: absolute; @@ -53,7 +51,7 @@ const getAgpPeriodInDays = (period) => { } }; -const DrawerContent = ({ patientId, onClose, api, trackMetric, period }) => { +const DrawerContent = ({ patientId, onClose, api, period }) => { // Only rendered when patient is selected and isOpen is true // this will also allow the hook to dismount and for the cleanup to be called const location = useLocation(); @@ -64,7 +62,6 @@ const DrawerContent = ({ patientId, onClose, api, trackMetric, period }) => { const agpPeriodInDays = getAgpPeriodInDays(period); const agpCGMData = useAgpCGM(api, patientId, agpPeriodInDays); const contentRef = React.useRef(undefined); - useScrollToTop(contentRef?.current, [selectedTab]); function setDrawerTabParam(tabIndex) { const { search, pathname } = location; @@ -76,16 +73,17 @@ const DrawerContent = ({ patientId, onClose, api, trackMetric, period }) => { function handleSelectTab(tabIndex) { setSelectedTab(parseInt(tabIndex, 10)); setDrawerTabParam(tabIndex); + contentRef?.current?.scrollTo(0, 0); } const handleContentScroll = (e) => { setScrolledToTop((e.target.scrollTop || 0) <= 5); - } + }; return ( <> - + @@ -108,12 +106,10 @@ const DrawerContent = ({ patientId, onClose, api, trackMetric, period }) => { ) } -const PatientDrawer = ({ patientId, onClose, api, trackMetric, period }) => { +const PatientDrawer = ({ patientId, onClose, api, period }) => { const classes = useStyles(); - const { showTideDashboardPatientDrawer } = useFlags(); - const isOpen = !!patientId && isValidAgpPeriod(period); - if (!showTideDashboardPatientDrawer) return null; + const isOpen = !!patientId && isValidAgpPeriod(period); return ( { flexDirection: 'column', }} > - {isOpen && } + {isOpen && } ); diff --git a/app/pages/dashboard/PatientDrawer/StackedDaily.js b/app/components/PatientDrawer/StackedDaily.js similarity index 97% rename from app/pages/dashboard/PatientDrawer/StackedDaily.js rename to app/components/PatientDrawer/StackedDaily.js index 246c50175c..d09fab05ea 100644 --- a/app/pages/dashboard/PatientDrawer/StackedDaily.js +++ b/app/components/PatientDrawer/StackedDaily.js @@ -15,13 +15,13 @@ const { getLocalizedCeiling } = vizUtils.datetime; import tidelineBlip from 'tideline/plugins/blip'; const chartDailyFactory = tidelineBlip.oneday; -import { MS_IN_DAY } from '../../../core/constants'; +import { MS_IN_DAY } from '../../core/constants'; import { NoPatientData } from './Overview'; import { STATUS } from './useAgpCGM'; -import { Body1, Body2 } from '../../../components/elements/FontStyles'; -import Button from '../../../components/elements/Button'; +import { Body1, Body2 } from '../../components/elements/FontStyles'; +import Button from '../../components/elements/Button'; import { STACKED_DAILY_TAB_INDEX } from './MenuBar'; -import BgLegend from '../../../components/chart/BgLegend'; +import BgLegend from '../../components/chart/BgLegend'; const CHART_HEIGHT = 200; diff --git a/app/pages/dashboard/PatientDrawer/getReportDaysText.js b/app/components/PatientDrawer/getReportDaysText.js similarity index 94% rename from app/pages/dashboard/PatientDrawer/getReportDaysText.js rename to app/components/PatientDrawer/getReportDaysText.js index 7e88335997..36f62dc66a 100644 --- a/app/pages/dashboard/PatientDrawer/getReportDaysText.js +++ b/app/components/PatientDrawer/getReportDaysText.js @@ -1,5 +1,5 @@ import moment from 'moment'; -import { MS_IN_MIN } from '../../../core/constants'; +import { MS_IN_MIN } from '../../core/constants'; import isNumber from 'lodash/isNumber'; import { utils as vizUtils } from '@tidepool/viz'; const { getOffset, formatDateRange } = vizUtils.datetime; diff --git a/app/pages/dashboard/PatientDrawer/index.js b/app/components/PatientDrawer/index.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/index.js rename to app/components/PatientDrawer/index.js diff --git a/app/pages/dashboard/PatientDrawer/useAgpCGM/getOpts.js b/app/components/PatientDrawer/useAgpCGM/getOpts.js similarity index 95% rename from app/pages/dashboard/PatientDrawer/useAgpCGM/getOpts.js rename to app/components/PatientDrawer/useAgpCGM/getOpts.js index f97ece5587..d3597c9f2f 100644 --- a/app/pages/dashboard/PatientDrawer/useAgpCGM/getOpts.js +++ b/app/components/PatientDrawer/useAgpCGM/getOpts.js @@ -2,8 +2,8 @@ import moment from 'moment-timezone'; import _ from 'lodash'; import get from 'lodash/get'; import { utils as vizUtils } from '@tidepool/viz'; -import utils from '../../../../core/utils'; -import { getMostRecentDatumTimeByChartType } from '../../../../core/dataViewUtils'; +import utils from '../../../core/utils'; +import { getMostRecentDatumTimeByChartType } from '../../../core/dataViewUtils'; const getTimezoneFromTimePrefs = vizUtils.datetime.getTimezoneFromTimePrefs; diff --git a/app/pages/dashboard/PatientDrawer/useAgpCGM/getQueries.js b/app/components/PatientDrawer/useAgpCGM/getQueries.js similarity index 95% rename from app/pages/dashboard/PatientDrawer/useAgpCGM/getQueries.js rename to app/components/PatientDrawer/useAgpCGM/getQueries.js index 6d6750c203..1d1af429f0 100644 --- a/app/pages/dashboard/PatientDrawer/useAgpCGM/getQueries.js +++ b/app/components/PatientDrawer/useAgpCGM/getQueries.js @@ -2,8 +2,8 @@ import _ from 'lodash'; import { utils as vizUtils } from '@tidepool/viz'; const { commonStats } = vizUtils.stat; -import utils from '../../../../core/utils'; -import { DEFAULT_GLYCEMIC_RANGES } from '../../../../core/glycemicRangesUtils'; +import utils from '../../../core/utils'; +import { DEFAULT_GLYCEMIC_RANGES } from '../../../core/glycemicRangesUtils'; const getQueries = ( data, diff --git a/app/pages/dashboard/PatientDrawer/useAgpCGM/index.js b/app/components/PatientDrawer/useAgpCGM/index.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/useAgpCGM/index.js rename to app/components/PatientDrawer/useAgpCGM/index.js diff --git a/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.js b/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js similarity index 94% rename from app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.js rename to app/components/PatientDrawer/useAgpCGM/useAgpCGM.js index 5104fbdc3f..920a57f85e 100644 --- a/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.js +++ b/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js @@ -1,12 +1,12 @@ import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import * as actions from '../../../../redux/actions'; +import * as actions from '../../../redux/actions'; import moment from 'moment'; import getOpts from './getOpts'; import getQueries from './getQueries'; import { cloneDeep } from 'lodash'; -import { useGenerateAGPImages } from '../../../../core/agpUtils'; +import { useGenerateAGPImages } from '../../../core/agpUtils'; export const STATUS = { // States in order of happy path AGP generation sequence @@ -134,8 +134,8 @@ const useAgpCGM = ( return { status: lastCompletedStep, svgDataURLS: isCorrectPatientInState ? pdf.opts?.svgDataURLS : null, - agpCGM: isCorrectPatientInState ? cloneDeep(pdf.data?.agpCGM) : null, - offsetAgpCGM: isCorrectPatientInState ? cloneDeep(pdf.data?.offsetAgpCGM) : null, + agpCGM: isCorrectPatientInState ? pdf.data?.agpCGM : null, + offsetAgpCGM: isCorrectPatientInState ? pdf.data?.offsetAgpCGM : null, }; }; diff --git a/app/core/navutils.js b/app/core/navutils.js index 375e32273e..55d3b92c77 100644 --- a/app/core/navutils.js +++ b/app/core/navutils.js @@ -44,11 +44,11 @@ export const getPatientListLink = (clinicFlowActive, selectedClinicId, query, pa const drawerTab = query?.drawerTab; if (dashboard && drawerTab !== undefined && patientId) { - return `/dashboard/${dashboard}?drawerPatientId=${patientId}&drawerTab=${drawerTab}`; + return `/clinic-workspace/tide-dashboard?drawerPatientId=${patientId}&drawerTab=${drawerTab}`; } if (dashboard) { - return `/dashboard/${dashboard}`; + return '/clinic-workspace/tide-dashboard'; } if (clinicFlowActive && selectedClinicId) { diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index 31f5478f4c..e179f9e7a4 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -1,19 +1,251 @@ import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { Box, Text } from 'theme-ui'; +import { useDispatch, useSelector } from 'react-redux'; +import { useLocation, useHistory } from 'react-router-dom'; +import { useTranslation, withTranslation } from 'react-i18next'; +import { Box, Flex, Text } from 'theme-ui'; +import { utils as vizUtils, colors as vizColors } from '@tidepool/viz'; +const { bankersRound } = vizUtils.stat; +import { MGDL_UNITS } from '../../../core/constants'; +import { push } from 'connected-react-router'; +import { colors } from '../../../themes/baseTheme'; + +import BgSummaryCell from '../../../components/clinic/BgSummaryCell'; +import DeltaBar from '../../../components/elements/DeltaBar'; +import utils from '../../../core/utils'; +import { OVERVIEW_TAB_INDEX } from '../../../components/PatientDrawer/MenuBar/MenuBar'; +import { useFlags } from 'launchdarkly-react-client-sdk'; +import { CATEGORY } from './FilterByCategory'; +import isUndefined from 'lodash/isUndefined'; + +export const COMPACT = '@container (max-width: 1200px)'; export const PatientCell = ({ patient }) => { const { t } = useTranslation(); + const { search, pathname } = useLocation(); + const history = useHistory(); + const dispatch = useDispatch(); const { fullName, birthDate, mrn } = patient || {}; - return - {fullName} - {t('DOB:')} {birthDate} - {mrn && , {t('MRN: {{mrn}}', { mrn: mrn })}} + const handleClick = () => { + if (!patient.id) return; + + const params = new URLSearchParams(search); + params.set('drawerPatientId', patient.id); + params.set('drawerTab', OVERVIEW_TAB_INDEX); + + history.replace({ pathname, search: params.toString() }); + }; + + return + {fullName} + {t('DOB:')} {birthDate} + {mrn && {t('MRN: {{mrn}}', { mrn: mrn })}} ; }; +export const NumericTemplateCell = ({ value, isPercent = false }) => { + if (!value) return ; + + return {value} {isPercent && '%'}; +}; + +export const AvgGlucoseHeader = withTranslation()(({ t }) => ( + <> + {t('Avg Glucose')} + {t('Avg Gluc.')} + +)); + +export const AvgGlucoseCell = ({ patient, units }) => { // TODO: Fix for units + const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); + const rawValue = patient?.summary?.cgmStats?.periods?.[summaryPeriod]?.averageGlucoseMmol; + const value = utils.formatDecimal(rawValue, 1); + + return ; +}; + +export const TimeInRangePercentBarChartCell = ({ patient }) => { + const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); + const selectedClinicId = useSelector(state => state.blip.selectedClinicId); + const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]); + const clinicBgUnits = clinic?.preferredBgUnits || MGDL_UNITS; + + // TODO: need to add showExtremeHigh + + return ; +}; + +export const TimeInTargetPercentCell = ({ patient }) => { + const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); + const rawValue = patient?.summary?.cgmStats?.periods?.[summaryPeriod]?.timeInTargetPercent; + let value = utils.formatDecimal(rawValue * 100, 0); + + if (isUndefined(rawValue)) value = ''; + + return ; +}; + +export const TimeInVeryLowPercentCell = ({ patient }) => { + const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); + const rawValue = patient?.summary?.cgmStats?.periods?.[summaryPeriod]?.timeInVeryLowPercent; + const value = utils.formatDecimal(rawValue * 100, 0); + + return ; +}; + +export const TimeInAnyLowPercentCell = ({ patient }) => { + const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); + const rawValue = patient?.summary?.cgmStats?.periods?.[summaryPeriod]?.timeInAnyLowPercent; + const value = utils.formatDecimal(rawValue * 100, 0); + + return ; +}; + +export const TimeInVeryHighPercentCell = ({ patient }) => { + const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); + const rawValue = patient?.summary?.cgmStats?.periods?.[summaryPeriod]?.timeInVeryHighPercent; + const value = utils.formatDecimal(rawValue * 100, 0); + + return ; +}; + +export const TimeInAnyHighPercentCell = ({ patient }) => { + const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); + const rawValue = patient?.summary?.cgmStats?.periods?.[summaryPeriod]?.timeInAnyHighPercent; + const value = utils.formatDecimal(rawValue * 100, 0); + + return ; +}; + +export const GMICell = ({ patient }) => { + const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); + const value = patient?.summary?.cgmStats?.periods?.[summaryPeriod]?.glucoseManagementIndicator; + + return ; +}; + +export const CGMUseCell = ({ patient }) => { + const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); + const rawValue = patient?.summary?.cgmStats?.periods?.[summaryPeriod]?.timeCGMUsePercent; + const value = utils.formatDecimal(rawValue * 100, 0); + + return ; +}; + +export const ChangeTIRHeader = withTranslation()(({ t }) => ( + <> + {t('% Change in TIR')} + {t('% Δ TIR')} + +)); + +export const ChangeTIRCell = ({ patient }) => { + const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); + const timeInTargetPercentDelta = patient?.summary?.cgmStats?.periods?.[summaryPeriod]?.timeInTargetPercentDelta; + + if (!timeInTargetPercentDelta) return -; + + const compactDisplayValue = utils.formatDecimal(timeInTargetPercentDelta * 100, 1); + + return <> + + + + + + + + ; +}; + +export const FlagCell = ({ patient, category = null, }) => { + const { t } = useTranslation(); + const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); + const period = patient?.summary?.cgmStats?.periods?.[summaryPeriod]; + + const { VERY_LOW, ANY_LOW, DROP_IN_TIR, ANY_HIGH, VERY_HIGH, LOW_CGM_WEAR, TARGET } = CATEGORY; + + if (!period) return null; + + const rangeName = (() => { + switch(true) { + // Current dashboard category takes priority + case category === VERY_LOW: return 'veryLow'; + case category === ANY_LOW: return 'anyLow'; + case category === VERY_HIGH: return 'veryHigh'; + case category === ANY_HIGH: return 'anyHigh'; + case category === DROP_IN_TIR: return 'dropInTIR'; + case category === LOW_CGM_WEAR: return 'lowSensorUsage'; + case category === TARGET: return 'target'; + + // If no category, then read from summary + case period.timeInVeryLowPercent >= 0.005: return 'veryLow'; // >=1% + case period.timeInAnyLowPercent >= 0.035: return 'anyLow'; // >=4% + case period.timeInTargetPercentDelta <= -0.145: return 'dropInTIR'; // <=-15% + case period.timeInAnyHighPercent >= 0.245: return 'anyHigh'; // >=25% + case period.timeInVeryHighPercent >= 0.045: return 'veryHigh'; // >=5% + case period.timeCGMUsePercent < 0.695: return 'lowSensorUsage'; // <70% + + default: return null; + } + })(); + + if (!rangeName) return null; + + const flagLabels = { + veryLow: t('Very Low'), + anyLow: t('Low'), + veryHigh: t('Very High'), + anyHigh: t('High'), + dropInTIR: t('Large Drop in TIR'), + lowSensorUsage: t('Low CGM Wear Time'), + target: t('Meeting Targets'), + }; + + const flagColor = colors.bg[rangeName] || vizColors.gold30; + + return ( + + + + + + {flagLabels[rangeName] || ''} + + + + ); +}; + export default { PatientCell, + NumericTemplateCell, + AvgGlucoseCell, + TimeInRangePercentBarChartCell, + TimeInVeryLowPercentCell, + ChangeTIRCell, + GMICell, + CGMUseCell, + FlagCell, }; diff --git a/app/pages/clinicworkspace/TideDashboardV2/PatientDrawerController.js b/app/pages/clinicworkspace/TideDashboardV2/PatientDrawerController.js new file mode 100644 index 0000000000..aae581d4d5 --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/PatientDrawerController.js @@ -0,0 +1,30 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { useLocation, useHistory } from 'react-router-dom'; +import PatientDrawer from '../../../components/PatientDrawer/PatientDrawer'; + +const PatientDrawerController = ({ api }) => { + const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); + const { search, pathname } = useLocation(); + const history = useHistory(); + + const drawerPatientId = new URLSearchParams(search)?.get('drawerPatientId') || null; + + const handleClose = () => { + const params = new URLSearchParams(search); + params.delete('drawerPatientId'); + params.delete('drawerTab'); + history.replace({ pathname, search: params.toString() }); + }; + + return ( + + ); +}; + +export default PatientDrawerController; diff --git a/app/pages/clinicworkspace/TideDashboardV2/TableCategoryHeader.js b/app/pages/clinicworkspace/TideDashboardV2/TableCategoryHeader.js index 7d50d6fcb3..4844fb8dad 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/TableCategoryHeader.js +++ b/app/pages/clinicworkspace/TideDashboardV2/TableCategoryHeader.js @@ -13,9 +13,8 @@ const formatThreshold = (value, bgUnits) => bgUnits === MGDL_UNITS ? value : utils.formatDecimal(value, 1); -const useCategoryHeaderCopy = () => { +const useCategoryHeaderCopy = (category) => { const { t } = useTranslation(); - const category = useSelector(state => state.blip.tideDashboard.category); const selectedClinicId = useSelector(state => state.blip.selectedClinicId); const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]); const bgUnits = clinic?.preferredBgUnits || MGDL_UNITS; @@ -68,8 +67,8 @@ const useCategoryHeaderCopy = () => { } }; -const TableCategoryHeader = () => { - const { title, label } = useCategoryHeaderCopy(); +const TableCategoryHeader = ({ category }) => { + const { title, label } = useCategoryHeaderCopy(category); return diff --git a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js index 78abd0fb07..a4e7bf042a 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js +++ b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js @@ -11,13 +11,13 @@ import FilterBySummaryPeriod from './FilterBySummaryPeriod'; import TableCategoryHeader from './TableCategoryHeader'; import PaginationControls from '../components/PaginationControls'; +import PatientDrawerController from './PatientDrawerController'; -import TagListCell from '../components/TagListCell'; -import { PatientCell } from './Cells'; import { setOffset } from './tideDashboardSlice'; import { useGetTideDashboardPatientsQuery } from './tideDashboardApi'; import useDerivedDataRecencyEndpoints from './useDerivedDataRecencyEndpoints'; import usePruneInvalidFilters from './usePruneInvalidFilters'; +import useTableColumns from './useTableColumns'; import EmptyContentNode from './EmptyContentNode'; import FilterBySites from './FilterBySites'; import PatientCount from '../components/PatientCount'; @@ -27,7 +27,7 @@ const LIMIT = 12; const Gap = () => ; -const TideDashboard = () => { +const TideDashboard = ({ api }) => { const { t } = useTranslation(); const dispatch = useDispatch(); @@ -45,6 +45,12 @@ const TideDashboard = () => { { skip: !selectedClinicId } ); + // Sync category to data fetching resolution; prevents visual glitch due to + // category updating view before the API call resolves and updates it again + const resolvedCategory = data?.category || category; + + const tableColumns = useTableColumns(resolvedCategory); + const handleChangeOffset = (newOffset) => dispatch(setOffset(newOffset)); if (!data) return null; @@ -68,28 +74,17 @@ const TideDashboard = () => { - + }, - { title: t('Flag'), field: '', align: 'left' }, - { title: t('Avg Glucose'), field: '', align: 'left' }, - { title: t('% TIR'), field: '', align: 'left' }, - { title: t('% Time in Range'), field: '', align: 'left' }, - { title: t('% Change in TIR'), field: '', align: 'left' }, - { title: t('GMI'), field: '', align: 'left' }, - { title: t('CGM Use'), field: '', align: 'left' }, - { title: t('Tags'), field: 'tags', align: 'left', render: patient => }, - { title: t('Last Reviewed'), field: '', align: 'left' }, - { title: t(''), field: '', align: 'left' }, // More - ]} + columns={tableColumns} data={tableData} emptyContentNode={} + containerProps={{ sx: { containerType: 'inline-size' } }} /> @@ -106,6 +101,8 @@ const TideDashboard = () => { + + ); }; diff --git a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js index 4e41a5e40e..2cec1cd032 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js +++ b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js @@ -49,6 +49,10 @@ const tideDashboardApi = RTKQueryApi.injectEndpoints({ params, }; }, + transformResponse: (response, _meta, arg) => ({ + ...response, + category: arg.category, + }), }), }), }); diff --git a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardSlice.js b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardSlice.js index 16a1e851fd..b78d9d9905 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardSlice.js +++ b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardSlice.js @@ -21,5 +21,9 @@ const tideDashboardSlice = createSlice({ }, }); -export const { setCategory, setOffset, resetTideDashboardState } = tideDashboardSlice.actions; +export const { + setCategory, + setOffset, + resetTideDashboardState, +} = tideDashboardSlice.actions; export default tideDashboardSlice.reducer; diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js new file mode 100644 index 0000000000..ceaf67bb6e --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js @@ -0,0 +1,196 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { CATEGORY } from './FilterByCategory'; +import { MGDL_UNITS } from '../../../core/constants'; +import mapValues from 'lodash/mapValues'; +import { utils as vizUtils } from '@tidepool/viz'; +const { DEFAULT_BG_BOUNDS } = vizUtils.constants; + +import { + AvgGlucoseHeader, + AvgGlucoseCell, + CGMUseCell, + ChangeTIRHeader, + ChangeTIRCell, + GMICell, + PatientCell, + TimeInRangePercentBarChartCell, + TimeInVeryLowPercentCell, + TimeInAnyLowPercentCell, + TimeInVeryHighPercentCell, + TimeInAnyHighPercentCell, + TimeInTargetPercentCell, + FlagCell, +} from './Cells'; + +import TagListCell from '../components/TagListCell'; + +const getColumnTypes = (t, category, thresholds) => ({ + patientDetails: { + title: t('Patient Details'), + field: 'fullName', + align: 'left', + render: patient => , + }, + flag: { + title: t('Flag'), + field: 'flag', + align: 'center', + render: patient => , + }, + avgGlucose: { + title: t('Avg Glucose'), + field: 'avgGlucose', + align: 'center', + titleComponent: () => , + render: patient => , + }, + timeInRangeBarChart: { + title: t('Time in Range'), + field: 'timeInRangeBarChart', + align: 'center', + render: patient => , + }, + changeInTIR: { + title: t('% Change in TIR'), + field: 'changeInTIR', + align: 'center', + titleComponent: () => , + render: patient => , + }, + timeInVeryLow: { + title: `${t('% Time')} < ${thresholds.veryLowThreshold}`, + field: 'timeInVeryLow', + align: 'center', + render: patient => , + }, + timeInAnyLow: { + title: `${t('% Time')} < ${thresholds.targetLowerBound}`, + field: 'timeInAnyLow', + align: 'center', + render: patient => , + }, + timeInVeryHigh: { + title: `${t('% Time')} > ${thresholds.veryHighThreshold}`, + field: 'timeInVeryHigh', + align: 'center', + render: patient => , + }, + timeInAnyHigh: { + title: `${t('% Time')} > ${thresholds.targetUpperBound}`, + field: 'timeInAnyHigh', + align: 'center', + render: patient => , + }, + timeInTarget: { + title: `${t('% TIR')} ${thresholds.targetLowerBound}-${thresholds.targetUpperBound}`, + field: 'timeInTarget', + align: 'center', + render: patient => , + }, + gmi: { + title: t('GMI'), + field: 'gmi', + align: 'center', + render: patient => , + }, + cgmUse: { + title: t('CGM Use'), + field: 'cgmUse', + align: 'center', + render: patient => , + }, + tags: { + title: t('Tags'), + field: 'tags', + align: 'center', + render: patient => , + }, + lastReviewed: { + title: t('Last Reviewed'), + field: 'lastReviewed', + align: 'center', + }, + moreMenu: { + title: t(''), + field: 'moreMenu', + align: 'center', + }, // More +}); + +const getFormattedThresholds = (clinicBgUnits) => { + const thresholds = DEFAULT_BG_BOUNDS[clinicBgUnits]; + const precision = clinicBgUnits === MGDL_UNITS ? 0 : 1; + + return mapValues(thresholds, value => value.toFixed(precision)); +}; + +const useTableColumns = (category) => { + const { t } = useTranslation(); + const selectedClinicId = useSelector((state) => state.blip.selectedClinicId); + const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]); + const clinicBgUnits = clinic?.preferredBgUnits || MGDL_UNITS; + + const columns = useMemo(() => { + const thresholds = getFormattedThresholds(clinicBgUnits); + const columnTypes = getColumnTypes(t, category, thresholds); + + const standardColumnSet = [ + columnTypes.patientDetails, + columnTypes.flag, + columnTypes.avgGlucose, + columnTypes.timeInRangeBarChart, + columnTypes.changeInTIR, + columnTypes.gmi, + columnTypes.cgmUse, + columnTypes.tags, + columnTypes.lastReviewed, + columnTypes.moreMenu, + ]; + + const lowColumnSet = [ + columnTypes.patientDetails, + columnTypes.flag, + columnTypes.avgGlucose, + columnTypes.timeInVeryLow, + columnTypes.timeInAnyLow, + columnTypes.timeInTarget, + columnTypes.timeInRangeBarChart, + columnTypes.changeInTIR, + columnTypes.tags, + columnTypes.lastReviewed, + columnTypes.moreMenu, + ]; + + const highColumnSet = [ + columnTypes.patientDetails, + columnTypes.flag, + columnTypes.avgGlucose, + columnTypes.timeInVeryHigh, + columnTypes.timeInAnyHigh, + columnTypes.timeInTarget, + columnTypes.timeInRangeBarChart, + columnTypes.changeInTIR, + columnTypes.tags, + columnTypes.lastReviewed, + columnTypes.moreMenu, + ]; + + switch(category) { + case CATEGORY.DEFAULT: return standardColumnSet; + case CATEGORY.VERY_LOW: return lowColumnSet; + case CATEGORY.ANY_LOW: return lowColumnSet; + case CATEGORY.DROP_IN_TIR: return standardColumnSet; + case CATEGORY.ANY_HIGH: return highColumnSet; + case CATEGORY.VERY_HIGH: return highColumnSet; + case CATEGORY.LOW_CGM_WEAR: return standardColumnSet; + case CATEGORY.TARGET: return standardColumnSet; + default: return standardColumnSet; + } + }, [category, clinicBgUnits]); + + return columns; +}; + +export default useTableColumns; diff --git a/app/pages/clinicworkspace/components/TagListCell.js b/app/pages/clinicworkspace/components/TagListCell.js index 7f5a8fda77..38d2654858 100644 --- a/app/pages/clinicworkspace/components/TagListCell.js +++ b/app/pages/clinicworkspace/components/TagListCell.js @@ -2,6 +2,8 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { TagList } from '../../../components/elements/Tag'; +const MAX_TAGS = 2; + const TagListCell = ({ patient }) => { const selectedClinicId = useSelector(state => state.blip.selectedClinicId); const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]); @@ -10,7 +12,7 @@ const TagListCell = ({ patient }) => { const tagIds = patient?.tags || []; const tags = tagIds.map(tag => patientTags.find(ptTag => ptTag.id === tag)); // TODO: index - return ; + return ; }; export default TagListCell; diff --git a/app/pages/dashboard/TideDashboard.js b/app/pages/dashboard/TideDashboard.js index bd0f8e5173..c5a53b158f 100644 --- a/app/pages/dashboard/TideDashboard.js +++ b/app/pages/dashboard/TideDashboard.js @@ -58,7 +58,7 @@ import PopoverMenu from '../../components/elements/PopoverMenu'; import RadioGroup from '../../components/elements/RadioGroup'; import DeltaBar from '../../components/elements/DeltaBar'; import Pill from '../../components/elements/Pill'; -import PatientDrawer, { isValidAgpPeriod } from './PatientDrawer'; +import PatientDrawer, { isValidAgpPeriod } from '../../components/PatientDrawer'; import utils from '../../core/utils'; import { @@ -85,7 +85,7 @@ import DataInIcon from '../../core/icons/DataInIcon.svg'; import { colors, fontWeights, radii } from '../../themes/baseTheme'; import PatientLastReviewed from '../../components/clinic/PatientLastReviewed'; import { DEFAULT_GLYCEMIC_RANGES } from '../../core/glycemicRangesUtils'; -import { OVERVIEW_TAB_INDEX } from './PatientDrawer/MenuBar/MenuBar'; +import { OVERVIEW_TAB_INDEX } from '../../components/PatientDrawer/MenuBar/MenuBar'; const { Loader } = vizComponents; const { formatBgValue } = vizUtils.bg;