From e1b4a606714470ac4f088b21836458d70fde1b6b Mon Sep 17 00:00:00 2001 From: henry-tp Date: Mon, 30 Mar 2026 22:44:44 -0700 Subject: [PATCH 1/7] WEB-4460 implement last reviewed --- .../TideDashboardV2/PatientLastReviewed.js | 112 ++++++++++++++++++ .../TideDashboardV2/tideDashboardApi.js | 26 +++- .../TideDashboardV2/useTableColumns.js | 2 + app/redux/api/baseApi.js | 1 + 4 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js diff --git a/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js b/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js new file mode 100644 index 0000000000..5f75f00467 --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js @@ -0,0 +1,112 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { Text, Box, FlexProps } from 'theme-ui'; +import moment from 'moment-timezone'; +import CheckRoundedIcon from '@material-ui/icons/CheckRounded'; +import { utils as vizUtils } from '@tidepool/viz'; +import upperFirst from 'lodash/upperFirst'; + +import HoverButton from '../../../components/elements/HoverButton'; +import Icon from '../../../components/elements/Icon'; +import { useToasts } from '../../../providers/ToastProvider'; +import { useSetClinicPatientLastReviewedMutation, useRevertClinicPatientLastReviewedMutation } from './tideDashboardApi'; + +const { + formatTimeAgo, + getTimezoneFromTimePrefs, +} = vizUtils.datetime; + +const trackMetric = () => {}; + +const recentlyReviewedThresholdDate = null; + +const PatientLastReviewed = ({ patient }) => { + const { t } = useTranslation(); + const { set: setToast } = useToasts(); + const selectedClinicId = useSelector((state) => state.blip.selectedClinicId); + const loggedInUserId = useSelector((state) => state.blip.loggedInUserId); + const timePrefs = useSelector((state) => state.blip.timePrefs); + const patientId = patient?.id; + + const [setClinicPatientLastReviewed, { isLoading: isSetting }] = useSetClinicPatientLastReviewedMutation(); + const [revertClinicPatientLastReviewed, { isLoading: isReverting }] = useRevertClinicPatientLastReviewedMutation(); + + const handleReview = () => { + // trackMetric('Clinic - Mark patient reviewed', { clinicId: selectedClinicId, source: metricSource }); + setClinicPatientLastReviewed({ clinicId: selectedClinicId, patientId }); + // onReview && onReview(); + }; + + const handleUndo = () => { + // trackMetric('Clinic - Undo mark patient reviewed', { clinicId: selectedClinicId, source: metricSource }); + revertClinicPatientLastReviewed({ clinicId: 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'; + } + } + + + return ( + + + + + {reviewIsRecent && } + {upperFirst(formattedLastReviewed.daysText)} + + + + + ); +}; + +export default PatientLastReviewed; diff --git a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js index 3b1d8b833e..b1c44bfc9f 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js +++ b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js @@ -27,6 +27,11 @@ export const buildGetTideDashboardPatientsParams = (offset, limit, category, las }; }; +const TAGS = { + TIDE_DASHBOARD_PATIENTS: 'TideDashboardPatients', +}; + +const { TIDE_DASHBOARD_PATIENTS } = TAGS; const tideDashboardApi = RTKQueryApi.injectEndpoints({ endpoints: (builder) => ({ @@ -43,8 +48,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; diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js index 37ac9cf77f..58dd3509b3 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js +++ b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js @@ -22,6 +22,7 @@ import { } from './Cells'; import TagListCell from '../components/TagListCell'; +import PatientLastReviewed from './PatientLastReviewed'; const getColumnTypes = (t, thresholds) => ({ patientDetails: { @@ -105,6 +106,7 @@ const getColumnTypes = (t, thresholds) => ({ title: t('Last Reviewed'), field: 'lastReviewed', align: 'center', + render: patient => , }, moreMenu: { title: t(''), diff --git a/app/redux/api/baseApi.js b/app/redux/api/baseApi.js index b8b223a09a..2d730c17d2 100644 --- a/app/redux/api/baseApi.js +++ b/app/redux/api/baseApi.js @@ -42,6 +42,7 @@ export const RTKQueryApi = createApi({ baseQuery, { maxRetries: RETRY_COUNT }, ), + tagTypes: ['TideDashboardPatients'], refetchOnMountOrArgChange: true, endpoints: () => ({}), }); From 1ce5959aaf901530f1832d1020b4da0998b03d1a Mon Sep 17 00:00:00 2001 From: henry-tp Date: Mon, 30 Mar 2026 23:02:02 -0700 Subject: [PATCH 2/7] WEB-4460 add in refetch --- .../TideDashboardV2/PatientLastReviewed.js | 7 +++++-- .../TideDashboardV2/TideDashboardV2.js | 14 ++----------- .../useTideDashboardPatients.js | 21 +++++++++++++++++++ 3 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 app/pages/clinicworkspace/TideDashboardV2/useTideDashboardPatients.js diff --git a/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js b/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js index 5f75f00467..1c21664e13 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js +++ b/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js @@ -11,6 +11,7 @@ import HoverButton from '../../../components/elements/HoverButton'; import Icon from '../../../components/elements/Icon'; import { useToasts } from '../../../providers/ToastProvider'; import { useSetClinicPatientLastReviewedMutation, useRevertClinicPatientLastReviewedMutation } from './tideDashboardApi'; +import useTideDashboardPatients from './useTideDashboardPatients'; const { formatTimeAgo, @@ -23,12 +24,14 @@ const recentlyReviewedThresholdDate = null; const PatientLastReviewed = ({ patient }) => { const { t } = useTranslation(); - const { set: setToast } = useToasts(); + // const { set: setToast } = useToasts(); const selectedClinicId = useSelector((state) => state.blip.selectedClinicId); const loggedInUserId = useSelector((state) => state.blip.loggedInUserId); const timePrefs = useSelector((state) => state.blip.timePrefs); const patientId = patient?.id; + const { isFetching } = useTideDashboardPatients(); + const [setClinicPatientLastReviewed, { isLoading: isSetting }] = useSetClinicPatientLastReviewedMutation(); const [revertClinicPatientLastReviewed, { isLoading: isReverting }] = useRevertClinicPatientLastReviewedMutation(); @@ -85,7 +88,7 @@ const PatientLastReviewed = ({ patient }) => { onClick: clickHandler, variant: 'quickActionCondensed', ml: canReview ? -2 : 0, - processing: isSetting || isReverting, + disabled: isSetting || isReverting || isFetching, }} hideChildrenOnHover={canReview} > diff --git a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js index 44a833ea2a..ede00b4fdd 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js +++ b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js @@ -14,33 +14,23 @@ import PaginationControls from '../components/PaginationControls'; import ActiveFilterCount from '../components/ActiveFilterCount'; import { resetTideDashboardState, setOffset } from './tideDashboardSlice'; -import { useGetTideDashboardPatientsQuery } from './tideDashboardApi'; +import useTideDashboardPatients, { LIMIT } from './useTideDashboardPatients'; import ResetFilters from '../components/ResetFilters'; import useActiveFiltersCount from './useActiveFiltersCount'; -import useDerivedDataRecencyEndpoints from './useDerivedDataRecencyEndpoints'; import { resetTideDashboardFilters } from './tideDashboardFiltersSlice'; import useTableColumns from './useTableColumns'; import EmptyContentNode from './EmptyContentNode'; -const LIMIT = 12; - const Divider = () => ; const TideDashboard = () => { const { t } = useTranslation(); const dispatch = useDispatch(); - 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(); - const { data } = useGetTideDashboardPatientsQuery( - { clinicId: selectedClinicId, offset, category, lastDataTo, lastDataFrom, tags: patientTags, 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 diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTideDashboardPatients.js b/app/pages/clinicworkspace/TideDashboardV2/useTideDashboardPatients.js new file mode 100644 index 0000000000..cf8e438cc3 --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/useTideDashboardPatients.js @@ -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; From 6d33508f8fb52e9497caabbf4055514768169722 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 31 Mar 2026 10:50:04 -0700 Subject: [PATCH 3/7] WEB-4406 implement recentlyReviewedThresholdDate --- .../clinicworkspace/TideDashboardV2/PatientLastReviewed.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js b/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js index 1c21664e13..4e50aa2a80 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js +++ b/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js @@ -18,9 +18,7 @@ const { getTimezoneFromTimePrefs, } = vizUtils.datetime; -const trackMetric = () => {}; - -const recentlyReviewedThresholdDate = null; +const trackMetric = () => {}; // TODO: FIX const PatientLastReviewed = ({ patient }) => { const { t } = useTranslation(); @@ -46,6 +44,8 @@ const PatientLastReviewed = ({ patient }) => { revertClinicPatientLastReviewed({ clinicId: selectedClinicId, patientId }); }; + const recentlyReviewedThresholdDate = moment().startOf('isoWeek').toISOString(); + let clickHandler = handleReview; let buttonText = t('Mark Reviewed'); From 5e750d5557f5231ff73392fbcc2fe2f85a712b2b Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 31 Mar 2026 15:08:49 -0700 Subject: [PATCH 4/7] WEb-4460 fix missing flag colors --- app/pages/clinicworkspace/TideDashboardV2/Cells.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index ffe1a1ff71..44c4176ee4 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -141,9 +141,8 @@ export const FlagCell = ({ patient, category = null, }) => { case category === VERY_HIGH: return 'veryHigh'; case category === ANY_HIGH: return 'anyHigh'; case category === DROP_IN_TIR: return 'dropInTIR'; - case category === VERY_LOW: return 'lowTIR'; case category === LOW_CGM_WEAR: return 'lowSensorUsage'; - case category === TARGET: return 'meetingTargets'; + case category === TARGET: return 'target'; // If no category, then read from summary case period.timeInVeryLowPercent > 0.01: return 'veryLow'; @@ -151,7 +150,6 @@ export const FlagCell = ({ patient, category = null, }) => { case period.timeInVeryHighPercent > 0.05: return 'veryHigh'; case period.timeInAnyHighPercent > 0.25: return 'anyHigh'; case period.timeInTargetPercentDelta < -0.15: return 'dropInTIR'; - case period.timeInTargetPercent < 0.70: return 'lowTIR'; case period.timeCGMUsePercent < 0.70: return 'lowSensorUsage'; default: return null; @@ -166,22 +164,23 @@ export const FlagCell = ({ patient, category = null, }) => { veryHigh: t('Very High'), anyHigh: t('High'), dropInTIR: t('Large Drop in TIR'), - lowTIR: t('Low TIR'), lowSensorUsage: t('Low CGM Wear Time'), - meetingTargets: t('Meeting Targets'), + target: t('Meeting Targets'), }; + const flagColor = colors.bg[rangeName] || vizColors.gold30; + return ( From 10f9e51cf51ee831a2ae6217b65e744c41442bee Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 12 May 2026 11:51:17 -0700 Subject: [PATCH 5/7] WEB-4460 hide value it TimeInTargetPercent not shown --- app/pages/clinicworkspace/TideDashboardV2/Cells.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index 44c4176ee4..ef0efe5c89 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -11,6 +11,7 @@ import BgSummaryCell from '../../../components/clinic/BgSummaryCell'; import DeltaBar from '../../../components/elements/DeltaBar'; import utils from '../../../core/utils'; import { CATEGORY } from './FilterByCategory'; +import isUndefined from 'lodash/isUndefined'; export const PatientCell = ({ patient }) => { const { t } = useTranslation(); @@ -59,7 +60,9 @@ export const TimeInRangePercentBarChartCell = ({ patient }) => { export const TimeInTargetPercentCell = ({ patient }) => { const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); const rawValue = patient?.summary?.cgmStats?.periods?.[summaryPeriod]?.timeInTargetPercent; - const value = utils.formatDecimal(rawValue * 100, 0); + let value = utils.formatDecimal(rawValue * 100, 0); + + if (isUndefined(rawValue)) value = ''; return ; }; From e45c78dd616c2fddf455a5b382b6c8759eae6c95 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Wed, 24 Jun 2026 14:58:17 -0700 Subject: [PATCH 6/7] WEB-4460 remove showTideDashboardLastReviewed flag --- .../PatientDrawer/MenuBar/MenuBar.js | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js b/app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js index 8cc65c6bf8..7a47a771db 100644 --- a/app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js +++ b/app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js @@ -35,7 +35,6 @@ const tabs = { const MenuBar = ({ patientId, onClose, onSelectTab, selectedTab, trackMetric }) => { 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]); @@ -87,26 +86,24 @@ const MenuBar = ({ patientId, onClose, onSelectTab, selectedTab, trackMetric }) - {showTideDashboardLastReviewed && ( - - - {t('Last Reviewed')} - - - - - )} + + + {t('Last Reviewed')} + + + + From c91a4e24b7deaad693ad1f9602d34ab32ad5bb70 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 30 Jun 2026 15:51:20 -0700 Subject: [PATCH 7/7] WEB-4460 fix missing tag display --- app/pages/clinicworkspace/components/TagListCell.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pages/clinicworkspace/components/TagListCell.js b/app/pages/clinicworkspace/components/TagListCell.js index 38d2654858..b500a5cb60 100644 --- a/app/pages/clinicworkspace/components/TagListCell.js +++ b/app/pages/clinicworkspace/components/TagListCell.js @@ -10,7 +10,7 @@ const TagListCell = ({ patient }) => { const patientTags = clinic?.patientTags || []; const tagIds = patient?.tags || []; - const tags = tagIds.map(tag => patientTags.find(ptTag => ptTag.id === tag)); // TODO: index + const tags = tagIds.map(tag => patientTags.find(ptTag => ptTag.id === tag)).filter(Boolean); // TODO: index return ; };