diff --git a/__tests__/unit/app/pages/clinicworkspace/ClinicPatients.test.js b/__tests__/unit/app/pages/clinicworkspace/ClinicPatients.test.js
index 9557f5f23d..de9b75a3e1 100644
--- a/__tests__/unit/app/pages/clinicworkspace/ClinicPatients.test.js
+++ b/__tests__/unit/app/pages/clinicworkspace/ClinicPatients.test.js
@@ -405,6 +405,25 @@ describe('ClinicPatients', () => {
);
}, TEST_TIMEOUT_MS);
+ it('maps an applied summary period filter into the getPatientsForClinic query', async () => {
+ render(
+
+
+
+ );
+
+ // Open the Summary Period filter dropdown, select 30 days, and apply
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+ await userEvent.click(screen.getByRole('radio', { name: /30 days/ }));
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+
+ expect(defaultProps.api.clinics.getPatientsForClinic).toHaveBeenLastCalledWith(
+ 'clinicID123',
+ { limit: 50, offset: 0, period: '30d', sortType: 'cgm', sort: '-lastData' },
+ expect.any(Function),
+ );
+ }, TEST_TIMEOUT_MS);
+
it('maps an applied site filter into the getPatientsForClinic query', async () => {
render(
diff --git a/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySummaryPeriod.test.js b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySummaryPeriod.test.js
new file mode 100644
index 0000000000..13485afc8b
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySummaryPeriod.test.js
@@ -0,0 +1,79 @@
+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 { MemoryRouter } from 'react-router-dom';
+
+import FilterBySummaryPeriod from '@app/pages/clinicworkspace/clinicPatientsFilters/FilterBySummaryPeriod';
+import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
+
+const mockStore = configureStore([thunk]);
+
+describe('FilterBySummaryPeriod', () => {
+ let store;
+ let wrapper;
+
+ const selectedClinicId = 'clinic123';
+
+ const setActiveSummaryPeriod = jest.fn();
+
+ const ui = (props = {}) => (
+
+
+
+
+
+ );
+
+ const renderComponent = (props = {}) => render(ui(props));
+
+ beforeEach(() => {
+ store = mockStore({
+ blip: {
+ selectedClinicId,
+ clinics: { [selectedClinicId]: { id: selectedClinicId } },
+ },
+ });
+
+ setActiveSummaryPeriod.mockClear();
+ mockTrackMetric.mockClear();
+ });
+
+ describe('handleChange', () => {
+ it('calls setActiveSummaryPeriod with the newly selected period', async () => {
+ wrapper = renderComponent({ activeSummaryPeriod: '14d' });
+
+ // Open the dropdown via the trigger's icon label (its text label is the dynamic
+ // "Summarizing ..." string that changes with the active period).
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+ await screen.findByRole('radio', { name: /30 days/ });
+
+ await userEvent.click(screen.getByRole('radio', { name: /30 days/ }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // The period value is passed straight through, not merged into an activeFilters object
+ expect(setActiveSummaryPeriod).toHaveBeenCalledTimes(1);
+ expect(setActiveSummaryPeriod).toHaveBeenCalledWith('30d');
+ });
+ });
+
+ describe('activeSummaryPeriod passthrough', () => {
+ it('reflects the active summary period in the trigger label and the pre-selected radio', async () => {
+ wrapper = renderComponent({ activeSummaryPeriod: '7d' });
+
+ // Trigger label reflects the active period
+ expect(screen.getByRole('button', { name: /Summarizing 7 days of data/ })).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+
+ expect(screen.getByRole('radio', { name: /7 days/ })).toBeChecked();
+ expect(screen.getByRole('radio', { name: /14 days/ })).not.toBeChecked();
+ expect(screen.getByRole('radio', { name: /30 days/ })).not.toBeChecked();
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown.test.js
new file mode 100644
index 0000000000..77c1f329ba
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown.test.js
@@ -0,0 +1,109 @@
+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 { MemoryRouter } from 'react-router-dom';
+
+import SummaryPeriodFilterDropdown from '@app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown';
+
+const mockStore = configureStore([thunk]);
+
+describe('SummaryPeriodFilterDropdown', () => {
+ let store;
+ let wrapper;
+
+ const selectedClinicId = 'clinic123';
+
+ let onChange = jest.fn();
+
+ const ui = (props = {}) => (
+
+
+
+
+
+ );
+
+ const renderComponent = (props = {}) => render(ui(props));
+
+ beforeEach(() => {
+ store = mockStore({
+ blip: {
+ selectedClinicId,
+ clinics: { [selectedClinicId]: { id: selectedClinicId } },
+ },
+ });
+
+ onChange.mockClear();
+ });
+
+ describe('filtering for summary period', () => {
+ it('applies the summary period based on the radio selected', async () => {
+ renderComponent({ activeSummaryPeriod: '14d' });
+
+ // Should have correct label
+ expect(screen.getByRole('button', { name: /Summarizing 14 days of data/ })).toBeInTheDocument();
+
+ // Dropdown closed initially
+ expect(screen.queryByTestId('summary-period-filter-dropdown')).not.toBeInTheDocument();
+
+ // Open the dropdown
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+ expect(screen.getByRole('radio', { name: /24 hours/ })).toBeInTheDocument();
+
+ // The active period is pre-selected
+ expect(screen.getByRole('radio', { name: /14 days/ })).toBeChecked();
+ expect(screen.getByRole('radio', { name: /30 days/ })).not.toBeChecked();
+
+ // Selecting a different period and applying sets the filter
+ await userEvent.click(screen.getByRole('radio', { name: /30 days/ }));
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(onChange).toHaveBeenCalledWith('30d');
+
+ // Dropdown should automatically close
+ expect(screen.queryByTestId('summary-period-filter-dropdown')).not.toBeInTheDocument();
+ });
+
+ it('disables the Apply button until a different period is selected', async () => {
+ renderComponent({ activeSummaryPeriod: '14d' });
+
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+
+ // Disabled while the pending selection matches the active period
+ expect(screen.getByRole('button', { name: /Apply/ })).toBeDisabled();
+
+ // Enabled once a different period is selected
+ await userEvent.click(screen.getByRole('radio', { name: /30 days/ }));
+ expect(screen.getByRole('button', { name: /Apply/ })).toBeEnabled();
+
+ // Disabled again when re-selecting the active period
+ await userEvent.click(screen.getByRole('radio', { name: /14 days/ }));
+ expect(screen.getByRole('button', { name: /Apply/ })).toBeDisabled();
+ });
+
+ it('cancels without applying and resets the pending selection', async () => {
+ renderComponent({ activeSummaryPeriod: '14d' });
+
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+
+ // Change the selection, then cancel
+ await userEvent.click(screen.getByRole('radio', { name: /30 days/ }));
+ await userEvent.click(screen.getByRole('button', { name: /Cancel/ }));
+
+ // No change is applied and the dropdown closes
+ expect(onChange).not.toHaveBeenCalled();
+ expect(screen.queryByTestId('summary-period-filter-dropdown')).not.toBeInTheDocument();
+
+ // Re-opening shows the original active period still selected (pending was reset)
+ await userEvent.click(screen.getByRole('button', { name: /Filter by summary period duration/ }));
+ expect(screen.getByRole('radio', { name: /14 days/ })).toBeChecked();
+ expect(screen.getByRole('radio', { name: /30 days/ })).not.toBeChecked();
+ });
+ });
+});
diff --git a/app/pages/clinicworkspace/ClinicPatients.js b/app/pages/clinicworkspace/ClinicPatients.js
index fe1a26994a..f185a3e9e2 100644
--- a/app/pages/clinicworkspace/ClinicPatients.js
+++ b/app/pages/clinicworkspace/ClinicPatients.js
@@ -123,6 +123,7 @@ import { getGlycemicRangesPreset } from '../../core/glycemicRangesUtils';
import FilterByTags from './clinicPatientsFilters/FilterByTags';
import FilterBySites from './clinicPatientsFilters/FilterBySites';
import FilterByDataRecency from './clinicPatientsFilters/FilterByDataRecency';
+import FilterBySummaryPeriod from './clinicPatientsFilters/FilterBySummaryPeriod';
import ClinicPatientsPrintModal from './ClinicPatientsPrintModal';
import AppliedFiltersList, { getPatientQueryState } from './clinicPatientsFilters/AppliedFiltersList';
import useIsClinicAdmin from './useIsClinicAdmin';
@@ -696,13 +697,6 @@ export const ClinicPatients = (props) => {
{ value: '>=0.7', label: t('70% or more') },
];
- const summaryPeriodOptions = [
- { value: '1d', label: t('24 hours') },
- { value: '7d', label: t('7 days') },
- { value: '14d', label: t('14 days') },
- { value: '30d', label: t('30 days') },
- ];
-
const clinicSites = useMemo(() => keyBy(clinic?.sites, 'id'), [clinic?.sites]);
const patientTags = useMemo(() => keyBy(clinic?.patientTags, 'id'), [clinic?.patientTags]);
@@ -720,19 +714,8 @@ export const ClinicPatients = (props) => {
const defaultSummaryPeriod = '14d';
const [activeSummaryPeriod, setActiveSummaryPeriod] = useLocalStorage('activePatientSummaryPeriod', defaultSummaryPeriod);
- const [pendingSummaryPeriod, setPendingSummaryPeriod] = useState(activeSummaryPeriod);
const previousSummaryPeriod = usePrevious(activeSummaryPeriod);
- const summaryPeriodPopupFilterState = usePopupState({
- variant: 'popover',
- popupId: 'summaryPeriodFilters',
- });
-
- const lastDataPopupFilterState = usePopupState({
- variant: 'popover',
- popupId: 'lastDataFilters',
- });
-
const timeInRangePopupFilterState = usePopupState({
variant: 'popover',
popupId: 'timeInRangeFilters',
@@ -1962,99 +1945,19 @@ export const ClinicPatients = (props) => {
)}
{/* Flex Group 2b: Range select and Info/Visibility Icons */}
-
+
{/* Range select */}
{showSummaryData && (
-
-
- {t('Summarizing')}
-
-
- {
- if (!summaryPeriodPopupFilterState.isOpen) trackMetric(prefixPopHealthMetric('Summary period filter open'), { clinicId: selectedClinicId });
- }}
- >
-
-
-
- {
- trackMetric(prefixPopHealthMetric('Summary period filter close'), { clinicId: selectedClinicId });
- }}
- onClose={() => {
- summaryPeriodPopupFilterState.close();
- setPendingSummaryPeriod(activeSummaryPeriod);
- }}
+ sx={{ gap: 3, justifyContent: 'flex-end', alignItems: 'center', flexShrink: 0 }}
>
-
- {t('Tidepool will generate health summaries for the selected number of days.')}
-
- setPendingSummaryPeriod(event.target.value)}
- />
-
-
-
-
-
-
-
+
{showRpmReportUI && (
{
+ const handleChange = (summaryPeriod) => {
+ setActiveSummaryPeriod(summaryPeriod);
+ };
+
+ return (
+
+ );
+};
+
+export default FilterBySummaryPeriod;
diff --git a/app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown.js b/app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown.js
new file mode 100644
index 0000000000..42e40c37d0
--- /dev/null
+++ b/app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown.js
@@ -0,0 +1,172 @@
+import React, { useState } from 'react';
+import { useSelector } from 'react-redux';
+import { useTranslation } from 'react-i18next';
+import { trackMetric } from '../../../core/metricUtils';
+import { colors as vizColors } from '@tidepool/viz';
+
+import { Box, Grid } from 'theme-ui';
+import KeyboardArrowDownRoundedIcon from '@material-ui/icons/KeyboardArrowDownRounded';
+
+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, Body1 } from '../../../components/elements/FontStyles';
+import useClinicMetricsPageName from '../useClinicMetricsPageName';
+
+const getSummaryPeriodSelectLabel = (t, activeSummaryPeriod) => {
+ switch (activeSummaryPeriod) {
+ case '1d': return t('Summarizing 24 hours of data');
+ case '7d': return t('Summarizing 7 days of data');
+ case '14d': return t('Summarizing 14 days of data');
+ case '30d': return t('Summarizing 30 days of data');
+ }
+
+ return null;
+};
+
+const getSummaryPeriodOptions = (t) => ([
+ { value: '1d', label: t('24 hours') },
+ { value: '7d', label: t('7 days') },
+ { value: '14d', label: t('14 days') },
+ { value: '30d', label: t('30 days') },
+]);
+
+const DropdownContent = ({
+ onClose,
+ onChange,
+ activeSummaryPeriod,
+}) => {
+ const { t } = useTranslation();
+ const pageName = useClinicMetricsPageName();
+ const selectedClinicId = useSelector((state) => state.blip.selectedClinicId);
+
+ const [pendingSummaryPeriod, setPendingSummaryPeriod] = useState(activeSummaryPeriod);
+
+ const handleChange = (summaryPeriod) => onChange(summaryPeriod);
+
+ return (
+
+
+
+ {t('Summarizing Data')}
+
+
+
+ {t('Tidepool will generate health summaries for the selected number of days.')}
+
+
+
+ setPendingSummaryPeriod(event.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const SummaryPeriodFilterDropdown = ({
+ onChange = noop,
+ activeSummaryPeriod = null,
+}) => {
+ const { t } = useTranslation();
+ const pageName = useClinicMetricsPageName();
+
+ const summaryPeriodPopupFilterState = usePopupState({
+ variant: 'popover',
+ popupId: 'summaryPeriodFilters',
+ });
+
+ const selectedClinicId = useSelector((state) => state.blip.selectedClinicId);
+
+ const handleCloseDropdown = () => summaryPeriodPopupFilterState.close();
+
+ return (
+ <>
+ {
+ if (!summaryPeriodPopupFilterState.isOpen) trackMetric('Clinic - Summary period filter open', { clinicId: selectedClinicId, pageName });
+ }}
+ sx={{ flexShrink: 0 }}
+ >
+
+
+
+ {
+ trackMetric('Clinic - Summary period filter close', { clinicId: selectedClinicId, pageName });
+ }}
+ onClose={handleCloseDropdown}
+ >
+ { summaryPeriodPopupFilterState.isOpen &&
+
+ }
+
+ >
+ );
+};
+
+export default SummaryPeriodFilterDropdown;
diff --git a/test/unit/pages/ClinicPatients.test.js b/test/unit/pages/ClinicPatients.test.js
index 26f04e2921..004d7b6f50 100644
--- a/test/unit/pages/ClinicPatients.test.js
+++ b/test/unit/pages/ClinicPatients.test.js
@@ -1763,140 +1763,35 @@ describe('ClinicPatients', () => {
});
context('summary period filtering', () => {
- let mockedLocalStorage;
-
- beforeEach(() => {
- mockedLocalStorage = {
- 'activePatientFilters/clinicianUserId123/clinicID123': {
- timeInRange: [
- 'timeInAnyLowPercent',
- 'timeInAnyHighPercent'
- ],
- patientTags: [],
- meetsGlycemicTargets: false,
- },
- activePatientSummaryPeriod: '14d',
- };
+ const emptyStatText = '--';
+ const rowData = row => container.querySelectorAll('table tbody tr')[row].querySelectorAll('.MuiTableCell-root');
- mockUseLocalStorage.mockImplementation(key => {
- defaults(mockedLocalStorage, { [key]: {} })
- return [
- mockedLocalStorage[key],
- sinon.stub().callsFake(val => mockedLocalStorage[key] = val)
- ];
+ const mountWithSummaryPeriod = period => {
+ mockUseLocalStorage.mockImplementation((key, fallback = {}) => {
+ return [key === 'activePatientSummaryPeriod' ? period : fallback, sinon.stub()];
});
- mockUseClinicPatientsFilters.mockImplementation(() => (
- [
- {
- timeInRange: ['timeInAnyLowPercent', 'timeInAnyHighPercent'],
- patientTags: [],
- meetsGlycemicTargets: false,
- },
- sinon.stub(),
- ]
- ));
-
mountWrapper(store);
- });
-
- it('should allow filtering by summary period', () => {
- const summaryPeriodFilterTrigger = container.querySelector('#summary-period-filter-trigger');
- expect(summaryPeriodFilterTrigger).to.exist;
-
- const popover = () => document.querySelector('#summaryPeriodFilters');
- expect(popover()).to.exist;
- expect(popover().style.visibility).to.equal('hidden');
-
- // Open filters popover
- fireEvent.click(summaryPeriodFilterTrigger);
- expect(popover().style.visibility).to.equal('');
-
- // Ensure filter options present
- const filterOptions = document.querySelectorAll('#summary-period-filters label');
- expect(filterOptions.length).to.equal(4);
- expect(filterOptions[0].textContent).to.equal('24 hours');
- expect(filterOptions[0].querySelector('input').value).to.equal('1d');
-
- expect(filterOptions[1].textContent).to.equal('7 days');
- expect(filterOptions[1].querySelector('input').value).to.equal('7d');
-
- expect(filterOptions[2].textContent).to.equal('14 days');
- expect(filterOptions[2].querySelector('input').value).to.equal('14d');
-
- expect(filterOptions[3].textContent).to.equal('30 days');
- expect(filterOptions[3].querySelector('input').value).to.equal('30d');
-
- // Default should be 14 days
- expect(filterOptions[2].querySelector('input').checked).to.be.true;
-
- // Set to 7 days
- fireEvent.click(filterOptions[1].querySelector('input'));
-
- defaultProps.api.clinics.getPatientsForClinic.resetHistory();
- const applyButton = document.querySelector('#apply-summary-period-filter');
- fireEvent.click(applyButton);
-
- // Ensure resulting patient fetch is requesting the 7 day period for time in range filters
- sinon.assert.calledWith(defaultProps.api.clinics.getPatientsForClinic, 'clinicID123', sinon.match({
- ...defaultFetchOptions,
- sort: '-lastData',
- period: '7d',
- 'cgm.timeInAnyHighPercent': '>0.25',
- 'cgm.timeInAnyLowPercent': '>0.04',
- }));
+ };
- sinon.assert.calledWith(defaultProps.trackMetric, 'Clinic - Population Health - Summary period apply filter', sinon.match({ clinicId: 'clinicID123', summaryPeriod: '7d' }));
+ it('should show the GMI when the selected period is 14 days', () => {
+ mountWithSummaryPeriod('14d');
+ expect(rowData(2)[4].textContent).to.contain('6.5 %');
});
- it('should not show the GMI if selected period is less than 14 days', () => {
- const emptyStatText = '--';
- const summaryPeriodFilterTrigger = container.querySelector('#summary-period-filter-trigger');
- expect(summaryPeriodFilterTrigger).to.exist;
-
- const popover = () => document.querySelector('#summaryPeriodFilters');
- expect(popover()).to.exist;
- expect(popover().style.visibility).to.equal('hidden');
-
- const applyButton = () => document.querySelector('#apply-summary-period-filter');
-
- // Open filters popover
- fireEvent.click(summaryPeriodFilterTrigger);
- expect(popover().style.visibility).to.equal('');
-
- // Ensure filter options present
- const filterOptions = () => document.querySelectorAll('#summary-period-filters label');
-
- // Default should be 14 days
- expect(filterOptions()[2].querySelector('input').checked).to.be.true;
-
- const dataRows = container.querySelectorAll('table tbody tr');
- expect(dataRows.length).to.equal(5);
-
- const rowData = row => container.querySelectorAll('table tbody tr')[row].querySelectorAll('.MuiTableCell-root');
-
- expect(rowData(2)[4].textContent).to.contain('6.5 %'); // shows for 14 days
-
- // Open filters popover and set to 30 days
- fireEvent.click(summaryPeriodFilterTrigger);
- fireEvent.click(filterOptions()[3].querySelector('input'));
- expect(filterOptions()[3].querySelector('input').checked).to.be.true;
- fireEvent.click(applyButton());
- expect(rowData(2)[4].textContent).to.contain('7.5 %'); // shows for 30 days
+ it('should show the GMI when the selected period is 30 days', () => {
+ mountWithSummaryPeriod('30d');
+ expect(rowData(2)[4].textContent).to.contain('7.5 %');
+ });
- // Open filters popover and set to 7 days
- fireEvent.click(summaryPeriodFilterTrigger);
- fireEvent.click(filterOptions()[1].querySelector('input'));
- expect(filterOptions()[1].querySelector('input').checked).to.be.true;
- fireEvent.click(applyButton());
- expect(rowData(2)[4].textContent).to.contain(emptyStatText); // hidden for 7 days
+ it('should not show the GMI when the selected period is 7 days', () => {
+ mountWithSummaryPeriod('7d');
+ expect(rowData(2)[4].textContent).to.contain(emptyStatText);
+ });
- // Open filters popover and set to 1 day
- fireEvent.click(summaryPeriodFilterTrigger);
- fireEvent.click(filterOptions()[0].querySelector('input'));
- expect(filterOptions()[0].querySelector('input').checked).to.be.true;
- fireEvent.click(applyButton());
- expect(rowData(2)[4].textContent).to.contain(emptyStatText); // hidden for 1 day
+ it('should not show the GMI when the selected period is 1 day', () => {
+ mountWithSummaryPeriod('1d');
+ expect(rowData(2)[4].textContent).to.contain(emptyStatText);
});
});