diff --git a/app/components/PatientDrawer/MenuBar/MenuBar.js b/app/components/PatientDrawer/MenuBar/MenuBar.js index 724221191a..ceac651ebf 100644 --- a/app/components/PatientDrawer/MenuBar/MenuBar.js +++ b/app/components/PatientDrawer/MenuBar/MenuBar.js @@ -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]); @@ -88,26 +87,24 @@ const MenuBar = ({ patientId, onClose, onSelectTab, selectedTab }) => { - {showTideDashboardLastReviewed && ( - - - {t('Last Reviewed')} - - - - - )} + + + {t('Last Reviewed')} + + + + diff --git a/app/components/clinic/PatientLastReviewed.js b/app/components/clinic/PatientLastReviewed.js index 822851733b..3b46d59f38 100644 --- a/app/components/clinic/PatientLastReviewed.js +++ b/app/components/clinic/PatientLastReviewed.js @@ -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 { @@ -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 ( - - - - {reviewIsRecent && } - {upperFirst(formattedLastReviewed.daysText)} - - - + ); }; 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; diff --git a/app/pages/clinicworkspace/ClinicPatients.js b/app/pages/clinicworkspace/ClinicPatients.js index 4b391fc236..8f4478280c 100644 --- a/app/pages/clinicworkspace/ClinicPatients.js +++ b/app/pages/clinicworkspace/ClinicPatients.js @@ -2834,10 +2834,10 @@ export const ClinicPatients = (props) => { const renderLastReviewed = useCallback((patient) => { return ( event.stopPropagation()}> - + ); - }, [api, trackMetric]); + }, [api]); const renderMore = useCallback((patient) => { return { ); }; +export const PatientLastReviewedCell = ({ patient }) => { + return ; +}; + export default { PatientCell, NumericTemplateCell, @@ -248,4 +253,5 @@ export default { GMICell, CGMUseCell, FlagCell, + PatientLastReviewedCell, }; diff --git a/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js b/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js new file mode 100644 index 0000000000..51c08b4d70 --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/PatientLastReviewed.js @@ -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 ( + + ); +}; + +export default PatientLastReviewed; diff --git a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js index a4e7bf042a..45864a6110 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js +++ b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js @@ -14,8 +14,7 @@ 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'; @@ -23,8 +22,6 @@ import FilterBySites from './FilterBySites'; import PatientCount from '../components/PatientCount'; import AppliedFiltersList from './AppliedFiltersList'; -const LIMIT = 12; - const Gap = () => ; const TideDashboard = ({ api }) => { @@ -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 diff --git a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js index 2cec1cd032..8da4915ba7 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js +++ b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardApi.js @@ -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) => ({ @@ -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; diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js index ceaf67bb6e..3c6e49de0f 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js +++ b/app/pages/clinicworkspace/TideDashboardV2/useTableColumns.js @@ -22,6 +22,7 @@ import { TimeInAnyHighPercentCell, TimeInTargetPercentCell, FlagCell, + PatientLastReviewedCell, } from './Cells'; import TagListCell from '../components/TagListCell'; @@ -111,6 +112,7 @@ const getColumnTypes = (t, category, thresholds) => ({ title: t('Last Reviewed'), field: 'lastReviewed', align: 'center', + render: patient => , }, moreMenu: { title: t(''), 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; diff --git a/app/pages/clinicworkspace/components/ReviewPatientToggle.js b/app/pages/clinicworkspace/components/ReviewPatientToggle.js new file mode 100644 index 0000000000..29e5a2b166 --- /dev/null +++ b/app/pages/clinicworkspace/components/ReviewPatientToggle.js @@ -0,0 +1,114 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +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 noop from 'lodash/noop'; +import upperFirst from 'lodash/upperFirst'; + +import HoverButton from '../../../components/elements/HoverButton'; +import Icon from '../../../components/elements/Icon'; +import useClinicMetricsPageName from '../useClinicMetricsPageName'; +import { trackMetric } from '../../../core/metricUtils'; +const { formatTimeAgo, getTimezoneFromTimePrefs } = vizUtils.datetime; + +const ReviewPatientToggle = ({ + patient, + onReview = noop, + onUndo = noop, + processing = false, + recentlyReviewedThresholdDate = moment().startOf('isoWeek').toISOString(), +}) => { + const { t } = useTranslation(); + const pageName = useClinicMetricsPageName(); + const selectedClinicId = useSelector((state) => state.blip.selectedClinicId); + const loggedInUserId = useSelector((state) => state.blip.loggedInUserId); + const timePrefs = useSelector((state) => state.blip.timePrefs); + + const handleReview = () => { + trackMetric('Clinic - Mark patient reviewed', { clinicId: selectedClinicId, pageName }); + onReview(); + }; + + const handleUndo = () => { + trackMetric('Clinic - Undo mark patient reviewed', { clinicId: selectedClinicId, pageName }); + onUndo(); + }; + + 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)} + + + + + ); +}; + +ReviewPatientToggle.propTypes = { + patient: PropTypes.object, + onReview: PropTypes.func, + onUndo: PropTypes.func, + processing: PropTypes.bool, + recentlyReviewedThresholdDate: PropTypes.string, +}; + +export default ReviewPatientToggle; 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 ; }; diff --git a/app/redux/api/baseApi.js b/app/redux/api/baseApi.js index 92bc608cec..c70ae24e9e 100644 --- a/app/redux/api/baseApi.js +++ b/app/redux/api/baseApi.js @@ -45,6 +45,7 @@ export const RTKQueryApi = createApi({ baseQuery, { maxRetries: RETRY_COUNT }, ), + tagTypes: ['TideDashboardPatients'], refetchOnMountOrArgChange: true, endpoints: () => ({}), });