@digihmis/esm-home-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/117.js +1 -0
- package/dist/117.js.map +1 -0
- package/dist/132.js +1 -0
- package/dist/132.js.map +1 -0
- package/dist/137.js +1 -0
- package/dist/137.js.map +1 -0
- package/dist/150.js +1 -0
- package/dist/150.js.map +1 -0
- package/dist/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/209.js +1 -0
- package/dist/209.js.map +1 -0
- package/dist/248.js +1 -0
- package/dist/248.js.map +1 -0
- package/dist/252.js +12 -0
- package/dist/252.js.map +1 -0
- package/dist/317.js +1 -0
- package/dist/317.js.map +1 -0
- package/dist/349.js +1 -0
- package/dist/349.js.map +1 -0
- package/dist/371.js +1 -0
- package/dist/371.js.map +1 -0
- package/dist/378.js +1 -0
- package/dist/378.js.map +1 -0
- package/dist/442.js +1 -0
- package/dist/442.js.map +1 -0
- package/dist/45.js +1 -0
- package/dist/45.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/49.js +1 -0
- package/dist/49.js.map +1 -0
- package/dist/508.js +1 -0
- package/dist/508.js.map +1 -0
- package/dist/567.js +1 -0
- package/dist/567.js.map +1 -0
- package/dist/60.js +1 -0
- package/dist/60.js.map +1 -0
- package/dist/61.js +1 -0
- package/dist/61.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/712.js +1 -0
- package/dist/712.js.map +1 -0
- package/dist/720.js +1 -0
- package/dist/720.js.map +1 -0
- package/dist/742.js +1 -0
- package/dist/742.js.map +1 -0
- package/dist/771.js +1 -0
- package/dist/771.js.map +1 -0
- package/dist/806.js +1 -0
- package/dist/806.js.map +1 -0
- package/dist/912.js +1 -0
- package/dist/912.js.map +1 -0
- package/dist/913.js +1 -0
- package/dist/913.js.map +1 -0
- package/dist/940.js +1 -0
- package/dist/940.js.map +1 -0
- package/dist/987.js +15 -0
- package/dist/987.js.map +1 -0
- package/dist/digihmis-esm-home-app.js +6 -0
- package/dist/digihmis-esm-home-app.js.buildmanifest.json +896 -0
- package/dist/digihmis-esm-home-app.js.map +1 -0
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +26 -0
- package/package.json +51 -0
- package/rspack.config.js +1 -0
- package/src/config-schema.ts +358 -0
- package/src/dashboards/administration-dashboard/administration-dashboard.component.tsx +68 -0
- package/src/dashboards/billing-dashboard/billing-dashboard.component.tsx +71 -0
- package/src/dashboards/outpatient-dashboard/outpatient-dashboard.component.tsx +71 -0
- package/src/dashboards/registration-dashboard/registration-dashboard.component.tsx +58 -0
- package/src/dashboards/triage-dashboard/triage-dashboard.component.tsx +72 -0
- package/src/declarations.d.ts +6 -0
- package/src/generic-clock-in-modal/clock-in.scss +41 -0
- package/src/generic-clock-in-modal/generic-clock-in.modal.tsx +295 -0
- package/src/generic-clock-in-modal/generic-clock-in.resource.ts +107 -0
- package/src/generic-clock-in-modal/type/index.ts +45 -0
- package/src/generic-clock-in-modal/util/session-cache.ts +48 -0
- package/src/generic-dashboard/generic-dashboard-view.component.tsx +23 -0
- package/src/generic-dashboard/generic-dashboard-view.scss +5 -0
- package/src/generic-dashboard/generic-dashboard.scss +32 -0
- package/src/home-grid-dashboard/home-grid-dashboard.component.tsx +33 -0
- package/src/home-grid-dashboard/home-grid-dashboard.scss +56 -0
- package/src/index.ts +31 -0
- package/src/module-card/module-card-utils.component.tsx +175 -0
- package/src/module-card/module-card.component.tsx +48 -0
- package/src/module-card/module-card.scss +103 -0
- package/src/provider-banner/page-header.extension.tsx +86 -0
- package/src/provider-banner/page-header.scss +48 -0
- package/src/root.component.tsx +43 -0
- package/src/root.scss +15 -0
- package/src/routes.json +74 -0
- package/src/setup-tests.ts +1 -0
- package/src/side-nav/generic-side-nav.component.tsx +17 -0
- package/src/type/index.ts +5 -0
- package/src/welcome-back-banner/welcome-back-banner.component.tsx +112 -0
- package/src/welcome-back-banner/welcome-back-banner.resource.ts +29 -0
- package/src/welcome-back-banner/welcome-back-banner.scss +123 -0
- package/translations/am.json +33 -0
- package/translations/ar.json +33 -0
- package/translations/en.json +33 -0
- package/translations/es.json +33 -0
- package/translations/fr.json +33 -0
- package/translations/he.json +33 -0
- package/translations/km.json +33 -0
- package/translations/pt.json +33 -0
- package/translations/sw.json +33 -0
- package/translations/vi.json +33 -0
- package/translations/zh.json +33 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
5
|
+
|
|
6
|
+
.patientHeader {
|
|
7
|
+
padding: layout.$spacing-05;
|
|
8
|
+
border-bottom: 1px solid colors.$gray-20;
|
|
9
|
+
background-color: colors.$gray-10;
|
|
10
|
+
|
|
11
|
+
.patientName {
|
|
12
|
+
@include type.type-style('heading-compact-01');
|
|
13
|
+
font-weight: 600;
|
|
14
|
+
margin-right: layout.$spacing-03;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.patientId {
|
|
18
|
+
@include type.type-style('body-compact-01');
|
|
19
|
+
font-weight: 600;
|
|
20
|
+
margin-right: layout.$spacing-02;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.patientDetails {
|
|
24
|
+
@include type.type-style('body-compact-01');
|
|
25
|
+
color: colors.$gray-70;
|
|
26
|
+
}
|
|
27
|
+
.currentLocation {
|
|
28
|
+
@include type.type-style('body-compact-01');
|
|
29
|
+
color: red;
|
|
30
|
+
float: right;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
:global(.cds--select-input) {
|
|
34
|
+
background-color: colors.$gray-20;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@media (min-width: 82rem) {
|
|
38
|
+
:global(.cds--modal-container) {
|
|
39
|
+
width: 38%;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
ModalFooter,
|
|
4
|
+
ModalHeader,
|
|
5
|
+
ModalBody,
|
|
6
|
+
InlineLoading,
|
|
7
|
+
Select,
|
|
8
|
+
SelectItem,
|
|
9
|
+
InlineNotification,
|
|
10
|
+
Tag,
|
|
11
|
+
} from '@carbon/react';
|
|
12
|
+
import React, { useState, useEffect } from 'react';
|
|
13
|
+
import { useTranslation } from 'react-i18next';
|
|
14
|
+
import { showSnackbar, useSession } from '@openmrs/esm-framework';
|
|
15
|
+
import { Logout } from '@carbon/react/icons';
|
|
16
|
+
import styles from './clock-in.scss';
|
|
17
|
+
import { useQueueRooms, clockInProvider, useActiveSessions, clockOutProvider } from './generic-clock-in.resource';
|
|
18
|
+
|
|
19
|
+
interface GenericClockInProps {
|
|
20
|
+
moduleName: string;
|
|
21
|
+
url: string;
|
|
22
|
+
queueUuid?: string;
|
|
23
|
+
closeModal?: () => void;
|
|
24
|
+
onClockInSuccess?: (sessionData: any) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const GenericClockIn: React.FC<GenericClockInProps> = ({
|
|
28
|
+
moduleName,
|
|
29
|
+
url,
|
|
30
|
+
queueUuid,
|
|
31
|
+
closeModal,
|
|
32
|
+
onClockInSuccess,
|
|
33
|
+
}) => {
|
|
34
|
+
const { t } = useTranslation();
|
|
35
|
+
const { currentProvider } = useSession();
|
|
36
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
37
|
+
const [selectedQueueRoom, setSelectedQueueRoom] = useState('');
|
|
38
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
39
|
+
const { queueRooms, isLoading, error } = useQueueRooms(queueUuid);
|
|
40
|
+
|
|
41
|
+
// ✅ Check for active sessions
|
|
42
|
+
const { activeSessions, isLoading: isLoadingSessions, mutate } = useActiveSessions(currentProvider?.uuid || '');
|
|
43
|
+
|
|
44
|
+
// ✅ Filter for sessions in OTHER queues (not the current one)
|
|
45
|
+
const otherQueueSession = activeSessions?.find((session) => session.queueRoom?.queue?.uuid !== queueUuid);
|
|
46
|
+
|
|
47
|
+
const handleQueueRoomChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
48
|
+
setSelectedQueueRoom(event.target.value);
|
|
49
|
+
setErrorMessage('');
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleClockOutAndClockIn = async () => {
|
|
53
|
+
if (!otherQueueSession?.uuid || !selectedQueueRoom) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setIsSubmitting(true);
|
|
58
|
+
setErrorMessage('');
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Step 1: Clock out from current session
|
|
62
|
+
await clockOutProvider(otherQueueSession.uuid);
|
|
63
|
+
|
|
64
|
+
showSnackbar({
|
|
65
|
+
title: t('clockedOut', 'Clocked Out'),
|
|
66
|
+
subtitle: t('clockedOutFromPrevious', 'Clocked out from {{location}}', {
|
|
67
|
+
location: otherQueueSession.queueRoom?.display,
|
|
68
|
+
}),
|
|
69
|
+
kind: 'info',
|
|
70
|
+
timeoutInMs: 2000,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Small delay to ensure backend state is updated
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
75
|
+
|
|
76
|
+
// Step 2: Clock in to new session
|
|
77
|
+
const response = await clockInProvider(currentProvider.uuid, selectedQueueRoom);
|
|
78
|
+
const sessionData = response.data;
|
|
79
|
+
|
|
80
|
+
// Refresh sessions list
|
|
81
|
+
await mutate();
|
|
82
|
+
|
|
83
|
+
showSnackbar({
|
|
84
|
+
title: t('clockInSuccess', 'Clock In Successful'),
|
|
85
|
+
subtitle: t('switchedStations', 'Successfully switched stations'),
|
|
86
|
+
kind: 'success',
|
|
87
|
+
timeoutInMs: 3000,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (onClockInSuccess) {
|
|
91
|
+
onClockInSuccess(sessionData);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
closeModal?.();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('Switch station error:', error);
|
|
97
|
+
const errorMsg =
|
|
98
|
+
error?.responseBody?.error?.message ||
|
|
99
|
+
error?.responseBody?.error ||
|
|
100
|
+
error?.message ||
|
|
101
|
+
t('switchStationFailed', 'Failed to switch stations');
|
|
102
|
+
setErrorMessage(errorMsg);
|
|
103
|
+
|
|
104
|
+
showSnackbar({
|
|
105
|
+
title: t('switchStationError', 'Switch Station Failed'),
|
|
106
|
+
subtitle: errorMsg,
|
|
107
|
+
kind: 'error',
|
|
108
|
+
timeoutInMs: 5000,
|
|
109
|
+
});
|
|
110
|
+
} finally {
|
|
111
|
+
setIsSubmitting(false);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleClockIn = async () => {
|
|
116
|
+
setErrorMessage('');
|
|
117
|
+
|
|
118
|
+
if (!selectedQueueRoom) {
|
|
119
|
+
setErrorMessage(t('pleaseSelectQueueRoom', 'Please select a queue room'));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!currentProvider?.uuid) {
|
|
124
|
+
setErrorMessage(t('providerNotFound', 'Provider information not found'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setIsSubmitting(true);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const response = await clockInProvider(currentProvider.uuid, selectedQueueRoom);
|
|
132
|
+
const sessionData = response.data;
|
|
133
|
+
|
|
134
|
+
// Refresh sessions list
|
|
135
|
+
await mutate();
|
|
136
|
+
|
|
137
|
+
showSnackbar({
|
|
138
|
+
title: t('clockInSuccess', 'Clock In Successful'),
|
|
139
|
+
subtitle: t('clockInSuccessMessage', 'You have been clocked in successfully'),
|
|
140
|
+
kind: 'success',
|
|
141
|
+
timeoutInMs: 3000,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (onClockInSuccess) {
|
|
145
|
+
onClockInSuccess(sessionData);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
closeModal?.();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error('Clock in error:', error);
|
|
151
|
+
const errorMsg =
|
|
152
|
+
error?.responseBody?.error?.message ||
|
|
153
|
+
error?.responseBody?.error ||
|
|
154
|
+
error?.message ||
|
|
155
|
+
t('clockInFailed', 'Failed to clock in');
|
|
156
|
+
setErrorMessage(errorMsg);
|
|
157
|
+
|
|
158
|
+
showSnackbar({
|
|
159
|
+
title: t('clockInError', 'Clock In Failed'),
|
|
160
|
+
subtitle: errorMsg,
|
|
161
|
+
kind: 'error',
|
|
162
|
+
timeoutInMs: 5000,
|
|
163
|
+
});
|
|
164
|
+
} finally {
|
|
165
|
+
setIsSubmitting(false);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (error) {
|
|
170
|
+
return (
|
|
171
|
+
<>
|
|
172
|
+
<ModalHeader closeModal={closeModal} title={t('clockInTo', 'Clock in to {{module}}', { module: moduleName })} />
|
|
173
|
+
<ModalBody>
|
|
174
|
+
<InlineNotification
|
|
175
|
+
kind="error"
|
|
176
|
+
title={t('error', 'Error')}
|
|
177
|
+
subtitle={t('errorLoadingRooms', 'Error loading queue rooms. Please try again.')}
|
|
178
|
+
lowContrast
|
|
179
|
+
hideCloseButton
|
|
180
|
+
/>
|
|
181
|
+
</ModalBody>
|
|
182
|
+
<ModalFooter>
|
|
183
|
+
<Button kind="secondary" onClick={closeModal} type="button">
|
|
184
|
+
{t('close', 'Close')}
|
|
185
|
+
</Button>
|
|
186
|
+
</ModalFooter>
|
|
187
|
+
</>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const isLoaderActive = isLoading || isLoadingSessions;
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<>
|
|
195
|
+
<ModalHeader closeModal={closeModal}>
|
|
196
|
+
<div className={styles.patientHeader}>
|
|
197
|
+
<span className={styles.patientName}>{t('clockInTo', 'Clock in to {{module}}', { module: moduleName })}</span>
|
|
198
|
+
{otherQueueSession && (
|
|
199
|
+
<span className={styles.currentLocation}>
|
|
200
|
+
·{' '}
|
|
201
|
+
{t('currentlyActiveIn', 'Currently active in {{location}}', {
|
|
202
|
+
location: otherQueueSession.queueRoom?.queue?.display || otherQueueSession.queueRoom?.display,
|
|
203
|
+
})}
|
|
204
|
+
</span>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
</ModalHeader>
|
|
208
|
+
<ModalBody>
|
|
209
|
+
{errorMessage && (
|
|
210
|
+
<InlineNotification
|
|
211
|
+
kind="error"
|
|
212
|
+
title={t('error', 'Error')}
|
|
213
|
+
subtitle={errorMessage}
|
|
214
|
+
lowContrast
|
|
215
|
+
hideCloseButton
|
|
216
|
+
style={{ marginBottom: '1rem' }}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{isLoaderActive ? (
|
|
221
|
+
<InlineLoading description={t('loadingRooms', 'Loading rooms...')} />
|
|
222
|
+
) : (
|
|
223
|
+
<div className={styles.instructions}>
|
|
224
|
+
{otherQueueSession && (
|
|
225
|
+
<div
|
|
226
|
+
style={{
|
|
227
|
+
marginBottom: '1rem',
|
|
228
|
+
padding: '0.75rem',
|
|
229
|
+
backgroundColor: '#fff3cd',
|
|
230
|
+
borderLeft: '4px solid #ffc107',
|
|
231
|
+
borderRadius: '4px',
|
|
232
|
+
}}>
|
|
233
|
+
<p style={{ margin: 0, fontSize: '0.875rem' }}>
|
|
234
|
+
<strong>{t('switchingStations', 'Switching Stations')}</strong>
|
|
235
|
+
</p>
|
|
236
|
+
<p style={{ margin: '0.25rem 0 0 0', fontSize: '0.875rem' }}>
|
|
237
|
+
{t(
|
|
238
|
+
'willAutoClockOut',
|
|
239
|
+
'You will be automatically clocked out from {{current}} and clocked in to your selected room.',
|
|
240
|
+
{
|
|
241
|
+
current: otherQueueSession.queueRoom?.display,
|
|
242
|
+
},
|
|
243
|
+
)}
|
|
244
|
+
</p>
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
<Select
|
|
248
|
+
id="queueRoom"
|
|
249
|
+
labelText={
|
|
250
|
+
otherQueueSession
|
|
251
|
+
? t('selectRoomToSwitch', 'Select a queue room to switch to:')
|
|
252
|
+
: t('selectRoomToClockIn', 'Select a queue room to clock in:')
|
|
253
|
+
}
|
|
254
|
+
value={selectedQueueRoom}
|
|
255
|
+
onChange={handleQueueRoomChange}
|
|
256
|
+
disabled={isSubmitting}>
|
|
257
|
+
<SelectItem value="" text={t('chooseQueueRoom', 'Choose queue room')} />
|
|
258
|
+
{queueRooms?.map((room) => (
|
|
259
|
+
<SelectItem key={room.uuid} value={room.uuid} text={room.display} />
|
|
260
|
+
))}
|
|
261
|
+
</Select>
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
</ModalBody>
|
|
265
|
+
<ModalFooter>
|
|
266
|
+
<Button kind="secondary" onClick={closeModal} type="button" disabled={isSubmitting}>
|
|
267
|
+
{t('cancel', 'Cancel')}
|
|
268
|
+
</Button>
|
|
269
|
+
|
|
270
|
+
{/* ✅ Smart button that changes based on context */}
|
|
271
|
+
{otherQueueSession ? (
|
|
272
|
+
<Button
|
|
273
|
+
onClick={handleClockOutAndClockIn}
|
|
274
|
+
type="button"
|
|
275
|
+
disabled={isSubmitting || !selectedQueueRoom || isLoaderActive}
|
|
276
|
+
renderIcon={Logout}>
|
|
277
|
+
{isSubmitting ? (
|
|
278
|
+
<InlineLoading description={t('switchingStation', 'Switching station...')} />
|
|
279
|
+
) : (
|
|
280
|
+
t('switchStation', 'Switch Station')
|
|
281
|
+
)}
|
|
282
|
+
</Button>
|
|
283
|
+
) : (
|
|
284
|
+
<Button onClick={handleClockIn} type="button" disabled={isSubmitting || !selectedQueueRoom || isLoaderActive}>
|
|
285
|
+
{isSubmitting ? (
|
|
286
|
+
<InlineLoading description={t('clockingIn', 'Clocking in...')} />
|
|
287
|
+
) : (
|
|
288
|
+
t('clockIn', 'Clock In')
|
|
289
|
+
)}
|
|
290
|
+
</Button>
|
|
291
|
+
)}
|
|
292
|
+
</ModalFooter>
|
|
293
|
+
</>
|
|
294
|
+
);
|
|
295
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
import { ProviderSessionResponse, QueueResponse, QueueMetricsResponse } from './type';
|
|
4
|
+
import { invalidateAllSessionData } from './util/session-cache';
|
|
5
|
+
|
|
6
|
+
export const useQueueRooms = (queueUuid?: string) => {
|
|
7
|
+
const customRepresentation = 'custom:(uuid,display,queueRooms:(uuid,display))';
|
|
8
|
+
const apiUrl = `${restBaseUrl}/queue?v=${customRepresentation}`;
|
|
9
|
+
const { data, ...rest } = useSWR<FetchResponse<QueueResponse>>(apiUrl, openmrsFetch);
|
|
10
|
+
|
|
11
|
+
const queue = data?.data?.results.find((q) => q.uuid === queueUuid);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
queueRooms: queue?.queueRooms ?? [],
|
|
15
|
+
...rest,
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Clock in API call
|
|
20
|
+
export const clockInProvider = async (providerUuid: string, queueRoomUuid: string) => {
|
|
21
|
+
const postUrl = `${restBaseUrl}/provider-clock/clock-in`;
|
|
22
|
+
|
|
23
|
+
const response = await openmrsFetch(postUrl, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
providerUuid,
|
|
30
|
+
queueRoomUuid,
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await invalidateAllSessionData();
|
|
35
|
+
|
|
36
|
+
return response;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Clock out API call
|
|
40
|
+
export const clockOutProvider = async (sessionUuid?: string, providerUuid?: string) => {
|
|
41
|
+
const postUrl = `${restBaseUrl}/provider-clock/clock-out`;
|
|
42
|
+
|
|
43
|
+
const response = await openmrsFetch(postUrl, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
...(sessionUuid && { sessionUuid }),
|
|
50
|
+
...(providerUuid && { providerUuid }),
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ✅ Invalidate after successful clock out
|
|
55
|
+
await invalidateAllSessionData();
|
|
56
|
+
|
|
57
|
+
return response;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Get active sessions for provider
|
|
61
|
+
export const useActiveSessions = (providerUuid: string, shouldRefresh: boolean = false) => {
|
|
62
|
+
const apiUrl = providerUuid ? `${restBaseUrl}/provider-clock/active-sessions?provider=${providerUuid}` : null;
|
|
63
|
+
|
|
64
|
+
const { data, ...rest } = useSWR<FetchResponse<ProviderSessionResponse>>(apiUrl, openmrsFetch, {
|
|
65
|
+
refreshInterval: shouldRefresh ? 60000 : 0,
|
|
66
|
+
revalidateOnFocus: true,
|
|
67
|
+
dedupingInterval: 30000,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
activeSessions: data?.data ?? [],
|
|
72
|
+
...rest,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Get queue metrics
|
|
77
|
+
export const useQueueMetrics = (
|
|
78
|
+
queueUuid?: string,
|
|
79
|
+
queueRoomUuid?: string,
|
|
80
|
+
providerUuid?: string,
|
|
81
|
+
isActive: boolean = false,
|
|
82
|
+
) => {
|
|
83
|
+
const params = new URLSearchParams();
|
|
84
|
+
if (queueUuid) {
|
|
85
|
+
params.append('queue', queueUuid);
|
|
86
|
+
}
|
|
87
|
+
if (queueRoomUuid) {
|
|
88
|
+
params.append('queueRoom', queueRoomUuid);
|
|
89
|
+
}
|
|
90
|
+
if (providerUuid) {
|
|
91
|
+
params.append('provider', providerUuid);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const apiUrl = `${restBaseUrl}/provider-clock/metrics${params.toString() ? `?${params.toString()}` : ''}`;
|
|
95
|
+
|
|
96
|
+
const { data, ...rest } = useSWR<FetchResponse<QueueMetricsResponse>>(isActive ? apiUrl : null, openmrsFetch, {
|
|
97
|
+
refreshInterval: isActive ? 60000 : 0,
|
|
98
|
+
revalidateOnFocus: true,
|
|
99
|
+
revalidateOnReconnect: true,
|
|
100
|
+
dedupingInterval: 30000,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
metrics: data?.data,
|
|
105
|
+
...rest,
|
|
106
|
+
};
|
|
107
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface Queue {
|
|
2
|
+
uuid: string;
|
|
3
|
+
display: string;
|
|
4
|
+
queueRooms: Array<{
|
|
5
|
+
uuid: string;
|
|
6
|
+
display: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface QueueResponse {
|
|
11
|
+
results: Array<Queue>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ProviderSession {
|
|
15
|
+
uuid: string;
|
|
16
|
+
provider: {
|
|
17
|
+
uuid: string;
|
|
18
|
+
display: string;
|
|
19
|
+
};
|
|
20
|
+
queueRoom: {
|
|
21
|
+
uuid: string;
|
|
22
|
+
display: string;
|
|
23
|
+
queue: {
|
|
24
|
+
uuid: string;
|
|
25
|
+
display: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
clockInTime: string;
|
|
29
|
+
clockOutTime: string | null;
|
|
30
|
+
status: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ProviderSessionResponse = Array<ProviderSession>;
|
|
34
|
+
|
|
35
|
+
export interface QueueMetricsResponse {
|
|
36
|
+
myQueueWaiting: number;
|
|
37
|
+
emergencyCases: number;
|
|
38
|
+
inProgress: number;
|
|
39
|
+
averageWaitTime: number;
|
|
40
|
+
totalWaiting: number;
|
|
41
|
+
completedToday: number;
|
|
42
|
+
activeStaff: number;
|
|
43
|
+
longestWait: number;
|
|
44
|
+
myCompletedToday: number;
|
|
45
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mutate } from 'swr';
|
|
2
|
+
import { restBaseUrl } from '@openmrs/esm-framework';
|
|
3
|
+
|
|
4
|
+
// ✅ Helper to build the cache key
|
|
5
|
+
export const getActiveSessionsCacheKey = (providerUuid: string) => {
|
|
6
|
+
return `${restBaseUrl}/provider-clock/active-sessions?provider=${providerUuid}`;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const getMetricsCacheKey = (queueUuid?: string, queueRoomUuid?: string, providerUuid?: string) => {
|
|
10
|
+
const params = new URLSearchParams();
|
|
11
|
+
if (queueUuid) {
|
|
12
|
+
params.append('queue', queueUuid);
|
|
13
|
+
}
|
|
14
|
+
if (queueRoomUuid) {
|
|
15
|
+
params.append('queueRoom', queueRoomUuid);
|
|
16
|
+
}
|
|
17
|
+
if (providerUuid) {
|
|
18
|
+
params.append('provider', providerUuid);
|
|
19
|
+
}
|
|
20
|
+
return `${restBaseUrl}/provider-clock/metrics${params.toString() ? `?${params.toString()}` : ''}`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ✅ Invalidate ALL active sessions regardless of provider
|
|
24
|
+
export const invalidateAllActiveSessions = async () => {
|
|
25
|
+
await mutate(
|
|
26
|
+
(key) => {
|
|
27
|
+
return typeof key === 'string' && key.includes('/provider-clock/active-sessions');
|
|
28
|
+
},
|
|
29
|
+
undefined,
|
|
30
|
+
{ revalidate: true },
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ✅ Invalidate ALL metrics
|
|
35
|
+
export const invalidateAllMetrics = async () => {
|
|
36
|
+
await mutate(
|
|
37
|
+
(key) => {
|
|
38
|
+
return typeof key === 'string' && key.includes('/provider-clock/metrics');
|
|
39
|
+
},
|
|
40
|
+
undefined,
|
|
41
|
+
{ revalidate: true },
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ✅ Invalidate everything
|
|
46
|
+
export const invalidateAllSessionData = async () => {
|
|
47
|
+
await Promise.all([invalidateAllActiveSessions(), invalidateAllMetrics()]);
|
|
48
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ExtensionSlot } from '@openmrs/esm-framework';
|
|
3
|
+
import styles from './generic-dashboard-view.scss';
|
|
4
|
+
|
|
5
|
+
interface DashboardViewProps {
|
|
6
|
+
dashboardSlot: string;
|
|
7
|
+
title: string;
|
|
8
|
+
state?: Record<string, any>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DashboardView: React.FC<DashboardViewProps> = ({ dashboardSlot, title, state = {} }) => {
|
|
12
|
+
return (
|
|
13
|
+
<div className={styles.dashboardViewWrapper}>
|
|
14
|
+
<ExtensionSlot
|
|
15
|
+
className={styles.dashboardView}
|
|
16
|
+
name={dashboardSlot}
|
|
17
|
+
state={{ dashboardTitle: title, ...state }}
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default DashboardView;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
|
|
3
|
+
.homePageWrapper {
|
|
4
|
+
display: flex;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.dashboardContainer {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-grow: 1;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
margin: 0;
|
|
12
|
+
height: calc(100vh - 3rem);
|
|
13
|
+
width: calc(100% - 16rem);
|
|
14
|
+
background-color: colors.$gray-10;
|
|
15
|
+
|
|
16
|
+
& a {
|
|
17
|
+
width: 100%;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&.hasLeftNav {
|
|
21
|
+
margin-left: 16rem;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.dashboardContainerTablet {
|
|
26
|
+
flex-grow: 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.logoSection {
|
|
30
|
+
display: flex;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from './home-grid-dashboard.scss';
|
|
3
|
+
import ModuleCard from '../module-card/module-card.component';
|
|
4
|
+
import { useModuleLinks } from '../module-card/module-card-utils.component';
|
|
5
|
+
import UserBanner from '../welcome-back-banner/welcome-back-banner.component';
|
|
6
|
+
|
|
7
|
+
const GridDashboard: React.FC = () => {
|
|
8
|
+
const moduleLinks = useModuleLinks();
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<div className={styles.dashboardContainer}>
|
|
12
|
+
<div className={styles.dashboardHeader}>
|
|
13
|
+
<UserBanner />
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div className={styles.gridContainer}>
|
|
17
|
+
{moduleLinks.map((link, index) => (
|
|
18
|
+
<ModuleCard
|
|
19
|
+
key={`${link.label}-${index}`}
|
|
20
|
+
imgSrc={link.imgSrc}
|
|
21
|
+
alt={link.imgAlt}
|
|
22
|
+
label={link.label}
|
|
23
|
+
url={link.url}
|
|
24
|
+
privilege={link.privilege}
|
|
25
|
+
/>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default GridDashboard;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
|
|
5
|
+
.dashboardContainer {
|
|
6
|
+
padding: layout.$spacing-05;
|
|
7
|
+
background-color: colors.$gray-10;
|
|
8
|
+
|
|
9
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
10
|
+
padding: layout.$spacing-03;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.dashboardHeader {
|
|
15
|
+
margin-bottom: layout.$spacing-06;
|
|
16
|
+
|
|
17
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
18
|
+
margin-bottom: layout.$spacing-05;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.gridContainer {
|
|
23
|
+
display: grid;
|
|
24
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
25
|
+
gap: layout.$spacing-06;
|
|
26
|
+
padding: layout.$spacing-03;
|
|
27
|
+
margin: 0 auto;
|
|
28
|
+
|
|
29
|
+
:global(.omrs-breakpoint-gt-lg) & {
|
|
30
|
+
grid-template-columns: repeat(6, 1fr);
|
|
31
|
+
gap: layout.$spacing-06;
|
|
32
|
+
padding: layout.$spacing-05;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
:global(.omrs-breakpoint-lt-desktop) & {
|
|
36
|
+
gap: layout.$spacing-05;
|
|
37
|
+
max-width: 900px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
41
|
+
grid-template-columns: repeat(2, 1fr);
|
|
42
|
+
gap: layout.$spacing-04;
|
|
43
|
+
padding: layout.$spacing-03;
|
|
44
|
+
max-width: 600px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:global(.omrs-breakpoint-lt-phone) & {
|
|
48
|
+
grid-template-columns: 1fr;
|
|
49
|
+
gap: layout.$spacing-03;
|
|
50
|
+
padding: layout.$spacing-02;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.breadcrumbsContainer {
|
|
55
|
+
margin-top: layout.$spacing-07;
|
|
56
|
+
}
|