From 8a1cdd71875f3d8d772bd86c7dc2406c6e345b2a Mon Sep 17 00:00:00 2001 From: henry-tp Date: Mon, 17 Aug 2026 17:01:37 -0700 Subject: [PATCH 01/23] WEB-4460 initialize base branch From fa763c013499ef1017cf0cfc8dd8ee9187f93600 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Mon, 17 Aug 2026 15:41:47 -0700 Subject: [PATCH 02/23] WEB-4460 add cell data to table --- .../clinicworkspace/TideDashboardV2/Cells.js | 274 +++++++++++++++++- .../TideDashboardV2/useTableColumns.js | 105 ++++++- 2 files changed, 372 insertions(+), 7 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index 460136296a..ce8974d5a3 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -1,6 +1,28 @@ import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { Box } 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 { colors as vizColors } from '@tidepool/viz'; +import { MGDL_UNITS } from '../../../core/constants'; +import { colors } from '../../../themes/baseTheme'; + +import BgSummaryCell from '../../../components/clinic/BgSummaryCell'; +import DeltaBar from '../../../components/elements/DeltaBar'; +import utils from '../../../core/utils'; +import { CATEGORY } from './FilterByCategory'; +import isUndefined from 'lodash/isUndefined'; + +import PopoverMenu from '../../../components/elements/PopoverMenu'; +import EditIcon from '@material-ui/icons/EditRounded'; +import DataInIcon from '../../../core/icons/DataInIcon.svg'; + +import { + setEditPatientDialogIsOpen, + setEditPatientDialogPatientId, + setDataConnectionsModalIsOpen, + setDataConnectionsModalPatientId, +} from './tideDashboardSlice'; export const COMPACT = '@container (max-width: 1200px)'; @@ -18,6 +40,254 @@ export const PatientCell = ({ patient }) => { ; }; +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 const MoreMenuCell = ({ patient }) => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const handleOpenEditPatientDialog = () => { + dispatch(setEditPatientDialogIsOpen(true)); + dispatch(setEditPatientDialogPatientId(patient.id)); + }; + + const handleOpenDataConnectionsModal = () => { + dispatch(setDataConnectionsModalIsOpen(true)); + dispatch(setDataConnectionsModalPatientId(patient.id)); + }; + + return ( + { + _popupState.close(); + handleOpenEditPatientDialog(); + }, + text: t('Edit Patient Details'), + }, { + iconSrc: DataInIcon, + iconLabel: t('Bring Data into Tidepool'), + iconPosition: 'left', + id: `edit-data-connections-${patient?.id}`, + variant: 'actionListItem', + onClick: (_popupState) => { + _popupState.close(); + handleOpenDataConnectionsModal(); + }, + text: t('Bring Data into Tidepool'), + }]} + sx={{ position: 'relative', left: '-2px' }} + /> + ); +}; + export default { PatientCell, + NumericTemplateCell, + AvgGlucoseCell, + TimeInRangePercentBarChartCell, + TimeInVeryLowPercentCell, + ChangeTIRCell, + GMICell, + CGMUseCell, + FlagCell, }; diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js index a45c08dda9..93c95b72de 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js +++ b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js @@ -3,21 +3,116 @@ import { useTranslation } from 'react-i18next'; import { PatientCell, + AvgGlucoseHeader, + AvgGlucoseCell, + CGMUseCell, + ChangeTIRHeader, + ChangeTIRCell, + GMICell, + TimeInRangePercentBarChartCell, + TimeInVeryLowPercentCell, + TimeInAnyLowPercentCell, + TimeInVeryHighPercentCell, + TimeInAnyHighPercentCell, + TimeInTargetPercentCell, + FlagCell, } from './Cells'; -const getColumnTypes = (t, category) => ({ +import TagListCell from '../components/TagListCell'; + +const getColumnTypes = (t, category, thresholds) => ({ patientDetails: { title: t('Patient Details'), field: 'fullName', align: 'left', render: patient => , }, - placeholder: { - title: t('Placeholder'), - field: 'placeholder', + 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 => null, + 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', + render: patient => null, // TODO: Implement + }, + moreMenu: { + title: '', + field: 'moreMenu', + align: 'center', + render: patient => null, // TODO: Implement + }, // More }); const useTableColumns = (category) => { From 9535388ec95fbac27ecea20bb46f7c96b5db2254 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Mon, 17 Aug 2026 15:59:07 -0700 Subject: [PATCH 03/23] WEB-4460 fix chained fn indent --- .../clinicworkspace/components/TagListCell.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/pages/clinicworkspace/components/TagListCell.js diff --git a/app/pages/clinicworkspace/components/TagListCell.js b/app/pages/clinicworkspace/components/TagListCell.js new file mode 100644 index 0000000000..44aaa7ddd8 --- /dev/null +++ b/app/pages/clinicworkspace/components/TagListCell.js @@ -0,0 +1,21 @@ +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]); + const patientTags = clinic?.patientTags || []; + + const tagIds = patient?.tags || []; + const tags = tagIds + .map(tag => patientTags + .find(ptTag => ptTag.id === tag)) // TODO: index + .filter(Boolean); + + return ; +}; + +export default TagListCell; From d5f2453c80279062dea178ee60ffef656f20cd2a Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 18 Aug 2026 12:34:18 -0700 Subject: [PATCH 04/23] WEB-4460 add in table columns --- .../TideDashboardV2/useTableColumns.js | 87 ++++++++++++++++--- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js index 93c95b72de..836a1526e7 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js +++ b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js @@ -1,5 +1,11 @@ -import React from 'react'; +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 { PatientCell, @@ -115,21 +121,78 @@ const getColumnTypes = (t, category, thresholds) => ({ }, // 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, + ]; - const columnTypes = getColumnTypes(t, category); + 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 [ - columnTypes.patientDetails, - columnTypes.placeholder, - columnTypes.placeholder, - columnTypes.placeholder, - columnTypes.placeholder, - columnTypes.placeholder, - columnTypes.placeholder, - columnTypes.placeholder, - ]; + return columns; }; export default useTableColumns; From b85d4b4af0d9e090d8d9ace4f76b092f14727bb9 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 20 Aug 2026 00:21:28 -0700 Subject: [PATCH 05/23] WEB-4460 tie flag rules to CGM Exclusion Query --- .../TideDashboardV2/CGMExclusionQuery.js | 4 ++++ .../clinicworkspace/TideDashboardV2/Cells.js | 19 +++++++++++-------- .../TideDashboardV2/tideDashboardApi.js | 4 ++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/CGMExclusionQuery.js b/app/pages/clinicworkspace/TideDashboardV2/CGMExclusionQuery.js index 36ac4920ac..345eb3edc4 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/CGMExclusionQuery.js +++ b/app/pages/clinicworkspace/TideDashboardV2/CGMExclusionQuery.js @@ -55,4 +55,8 @@ export default class CGMExclusionQuery { getQueryParams(name) { return this.queryParams[name] || {}; } + + getRule(name) { + return this.rules[name] || {}; + } }; diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index ce8974d5a3..f552609134 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -24,6 +24,8 @@ import { setDataConnectionsModalPatientId, } from './tideDashboardSlice'; +import { tideDashboardCgmParams } from './tideDashboardApi'; + export const COMPACT = '@container (max-width: 1200px)'; export const PatientCell = ({ patient }) => { @@ -171,7 +173,9 @@ export const FlagCell = ({ patient, category = null, }) => { 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; + const { VERY_LOW, ANY_LOW, DROP_IN_TIR, ANY_HIGH, VERY_HIGH, LOW_CGM_WEAR } = CATEGORY; + + const getThreshold = (category) => tideDashboardCgmParams.getRule(category).threshold; if (!period) return null; @@ -184,15 +188,14 @@ export const FlagCell = ({ patient, category = null, }) => { 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% + case period.timeInVeryLowPercent >= getThreshold(VERY_LOW): return 'veryLow'; // >=1% + case period.timeInAnyLowPercent >= getThreshold(ANY_LOW): return 'anyLow'; // >=4% + case period.timeInTargetPercentDelta <= getThreshold(DROP_IN_TIR): return 'dropInTIR'; // <=-15% + case period.timeInAnyHighPercent >= getThreshold(ANY_HIGH): return 'anyHigh'; // >=25% + case period.timeInVeryHighPercent >= getThreshold(VERY_HIGH): return 'veryHigh'; // >=5% + case period.timeCGMUsePercent < getThreshold(ANY_LOW): return 'lowSensorUsage'; // <70% default: return null; } diff --git a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js index 8ecf6a865b..4813fe1c3e 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js +++ b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js @@ -5,7 +5,7 @@ import CGMExclusionQuery from './CGMExclusionQuery'; // Each rule matches a category and automatically negates all preceding // rules, ensuring patients appear in at most one category. // -export const tideDashboardExclusionQuery = new CGMExclusionQuery() +export const tideDashboardCgmParams = new CGMExclusionQuery() .addRule(CATEGORY.VERY_LOW, 'cgm.timeInVeryLowPercent', '>=', 0.01) // queries >=0.005 .addRule(CATEGORY.ANY_LOW, 'cgm.timeInAnyLowPercent', '>=', 0.04) // queries >=0.035 .addRule(CATEGORY.DROP_IN_TIR, 'cgm.timeInTargetPercentDelta', '<=', -0.15) // queries <=-0.145 @@ -18,7 +18,7 @@ export const buildGetTideDashboardPatientsParams = (offset, limit, category, sum const formattedTags = tags?.length > 0 ? tags.join(',') : undefined; const formattedSites = sites?.length > 0 ? sites.join(',') : undefined; - const cgmQueryParams = tideDashboardExclusionQuery.getQueryParams(category); + const cgmQueryParams = tideDashboardCgmParams.getQueryParams(category); return { offset, From 13d3e95e391a36585addba299bbae992090404b7 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 20 Aug 2026 10:06:56 -0700 Subject: [PATCH 06/23] WEB-4460 shorten flag labels --- app/pages/clinicworkspace/TideDashboardV2/Cells.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index f552609134..d470d6e49b 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -208,8 +208,8 @@ export const FlagCell = ({ patient, category = null, }) => { anyLow: t('Low'), veryHigh: t('Very High'), anyHigh: t('High'), - dropInTIR: t('Large Drop in TIR'), - lowSensorUsage: t('Low CGM Wear Time'), + dropInTIR: t('Drop in TIR'), + lowSensorUsage: t('Low CGM Wear'), target: t('Meeting Targets'), }; From 32c9846739b88eb89efe8ed27a9ffecef48104b1 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 20 Aug 2026 16:11:31 -0700 Subject: [PATCH 07/23] WEB-4460 fix CATEGORY enum import --- app/pages/clinicworkspace/TideDashboardV2/Cells.js | 2 +- app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index d470d6e49b..4d4778f79d 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -10,7 +10,7 @@ import { colors } from '../../../themes/baseTheme'; import BgSummaryCell from '../../../components/clinic/BgSummaryCell'; import DeltaBar from '../../../components/elements/DeltaBar'; import utils from '../../../core/utils'; -import { CATEGORY } from './FilterByCategory'; +import { CATEGORY } from './tideDashboardSlice'; import isUndefined from 'lodash/isUndefined'; import PopoverMenu from '../../../components/elements/PopoverMenu'; diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js index 836a1526e7..98c5ee003a 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js +++ b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; -import { CATEGORY } from './FilterByCategory'; +import { CATEGORY } from './tideDashboardSlice'; import { MGDL_UNITS } from '../../../core/constants'; import mapValues from 'lodash/mapValues'; import { utils as vizUtils } from '@tidepool/viz'; From e153b0efe2aa48ea88574b06992a139247c8e0f2 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 25 Aug 2026 15:06:55 -0700 Subject: [PATCH 08/23] WEB-4460 abstract out columnSets definition --- .../TideDashboardV2/useTableColumns.js | 134 +++++++++++------- .../clinicworkspace/components/TagListCell.js | 5 +- 2 files changed, 86 insertions(+), 53 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js index 98c5ee003a..7e3ea563b7 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js +++ b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js @@ -121,6 +121,80 @@ const getColumnTypes = (t, category, thresholds) => ({ }, // More }); +const buildColumnSets = (columnTypes) => ({ + default: [ + columnTypes.patientDetails, + columnTypes.flag, + columnTypes.avgGlucose, + columnTypes.timeInRangeBarChart, + columnTypes.changeInTIR, + columnTypes.gmi, + columnTypes.cgmUse, + columnTypes.tags, + columnTypes.lastReviewed, + columnTypes.moreMenu, + ], + target: [ + columnTypes.patientDetails, + columnTypes.avgGlucose, + columnTypes.timeInRangeBarChart, + columnTypes.changeInTIR, + columnTypes.gmi, + columnTypes.cgmUse, + columnTypes.tags, + columnTypes.lastReviewed, + columnTypes.moreMenu, + ], + low: [ + columnTypes.patientDetails, + columnTypes.avgGlucose, + columnTypes.timeInVeryLow, + columnTypes.timeInAnyLow, + columnTypes.timeInTarget, + columnTypes.timeInRangeBarChart, + columnTypes.changeInTIR, + columnTypes.tags, + columnTypes.lastReviewed, + columnTypes.moreMenu, + ], + high: [ + columnTypes.patientDetails, + columnTypes.avgGlucose, + columnTypes.timeInVeryHigh, + columnTypes.timeInAnyHigh, + columnTypes.timeInTarget, + columnTypes.timeInRangeBarChart, + columnTypes.changeInTIR, + columnTypes.tags, + columnTypes.lastReviewed, + columnTypes.moreMenu, + ], + dropInTIR: [ + columnTypes.patientDetails, + columnTypes.avgGlucose, + columnTypes.timeInTarget, + columnTypes.timeInRangeBarChart, + columnTypes.changeInTIR, + columnTypes.gmi, + columnTypes.cgmUse, + columnTypes.tags, + columnTypes.lastReviewed, + columnTypes.moreMenu, + ], + lowCgmWear: [ + columnTypes.patientDetails, + columnTypes.cgmUse, + columnTypes.avgGlucose, + columnTypes.timeInTarget, + columnTypes.timeInRangeBarChart, + columnTypes.changeInTIR, + columnTypes.gmi, + columnTypes.tags, + columnTypes.lastReviewed, + columnTypes.moreMenu, + ], +}); + const getFormattedThresholds = (clinicBgUnits) => { const thresholds = DEFAULT_BG_BOUNDS[clinicBgUnits]; const precision = clinicBgUnits === MGDL_UNITS ? 0 : 1; @@ -137,58 +211,18 @@ const useTableColumns = (category) => { 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, - ]; + const columnSets = buildColumnSets(columnTypes); 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; + case CATEGORY.DEFAULT: return columnSets.default; + case CATEGORY.VERY_LOW: return columnSets.low; + case CATEGORY.ANY_LOW: return columnSets.low; + case CATEGORY.DROP_IN_TIR: return columnSets.dropInTIR; + case CATEGORY.ANY_HIGH: return columnSets.high; + case CATEGORY.VERY_HIGH: return columnSets.high; + case CATEGORY.LOW_CGM_WEAR: return columnSets.lowCgmWear; + case CATEGORY.TARGET: return columnSets.target; + default: return columnSets.default; } }, [category, clinicBgUnits]); diff --git a/app/pages/clinicworkspace/components/TagListCell.js b/app/pages/clinicworkspace/components/TagListCell.js index 44aaa7ddd8..5a6c8e1646 100644 --- a/app/pages/clinicworkspace/components/TagListCell.js +++ b/app/pages/clinicworkspace/components/TagListCell.js @@ -2,7 +2,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { TagList } from '../../../components/elements/Tag'; -const MAX_TAGS = 2; +const MAX_TAGS = 3; const TagListCell = ({ patient }) => { const selectedClinicId = useSelector(state => state.blip.selectedClinicId); @@ -11,8 +11,7 @@ const TagListCell = ({ patient }) => { const tagIds = patient?.tags || []; const tags = tagIds - .map(tag => patientTags - .find(ptTag => ptTag.id === tag)) // TODO: index + .map(tag => patientTags.find(ptTag => ptTag.id === tag)) .filter(Boolean); return ; From b9844f010dd46450a1d9c2b4cd99a50bfebbd65f Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 25 Aug 2026 15:40:37 -0700 Subject: [PATCH 09/23] WEB-4460 expect correct columns per category --- .../TideDashboardV2/TideDashboardV2.test.js | 97 +++++++++++++++++++ .../clinicworkspace/TideDashboardV2/Cells.js | 7 +- .../TideDashboardV2/useTableColumns.js | 2 + 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js index 39514ce732..ff0ed55234 100644 --- a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js +++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js @@ -222,9 +222,23 @@ describe('TideDashboardV2', () => { // All Patients is the pre-selected category expect(await screen.findByText('Default Patient 1')).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /All Patients/ })).toBeChecked(); expect(screen.getByText('Default Patient 2')).toBeInTheDocument(); expect(screen.getByText('DOB: 2001-01-01')).toBeInTheDocument(); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); + + expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Flag/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /GMI/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); // Selecting Very Low fetches and shows the Very Low cohort await userEvent.click(screen.getByRole('radio', { name: /Very Low/ })); @@ -233,6 +247,18 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Very Low Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Default Patient 1')).not.toBeInTheDocument(); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Time < 54/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Time < 70/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + // Selecting Low fetches and shows the Low cohort await userEvent.click(screen.getByRole('radio', { name: /^Low$/ })); expect(await screen.findByText('Low Patient 1')).toBeInTheDocument(); @@ -240,6 +266,18 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Low Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Very Low Patient 1')).not.toBeInTheDocument(); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Time < 54/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Time < 70/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + // Selecting Drop in TIR fetches and shows the Drop in TIR cohort await userEvent.click(screen.getByRole('radio', { name: /Drop in TIR/ })); expect(await screen.findByText('Drop In TIR Patient 1')).toBeInTheDocument(); @@ -247,6 +285,18 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Drop In TIR Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Low Patient 1')).not.toBeInTheDocument(); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /GMI/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + // Selecting High fetches and shows the High cohort await userEvent.click(screen.getByRole('radio', { name: /^High$/ })); expect(await screen.findByText('High Patient 1')).toBeInTheDocument(); @@ -254,6 +304,18 @@ describe('TideDashboardV2', () => { expect(screen.getByText('High Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Drop In TIR Patient 1')).not.toBeInTheDocument(); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Time > 250/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Time > 180/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + // Selecting Very High fetches and shows the Very High cohort await userEvent.click(screen.getByRole('radio', { name: /Very High/ })); expect(await screen.findByText('Very High Patient 1')).toBeInTheDocument(); @@ -261,6 +323,18 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Very High Patient 2')).toBeInTheDocument(); expect(screen.queryByText('High Patient 1')).not.toBeInTheDocument(); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Time > 250/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Time > 180/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + // Selecting Low CGM Wear fetches and shows the Low CGM Wear cohort await userEvent.click(screen.getByRole('radio', { name: /Low CGM Wear/ })); expect(await screen.findByText('Low CGM Wear Patient 1')).toBeInTheDocument(); @@ -268,12 +342,35 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Low CGM Wear Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Very High Patient 1')).not.toBeInTheDocument(); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /GMI/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + // Selecting Meeting Targets fetches and shows the Meeting Targets cohort await userEvent.click(screen.getByRole('radio', { name: /Meeting Targets/ })); expect(await screen.findByText('Meeting Targets Patient 1')).toBeInTheDocument(); expect(screen.getByRole('radio', { name: /Meeting Targets/ })).toBeChecked(); expect(screen.getByText('Meeting Targets Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Low CGM Wear Patient 1')).not.toBeInTheDocument(); + + expect(screen.getAllByRole('columnheader')).toHaveLength(9); + expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /GMI/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); }, TEST_TIMEOUT_MS); it('fetches with filters', async () => { diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index 4d4778f79d..e72bf235ad 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -168,7 +168,7 @@ export const ChangeTIRCell = ({ patient }) => { ; }; -export const FlagCell = ({ patient, category = null, }) => { +export const FlagCell = ({ patient, category = null }) => { const { t } = useTranslation(); const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); const period = patient?.summary?.cgmStats?.periods?.[summaryPeriod]; @@ -237,6 +237,11 @@ export const FlagCell = ({ patient, category = null, }) => { ); }; +export const MoreMenuHeader = () => { + const { t } = useTranslation(); + + return ; +}; export const MoreMenuCell = ({ patient }) => { const { t } = useTranslation(); diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js index 7e3ea563b7..4b35770e8f 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js +++ b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js @@ -22,6 +22,7 @@ import { TimeInAnyHighPercentCell, TimeInTargetPercentCell, FlagCell, + MoreMenuHeader, } from './Cells'; import TagListCell from '../components/TagListCell'; @@ -117,6 +118,7 @@ const getColumnTypes = (t, category, thresholds) => ({ title: '', field: 'moreMenu', align: 'center', + titleComponent: () => , render: patient => null, // TODO: Implement }, // More }); From beac06c6ed19aacabfe3487c1e332d18bee0a947 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 25 Aug 2026 16:23:12 -0700 Subject: [PATCH 10/23] WEB-4460 add tests for cells --- .../TideDashboardV2/Cells.test.js | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 __tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js new file mode 100644 index 0000000000..a1119995d4 --- /dev/null +++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js @@ -0,0 +1,177 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import { thunk } from 'redux-thunk'; + +import { + PatientCell, + NumericTemplateCell, + AvgGlucoseCell, + TimeInRangePercentBarChartCell, + TimeInTargetPercentCell, + TimeInVeryLowPercentCell, + TimeInAnyLowPercentCell, + TimeInVeryHighPercentCell, + TimeInAnyHighPercentCell, + ChangeTIRCell, + GMICell, + CGMUseCell, + MoreMenuCell, +} from '@app/pages/clinicworkspace/TideDashboardV2/Cells'; + +const mockStore = configureStore([thunk]); + +const patient = { + id: 'patient-1', + fullName: 'James Jellyfish', + birthDate: '2010-10-10', + mrn: 'mrn-123', + summary: { + cgmStats: { + config: { + lowGlucoseThreshold: 3.9, + highGlucoseThreshold: 10, + }, + periods: { + '14d': { + averageGlucoseMmol: 8.26, + glucoseManagementIndicator: 7.2, + timeInVeryLowPercent: 0.0134, + timeInLowPercent: 0.0434, + timeInAnyLowPercent: 0.0568, + timeInTargetPercent: 0.6712, + timeInTargetPercentDelta: -0.1523, + timeInHighPercent: 0.2011, + timeInAnyHighPercent: 0.2733, + timeInVeryHighPercent: 0.0722, + timeCGMUsePercent: 0.9312, + timeCGMUseMinutes: 18780, + }, + }, + }, + }, +}; + +describe('Cells', () => { + let store; + + const renderComponent = (cell) => { + render({cell}); + }; + + beforeEach(() => { + store = mockStore({ + blip: { + selectedClinicId: 'clinic123', + clinics: { clinic123: { id: 'clinic123', preferredBgUnits: 'mg/dL' } }, + tideDashboardFilters: { summaryPeriod: '14d' }, + }, + }); + }); + + describe('PatientCell', () => { + it('renders the patient name, date of birth and MRN', () => { + renderComponent(); + + expect(screen.getByText('James Jellyfish')).toBeInTheDocument(); + expect(screen.getByText('DOB: 2010-10-10')).toBeInTheDocument(); + expect(screen.getByText('MRN: mrn-123')).toBeInTheDocument(); + }); + }); + + describe('AvgGlucoseCell', () => { + it('renders the average glucose of the active summary period to one decimal place', () => { + renderComponent(); + + expect(screen.getByText('8.3')).toBeInTheDocument(); // averageGlucoseMmol 8.26 + }); + }); + + describe('TimeInRangePercentBarChartCell', () => { + it('renders a bar summary of the time spent in each range', () => { + renderComponent(); + + // Ranges are labelled using the patient's glucose thresholds, in the clinic's preferred units + expect(screen.getByText('<54')).toBeInTheDocument(); + expect(screen.getByText('54-69')).toBeInTheDocument(); + expect(screen.getByText('70-180')).toBeInTheDocument(); // lowGlucoseThreshold 3.9, highGlucoseThreshold 10 + expect(screen.getByText('181-250')).toBeInTheDocument(); + expect(screen.getByText('>250')).toBeInTheDocument(); + expect(screen.getByText('Units in mg/dL')).toBeInTheDocument(); + + expect(screen.getByText('1')).toBeInTheDocument(); // timeInVeryLowPercent 0.0134 + expect(screen.getByText('4')).toBeInTheDocument(); // timeInLowPercent 0.0434 + expect(screen.getByText('67')).toBeInTheDocument(); // timeInTargetPercent 0.6712 + expect(screen.getByText('21')).toBeInTheDocument(); // timeInHighPercent 0.2011 + expect(screen.getByText('7')).toBeInTheDocument(); // timeInVeryHighPercent 0.0722 + + expect(screen.getByText('93 %')).toBeInTheDocument(); // timeCGMUsePercent 0.9312 + }); + }); + + describe('TimeInTargetPercentCell', () => { + it('renders the time in target as a whole percentage', () => { + renderComponent(); + + expect(screen.getByText('67 %')).toBeInTheDocument(); // timeInTargetPercent 0.6712 + }); + }); + + describe('TimeInVeryLowPercentCell', () => { + it('renders the time in very low as a whole percentage', () => { + renderComponent(); + + expect(screen.getByText('1 %')).toBeInTheDocument(); // timeInVeryLowPercent 0.0134 + }); + }); + + describe('TimeInAnyLowPercentCell', () => { + it('renders the time in any low as a whole percentage', () => { + renderComponent(); + + expect(screen.getByText('6 %')).toBeInTheDocument(); // timeInAnyLowPercent 0.0568 + }); + }); + + describe('TimeInVeryHighPercentCell', () => { + it('renders the time in very high as a whole percentage', () => { + renderComponent(); + + expect(screen.getByText('7 %')).toBeInTheDocument(); // timeInVeryHighPercent 0.0722 + }); + }); + + describe('TimeInAnyHighPercentCell', () => { + it('renders the time in any high as a whole percentage', () => { + renderComponent(); + + expect(screen.getByText('27 %')).toBeInTheDocument(); // timeInAnyHighPercent 0.2733 + }); + }); + + describe('ChangeTIRCell', () => { + it('renders the change in time in range as a bar and as a percentage', () => { + renderComponent(); + + expect(screen.getByText('-15.2 %')).toBeInTheDocument(); // compact layout value + }); + }); + + describe('GMICell', () => { + it('renders the glucose management indicator as a percentage', () => { + renderComponent(); + + expect(screen.getByText('7.2 %')).toBeInTheDocument(); + }); + }); + + describe('CGMUseCell', () => { + it('renders the CGM use as a whole percentage', () => { + renderComponent(); + + expect(screen.getByText('93 %')).toBeInTheDocument(); // timeCGMUsePercent 0.9312 + }); + }); +}); From 6139d7dd2f1caa2c43c4e654ce7bd704074e28e9 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 25 Aug 2026 16:23:44 -0700 Subject: [PATCH 11/23] WEB-4460 remove MoreMenu to be implemented later --- .../clinicworkspace/TideDashboardV2/Cells.js | 45 +------------------ 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index e72bf235ad..07903f143a 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -243,50 +243,7 @@ export const MoreMenuHeader = () => { return ; }; -export const MoreMenuCell = ({ patient }) => { - const { t } = useTranslation(); - const dispatch = useDispatch(); - - const handleOpenEditPatientDialog = () => { - dispatch(setEditPatientDialogIsOpen(true)); - dispatch(setEditPatientDialogPatientId(patient.id)); - }; - - const handleOpenDataConnectionsModal = () => { - dispatch(setDataConnectionsModalIsOpen(true)); - dispatch(setDataConnectionsModalPatientId(patient.id)); - }; - - return ( - { - _popupState.close(); - handleOpenEditPatientDialog(); - }, - text: t('Edit Patient Details'), - }, { - iconSrc: DataInIcon, - iconLabel: t('Bring Data into Tidepool'), - iconPosition: 'left', - id: `edit-data-connections-${patient?.id}`, - variant: 'actionListItem', - onClick: (_popupState) => { - _popupState.close(); - handleOpenDataConnectionsModal(); - }, - text: t('Bring Data into Tidepool'), - }]} - sx={{ position: 'relative', left: '-2px' }} - /> - ); -}; +export const MoreMenuCell = () => <>; // TEMPORARY export default { PatientCell, From ff50e6b0b1b18894e8576a610a4f2667d80d41d4 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 25 Aug 2026 16:25:30 -0700 Subject: [PATCH 12/23] WEB-4460 remove unused imports --- app/pages/clinicworkspace/TideDashboardV2/Cells.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index 07903f143a..612255e1d1 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -1,6 +1,5 @@ import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useLocation, useHistory } from 'react-router-dom'; +import { useSelector } from 'react-redux'; import { useTranslation, withTranslation } from 'react-i18next'; import { Box, Flex, Text } from 'theme-ui'; import { colors as vizColors } from '@tidepool/viz'; @@ -13,16 +12,6 @@ import utils from '../../../core/utils'; import { CATEGORY } from './tideDashboardSlice'; import isUndefined from 'lodash/isUndefined'; -import PopoverMenu from '../../../components/elements/PopoverMenu'; -import EditIcon from '@material-ui/icons/EditRounded'; -import DataInIcon from '../../../core/icons/DataInIcon.svg'; - -import { - setEditPatientDialogIsOpen, - setEditPatientDialogPatientId, - setDataConnectionsModalIsOpen, - setDataConnectionsModalPatientId, -} from './tideDashboardSlice'; import { tideDashboardCgmParams } from './tideDashboardApi'; From 862682f7a6b3162ae36dfadadcee125abb4fdfcd Mon Sep 17 00:00:00 2001 From: henry-tp Date: Wed, 26 Aug 2026 17:29:58 -0700 Subject: [PATCH 13/23] WEB-4460 fix tideDashboardExclusionQuery naming issue --- app/pages/clinicworkspace/TideDashboardV2/Cells.js | 4 ++-- app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index 612255e1d1..05cacf1673 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -13,7 +13,7 @@ import { CATEGORY } from './tideDashboardSlice'; import isUndefined from 'lodash/isUndefined'; -import { tideDashboardCgmParams } from './tideDashboardApi'; +import { tideDashboardExclusionQueryParams } from './tideDashboardApi'; export const COMPACT = '@container (max-width: 1200px)'; @@ -164,7 +164,7 @@ export const FlagCell = ({ patient, category = null }) => { const { VERY_LOW, ANY_LOW, DROP_IN_TIR, ANY_HIGH, VERY_HIGH, LOW_CGM_WEAR } = CATEGORY; - const getThreshold = (category) => tideDashboardCgmParams.getRule(category).threshold; + const getThreshold = (category) => tideDashboardExclusionQueryParams.getRule(category).threshold; if (!period) return null; diff --git a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js index 4813fe1c3e..fdf6d22925 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js +++ b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js @@ -5,7 +5,7 @@ import CGMExclusionQuery from './CGMExclusionQuery'; // Each rule matches a category and automatically negates all preceding // rules, ensuring patients appear in at most one category. // -export const tideDashboardCgmParams = new CGMExclusionQuery() +export const tideDashboardExclusionQueryParams = new CGMExclusionQuery() .addRule(CATEGORY.VERY_LOW, 'cgm.timeInVeryLowPercent', '>=', 0.01) // queries >=0.005 .addRule(CATEGORY.ANY_LOW, 'cgm.timeInAnyLowPercent', '>=', 0.04) // queries >=0.035 .addRule(CATEGORY.DROP_IN_TIR, 'cgm.timeInTargetPercentDelta', '<=', -0.15) // queries <=-0.145 @@ -18,7 +18,7 @@ export const buildGetTideDashboardPatientsParams = (offset, limit, category, sum const formattedTags = tags?.length > 0 ? tags.join(',') : undefined; const formattedSites = sites?.length > 0 ? sites.join(',') : undefined; - const cgmQueryParams = tideDashboardCgmParams.getQueryParams(category); + const cgmQueryParams = tideDashboardExclusionQueryParams.getQueryParams(category); return { offset, From bc856fa2389520738b2898cb72abbf986916837a Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 27 Aug 2026 11:48:46 -0700 Subject: [PATCH 14/23] WEB-4460 fix var naming error for tideDashboardExclusionQuery --- app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js index fdf6d22925..8ecf6a865b 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js +++ b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js @@ -5,7 +5,7 @@ import CGMExclusionQuery from './CGMExclusionQuery'; // Each rule matches a category and automatically negates all preceding // rules, ensuring patients appear in at most one category. // -export const tideDashboardExclusionQueryParams = new CGMExclusionQuery() +export const tideDashboardExclusionQuery = new CGMExclusionQuery() .addRule(CATEGORY.VERY_LOW, 'cgm.timeInVeryLowPercent', '>=', 0.01) // queries >=0.005 .addRule(CATEGORY.ANY_LOW, 'cgm.timeInAnyLowPercent', '>=', 0.04) // queries >=0.035 .addRule(CATEGORY.DROP_IN_TIR, 'cgm.timeInTargetPercentDelta', '<=', -0.15) // queries <=-0.145 @@ -18,7 +18,7 @@ export const buildGetTideDashboardPatientsParams = (offset, limit, category, sum const formattedTags = tags?.length > 0 ? tags.join(',') : undefined; const formattedSites = sites?.length > 0 ? sites.join(',') : undefined; - const cgmQueryParams = tideDashboardExclusionQueryParams.getQueryParams(category); + const cgmQueryParams = tideDashboardExclusionQuery.getQueryParams(category); return { offset, From d375b0a5ff146fd5d13959dc124b97b3b44dbd4f Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 27 Aug 2026 12:05:25 -0700 Subject: [PATCH 15/23] WEB-4460 fix var naming error for tideDashboardExclusionQuery --- app/pages/clinicworkspace/TideDashboardV2/Cells.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index 05cacf1673..c4b728d8f9 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -13,7 +13,7 @@ import { CATEGORY } from './tideDashboardSlice'; import isUndefined from 'lodash/isUndefined'; -import { tideDashboardExclusionQueryParams } from './tideDashboardApi'; +import { tideDashboardExclusionQuery } from './tideDashboardApi'; export const COMPACT = '@container (max-width: 1200px)'; @@ -164,7 +164,7 @@ export const FlagCell = ({ patient, category = null }) => { const { VERY_LOW, ANY_LOW, DROP_IN_TIR, ANY_HIGH, VERY_HIGH, LOW_CGM_WEAR } = CATEGORY; - const getThreshold = (category) => tideDashboardExclusionQueryParams.getRule(category).threshold; + const getThreshold = (category) => tideDashboardExclusionQuery.getRule(category).threshold; if (!period) return null; From ffba78343a2a036d1ed7972c03772cfeb2b321c8 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Fri, 28 Aug 2026 18:25:03 -0700 Subject: [PATCH 16/23] WEB-4460 add tests for FlagCell --- .../TideDashboardV2/Cells.test.js | 93 +++++++++++++++++++ .../clinicworkspace/TideDashboardV2/Cells.js | 2 +- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js index a1119995d4..8871d9b2e1 100644 --- a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js +++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js @@ -4,7 +4,9 @@ import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; import { thunk } from 'redux-thunk'; +import cloneDeep from 'lodash/cloneDeep'; +import { CATEGORY } from '@app/pages/clinicworkspace/TideDashboardV2/tideDashboardSlice'; import { PatientCell, NumericTemplateCell, @@ -18,6 +20,7 @@ import { ChangeTIRCell, GMICell, CGMUseCell, + FlagCell, MoreMenuCell, } from '@app/pages/clinicworkspace/TideDashboardV2/Cells'; @@ -174,4 +177,94 @@ describe('Cells', () => { expect(screen.getByText('93 %')).toBeInTheDocument(); // timeCGMUsePercent 0.9312 }); }); + + describe('FlagCell', () => { + // Patient whose summary stats sit within every flag threshold, so no flag applies + const meetingTargetsPatient = { + summary: { + cgmStats: { + periods: { + '14d': { + timeInVeryLowPercent: 0.004, + timeInAnyLowPercent: 0.03, + timeInTargetPercentDelta: -0.05, + timeInAnyHighPercent: 0.2, + timeInVeryHighPercent: 0.04, + timeCGMUsePercent: 0.85, + }, + }, + }, + }, + }; + + const ui = (props) => ; + + it('flags the highest-priority range whose threshold the summary meets', () => { + let patient; + + // No flag when the summary is within every threshold + patient = cloneDeep(meetingTargetsPatient); + const { container, rerender } = render(ui({ patient })); + expect(container).toBeEmptyDOMElement(); + + // Time in very low at or above 1% flags Very Low + patient = cloneDeep(meetingTargetsPatient); + patient.summary.cgmStats.periods['14d'].timeInVeryLowPercent = 0.0134; + rerender(ui({ patient })); + expect(screen.getByText('Very Low')).toBeInTheDocument(); + + // Time in any low at or above 4% flags Low + patient = cloneDeep(meetingTargetsPatient); + patient.summary.cgmStats.periods['14d'].timeInAnyLowPercent = 0.0568; + rerender(ui({ patient })); + expect(screen.getByText('Low')).toBeInTheDocument(); + expect(screen.queryByText('Very Low')).not.toBeInTheDocument(); + + // Drop in time in target at or below -15% flags Drop in TIR + patient = cloneDeep(meetingTargetsPatient); + patient.summary.cgmStats.periods['14d'].timeInTargetPercentDelta = -0.1523; + rerender(ui({ patient })); + expect(screen.getByText('Drop in TIR')).toBeInTheDocument(); + + // Time in any high at or above 25% flags High + patient = cloneDeep(meetingTargetsPatient); + patient.summary.cgmStats.periods['14d'].timeInAnyHighPercent = 0.2733; + rerender(ui({ patient })); + expect(screen.getByText('High')).toBeInTheDocument(); + + // Time in very high at or above 5% flags Very High + patient = cloneDeep(meetingTargetsPatient); + patient.summary.cgmStats.periods['14d'].timeInVeryHighPercent = 0.0722; + rerender(ui({ patient })); + expect(screen.getByText('Very High')).toBeInTheDocument(); + + // CGM use below 70% flags Low CGM Wear + patient = cloneDeep(meetingTargetsPatient); + patient.summary.cgmStats.periods['14d'].timeCGMUsePercent = 0.65; + rerender(ui({ patient })); + expect(screen.getByText('Low CGM Wear')).toBeInTheDocument(); + + // When several thresholds are met, only the highest-priority flag shows + patient = cloneDeep(meetingTargetsPatient); + patient.summary.cgmStats.periods['14d'].timeInVeryLowPercent = 0.0134; + patient.summary.cgmStats.periods['14d'].timeInAnyLowPercent = 0.0568; + rerender(ui({ patient })); + + expect(screen.getByText('Very Low')).toBeInTheDocument(); + expect(screen.queryByText('Low')).not.toBeInTheDocument(); + }); + + it('flags the current category ahead of a higher-priority flag', () => { + // The summary meets both the Very Low and Low thresholds, which would normally flag Very Low + let patient = cloneDeep(meetingTargetsPatient); + patient.summary.cgmStats.periods['14d'].timeInVeryLowPercent = 0.0134; + patient.summary.cgmStats.periods['14d'].timeInAnyLowPercent = 0.0568; + + renderComponent(); + + // The current dashboard category wins + expect(screen.getByText('Low')).toBeInTheDocument(); + expect(screen.queryByText('Very Low')).not.toBeInTheDocument(); + }); + }); }); diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index c4b728d8f9..fafd21fe89 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -184,7 +184,7 @@ export const FlagCell = ({ patient, category = null }) => { case period.timeInTargetPercentDelta <= getThreshold(DROP_IN_TIR): return 'dropInTIR'; // <=-15% case period.timeInAnyHighPercent >= getThreshold(ANY_HIGH): return 'anyHigh'; // >=25% case period.timeInVeryHighPercent >= getThreshold(VERY_HIGH): return 'veryHigh'; // >=5% - case period.timeCGMUsePercent < getThreshold(ANY_LOW): return 'lowSensorUsage'; // <70% + case period.timeCGMUsePercent < getThreshold(LOW_CGM_WEAR): return 'lowSensorUsage'; // <70% default: return null; } From 354c16999f30dfead3819566a13022188b4207c5 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 12:31:58 -0700 Subject: [PATCH 17/23] WEB-4460 fix units for AvgGlucoseCell --- app/pages/clinicworkspace/TideDashboardV2/Cells.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index fafd21fe89..9fe657a000 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -14,6 +14,7 @@ import isUndefined from 'lodash/isUndefined'; import { tideDashboardExclusionQuery } from './tideDashboardApi'; +import { useFlags } from 'launchdarkly-react-client-sdk'; export const COMPACT = '@container (max-width: 1200px)'; @@ -44,10 +45,16 @@ export const AvgGlucoseHeader = withTranslation()(({ t }) => ( )); -export const AvgGlucoseCell = ({ patient, units }) => { // TODO: Fix for units +export const AvgGlucoseCell = ({ 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; + const rawValue = patient?.summary?.cgmStats?.periods?.[summaryPeriod]?.averageGlucoseMmol; - const value = utils.formatDecimal(rawValue, 1); + const value = clinicBgUnits === MGDL_UNITS + ? utils.translateBg(rawValue, MGDL_UNITS) + : utils.formatDecimal(rawValue, 1); // MMOLL_UNITS return ; }; @@ -58,7 +65,7 @@ export const TimeInRangePercentBarChartCell = ({ patient }) => { const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]); const clinicBgUnits = clinic?.preferredBgUnits || MGDL_UNITS; - // TODO: need to add showExtremeHigh + const { showExtremeHigh } = useFlags(); return { activeSummaryPeriod={summaryPeriod} glycemicRanges={patient?.glycemicRanges} clinicBgUnits={clinicBgUnits} + showExtremeHigh={showExtremeHigh || false} />; }; From 302692e78e3caadcca14a1b52ab434dfe0e727c1 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 14:17:23 -0700 Subject: [PATCH 18/23] WEB-4460 rename table building functions --- .../TideDashboardV2/useTableColumns.js | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js index 4b35770e8f..a009bd1311 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js +++ b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js @@ -27,7 +27,7 @@ import { import TagListCell from '../components/TagListCell'; -const getColumnTypes = (t, category, thresholds) => ({ +const buildColumnTypes = (t, category, thresholds) => ({ patientDetails: { title: t('Patient Details'), field: 'fullName', @@ -123,7 +123,7 @@ const getColumnTypes = (t, category, thresholds) => ({ }, // More }); -const buildColumnSets = (columnTypes) => ({ +const getColumnSet = (columnTypes) => ({ default: [ columnTypes.patientDetails, columnTypes.flag, @@ -212,19 +212,19 @@ const useTableColumns = (category) => { const columns = useMemo(() => { const thresholds = getFormattedThresholds(clinicBgUnits); - const columnTypes = getColumnTypes(t, category, thresholds); - const columnSets = buildColumnSets(columnTypes); + const columnTypes = buildColumnTypes(t, category, thresholds); + const columnSet = getColumnSet(columnTypes); switch(category) { - case CATEGORY.DEFAULT: return columnSets.default; - case CATEGORY.VERY_LOW: return columnSets.low; - case CATEGORY.ANY_LOW: return columnSets.low; - case CATEGORY.DROP_IN_TIR: return columnSets.dropInTIR; - case CATEGORY.ANY_HIGH: return columnSets.high; - case CATEGORY.VERY_HIGH: return columnSets.high; - case CATEGORY.LOW_CGM_WEAR: return columnSets.lowCgmWear; - case CATEGORY.TARGET: return columnSets.target; - default: return columnSets.default; + case CATEGORY.DEFAULT: return columnSet.default; + case CATEGORY.VERY_LOW: return columnSet.low; + case CATEGORY.ANY_LOW: return columnSet.low; + case CATEGORY.DROP_IN_TIR: return columnSet.dropInTIR; + case CATEGORY.ANY_HIGH: return columnSet.high; + case CATEGORY.VERY_HIGH: return columnSet.high; + case CATEGORY.LOW_CGM_WEAR: return columnSet.lowCgmWear; + case CATEGORY.TARGET: return columnSet.target; + default: return columnSet.default; } }, [category, clinicBgUnits]); From 1548acebd854a839c12fb39ab2b01361f4c14ea0 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 14:26:03 -0700 Subject: [PATCH 19/23] WEB-4460 update table columsn to match designs --- .../TideDashboardV2/useTableColumns.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js index a009bd1311..3b7b44244b 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js +++ b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js @@ -136,19 +136,9 @@ const getColumnSet = (columnTypes) => ({ columnTypes.lastReviewed, columnTypes.moreMenu, ], - target: [ - columnTypes.patientDetails, - columnTypes.avgGlucose, - columnTypes.timeInRangeBarChart, - columnTypes.changeInTIR, - columnTypes.gmi, - columnTypes.cgmUse, - columnTypes.tags, - columnTypes.lastReviewed, - columnTypes.moreMenu, - ], low: [ columnTypes.patientDetails, + columnTypes.flag, columnTypes.avgGlucose, columnTypes.timeInVeryLow, columnTypes.timeInAnyLow, @@ -161,6 +151,7 @@ const getColumnSet = (columnTypes) => ({ ], high: [ columnTypes.patientDetails, + columnTypes.flag, columnTypes.avgGlucose, columnTypes.timeInVeryHigh, columnTypes.timeInAnyHigh, @@ -173,6 +164,7 @@ const getColumnSet = (columnTypes) => ({ ], dropInTIR: [ columnTypes.patientDetails, + columnTypes.flag, columnTypes.avgGlucose, columnTypes.timeInTarget, columnTypes.timeInRangeBarChart, @@ -185,6 +177,7 @@ const getColumnSet = (columnTypes) => ({ ], lowCgmWear: [ columnTypes.patientDetails, + columnTypes.flag, columnTypes.cgmUse, columnTypes.avgGlucose, columnTypes.timeInTarget, @@ -223,7 +216,7 @@ const useTableColumns = (category) => { case CATEGORY.ANY_HIGH: return columnSet.high; case CATEGORY.VERY_HIGH: return columnSet.high; case CATEGORY.LOW_CGM_WEAR: return columnSet.lowCgmWear; - case CATEGORY.TARGET: return columnSet.target; + case CATEGORY.TARGET: return columnSet.default; default: return columnSet.default; } }, [category, clinicBgUnits]); From c2ce38984d090ce6e2162cd25714a6026e39482e Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 14:28:23 -0700 Subject: [PATCH 20/23] WEB-4460 fix tests --- .../TideDashboardV2/TideDashboardV2.test.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js index ff0ed55234..2bb835a80a 100644 --- a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js +++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js @@ -226,7 +226,6 @@ describe('TideDashboardV2', () => { expect(screen.getByRole('radio', { name: /All Patients/ })).toBeChecked(); expect(screen.getByText('Default Patient 2')).toBeInTheDocument(); expect(screen.getByText('DOB: 2001-01-01')).toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); expect(screen.getAllByRole('columnheader')).toHaveLength(10); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); @@ -247,7 +246,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Very Low Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Default Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getAllByRole('columnheader')).toHaveLength(11); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /% Time < 54/ })).toBeInTheDocument(); @@ -266,7 +265,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Low Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Very Low Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getAllByRole('columnheader')).toHaveLength(11); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /% Time < 54/ })).toBeInTheDocument(); @@ -285,7 +284,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Drop In TIR Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Low Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getAllByRole('columnheader')).toHaveLength(11); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); @@ -304,7 +303,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('High Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Drop In TIR Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getAllByRole('columnheader')).toHaveLength(11); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /% Time > 250/ })).toBeInTheDocument(); @@ -323,7 +322,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Very High Patient 2')).toBeInTheDocument(); expect(screen.queryByText('High Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getAllByRole('columnheader')).toHaveLength(11); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /% Time > 250/ })).toBeInTheDocument(); @@ -342,7 +341,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Low CGM Wear Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Very High Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getAllByRole('columnheader')).toHaveLength(11); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); @@ -361,7 +360,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Meeting Targets Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Low CGM Wear Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(9); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); From bc47bcc53e84c9eb713b47389f1f4ae06b694fa3 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 14:49:21 -0700 Subject: [PATCH 21/23] WEB-4460 fix tests --- .../app/pages/clinicworkspace/TideDashboardV2/Cells.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js index 8871d9b2e1..4dc5f527b4 100644 --- a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js +++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js @@ -88,7 +88,7 @@ describe('Cells', () => { it('renders the average glucose of the active summary period to one decimal place', () => { renderComponent(); - expect(screen.getByText('8.3')).toBeInTheDocument(); // averageGlucoseMmol 8.26 + expect(screen.getByText('149')).toBeInTheDocument(); // averageGlucoseMmol 8.26 }); }); From 3573a74b89a8d10b8abe60872dd1403dc38208db Mon Sep 17 00:00:00 2001 From: henry-tp Date: Wed, 2 Sep 2026 09:43:44 -0700 Subject: [PATCH 22/23] WEB-4460 remove flag from all columns except default --- .../TideDashboardV2/TideDashboardV2.test.js | 14 +++++++------- .../TideDashboardV2/useTableColumns.js | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js index 2bb835a80a..91bacb61af 100644 --- a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js +++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js @@ -246,7 +246,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Very Low Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Default Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(11); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /% Time < 54/ })).toBeInTheDocument(); @@ -265,7 +265,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Low Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Very Low Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(11); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /% Time < 54/ })).toBeInTheDocument(); @@ -284,7 +284,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Drop In TIR Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Low Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(11); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); @@ -303,7 +303,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('High Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Drop In TIR Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(11); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /% Time > 250/ })).toBeInTheDocument(); @@ -322,7 +322,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Very High Patient 2')).toBeInTheDocument(); expect(screen.queryByText('High Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(11); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /% Time > 250/ })).toBeInTheDocument(); @@ -341,7 +341,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Low CGM Wear Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Very High Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(11); + expect(screen.getAllByRole('columnheader')).toHaveLength(10); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); @@ -360,7 +360,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Meeting Targets Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Low CGM Wear Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(screen.getAllByRole('columnheader')).toHaveLength(9); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js index 3b7b44244b..564fbda771 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js +++ b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js @@ -138,7 +138,6 @@ const getColumnSet = (columnTypes) => ({ ], low: [ columnTypes.patientDetails, - columnTypes.flag, columnTypes.avgGlucose, columnTypes.timeInVeryLow, columnTypes.timeInAnyLow, @@ -151,7 +150,6 @@ const getColumnSet = (columnTypes) => ({ ], high: [ columnTypes.patientDetails, - columnTypes.flag, columnTypes.avgGlucose, columnTypes.timeInVeryHigh, columnTypes.timeInAnyHigh, @@ -164,7 +162,6 @@ const getColumnSet = (columnTypes) => ({ ], dropInTIR: [ columnTypes.patientDetails, - columnTypes.flag, columnTypes.avgGlucose, columnTypes.timeInTarget, columnTypes.timeInRangeBarChart, @@ -177,7 +174,6 @@ const getColumnSet = (columnTypes) => ({ ], lowCgmWear: [ columnTypes.patientDetails, - columnTypes.flag, columnTypes.cgmUse, columnTypes.avgGlucose, columnTypes.timeInTarget, @@ -188,6 +184,17 @@ const getColumnSet = (columnTypes) => ({ columnTypes.lastReviewed, columnTypes.moreMenu, ], + target: [ + columnTypes.patientDetails, + columnTypes.avgGlucose, + columnTypes.timeInRangeBarChart, + columnTypes.changeInTIR, + columnTypes.gmi, + columnTypes.cgmUse, + columnTypes.tags, + columnTypes.lastReviewed, + columnTypes.moreMenu, + ], }); const getFormattedThresholds = (clinicBgUnits) => { @@ -216,7 +223,7 @@ const useTableColumns = (category) => { case CATEGORY.ANY_HIGH: return columnSet.high; case CATEGORY.VERY_HIGH: return columnSet.high; case CATEGORY.LOW_CGM_WEAR: return columnSet.lowCgmWear; - case CATEGORY.TARGET: return columnSet.default; + case CATEGORY.TARGET: return columnSet.target; default: return columnSet.default; } }, [category, clinicBgUnits]); From b13210600302ac86c33dbe1fa805b61a81bea5a0 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 3 Sep 2026 17:33:43 -0700 Subject: [PATCH 23/23] WEB-4460 address automated feedback --- app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js index 564fbda771..34b9a9b9dc 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js +++ b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js @@ -226,7 +226,7 @@ const useTableColumns = (category) => { case CATEGORY.TARGET: return columnSet.target; default: return columnSet.default; } - }, [category, clinicBgUnits]); + }, [category, clinicBgUnits, t]); return columns; };