Skip to content

Commit 1e33bc6

Browse files
committed
WEB-4460 add Data Issues segment
1 parent f4ec307 commit 1e33bc6

7 files changed

Lines changed: 413 additions & 15 deletions

File tree

app/components/datasources/DataConnections.js

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -332,26 +332,13 @@ export const getConnectStateUI = (patient, isLoggedInUser, providerName) => {
332332
}
333333
};
334334

335-
export const getDataConnectionProps = (patient, isLoggedInUser, selectedClinicId, setActiveHandler) => reduce(availableProviders, (result, providerName) => {
336-
337-
const provider = providers[providerName];
335+
export const resolveConnectState = (patient, providerName, isLoggedInUser = false) => {
338336
const dataSource = getCurrentDataSourceForProvider(patient, providerName);
339337
const connectStateUI = getConnectStateUI(patient, isLoggedInUser, providerName);
340338
const inviteExpired = dataSource?.expirationTime < moment.utc().toISOString();
341339

342-
// If the provider requires a logged in user to create the connection, then ensure that is the case.
343-
if (!!provider.requiresLoggedInUser && !isLoggedInUser) {
344-
return result;
345-
}
346-
347-
// If the provider requires an existing data source to create the connection, then ensure that is the case.
348-
// This mechanism can be used to limit access to certain providers to only users who have previously connected
349-
// or where Tidepool has created a data source on their behalf.
350-
if (!!provider.requiresExistingDataSource && !dataSource) {
351-
return result;
352-
}
353-
354340
let connectState;
341+
355342
if (dataSource?.state) {
356343
connectState = includes(keys(connectStateUI), dataSource.state)
357344
? dataSource.state
@@ -368,6 +355,15 @@ export const getDataConnectionProps = (patient, isLoggedInUser, selectedClinicId
368355
connectState = 'noPendingConnections';
369356
}
370357

358+
return connectState;
359+
};
360+
361+
export const getDataConnectionProps = (patient, isLoggedInUser, selectedClinicId, setActiveHandler) => reduce(availableProviders, (result, providerName) => {
362+
result[providerName] = {};
363+
364+
const connectStateUI = getConnectStateUI(patient, isLoggedInUser, providerName);
365+
const connectState = resolveConnectState(patient, providerName, isLoggedInUser);
366+
371367
const { color, icon, message, text, handler } = connectStateUI[connectState];
372368

373369
const {
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import React, { useMemo } from 'react';
2+
import { useSelector } from 'react-redux';
3+
import { useTranslation } from 'react-i18next';
4+
import moment from 'moment-timezone';
5+
import { Box, Text } from 'theme-ui';
6+
import { resolveConnectState } from '../../../../components/datasources/DataConnections';
7+
import includes from 'lodash/includes';
8+
9+
import Pill from '../../../../components/elements/Pill';
10+
import HoverButton from '../../../../components/elements/HoverButton';
11+
import PopoverMenu from '../../../../components/elements/PopoverMenu';
12+
import { colors, fontWeights } from '../../../../themes/baseTheme';
13+
import ErrorRoundedIcon from '@material-ui/icons/ErrorRounded';
14+
import EditIcon from '@material-ui/icons/EditRounded';
15+
import DataInIcon from '../../../../core/icons/DataInIcon.svg';
16+
17+
export const DexcomConnectionStatusCell = ({ patient, onOpenDataConnectionsModal }) => {
18+
const { t } = useTranslation();
19+
20+
const dexcomConnectStateUI = useMemo(() => ({
21+
noPendingConnections: { colorPalette: 'neutral', icon: null, text: t('No Pending Connections') },
22+
inviteJustSent: { colorPalette: 'info', icon: null, text: t('Invite Sent') },
23+
pending: { colorPalette: 'info', icon: null, text: t('Invite Sent') },
24+
pendingReconnect: { colorPalette: 'info', icon: null, text: t('Invite Sent') },
25+
pendingExpired: { colorPalette: 'warning', icon: ErrorRoundedIcon, text: t('Invite Expired') },
26+
connected: { colorPalette: 'info', icon: null, text: t('Connected') },
27+
disconnected: { colorPalette: 'warning', icon: ErrorRoundedIcon, text: t('Patient Disconnected') },
28+
error: { colorPalette: 'warning', icon: ErrorRoundedIcon, text: t('Error Connecting') },
29+
unknown: { colorPalette: 'warning', icon: ErrorRoundedIcon, text: t('Unknown Status') },
30+
}), [t]);
31+
32+
const dexcomConnectState = resolveConnectState(patient, 'dexcom');
33+
34+
if (!dexcomConnectState) return null;
35+
36+
const showViewButton = includes([
37+
'disconnected',
38+
'error',
39+
'noPendingConnections',
40+
'pendingExpired',
41+
'unknown',
42+
], dexcomConnectState);
43+
44+
const handleOpenDataConnectionsModal = () => onOpenDataConnectionsModal(patient.id);
45+
46+
const StatusBadge = () => (
47+
<Pill
48+
className="patient-dexcom-connection-status"
49+
icon={dexcomConnectStateUI[dexcomConnectState].icon}
50+
text={dexcomConnectStateUI[dexcomConnectState].text}
51+
label={t('dexcom connection status')}
52+
colorPalette={dexcomConnectStateUI[dexcomConnectState].colorPalette}
53+
condensed
54+
/>
55+
);
56+
57+
if (!showViewButton) return <StatusBadge />;
58+
59+
return (
60+
<HoverButton
61+
buttonText={t('View')}
62+
buttonProps={{
63+
onClick: handleOpenDataConnectionsModal,
64+
variant: 'textSecondary',
65+
ml: -2,
66+
sx: {
67+
fontSize: 0,
68+
fontWeight: fontWeights.medium,
69+
textDecoration: 'underline',
70+
color: colors.purpleMedium,
71+
':hover': {
72+
color: colors.purpleMedium,
73+
textDecoration: 'underline',
74+
},
75+
},
76+
}}
77+
>
78+
<Box sx={{ whiteSpace: 'nowrap' }}>
79+
<StatusBadge />
80+
</Box>
81+
</HoverButton>
82+
);
83+
};
84+
85+
export const DaysSinceLastDataCell = ({ patient }) => {
86+
const timePrefs = useSelector(state => state.blip.timePrefs);
87+
88+
const daysSinceLastData = useMemo(() => {
89+
if (!patient?.lastData) return null;
90+
91+
const timezone = timePrefs?.timezoneName || new Intl.DateTimeFormat().resolvedOptions().timeZone;
92+
const startOfLastDataDay = moment.utc(patient.lastData).tz(timezone).startOf('day');
93+
const startOfCurrentDay = moment.utc().tz(timezone).startOf('day');
94+
95+
return startOfCurrentDay.diff(startOfLastDataDay, 'days');
96+
}, [patient?.lastData, timePrefs?.timezoneName]);
97+
98+
return (
99+
<Text sx={{ fontWeight: 'medium' }}>{daysSinceLastData ?? '-'}</Text>
100+
);
101+
};
102+
103+
export const MoreMenuCell = ({ patient, onOpenEditPatientDialog, onOpenDataConnectionsModal }) => {
104+
const { t } = useTranslation();
105+
106+
return (
107+
<PopoverMenu
108+
id={`action-menu-${patient?.id}`}
109+
items={[{
110+
icon: EditIcon,
111+
iconLabel: t('Edit Patient Details'),
112+
iconPosition: 'left',
113+
id: `edit-${patient?.id}`,
114+
variant: 'actionListItem',
115+
onClick: (_popupState) => {
116+
_popupState.close();
117+
onOpenEditPatientDialog(patient.id);
118+
},
119+
text: t('Edit Patient Details'),
120+
}, {
121+
iconSrc: DataInIcon,
122+
iconLabel: t('Bring Data into Tidepool'),
123+
iconPosition: 'left',
124+
id: `edit-data-connections-${patient?.id}`,
125+
variant: 'actionListItem',
126+
onClick: (_popupState) => {
127+
_popupState.close();
128+
onOpenDataConnectionsModal(patient.id);
129+
},
130+
text: t('Bring Data into Tidepool'),
131+
}]}
132+
sx={{ position: 'relative', left: '-2px' }}
133+
/>
134+
);
135+
};
136+
137+
export default {
138+
DexcomConnectionStatusCell,
139+
DaysSinceLastDataCell,
140+
MoreMenuCell,
141+
};
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import React, { useCallback, useMemo, useState } from 'react';
2+
import { useSelector } from 'react-redux';
3+
import { useTranslation } from 'react-i18next';
4+
import { Box, Flex, Text } from 'theme-ui';
5+
import { colors as vizColors } from '@tidepool/viz';
6+
import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft';
7+
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
8+
9+
import Table from '../../../../components/elements/Table';
10+
11+
import { PatientCell } from '../Cells';
12+
import { DexcomConnectionStatusCell, DaysSinceLastDataCell, MoreMenuCell } from './Cells';
13+
import TagListCell from '../../components/TagListCell';
14+
import EmptyContentNode from '../EmptyContentNode';
15+
import useTideReportNoDataPatients from './useTideReportNoDataPatients';
16+
import EditPatientDialogController from './EditPatientDialogController';
17+
import DataConnectionsModalController from './DataConnectionsModalController';
18+
import { useGetPatientFromClinicQuery } from './tideDashboardLegacyApi';
19+
import Icon from '../../../../components/elements/Icon';
20+
21+
const usePatientFromClinic = (patientId) => {
22+
const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
23+
24+
const { data: patient } = useGetPatientFromClinicQuery(
25+
{ clinicId: selectedClinicId, patientId },
26+
{ skip: !selectedClinicId || !patientId }
27+
);
28+
29+
return patient;
30+
};
31+
32+
const DataIssues = ({ api }) => {
33+
const { t } = useTranslation();
34+
const { patients } = useTideReportNoDataPatients();
35+
36+
const [activePatientId, setActivePatientId] = useState(null);
37+
const [isAccordionOpen, setIsAccordionOpen] = useState(false);
38+
const [isEditPatientDialogOpen, setIsEditPatientDialogOpen] = useState(false);
39+
const [isDataConnectionsModalOpen, setIsDataConnectionsModalOpen] = useState(false);
40+
41+
const activePatient = usePatientFromClinic(activePatientId);
42+
43+
const handleOpenEditPatientDialog = useCallback((patientId) => {
44+
setActivePatientId(patientId);
45+
setIsEditPatientDialogOpen(true);
46+
}, []);
47+
48+
const handleOpenDataConnectionsModal = (patientId) => {
49+
setActivePatientId(patientId);
50+
setIsDataConnectionsModalOpen(true);
51+
};
52+
53+
const handleCloseModals = () => {
54+
setIsEditPatientDialogOpen(false);
55+
setIsDataConnectionsModalOpen(false);
56+
setActivePatientId(null);
57+
};
58+
59+
const columns = useMemo(() => ([
60+
{
61+
title: t('Patient Details'),
62+
field: 'fullName',
63+
align: 'left',
64+
render: patient => <PatientCell patient={patient} />,
65+
},
66+
{
67+
title: t('Dexcom Connection Status'),
68+
field: 'dexcomConnectionStatus',
69+
align: 'left',
70+
render: patient => <DexcomConnectionStatusCell patient={patient} onOpenDataConnectionsModal={handleOpenDataConnectionsModal} />,
71+
},
72+
{
73+
title: t('Days Since Last Data'),
74+
field: 'daysSinceLastData',
75+
align: 'center',
76+
render: patient => <DaysSinceLastDataCell patient={patient} />,
77+
},
78+
{
79+
title: t('Tags'),
80+
field: 'tags',
81+
align: 'center',
82+
render: patient => <TagListCell patient={patient} />,
83+
},
84+
{
85+
title: '',
86+
field: 'moreMenu',
87+
align: 'center',
88+
render: patient => (
89+
<MoreMenuCell
90+
patient={patient}
91+
onOpenEditPatientDialog={handleOpenEditPatientDialog}
92+
onOpenDataConnectionsModal={handleOpenDataConnectionsModal}
93+
/>
94+
),
95+
},
96+
]), [t, handleOpenEditPatientDialog, handleOpenDataConnectionsModal]);
97+
98+
if (!patients?.length) return null;
99+
100+
return (
101+
<Box id="tide-dashboard-data-issues" my={5}>
102+
<Flex
103+
onClick={() => setIsAccordionOpen(isOpen => !isOpen)}
104+
className="data-issues-section-label"
105+
sx={{
106+
justifyContent: 'space-between',
107+
transition: 'all 0.2s ease',
108+
borderBottom: '1px solid',
109+
borderColor: isAccordionOpen ? vizColors.white : vizColors.gray10,
110+
padding: 3,
111+
color: vizColors.blueGray50,
112+
'&:hover': { cursor: 'pointer', borderColor: vizColors.gray30 },
113+
}}
114+
>
115+
<Text sx={{ fontSize: 2 }}>{t('Device Issues ({{count}})', { count: patients.length })}</Text>
116+
117+
<Icon
118+
variant="static"
119+
icon={isAccordionOpen ? KeyboardArrowDownIcon : KeyboardArrowLeftIcon}
120+
label={isAccordionOpen ? t('Data Issues Close Icon') : t('Data Issues Open Icon')}
121+
title={isAccordionOpen ? t('Data Issues Close Icon') : t('Data Issues Open Icon')}
122+
sx={{ fontSize: 4 }}
123+
/>
124+
</Flex>
125+
126+
<Box sx={{
127+
display: 'grid',
128+
gridTemplateRows: isAccordionOpen ? 'minmax(0, 1fr)' : 'minmax(0, 0fr)',
129+
transition: 'grid-template-rows 0.5s ease',
130+
}}>
131+
<Box sx={{ minHeight: 0, overflow: 'hidden' }}>
132+
<Table
133+
id="tideDashboardDataIssuesTable"
134+
variant="condensed"
135+
label="tideDashboardDataIssuesTable"
136+
columns={columns}
137+
data={patients}
138+
emptyContentNode={<EmptyContentNode />}
139+
containerProps={{ sx: { containerType: 'inline-size' } }}
140+
/>
141+
</Box>
142+
</Box>
143+
144+
<EditPatientDialogController
145+
api={api}
146+
isOpen={isEditPatientDialogOpen}
147+
patient={activePatient}
148+
onClose={handleCloseModals}
149+
/>
150+
151+
<DataConnectionsModalController
152+
isOpen={isDataConnectionsModalOpen}
153+
patient={activePatient}
154+
onClose={handleCloseModals}
155+
/>
156+
</Box>
157+
);
158+
};
159+
160+
export default DataIssues;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import DataIssues from './DataIssues';
2+
3+
export default DataIssues;

0 commit comments

Comments
 (0)