@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.
Files changed (114) hide show
  1. package/README.md +5 -0
  2. package/dist/117.js +1 -0
  3. package/dist/117.js.map +1 -0
  4. package/dist/132.js +1 -0
  5. package/dist/132.js.map +1 -0
  6. package/dist/137.js +1 -0
  7. package/dist/137.js.map +1 -0
  8. package/dist/150.js +1 -0
  9. package/dist/150.js.map +1 -0
  10. package/dist/152.js +1 -0
  11. package/dist/152.js.map +1 -0
  12. package/dist/209.js +1 -0
  13. package/dist/209.js.map +1 -0
  14. package/dist/248.js +1 -0
  15. package/dist/248.js.map +1 -0
  16. package/dist/252.js +12 -0
  17. package/dist/252.js.map +1 -0
  18. package/dist/317.js +1 -0
  19. package/dist/317.js.map +1 -0
  20. package/dist/349.js +1 -0
  21. package/dist/349.js.map +1 -0
  22. package/dist/371.js +1 -0
  23. package/dist/371.js.map +1 -0
  24. package/dist/378.js +1 -0
  25. package/dist/378.js.map +1 -0
  26. package/dist/442.js +1 -0
  27. package/dist/442.js.map +1 -0
  28. package/dist/45.js +1 -0
  29. package/dist/45.js.map +1 -0
  30. package/dist/466.js +1 -0
  31. package/dist/466.js.map +1 -0
  32. package/dist/49.js +1 -0
  33. package/dist/49.js.map +1 -0
  34. package/dist/508.js +1 -0
  35. package/dist/508.js.map +1 -0
  36. package/dist/567.js +1 -0
  37. package/dist/567.js.map +1 -0
  38. package/dist/60.js +1 -0
  39. package/dist/60.js.map +1 -0
  40. package/dist/61.js +1 -0
  41. package/dist/61.js.map +1 -0
  42. package/dist/689.js +1 -0
  43. package/dist/689.js.map +1 -0
  44. package/dist/712.js +1 -0
  45. package/dist/712.js.map +1 -0
  46. package/dist/720.js +1 -0
  47. package/dist/720.js.map +1 -0
  48. package/dist/742.js +1 -0
  49. package/dist/742.js.map +1 -0
  50. package/dist/771.js +1 -0
  51. package/dist/771.js.map +1 -0
  52. package/dist/806.js +1 -0
  53. package/dist/806.js.map +1 -0
  54. package/dist/912.js +1 -0
  55. package/dist/912.js.map +1 -0
  56. package/dist/913.js +1 -0
  57. package/dist/913.js.map +1 -0
  58. package/dist/940.js +1 -0
  59. package/dist/940.js.map +1 -0
  60. package/dist/987.js +15 -0
  61. package/dist/987.js.map +1 -0
  62. package/dist/digihmis-esm-home-app.js +6 -0
  63. package/dist/digihmis-esm-home-app.js.buildmanifest.json +896 -0
  64. package/dist/digihmis-esm-home-app.js.map +1 -0
  65. package/dist/main.js +6 -0
  66. package/dist/main.js.map +1 -0
  67. package/dist/routes.json +1 -0
  68. package/jest.config.js +26 -0
  69. package/package.json +51 -0
  70. package/rspack.config.js +1 -0
  71. package/src/config-schema.ts +358 -0
  72. package/src/dashboards/administration-dashboard/administration-dashboard.component.tsx +68 -0
  73. package/src/dashboards/billing-dashboard/billing-dashboard.component.tsx +71 -0
  74. package/src/dashboards/outpatient-dashboard/outpatient-dashboard.component.tsx +71 -0
  75. package/src/dashboards/registration-dashboard/registration-dashboard.component.tsx +58 -0
  76. package/src/dashboards/triage-dashboard/triage-dashboard.component.tsx +72 -0
  77. package/src/declarations.d.ts +6 -0
  78. package/src/generic-clock-in-modal/clock-in.scss +41 -0
  79. package/src/generic-clock-in-modal/generic-clock-in.modal.tsx +295 -0
  80. package/src/generic-clock-in-modal/generic-clock-in.resource.ts +107 -0
  81. package/src/generic-clock-in-modal/type/index.ts +45 -0
  82. package/src/generic-clock-in-modal/util/session-cache.ts +48 -0
  83. package/src/generic-dashboard/generic-dashboard-view.component.tsx +23 -0
  84. package/src/generic-dashboard/generic-dashboard-view.scss +5 -0
  85. package/src/generic-dashboard/generic-dashboard.scss +32 -0
  86. package/src/home-grid-dashboard/home-grid-dashboard.component.tsx +33 -0
  87. package/src/home-grid-dashboard/home-grid-dashboard.scss +56 -0
  88. package/src/index.ts +31 -0
  89. package/src/module-card/module-card-utils.component.tsx +175 -0
  90. package/src/module-card/module-card.component.tsx +48 -0
  91. package/src/module-card/module-card.scss +103 -0
  92. package/src/provider-banner/page-header.extension.tsx +86 -0
  93. package/src/provider-banner/page-header.scss +48 -0
  94. package/src/root.component.tsx +43 -0
  95. package/src/root.scss +15 -0
  96. package/src/routes.json +74 -0
  97. package/src/setup-tests.ts +1 -0
  98. package/src/side-nav/generic-side-nav.component.tsx +17 -0
  99. package/src/type/index.ts +5 -0
  100. package/src/welcome-back-banner/welcome-back-banner.component.tsx +112 -0
  101. package/src/welcome-back-banner/welcome-back-banner.resource.ts +29 -0
  102. package/src/welcome-back-banner/welcome-back-banner.scss +123 -0
  103. package/translations/am.json +33 -0
  104. package/translations/ar.json +33 -0
  105. package/translations/en.json +33 -0
  106. package/translations/es.json +33 -0
  107. package/translations/fr.json +33 -0
  108. package/translations/he.json +33 -0
  109. package/translations/km.json +33 -0
  110. package/translations/pt.json +33 -0
  111. package/translations/sw.json +33 -0
  112. package/translations/vi.json +33 -0
  113. package/translations/zh.json +33 -0
  114. package/tsconfig.json +5 -0
@@ -0,0 +1,6 @@
1
+ declare module '@carbon/react';
2
+ declare module '*.css';
3
+ declare module '*.scss';
4
+ declare module '*.png';
5
+
6
+ declare type SideNavProps = object;
@@ -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,5 @@
1
+ @use '@carbon/colors';
2
+
3
+ .dashboardView {
4
+ background-color: colors.$white-0;
5
+ }
@@ -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
+ }