Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1785b26
WEB-4654 abstract out Data Recency
henry-tp Jul 14, 2026
e6d9b41
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 14, 2026
cefcc39
WEB-4654 update Data Recency visual styles
henry-tp Jul 14, 2026
fbb5f87
WEB-4654 fix minor spacing issue
henry-tp Jul 14, 2026
693b5e7
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 15, 2026
e86968e
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 15, 2026
453560b
WEB-4654 change directory for FilterByDataRecency
henry-tp Jul 15, 2026
a09b9c2
WEB-4654 pass in custom filter options from adapter component
henry-tp Jul 16, 2026
1a1180a
WEB-4654 clear unneeded props
henry-tp Jul 16, 2026
7739eb0
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 16, 2026
7fee3e6
WEB-4654 add tests for Data Recency filter
henry-tp Jul 16, 2026
675d246
WEB-4654 remove unnecessary tests
henry-tp Jul 17, 2026
332a835
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 17, 2026
41a107a
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 19, 2026
b8087c9
WEB-4654 use MemoryRouter instead of mocking out location hook
henry-tp Jul 19, 2026
1023fb1
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 20, 2026
c384324
WEB-4654 remove unused var
henry-tp Jul 20, 2026
66bc00f
WEB-4654 remove unused var
henry-tp Jul 20, 2026
b48f56c
WEB-4654 update pageName
henry-tp Jul 26, 2026
7e337a1
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 26, 2026
88e3baf
Merge branch 'WEB-4654-tags' into WEB-4654-data-recency
henry-tp Jul 26, 2026
990ffa4
WEB-4654 fix tests
henry-tp Jul 26, 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
124 changes: 6 additions & 118 deletions app/pages/clinicworkspace/ClinicPatients.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ import noop from 'lodash/noop';
import { getGlycemicRangesPreset } from '../../core/glycemicRangesUtils';
import FilterByTags from './FilterByTags';
import FilterBySites from './FilterBySites';
import FilterByDataRecency from './FilterByDataRecency';
import ClinicPatientsPrintModal from './ClinicPatientsPrintModal';

const { Loader } = vizComponents;
Expand Down Expand Up @@ -1732,124 +1733,6 @@ export const ClinicPatients = (props) => {
</Flex>

<Flex sx={{ flexShrink: 0, gap: 2 }}>

<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={!!activeFilters.lastData}
{...bindTrigger(lastDataPopupFilterState)}
icon={KeyboardArrowDownRoundedIcon}
iconLabel="Filter by last upload"
sx={{ fontSize: 0, lineHeight: 1.3 }}
>
{t('Data Recency')}
</Button>
</Box>

<Popover
width="13em"
closeIcon
{...bindPopover(lastDataPopupFilterState)}
onClickCloseIcon={() => {
trackMetric(prefixPopHealthMetric('Last upload filter close'), { clinicId: selectedClinicId });
}}
onClose={() => {
lastDataPopupFilterState.close();
setPendingFilters(activeFilters);
}}
>
<DialogContent px={2} py={3} dividers>
<Box sx={{ alignItems: 'center' }} mb={2}>
<Text sx={{ color: 'grays.4', fontWeight: 'medium', fontSize: 0, whiteSpace: 'nowrap' }}>
{t('Device Type')}
</Text>
</Box>

<RadioGroup
id="last-upload-type"
name="last-upload-type"
options={lastDataTypeFilterOptions}
variant="vertical"
sx={{ fontSize: 0 }}
value={pendingFilters.lastDataType || activeFilters.lastDataType}
onChange={event => {
setPendingFilters({ ...pendingFilters, lastDataType: event.target.value || null });
}}
/>

<Box
mt={3}
mb={2}
pt={3}
sx={{
alignItems: 'center',
borderTop: borders.divider,
}}
>
<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={pendingFilters.lastData || activeFilters.lastData}
onChange={event => {
setPendingFilters({ ...pendingFilters, lastData: parseInt(event.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 });
setPendingFilters({ ...activeFilters, lastData: defaultFilterState.lastData, lastDataType: defaultFilterState.lastDataType });
setActiveFilters({ ...activeFilters, lastData: defaultFilterState.lastData, lastDataType: defaultFilterState.lastDataType });
lastDataPopupFilterState.close();
}}
>
{t('Clear')}
</Button>

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

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

setActiveFilters(pendingFilters);
lastDataPopupFilterState.close();
}}
>
{t('Apply')}
</Button>
</DialogActions>
</Popover>

<FilterByTags
api={api}
activeFilters={activeFilters}
Expand All @@ -1864,6 +1747,11 @@ export const ClinicPatients = (props) => {
setShowClinicSitesDialog={setShowClinicSitesDialog}
/>

<FilterByDataRecency
activeFilters={activeFilters}
setActiveFilters={setActiveFilters}
/>

<Box
onClick={() => {
if (!timeInRangePopupFilterState.isOpen) trackMetric(prefixPopHealthMetric('Time in range filter open'), { clinicId: selectedClinicId });
Expand Down
25 changes: 25 additions & 0 deletions app/pages/clinicworkspace/FilterByDataRecency.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import noop from 'lodash/noop';

import DataRecencyFilterDropdown from './components/DataRecencyFilterDropdown';

const FilterByDataRecency = ({
activeFilters = {},
setActiveFilters = noop,
}) => {
const handleChange = ({ lastData, lastDataType }) => {
setActiveFilters({ ...activeFilters, lastData, lastDataType });
};

const { lastData, lastDataType } = activeFilters;

return (
<DataRecencyFilterDropdown
onChange={handleChange}
lastData={lastData}
lastDataType={lastDataType}
/>
);
};

export default FilterByDataRecency;
198 changes: 198 additions & 0 deletions app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { trackMetric } from '../../../core/metricUtils';

import { Box, Text } from 'theme-ui';
import KeyboardArrowDownRoundedIcon from '@material-ui/icons/KeyboardArrowDownRounded';

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

import { bindPopover, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';

import Button from '../../../components/elements/Button';
import Popover from '../../../components/elements/Popover';
import RadioGroup from '../../../components/elements/RadioGroup';
import { Body0 } from '../../../components/elements/FontStyles';

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

const prefixPopHealthMetric = () => noop; // TODO: FIX

const DropdownContent = ({
onClose = noop,
onChange = noop,
lastData = null,
lastDataType = null,
}) => {
const { t } = useTranslation();
const selectedClinicId = useSelector((state) => state.blip.selectedClinicId);

const [pending, setPending] = useState({ lastData, lastDataType });

const lastDataTypeFilterOptions = [
{ value: 'cgm', label: t('CGM') },
{ value: 'bgm', label: t('BGM') },
];

const customLastDataFilterOptions = reject(lastDataFilterOptions, { value: 7 });

const handleChange = (filters) => onChange(filters);

return (
<>
<DialogContent px={2} py={3} dividers>
<Box sx={{ alignItems: 'center' }} mb={2}>
<Text sx={{ color: 'grays.4', fontWeight: 'medium', fontSize: 0, whiteSpace: 'nowrap' }}>
{t('Device Type')}
</Text>
</Box>

<RadioGroup
id="last-upload-type"
name="last-upload-type"
options={lastDataTypeFilterOptions}
variant="vertical"
sx={{ fontSize: 0 }}
value={pending.lastDataType}
onChange={event => {
setPending({ ...pending, lastDataType: event.target.value || null });
}}
/>

<Box
mt={3}
mb={2}
pt={3}
sx={{
alignItems: 'center',
borderTop: borders.divider,
}}
>
<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={pending.lastData}
onChange={event => {
setPending({ ...pending, lastData: parseInt(event.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 });
setPending({ lastData: null, lastDataType: null });
handleChange({ lastData: null, lastDataType: null });
onClose();
}}
>
{t('Clear')}
</Button>

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

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

handleChange(pending);
onClose();
}}
>
{t('Apply')}
</Button>
</DialogActions>
</>
);
};

const DataRecencyFilterDropdown = ({
onChange = noop,
lastData = null,
lastDataType = null,
}) => {
const { t } = useTranslation();

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

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

const handleCloseDropdown = () => {
lastDataPopupFilterState.close();
};

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 }}
>
{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
lastData={lastData}
lastDataType={lastDataType}
onClose={handleCloseDropdown}
onChange={onChange}
/>
}
</Popover>
</>
);
};

export default DataRecencyFilterDropdown;