Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e1b4a60
WEB-4460 implement last reviewed
henry-tp Mar 31, 2026
1ce5959
WEB-4460 add in refetch
henry-tp Mar 31, 2026
6d33508
WEB-4406 implement recentlyReviewedThresholdDate
henry-tp Mar 31, 2026
d5ce27a
Merge branch 'WEB-4460-cells' into WEB-4460-last-reviewed
henry-tp Mar 31, 2026
5e750d5
WEb-4460 fix missing flag colors
henry-tp Mar 31, 2026
4d4e61a
Merge branch 'WEB-4460-cells' into WEB-4460-last-reviewed
henry-tp Mar 31, 2026
c0d6e52
Merge branch 'WEB-4460-cells' into WEB-4460-last-reviewed
henry-tp May 6, 2026
10f9e51
WEB-4460 hide value it TimeInTargetPercent not shown
henry-tp May 12, 2026
7dfc185
Merge branch 'WEB-4460-cells' into WEB-4460-last-reviewed
henry-tp May 15, 2026
de8ad70
Merge branch 'WEB-4460-cells' into WEB-4460-last-reviewed
henry-tp Jun 23, 2026
e45c78d
WEB-4460 remove showTideDashboardLastReviewed flag
henry-tp Jun 24, 2026
490b3f8
Merge branch 'WEB-4460-cells' into WEB-4460-last-reviewed
henry-tp Jun 30, 2026
751810d
Merge branch 'WEB-4460-cells' into WEB-4460-last-reviewed
henry-tp Jun 30, 2026
c91a4e2
WEB-4460 fix missing tag display
henry-tp Jun 30, 2026
b136503
Merge branch 'WEB-4460-cells' into WEB-4460-last-reviewed
henry-tp Jul 6, 2026
a17a0c2
Merge branch 'WEB-4460-cells' into WEB-4460-last-reviewed
henry-tp Jul 8, 2026
c6c3a08
Merge branch 'WEB-4460-cells' into WEB-4460-last-reviewed
henry-tp Aug 12, 2026
5d37244
Merge branch 'WEB-4460-cells' into WEB-4460-last-reviewed
henry-tp Aug 13, 2026
c870c24
WEB-4460 import trackMetric from metricUtils
henry-tp Aug 14, 2026
a80fa77
WEB-4460 abstract ReviewPatientToggle
henry-tp Aug 14, 2026
62c7060
WEB-4460 rename ReviewPatientToggle
henry-tp Aug 14, 2026
5519ffd
WEB-4460 abstract PatientLastReviewed for ClinicPatients
henry-tp Aug 14, 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
39 changes: 18 additions & 21 deletions app/components/PatientDrawer/MenuBar/MenuBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ const tabs = {
const MenuBar = ({ patientId, onClose, onSelectTab, selectedTab }) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const { showTideDashboardLastReviewed } = useFlags();

const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
const patient = useSelector(state => state.blip.clinics[state.blip.selectedClinicId]?.patients?.[patientId]);
Expand Down Expand Up @@ -88,26 +87,24 @@ const MenuBar = ({ patientId, onClose, onSelectTab, selectedTab }) => {
</Flex>

<Flex sx={{ fontSize: 0, alignItems: 'center' }}>
{showTideDashboardLastReviewed && (
<Flex data-testid="last-reviewed-section" sx={{ alignItems: 'center', justifyContent: 'space-between', gap: 3 }}>
<Text sx={{
color: vizColors.purple90,
fontWeight: 'medium',
}}>
{t('Last Reviewed')}
</Text>

<PatientLastReviewed
sx={{ flexGrow: 1 }}
api={api}
trackMetric={trackMetric}
metricSource="TIDE dashboard"
patientId={patientId}
recentlyReviewedThresholdDate={recentlyReviewedThresholdDate}
onReview={handleReviewSuccess}
/>
</Flex>
)}
<Flex data-testid="last-reviewed-section" sx={{ alignItems: 'center', justifyContent: 'space-between', gap: 3 }}>
<Text sx={{
color: vizColors.purple90,
fontWeight: 'medium',
}}>
{t('Last Reviewed')}
</Text>

<PatientLastReviewed
sx={{ flexGrow: 1 }}
api={api}
trackMetric={trackMetric}
metricSource="TIDE dashboard"
patientId={patientId}
recentlyReviewedThresholdDate={recentlyReviewedThresholdDate}
onReview={handleReviewSuccess}
/>
</Flex>
</Flex>
</Flex>

Expand Down
95 changes: 12 additions & 83 deletions app/components/clinic/PatientLastReviewed.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,19 @@
import React, { useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Text, Box, FlexProps } from 'theme-ui';
import { withTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import moment from 'moment-timezone';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import { utils as vizUtils } from '@tidepool/viz';
import get from 'lodash/get';
import upperFirst from 'lodash/upperFirst';

import HoverButton from '../elements/HoverButton';
import Icon from '../elements/Icon';
import i18next from '../../core/language';
import ReviewPatientToggle from '../../pages/clinicworkspace/components/ReviewPatientToggle';
import * as actions from '../../redux/actions';
import { useIsFirstRender } from '../../core/hooks';
import { useToasts } from '../../providers/ToastProvider';

const {
formatTimeAgo,
getTimezoneFromTimePrefs,
} = vizUtils.datetime;

const t = i18next.t.bind(i18next);

export const PatientLastReviewed = ({ api, patientId, recentlyReviewedThresholdDate, trackMetric, metricSource, onReview = null }) => {
export const PatientLastReviewed = ({ api, patientId, recentlyReviewedThresholdDate, onReview = null }) => {
const dispatch = useDispatch();
const isFirstRender = useIsFirstRender();
const { set: setToast } = useToasts();
const selectedClinicId = useSelector((state) => state.blip.selectedClinicId);
const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]);
const loggedInUserId = useSelector((state) => state.blip.loggedInUserId);
const timePrefs = useSelector((state) => state.blip.timePrefs);
const patient = clinic?.patients?.[patientId];

const {
Expand Down Expand Up @@ -67,86 +50,32 @@ export const PatientLastReviewed = ({ api, patientId, recentlyReviewedThresholdD
}, [revertingClinicPatientLastReviewed]);

const handleReview = () => {
trackMetric('Clinic - Mark patient reviewed', { clinicId: selectedClinicId, source: metricSource, patientID: patientId });
dispatch(actions.async.setClinicPatientLastReviewed(api, selectedClinicId, patientId));
onReview && onReview();
};

const handleUndo = () => {
trackMetric('Clinic - Undo mark patient reviewed', { clinicId: selectedClinicId, source: metricSource });
dispatch(actions.async.revertClinicPatientLastReviewed(api, selectedClinicId, patientId));
};

let clickHandler = handleReview;
let buttonText = t('Mark Reviewed');

let formattedLastReviewed = { daysText: '-' };
let lastReviewIsToday = false;
let reviewIsRecent = false;
let canReview = true;
let color = 'feedback.warning';

if (patient?.reviews?.[0]?.time) {
formattedLastReviewed = formatTimeAgo(patient.reviews[0].time, timePrefs);
lastReviewIsToday = moment.utc(patient.reviews[0].time).tz(getTimezoneFromTimePrefs(timePrefs)).isSame(moment(), 'day');

if (lastReviewIsToday) {
canReview = false;
clickHandler = null;
}

if (moment.utc(patient.reviews[0].time).isSameOrAfter(moment(recentlyReviewedThresholdDate))) {
reviewIsRecent = true;
}

if (lastReviewIsToday && patient.reviews[0].clinicianId === loggedInUserId) {
clickHandler = handleUndo;
buttonText = t('Undo');
};

if (reviewIsRecent) {
color = 'feedback.success';
}
}
const processing = settingClinicPatientLastReviewed.inProgress || revertingClinicPatientLastReviewed.inProgress;

return (
<HoverButton
{...FlexProps}
buttonText={buttonText}
buttonProps={{
onClick: clickHandler,
variant: 'quickActionCondensed',
ml: canReview ? -2 : 0,
processing: settingClinicPatientLastReviewed.inProgress || revertingClinicPatientLastReviewed.inProgress,
}}
hideChildrenOnHover={canReview}
>
<Box sx={{ whiteSpace: 'nowrap' }}>
<Text
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 1,
color: color,
fontWeight: 'medium',
whiteSpace: 'nowrap',
}}
>
{reviewIsRecent && <Icon variant="static" icon={CheckRoundedIcon} />}
{upperFirst(formattedLastReviewed.daysText)}
</Text>
</Box>
</HoverButton>
<ReviewPatientToggle
patient={patient}
onReview={handleReview}
onUndo={handleUndo}
processing={processing}
recentlyReviewedThresholdDate={recentlyReviewedThresholdDate}
/>
);
};

PatientLastReviewed.propTypes = {
...FlexProps,
api: PropTypes.object.isRequired,
metricSource: PropTypes.string.isRequired,
patientId: PropTypes.string.isRequired,
recentlyReviewedThresholdDate: PropTypes.string.isRequired,
trackMetric: PropTypes.func.isRequired,
onReview: PropTypes.func,
}

export default withTranslation()(PatientLastReviewed);
export default PatientLastReviewed;
4 changes: 2 additions & 2 deletions app/pages/clinicworkspace/ClinicPatients.js
Original file line number Diff line number Diff line change
Expand Up @@ -2834,10 +2834,10 @@ export const ClinicPatients = (props) => {
const renderLastReviewed = useCallback((patient) => {
return (
<Box onClick={event => event.stopPropagation()}>
<PatientLastReviewed api={api} trackMetric={trackMetric} metricSource="Patients list" patientId={patient.id} recentlyReviewedThresholdDate={moment().startOf('day').toISOString()} />
<PatientLastReviewed api={api} patientId={patient.id} recentlyReviewedThresholdDate={moment().startOf('day').toISOString()} />
</Box>
);
}, [api, trackMetric]);
}, [api]);

const renderMore = useCallback((patient) => {
return <MoreMenu
Expand Down
6 changes: 6 additions & 0 deletions app/pages/clinicworkspace/TideDashboardV2/Cells.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { OVERVIEW_TAB_INDEX } from '../../../components/PatientDrawer/MenuBar/Me
import { useFlags } from 'launchdarkly-react-client-sdk';
import { CATEGORY } from './FilterByCategory';
import isUndefined from 'lodash/isUndefined';
import PatientLastReviewed from './PatientLastReviewed';

export const COMPACT = '@container (max-width: 1200px)';

Expand Down Expand Up @@ -238,6 +239,10 @@ export const FlagCell = ({ patient, category = null, }) => {
);
};

export const PatientLastReviewedCell = ({ patient }) => {
return <PatientLastReviewed patient={patient} />;
};

export default {
PatientCell,
NumericTemplateCell,
Expand All @@ -248,4 +253,5 @@ export default {
GMICell,
CGMUseCell,
FlagCell,
PatientLastReviewedCell,
};
33 changes: 33 additions & 0 deletions app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { useSelector } from 'react-redux';

import ReviewPatientToggle from '../components/ReviewPatientToggle';
import { useSetClinicPatientLastReviewedMutation, useRevertClinicPatientLastReviewedMutation } from './tideDashboardApi';
import useTideDashboardPatients from './useTideDashboardPatients';

const PatientLastReviewed = ({ patient }) => {
const selectedClinicId = useSelector((state) => state.blip.selectedClinicId);
const patientId = patient?.id;

const { isFetching } = useTideDashboardPatients();

const [setClinicPatientLastReviewed, { isLoading: isSetting }] = useSetClinicPatientLastReviewedMutation();
const [revertClinicPatientLastReviewed, { isLoading: isReverting }] = useRevertClinicPatientLastReviewedMutation();

const handleReview = () => setClinicPatientLastReviewed({ clinicId: selectedClinicId, patientId });

const handleUndo = () => revertClinicPatientLastReviewed({ clinicId: selectedClinicId, patientId });

const processing = isSetting || isReverting || isFetching;

return (
<ReviewPatientToggle
patient={patient}
onReview={handleReview}
onUndo={handleUndo}
processing={processing}
/>
);
};

export default PatientLastReviewed;
14 changes: 2 additions & 12 deletions app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,14 @@ import PaginationControls from '../components/PaginationControls';
import PatientDrawerController from './PatientDrawerController';

import { setOffset } from './tideDashboardSlice';
import { useGetTideDashboardPatientsQuery } from './tideDashboardApi';
import useDerivedDataRecencyEndpoints from './useDerivedDataRecencyEndpoints';
import useTideDashboardPatients, { LIMIT } from './useTideDashboardPatients';
import usePruneInvalidFilters from './usePruneInvalidFilters';
import useTableColumns from './useTableColumns';
import EmptyContentNode from './EmptyContentNode';
import FilterBySites from './FilterBySites';
import PatientCount from '../components/PatientCount';
import AppliedFiltersList from './AppliedFiltersList';

const LIMIT = 12;

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

const TideDashboard = ({ api }) => {
Expand All @@ -33,17 +30,10 @@ const TideDashboard = ({ api }) => {

usePruneInvalidFilters();

const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
const category = useSelector(state => state.blip.tideDashboard.category);
const offset = useSelector(state => state.blip.tideDashboard.offset);
const { patientTags, clinicSites, summaryPeriod } = useSelector(state => state.blip.tideDashboardFilters);

const [lastDataFrom, lastDataTo] = useDerivedDataRecencyEndpoints();

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

// Sync category to data fetching resolution; prevents visual glitch due to
// category updating view before the API call resolves and updates it again
Expand Down
26 changes: 25 additions & 1 deletion app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export const buildGetTideDashboardPatientsParams = (offset, limit, category, sum
};
};

const TAGS = {
TIDE_DASHBOARD_PATIENTS: 'TideDashboardPatients',
};

const { TIDE_DASHBOARD_PATIENTS } = TAGS;

const tideDashboardApi = RTKQueryApi.injectEndpoints({
endpoints: (builder) => ({
Expand All @@ -53,8 +58,27 @@ const tideDashboardApi = RTKQueryApi.injectEndpoints({
...response,
category: arg.category,
}),
providesTags: [TIDE_DASHBOARD_PATIENTS],
}),
setClinicPatientLastReviewed: builder.mutation({
query: ({ clinicId, patientId }) => ({
url: `/clinics/${clinicId}/patients/${patientId}/reviews`,
method: 'PUT',
}),
invalidatesTags: [TIDE_DASHBOARD_PATIENTS],
}),
revertClinicPatientLastReviewed: builder.mutation({
query: ({ clinicId, patientId }) => ({
url: `/clinics/${clinicId}/patients/${patientId}/reviews`,
method: 'DELETE',
}),
invalidatesTags: [TIDE_DASHBOARD_PATIENTS],
}),
}),
});

export const { useGetTideDashboardPatientsQuery } = tideDashboardApi;
export const {
useGetTideDashboardPatientsQuery,
useSetClinicPatientLastReviewedMutation,
useRevertClinicPatientLastReviewedMutation,
} = tideDashboardApi;
2 changes: 2 additions & 0 deletions app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
TimeInAnyHighPercentCell,
TimeInTargetPercentCell,
FlagCell,
PatientLastReviewedCell,
} from './Cells';

import TagListCell from '../components/TagListCell';
Expand Down Expand Up @@ -111,6 +112,7 @@ const getColumnTypes = (t, category, thresholds) => ({
title: t('Last Reviewed'),
field: 'lastReviewed',
align: 'center',
render: patient => <PatientLastReviewedCell patient={patient} />,
},
moreMenu: {
title: t(''),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useSelector } from 'react-redux';
import { useGetTideDashboardPatientsQuery } from './tideDashboardApi';
import useDerivedDataRecencyEndpoints from './useDerivedDataRecencyEndpoints';

const LIMIT = 12;

const useTideDashboardPatients = () => {
const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
const category = useSelector(state => state.blip.tideDashboard.category);
const offset = useSelector(state => state.blip.tideDashboard.offset);
const patientTags = useSelector(state => state.blip.tideDashboardFilters.patientTags);
const [lastDataFrom, lastDataTo] = useDerivedDataRecencyEndpoints();

return useGetTideDashboardPatientsQuery(
{ clinicId: selectedClinicId, offset, category, lastDataTo, lastDataFrom, tags: patientTags, limit: LIMIT },
{ skip: !selectedClinicId }
);
};

export { LIMIT };
export default useTideDashboardPatients;
Loading
Loading