Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
db63bb1
WEB-4460 add data recency filter
henry-tp Mar 4, 2026
df4acdd
Merge branch 'WEB-4460-dashboard' into WEB-4460-data-recency
henry-tp Mar 5, 2026
be23bd7
WEB-4460 remove erroneous comment
henry-tp Mar 5, 2026
c3433d2
Merge branch 'WEB-4460-dashboard' into WEB-4460-data-recency
henry-tp Mar 5, 2026
db0421c
Merge branch 'WEB-4460-dashboard' into WEB-4460-data-recency
henry-tp Mar 6, 2026
a892531
WEB-4460 add note to refactor
henry-tp Mar 6, 2026
0b52a75
Merge branch 'WEB-4460-dashboard' into WEB-4460-data-recency
henry-tp Mar 12, 2026
dc787fa
WEB-4460 set offset to 0 on filter change
henry-tp Mar 13, 2026
42f20d9
WEB-4460 add period summary filter
henry-tp Mar 13, 2026
929ea62
WEB-4460 abstract out derived data recency endpoints
henry-tp Mar 13, 2026
3af885f
WEB-4460 add divider
henry-tp Mar 13, 2026
f1fd5c1
Merge branch 'WEB-4460-dashboard' into WEB-4460-data-recency
henry-tp Mar 13, 2026
0df758f
Merge branch 'WEB-4460-dashboard' into WEB-4460-data-recency
henry-tp May 6, 2026
a77a675
Merge branch 'WEB-4460-dashboard' into WEB-4460-data-recency
henry-tp May 15, 2026
495c034
Merge branch 'WEB-4460-dashboard' into WEB-4460-data-recency
henry-tp May 29, 2026
a593f14
Merge branch 'WEB-4460-dashboard' into WEB-4460-data-recency
henry-tp Jun 23, 2026
2cb4909
WEB-4460 fix data recency options
henry-tp Jun 26, 2026
8cec19d
WEB-4460 arrange data filters to new designs
henry-tp Jun 30, 2026
39a3c77
Merge branch 'WEB-4460-dashboard' into WEB-4460-data-recency
henry-tp Jul 6, 2026
dde6058
Merge branch 'WEB-4460-setup' into WEB-4460-data-recency
henry-tp Jul 8, 2026
54e5839
Merge branch 'WEB-4460-setup' into WEB-4460-data-recency
henry-tp Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions app/pages/clinicworkspace/TideDashboardV2/FilterByDataRecency.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React, { useState} from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { bindPopover, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
import { setLastDataFilter } from './tideDashboardFiltersSlice';
import { Box } from 'theme-ui';
import Button from '../../../components/elements/Button';
import Popover from '../../../components/elements/Popover';
import RadioGroup from '../../../components/elements/RadioGroup';

import KeyboardArrowDownRoundedIcon from '@material-ui/icons/KeyboardArrowDownRounded';

import noop from 'lodash/noop';
import reject from 'lodash/reject';
import find from 'lodash/find';

import { lastDataFilterOptions } from '../../../core/clinicUtils';
import { borders } from '../../../themes/baseTheme';
import { DialogActions, DialogContent } from '../../../components/elements/Dialog';
import { Body0 } from '../../../components/elements/FontStyles';
import { setOffset } from './tideDashboardSlice';

const trackMetric = noop;
const prefixPopHealthMetric = noop;

const customLastDataFilterOptions = lastDataFilterOptions.filter(opt => [1, 2, 7].includes(opt.value));

const DropdownContent = ({
onClose = noop,
}) => {
const { t } = useTranslation();
const dispatch = useDispatch();

const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
const lastData = useSelector(state => state.blip.tideDashboardFilters.lastData);
const [pendingLastData, setPendingLastData] = useState(lastData);

return (
<>
<DialogContent px={2} py={3} dividers>
<Box mb={2} sx={{ alignItems: 'center' }}>
<Body0 color="grays.4" sx={{ fontWeight: 'bold' }} mb={0}>{t('Data Recency')}</Body0>
<Body0 color="grays.4" sx={{ fontWeight: 'medium' }} mb={2}>{t('Tidepool will only show patients who have data within the selected number of days.')}</Body0>
</Box>

<RadioGroup
id="last-upload-filters"
name="last-upload-filters"
options={customLastDataFilterOptions}
variant="vertical"
sx={{ fontSize: 0 }}
mb={3}
value={pendingLastData}
onChange={evt => setPendingLastData(parseInt(evt.target.value) || null)}
/>
</DialogContent>

<DialogActions sx={{ justifyContent: 'space-between' }} p={1}>
<Button
id="clear-last-upload-filter"
sx={{ fontSize: 1 }}
variant="textSecondary"
onClick={() => {
trackMetric(prefixPopHealthMetric('Last upload clear filter'), { clinicId: selectedClinicId });
onClose();
}}
>
{t('Clear')}
</Button>

<Button
id="apply-last-upload-filter"
disabled={!pendingLastData}
sx={{ fontSize: 1 }}
variant="textPrimary"
onClick={() => {
const dateRange = pendingLastData === 1
? 'today'
: `${pendingLastData} days`;

trackMetric(prefixPopHealthMetric('Last upload apply filter'), {
clinicId: selectedClinicId,
dateRange,
type: 'cgm',
});

dispatch(setLastDataFilter(pendingLastData));
dispatch(setOffset(0));
onClose();
}}
>
{t('Apply')}
</Button>
</DialogActions>
</>
);
};

const FilterByDataRecency = () => {
const { t } = useTranslation();
const lastDataPopupFilterState = usePopupState({
variant: 'popover',
popupId: 'lastDataFilters',
});

const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
const lastData = useSelector(state => state.blip.tideDashboardFilters.lastData);

return (
<>
<Box
onClick={() => {
if (!lastDataPopupFilterState.isOpen) {
trackMetric(prefixPopHealthMetric('Last data filter open'), { clinicId: selectedClinicId });
}
}}
sx={{ flexShrink: 0 }}
>
<Button
variant="filter"
id="last-data-filter-trigger"
selected={!!lastData}
{...bindTrigger(lastDataPopupFilterState)}
icon={KeyboardArrowDownRoundedIcon}
iconLabel="Filter by last upload"
sx={{ fontSize: 0, lineHeight: 1.3 }}
>
{lastData
? lastData === 1
? t('Data within 1 day')
: t('Data within') + find(customLastDataFilterOptions, { value: lastData })?.label?.replace('Within', '')
: t('Data Recency')
}
</Button>
</Box>

<Popover
width="13em"
closeIcon
{...bindPopover(lastDataPopupFilterState)}
onClickCloseIcon={() => {
trackMetric(prefixPopHealthMetric('Last upload filter close'), { clinicId: selectedClinicId });
}}
onClose={() => lastDataPopupFilterState.close()}
>
{ lastDataPopupFilterState.isOpen &&
<DropdownContent
onClose={() => lastDataPopupFilterState.close()}
/>
}
</Popover>
</>
);
};

export default FilterByDataRecency;
149 changes: 149 additions & 0 deletions app/pages/clinicworkspace/TideDashboardV2/FilterBySummaryPeriod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { bindPopover, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
import { setSummaryPeriodFilter } from './tideDashboardFiltersSlice';
import { Box } from 'theme-ui';
import Button from '../../../components/elements/Button';
import Popover from '../../../components/elements/Popover';
import RadioGroup from '../../../components/elements/RadioGroup';

import KeyboardArrowDownRoundedIcon from '@material-ui/icons/KeyboardArrowDownRounded';

import noop from 'lodash/noop';
import find from 'lodash/find';

import { DialogActions, DialogContent } from '../../../components/elements/Dialog';
import { Body0 } from '../../../components/elements/FontStyles';
import { setOffset } from './tideDashboardSlice';

const trackMetric = noop;
const prefixPopHealthMetric = noop;

const summaryPeriodOptions = [
{ value: '1d', label: '24 hours' },
{ value: '7d', label: '7 days' },
{ value: '14d', label: '14 days' },
{ value: '30d', label: '30 days' },
];

const DropdownContent = ({
onClose = noop,
}) => {
const { t } = useTranslation();
const dispatch = useDispatch();

const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod);
const [pendingSummaryPeriod, setPendingSummaryPeriod] = useState(summaryPeriod);

return (
<>
<DialogContent px={2} py={3} dividers>
<Box mb={2} sx={{ alignItems: 'center' }}>
<Body0 color="grays.4" sx={{ fontWeight: 'bold' }} mb={0}>{t('Summary Period')}</Body0>
<Body0 color="grays.4" sx={{ fontWeight: 'medium' }} mb={2}>{t('Tidepool will generate health summaries for the selected number of days.')}</Body0>
</Box>

<RadioGroup
id="summary-period-filters"
name="summary-period-filters"
options={summaryPeriodOptions}
variant="vertical"
sx={{ fontSize: 0 }}
mb={3}
value={pendingSummaryPeriod}
onChange={event => setPendingSummaryPeriod(event.target.value)}
/>
</DialogContent>

<DialogActions sx={{ justifyContent: 'space-between' }} p={1}>
<Button
id="cancel-summary-period-filter"
sx={{ fontSize: 1 }}
variant="textSecondary"
onClick={() => {
trackMetric(prefixPopHealthMetric('Summary period filter cancel'), { clinicId: selectedClinicId });
setPendingSummaryPeriod(summaryPeriod);
onClose();
}}
>
{t('Cancel')}
</Button>

<Button
id="apply-summary-period-filter"
disabled={pendingSummaryPeriod === summaryPeriod}
sx={{ fontSize: 1 }}
variant="textPrimary"
onClick={() => {
trackMetric(prefixPopHealthMetric('Summary period apply filter'), {
clinicId: selectedClinicId,
summaryPeriod: pendingSummaryPeriod,
});

dispatch(setSummaryPeriodFilter(pendingSummaryPeriod));
dispatch(setOffset(0));
onClose();
}}
>
{t('Apply')}
</Button>
</DialogActions>
</>
);
};

const FilterBySummaryPeriod = () => {
const { t } = useTranslation();
const summaryPeriodPopupFilterState = usePopupState({
variant: 'popover',
popupId: 'summaryPeriodFilters',
});

const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod);

return (
<>
<Box
onClick={() => {
if (!summaryPeriodPopupFilterState.isOpen) {
trackMetric(prefixPopHealthMetric('Summary period filter open'), { clinicId: selectedClinicId });
}
}}
sx={{ flexShrink: 0 }}
>
<Button
variant="filter"
id="summary-period-filter-trigger"
selected={!!summaryPeriod}
{...bindTrigger(summaryPeriodPopupFilterState)}
icon={KeyboardArrowDownRoundedIcon}
iconLabel="Filter by summary period duration"
sx={{ fontSize: 0, lineHeight: 1.3 }}
>
{find(summaryPeriodOptions, { value: summaryPeriod })?.label} {t('of data')}
</Button>
</Box>

<Popover
width="13em"
closeIcon
{...bindPopover(summaryPeriodPopupFilterState)}
onClickCloseIcon={() => {
trackMetric(prefixPopHealthMetric('Summary period filter close'), { clinicId: selectedClinicId });
}}
onClose={() => summaryPeriodPopupFilterState.close()}
>
{ summaryPeriodPopupFilterState.isOpen &&
<DropdownContent
onClose={() => summaryPeriodPopupFilterState.close()}
/>
}
</Popover>
</>
);
};

export default FilterBySummaryPeriod;
24 changes: 18 additions & 6 deletions app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { useDispatch, useSelector } from 'react-redux';
import { useTranslation, Trans } from 'react-i18next';
import { colors as vizColors } from '@tidepool/viz';
import Table from '../../../components/elements/Table';
import { Box, Flex, Grid, Text } from 'theme-ui';
import { Flex, Text, Box, Grid } from 'theme-ui';

import FilterByCategory from './FilterByCategory';
import FilterByTags from './FilterByTags';
import FilterByDataRecency from './FilterByDataRecency';
import FilterBySummaryPeriod from './FilterBySummaryPeriod';

import TableCategoryHeader from './TableCategoryHeader';
import PaginationControls from '../components/PaginationControls';
import ActiveFilterCount from '../components/ActiveFilterCount';
Expand All @@ -18,6 +21,7 @@ import { resetTideDashboardState, setOffset } from './tideDashboardSlice';
import { useGetTideDashboardPatientsQuery } from './tideDashboardApi';
import ResetFilters from '../components/ResetFilters';
import useActiveFiltersCount from './useActiveFiltersCount';
import useDerivedDataRecencyEndpoints from './useDerivedDataRecencyEndpoints';
import usePruneInvalidFilters from './usePruneInvalidFilters';
import { resetTideDashboardFilters } from './tideDashboardFiltersSlice';
import EmptyContentNode from './EmptyContentNode';
Expand All @@ -26,6 +30,10 @@ import PatientCount from '../components/PatientCount';

const LIMIT = 12;

const Divider = () => <Box id='filter-divider' mx={2} sx={{ border: `1px solid ${vizColors.gray05}`, height: '24px' }}></Box>;

const Gap = () => <Box sx={{ marginLeft: 'auto' }}></Box>;

const TideDashboard = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
Expand All @@ -37,8 +45,10 @@ const TideDashboard = () => {
const offset = useSelector(state => state.blip.tideDashboard.offset);
const { patientTags, clinicSites } = useSelector(state => state.blip.tideDashboardFilters);

const [lastDataFrom, lastDataTo] = useDerivedDataRecencyEndpoints();

const { data } = useGetTideDashboardPatientsQuery(
{ clinicId: selectedClinicId, offset, category, tags: patientTags, sites: clinicSites, limit: LIMIT },
{ clinicId: selectedClinicId, offset, category, lastDataTo, lastDataFrom, tags: patientTags, sites: clinicSites, limit: LIMIT },
{ skip: !selectedClinicId }
);

Expand All @@ -51,6 +61,8 @@ const TideDashboard = () => {

const handleChangeOffset = (newOffset) => dispatch(setOffset(newOffset));

const handleResetFilters = () => dispatch(resetTideDashboardFilters());

if (!data) return null;

const tableData = data?.data || [];
Expand All @@ -63,10 +75,10 @@ const TideDashboard = () => {
<ActiveFilterCount count={activeFiltersCount} />
<FilterByTags />
<FilterBySites />
<ResetFilters
hidden={activeFiltersCount <= 0}
onClick={() => dispatch(resetTideDashboardFilters())}
/>
<FilterByDataRecency />
<ResetFilters hidden={activeFiltersCount <= 0} onClick={handleResetFilters} />
<Gap />
<FilterBySummaryPeriod />
</Flex>

<Flex mb={3} sx={{ justifyContent: 'center' }}>
Expand Down
Loading