Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5ae947a
WEB-4654 abstract out TIR range filter
henry-tp Jul 15, 2026
a4d7133
WEB-4654 adjust styling of TIR dropdown
henry-tp Jul 15, 2026
69ce56b
WEB-4654 reposition FilterByTIR to after CGM Use
henry-tp Jul 15, 2026
d3cd1db
Merge branch 'WEB-4654-summary-period' into WEB-4654-time-in-range
henry-tp Jul 15, 2026
4100d98
WEB-4654 change directory for FilterByTIR
henry-tp Jul 15, 2026
c12d0e9
WEB-4654 clear unneeded props
henry-tp Jul 16, 2026
3d8fe0e
Merge branch 'WEB-4654-summary-period' into WEB-4654-time-in-range
henry-tp Jul 17, 2026
b5b6699
WEB-4654 add tests for FilterByTimeInRange
henry-tp Jul 17, 2026
265160e
WEB-4654 add tests for time-in-range
henry-tp Jul 17, 2026
a97d665
Merge branch 'WEB-4654-summary-period' into WEB-4654-time-in-range
henry-tp Jul 17, 2026
a9b544d
WEB-4654 fix metrics tracking
henry-tp Jul 17, 2026
800407a
Merge branch 'WEB-4654-summary-period' into WEB-4654-time-in-range
henry-tp Jul 17, 2026
3963a49
WEB-4654 fix tests for WEB-4654-time-in-range
henry-tp Jul 17, 2026
b7c5d26
WEB-4654 fix tests
henry-tp Jul 17, 2026
c857c03
Merge branch 'WEB-4654-summary-period' into WEB-4654-time-in-range
henry-tp Jul 19, 2026
3ec296b
WEB-4654 use MemoryRouter instead of mocking out location hook
henry-tp Jul 19, 2026
f4768d4
Merge branch 'WEB-4654-summary-period' into WEB-4654-time-in-range
henry-tp Jul 20, 2026
331634c
WEB-4654 revert reverse order of TIR dropdown
henry-tp Jul 22, 2026
69c3cd8
WEB-4654 fix not meeting TIR label
henry-tp Jul 26, 2026
0c205fd
WEB-4654 fix trackMetric calls
henry-tp Jul 26, 2026
7db9b8e
WEB-4654 update pageName
henry-tp Jul 26, 2026
a59de3b
Merge branch 'WEB-4654-summary-period' into WEB-4654-time-in-range
henry-tp Jul 26, 2026
36dab83
Merge branch 'WEB-4654-summary-period' into WEB-4654-time-in-range
henry-tp Jul 26, 2026
741c5f9
WEB-4654 abstract timeInRangeFilterThresholds for consistency
henry-tp Jul 26, 2026
11d2e79
Merge branch 'WEB-4654-summary-period' into WEB-4654-time-in-range
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
279 changes: 6 additions & 273 deletions app/pages/clinicworkspace/ClinicPatients.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,11 @@ import { getGlycemicRangesPreset } from '../../core/glycemicRangesUtils';
import FilterByTags from './FilterByTags';
import FilterBySites from './FilterBySites';
import FilterByDataRecency from './FilterByDataRecency';
import FilterByTimeInRange from './FilterByTimeInRange';
import ClinicPatientsPrintModal from './ClinicPatientsPrintModal';

const { Loader } = vizComponents;
const { reshapeBgClassesToBgBounds, generateBgRangeLabels, formatBgValue } = vizUtils.bg;
const { formatBgValue } = vizUtils.bg;
const { getLocalizedCeiling, formatTimeAgo } = vizUtils.datetime;

const StyledScrollToTop = styled(ScrollToTop)`
Expand All @@ -150,51 +151,6 @@ const glycemicTargetThresholds = {
timeInExtremeHighPercent: { value: 1, comparator: '>' },
};

const getTimeInRangeFilterOptions = (showExtremeHigh = false, t) => [
(showExtremeHigh && {
title: t('Highest'),
value: 'timeInExtremeHighPercent',
threshold: glycemicTargetThresholds.timeInExtremeHighPercent.value,
prefix: t('Greater than'),
rangeName: 'extremeHigh',
}),
{
title: t('Very High'),
value: 'timeInVeryHighPercent',
threshold: glycemicTargetThresholds.timeInVeryHighPercent.value,
prefix: t('Greater than'),
rangeName: 'veryHigh',
},
{
title: t('High'),
value: 'timeInAnyHighPercent',
threshold: glycemicTargetThresholds.timeInAnyHighPercent.value,
prefix: t('Greater than'),
rangeName: 'anyHigh',
},
{
title: t('Not meeting TIR'),
value: 'timeInTargetPercent',
threshold: glycemicTargetThresholds.timeInTargetPercent.value,
prefix: t('Less than'),
rangeName: 'target',
},
{
title: t('Low'),
value: 'timeInAnyLowPercent',
threshold: glycemicTargetThresholds.timeInAnyLowPercent.value,
prefix: t('Greater than'),
rangeName: 'anyLow',
},
{
title: t('Very Low'),
value: 'timeInVeryLowPercent',
threshold: glycemicTargetThresholds.timeInVeryLowPercent.value,
prefix: t('Greater than'),
rangeName: 'veryLow',
},
].filter(Boolean);

const editPatient = (patient, setSelectedPatient, selectedClinicId, trackMetric, setShowEditPatientDialog, source) => {
trackMetric('Clinic - Edit patient', { clinicId: selectedClinicId, source });
setSelectedPatient(patient);
Expand Down Expand Up @@ -703,18 +659,6 @@ export const ClinicPatients = (props) => {
timeInVeryHighRecords: 'desc',
}), []);

const bgLabels = useMemo(
() =>
generateBgRangeLabels(
{
bgUnits: clinicBgUnits,
bgBounds: reshapeBgClassesToBgBounds({ bgUnits: clinicBgUnits }),
},
{ segmented: true }
),
[clinicBgUnits]
);

const [activeFilters, setActiveFilters] = useClinicPatientsFilters();
const [pendingFilters, setPendingFilters] = useState({ ...defaultFilterState, ...activeFilters });
const previousActiveFilters = usePrevious(activeFilters);
Expand Down Expand Up @@ -750,11 +694,6 @@ export const ClinicPatients = (props) => {
const [activeSummaryPeriod, setActiveSummaryPeriod] = useLocalStorage('activePatientSummaryPeriod', defaultSummaryPeriod);
const previousSummaryPeriod = usePrevious(activeSummaryPeriod);

const timeInRangePopupFilterState = usePopupState({
variant: 'popover',
popupId: 'timeInRangeFilters',
});

const cgmUsePopupFilterState = usePopupState({
variant: 'popover',
popupId: 'cgmUseFilters',
Expand Down Expand Up @@ -1494,35 +1433,6 @@ export const ClinicPatients = (props) => {
setPendingFilters(defaultFilterState);
}

const handleFilterTimeInRange = useCallback(() => {
trackMetric(prefixPopHealthMetric('Time in range apply filter'), {
clinicId: selectedClinicId,
meetsCriteria: pendingFilters.meetsGlycemicTargets,
severeHypo: includes(pendingFilters.timeInRange, 'timeInVeryLowPercent'),
hypo: includes(pendingFilters.timeInRange, 'timeInAnyLowPercent'),
inRange: includes(pendingFilters.timeInRange, 'timeInTargetPercent'),
hyper: includes(pendingFilters.timeInRange, 'timeInAnyHighPercent'),
severeHyper: includes(pendingFilters.timeInRange, 'timeInVeryHighPercent'),
extremeHyper: includes(pendingFilters.timeInRange, 'timeInExtremeHighPercent'),
});

setActiveFilters({
...activeFilters,
meetsGlycemicTargets: pendingFilters.meetsGlycemicTargets,
timeInRange: pendingFilters.timeInRange,
});

timeInRangePopupFilterState.close();
}, [
activeFilters,
pendingFilters.meetsGlycemicTargets,
pendingFilters.timeInRange,
prefixPopHealthMetric,
selectedClinicId,
setActiveFilters,
trackMetric
]);

const handleRemovePatient = useCallback(() => {
trackMetric('Clinic - Remove patient confirmed', { clinicId: selectedClinicId });
dispatch(actions.async.deletePatientFromClinic(api, selectedClinicId, selectedPatient?.id));
Expand Down Expand Up @@ -1724,187 +1634,10 @@ export const ClinicPatients = (props) => {
setActiveFilters={setActiveFilters}
/>

<Box
onClick={() => {
if (!timeInRangePopupFilterState.isOpen) trackMetric(prefixPopHealthMetric('Time in range filter open'), { clinicId: selectedClinicId });
}}
>
<Button
id="time-in-range-filter-trigger"
variant="filter"
selected={!!activeFilters.timeInRange?.length}
{...bindTrigger(timeInRangePopupFilterState)}
icon={KeyboardArrowDownRoundedIcon}
iconLabel="Filter by Time in Range"
sx={{ fontSize: 0, lineHeight: 1.3, flexShrink: 0 }}
>
<Flex sx={{ gap: 1 }}>
{t('% Time in Range')}
{!!activeFilters.timeInRange?.length && (
<Pill
id="time-in-range-filter-count"
label="filter count"
round
sx={{
width: '14px',
fontSize: '9px',
lineHeight: '15px',
textAlign: 'center',
display: 'inline-block',
}}
colorPalette={['purpleMedium', 'white']}
text={`${activeFilters.timeInRange?.length}`}
/>
)}
</Flex>
</Button>
</Box>

<Popover
minWidth="11em"
closeIcon
{...bindPopover(timeInRangePopupFilterState)}
onClickCloseIcon={() => {
trackMetric(prefixPopHealthMetric('Time in range filter close'), { clinicId: selectedClinicId });
}}
onClose={() => {
timeInRangePopupFilterState.close();
setPendingFilters(activeFilters);
}}
>
<DialogContent color="text.primary" pl={4} pr={6} pb={3}>
<Box mb={3} sx={{ fontSize: 1, fontWeight: 'medium' }}>
<Box mr={2} sx={{ color: vizColors.gray50, fontWeight: 'medium', fontSize: 1, whiteSpace: 'nowrap' }}>
{t('% Time in Range')}
</Box>
<Box mt={2} sx={{ color: vizColors.blue50, fontWeight: 'normal', fontStyle: 'italic', fontSize: 0, lineHeight: 1 }}>
<Text>{t('Only patients using the standard target range will be included.')}</Text>
</Box>
</Box>

{map(getTimeInRangeFilterOptions(showExtremeHigh, t), ({ value, title, rangeName, threshold, prefix }) => {
const {prefix: bgPrefix, suffix, value:glucoseTargetValue} = bgLabels[rangeName];

return (
<Flex
id={`time-in-range-filter-${rangeName}`}
key={rangeName}
mb={3}
ml={2}
sx={{ alignItems: 'center', gap: 2 }}
>
<Checkbox
id={`range-${value}-filter`}
name={`range-${value}-filter`}
key={value}
checked={includes([...pendingFilters.timeInRange], value)}
onChange={event => {
setPendingFilters(event.target.checked
? { ...pendingFilters, timeInRange: [...pendingFilters.timeInRange, value] }
: { ...pendingFilters, timeInRange: without(pendingFilters.timeInRange, value) }
);
}}
/>

<Box
px={1}
py={1}
ml={-2}
sx={{
backgroundColor: `${colors.bg[rangeName]}1A`, // Adding '1A' reduces opacity to 0.1
borderRadius: 4,
}}
>
<Flex as="label" htmlFor={`range-${value}-filter`} sx={{ alignItems: 'center' }}>
<Box
id={`range-${value}-filter-option-color-indicator`}
sx={{
position: 'relative',
borderRadius: 4,
backgroundColor: colors.bg[rangeName],
width: '12px',
height: '12px',

// The styles within the :after pseudo-class below create a diagonal line

border: value === 'timeInTargetPercent' && `1.5px solid ${colors.blueGreyDark}`,
'&::after': value === 'timeInTargetPercent' && {
content: '""',
height: '1.5px',
width: '141.421%',
backgroundColor: colors.blueGreyDark,
position: 'absolute',
bottom: '0px',
transform: 'rotate(-45deg)',
transformOrigin: '1px 1px',
},
}}
mr={2}
>
</Box>

<Text
id={`range-${value}-filter-option-title`}
sx={{ fontSize: 1, fontWeight: 'bold', color: 'black' }}
mr={2}
>
{title}
</Text>

<Text id={`range-${value}-filter-option-definition`} sx={{ fontSize: 1 }} mr={2}>
{prefix}{' '}
<Text sx={{ fontSize: 2, fontWeight: 'bold' }}>
{threshold}
</Text>
% {t('Time')}{' '}
{bgPrefix && `${t(bgPrefix)} `}
<Text sx={{ fontSize: 2, fontWeight: 'bold' }}>
{glucoseTargetValue}
</Text>{' '}
{suffix}
</Text>
</Flex>
</Box>
</Flex>
);
})}

<Button
variant="textSecondary"
px={0}
sx={{ fontSize: 0 }}
onClick={() => {
trackMetric(prefixPopHealthMetric('Time in range unselect all'), { clinicId: selectedClinicId });
setPendingFilters({ ...pendingFilters, timeInRange: defaultFilterState.timeInRange });
}}
>
{t('Unselect all')}
</Button>
</DialogContent>

<DialogActions sx={{ justifyContent: 'space-between' }} p={2}>
<Button
id="timeInRangeFilterClear"
variant="textSecondary"
onClick={() => {
trackMetric(prefixPopHealthMetric('Time in range clear filter'), { clinicId: selectedClinicId });
setPendingFilters({ ...activeFilters, timeInRange: defaultFilterState.timeInRange });
setActiveFilters({ ...activeFilters, timeInRange: defaultFilterState.timeInRange });
timeInRangePopupFilterState.close();
}}
>
{t('Clear')}
</Button>

<Button
id="timeInRangeFilterConfirm"
variant="textPrimary"
onClick={handleFilterTimeInRange}
>
{t('Apply')}
</Button>
</DialogActions>
</Popover>
<FilterByTimeInRange
activeFilters={activeFilters}
setActiveFilters={setActiveFilters}
/>

<Box
onClick={() => {
Expand Down
24 changes: 24 additions & 0 deletions app/pages/clinicworkspace/FilterByTimeInRange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import noop from 'lodash/noop';

import TimeInRangeFilterDropdown from './components/TimeInRangeFilterDropdown';

const FilterByTimeInRange = ({
activeFilters = {},
setActiveFilters = noop,
}) => {
const handleChange = (timeInRange) => {
setActiveFilters({ ...activeFilters, timeInRange });
};

const { timeInRange } = activeFilters;

return (
<TimeInRangeFilterDropdown
onChange={handleChange}
timeInRange={timeInRange}
/>
);
};

export default FilterByTimeInRange;
Loading