@digihmis/esm-triage-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 (148) hide show
  1. package/README.md +4 -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/152.js +1 -0
  7. package/dist/152.js.map +1 -0
  8. package/dist/183.js +66 -0
  9. package/dist/183.js.map +1 -0
  10. package/dist/209.js +1 -0
  11. package/dist/209.js.map +1 -0
  12. package/dist/22.js +1 -0
  13. package/dist/22.js.map +1 -0
  14. package/dist/222.js +1 -0
  15. package/dist/222.js.map +1 -0
  16. package/dist/317.js +1 -0
  17. package/dist/317.js.map +1 -0
  18. package/dist/349.js +1 -0
  19. package/dist/349.js.map +1 -0
  20. package/dist/371.js +1 -0
  21. package/dist/371.js.map +1 -0
  22. package/dist/378.js +1 -0
  23. package/dist/378.js.map +1 -0
  24. package/dist/442.js +1 -0
  25. package/dist/442.js.map +1 -0
  26. package/dist/45.js +1 -0
  27. package/dist/45.js.map +1 -0
  28. package/dist/466.js +1 -0
  29. package/dist/466.js.map +1 -0
  30. package/dist/508.js +1 -0
  31. package/dist/508.js.map +1 -0
  32. package/dist/61.js +1 -0
  33. package/dist/61.js.map +1 -0
  34. package/dist/640.js +1 -0
  35. package/dist/640.js.map +1 -0
  36. package/dist/689.js +1 -0
  37. package/dist/689.js.map +1 -0
  38. package/dist/709.js +1 -0
  39. package/dist/709.js.map +1 -0
  40. package/dist/712.js +1 -0
  41. package/dist/712.js.map +1 -0
  42. package/dist/720.js +1 -0
  43. package/dist/720.js.map +1 -0
  44. package/dist/737.js +12 -0
  45. package/dist/737.js.map +1 -0
  46. package/dist/742.js +1 -0
  47. package/dist/742.js.map +1 -0
  48. package/dist/771.js +1 -0
  49. package/dist/771.js.map +1 -0
  50. package/dist/806.js +1 -0
  51. package/dist/806.js.map +1 -0
  52. package/dist/809.js +1 -0
  53. package/dist/809.js.map +1 -0
  54. package/dist/9.js +15 -0
  55. package/dist/9.js.map +1 -0
  56. package/dist/912.js +1 -0
  57. package/dist/912.js.map +1 -0
  58. package/dist/913.js +1 -0
  59. package/dist/913.js.map +1 -0
  60. package/dist/940.js +1 -0
  61. package/dist/940.js.map +1 -0
  62. package/dist/974.js +1 -0
  63. package/dist/974.js.map +1 -0
  64. package/dist/976.js +1 -0
  65. package/dist/976.js.map +1 -0
  66. package/dist/digihmis-esm-triage-app.js +6 -0
  67. package/dist/digihmis-esm-triage-app.js.buildmanifest.json +905 -0
  68. package/dist/digihmis-esm-triage-app.js.map +1 -0
  69. package/dist/main.js +90 -0
  70. package/dist/main.js.map +1 -0
  71. package/dist/routes.json +1 -0
  72. package/jest.config.js +26 -0
  73. package/package.json +51 -0
  74. package/rspack.config.js +1 -0
  75. package/src/config-schema.ts +142 -0
  76. package/src/dashboard.meta.ts +16 -0
  77. package/src/declarations.d.ts +6 -0
  78. package/src/icon-wrapper/icon-wrapper.extension.tsx +17 -0
  79. package/src/index.ts +41 -0
  80. package/src/left-panel/left-panel.component.tsx +100 -0
  81. package/src/left-panel/left-panel.scss +13 -0
  82. package/src/page-header/page-header.component.tsx +35 -0
  83. package/src/page-header/page-header.scss +76 -0
  84. package/src/past-vitals/action-menu/vitals-anthropometrics.component.tsx +44 -0
  85. package/src/past-vitals/action-menu/vitals-anthropometrics.scss +0 -0
  86. package/src/past-vitals/paginated-antropometrics/paginated-antropometrics.component.tsx +132 -0
  87. package/src/past-vitals/paginated-antropometrics/paginated-antropometrics.scss +9 -0
  88. package/src/past-vitals/paginated-vitals/paginated-vitals.component.tsx +157 -0
  89. package/src/past-vitals/paginated-vitals/paginated-vitals.scss +94 -0
  90. package/src/past-vitals/paginated-vitals/type/index.ts +1 -0
  91. package/src/past-vitals/patient-banner/patient-banner.component.tsx +60 -0
  92. package/src/past-vitals/patient-banner/patient-banner.scss +72 -0
  93. package/src/past-vitals/patient-vitals-history.component.tsx +85 -0
  94. package/src/past-vitals/patient-vitals-history.resource.ts +30 -0
  95. package/src/past-vitals/patient-vitals-history.scss +75 -0
  96. package/src/past-vitals/triage-anthropometrics/antropometrics-chart.component.tsx +170 -0
  97. package/src/past-vitals/triage-anthropometrics/antropometrics-chart.scss +70 -0
  98. package/src/past-vitals/triage-anthropometrics/triage-antropometrics.component.tsx +124 -0
  99. package/src/past-vitals/triage-anthropometrics/triage-antropometrics.scss +37 -0
  100. package/src/past-vitals/triage-anthropometrics/type/index.ts +17 -0
  101. package/src/past-vitals/triage-notes/triage-note.scss +94 -0
  102. package/src/past-vitals/triage-notes/triage-notes.component.tsx +75 -0
  103. package/src/past-vitals/triage-notes/triage-notes.resource.ts +72 -0
  104. package/src/past-vitals/triage-notes/type/index.ts +50 -0
  105. package/src/past-vitals/triage-vitals/triage-vitals-history.component.tsx +229 -0
  106. package/src/past-vitals/triage-vitals/triage-vitals-history.scss +48 -0
  107. package/src/past-vitals/triage-vitals/type/index.ts +32 -0
  108. package/src/past-vitals/triage-vitals/vital-chart.component.tsx +198 -0
  109. package/src/past-vitals/triage-vitals/vitals-chart.scss +70 -0
  110. package/src/past-vitals/utils/index.ts +5 -0
  111. package/src/past-vitals/vitals-and-biometrics-header/vitals-header-item.component.tsx +61 -0
  112. package/src/past-vitals/vitals-and-biometrics-header/vitals-header-item.scss +171 -0
  113. package/src/past-vitals/vitals-and-biometrics-header/vitals-header-item.test.tsx +31 -0
  114. package/src/past-vitals/vitals-and-biometrics-header/vitals-header.extension.tsx +225 -0
  115. package/src/past-vitals/vitals-and-biometrics-header/vitals-header.scss +233 -0
  116. package/src/queue-room-table/queue-room-table.component.tsx +380 -0
  117. package/src/queue-room-table/queue-room-table.resource.ts +73 -0
  118. package/src/queue-room-table/queue-room-table.scss +193 -0
  119. package/src/queue-room-table/type/index.ts +30 -0
  120. package/src/queue-room-table/useQueueActions.ts +24 -0
  121. package/src/record-vitals-workspace/common/index.ts +19 -0
  122. package/src/record-vitals-workspace/component/vitals-biometrics-input.component.tsx +433 -0
  123. package/src/record-vitals-workspace/component/vitals-biometrics-input.scss +358 -0
  124. package/src/record-vitals-workspace/helper/index.ts +200 -0
  125. package/src/record-vitals-workspace/record-vitals.resource.ts +570 -0
  126. package/src/record-vitals-workspace/record-vitals.scss +140 -0
  127. package/src/record-vitals-workspace/record-vitals.workspace.tsx +612 -0
  128. package/src/record-vitals-workspace/schema/index.ts +28 -0
  129. package/src/record-vitals-workspace/type/index.ts +170 -0
  130. package/src/record-vitals-workspace/utils/index.ts +100 -0
  131. package/src/routes.json +70 -0
  132. package/src/setup-tests.ts +1 -0
  133. package/src/triage-queue-dashboard/triage-dashboard.resource.ts +75 -0
  134. package/src/triage-queue-dashboard/triage-queue-dashboard.component.tsx +261 -0
  135. package/src/triage-queue-dashboard/triage-queue-dashboard.scss +166 -0
  136. package/src/triage-queue-dashboard/type/index.ts +31 -0
  137. package/translations/am.json +92 -0
  138. package/translations/ar.json +92 -0
  139. package/translations/en.json +92 -0
  140. package/translations/es.json +92 -0
  141. package/translations/fr.json +92 -0
  142. package/translations/he.json +92 -0
  143. package/translations/km.json +92 -0
  144. package/translations/pt.json +92 -0
  145. package/translations/sw.json +92 -0
  146. package/translations/vi.json +92 -0
  147. package/translations/zh.json +92 -0
  148. package/tsconfig.json +5 -0
@@ -0,0 +1,233 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+ @use '@openmrs/esm-styleguide/src/vars' as *;
5
+
6
+ .container {
7
+ background-color: $openmrs-background-grey;
8
+ padding: layout.$spacing-03 layout.$spacing-05 layout.$spacing-05;
9
+ border-bottom: 1px solid $ui-03;
10
+
11
+ :global(.omrs-breakpoint-lt-tablet) & {
12
+ padding: layout.$spacing-03 layout.$spacing-04 layout.$spacing-04;
13
+ }
14
+ }
15
+
16
+ .vitalsHeader {
17
+ display: flex;
18
+ justify-content: space-between;
19
+ align-items: center;
20
+ min-height: layout.$spacing-07;
21
+ outline: none;
22
+ pointer-events: visibleFill;
23
+ }
24
+
25
+ .emptyStateVitalsHeader {
26
+ background-color: $openmrs-background-grey;
27
+ display: flex;
28
+ justify-content: space-between;
29
+ align-items: center;
30
+ padding: 0 layout.$spacing-05;
31
+ min-height: layout.$spacing-09;
32
+ outline: none;
33
+ pointer-events: visibleFill;
34
+ border-bottom: 1px solid $ui-03;
35
+
36
+ .container {
37
+ border-bottom: none;
38
+ padding: 0;
39
+ }
40
+
41
+ .bodyText {
42
+ margin-left: layout.$spacing-03;
43
+ }
44
+
45
+ :global(.omrs-breakpoint-lt-tablet) & {
46
+ flex-direction: column;
47
+ align-items: flex-start;
48
+ padding: layout.$spacing-03 layout.$spacing-04;
49
+
50
+ .bodyText {
51
+ margin-left: 0;
52
+ margin-top: layout.$spacing-02;
53
+ }
54
+ }
55
+ }
56
+
57
+ .headerItems {
58
+ display: flex;
59
+ align-items: baseline;
60
+ flex-flow: row wrap;
61
+ gap: layout.$spacing-02 layout.$spacing-04;
62
+
63
+ :global(.omrs-breakpoint-lt-tablet) & {
64
+ flex-direction: column;
65
+ align-items: flex-start;
66
+ gap: layout.$spacing-02;
67
+ }
68
+ }
69
+
70
+ .heading {
71
+ @include type.type-style('heading-compact-02');
72
+ color: $ui-05 !important;
73
+ }
74
+
75
+ .rowContainer {
76
+ width: 100%;
77
+ margin-top: layout.$spacing-04;
78
+ }
79
+
80
+ .row {
81
+ display: grid;
82
+ // Use auto-fit to dynamically adjust columns based on available space
83
+ grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
84
+ gap: layout.$spacing-04;
85
+ align-items: start;
86
+ max-width: 100%;
87
+
88
+ // Large desktop: allow up to 9 columns if there's space
89
+ @media (min-width: 1440px) {
90
+ grid-template-columns: repeat(9, 1fr);
91
+ gap: layout.$spacing-05;
92
+ }
93
+
94
+ // Desktop: 5 columns (better distribution)
95
+ @media (min-width: 1056px) and (max-width: 1439px) {
96
+ grid-template-columns: repeat(5, 1fr);
97
+ gap: layout.$spacing-04;
98
+ }
99
+
100
+ // Tablet landscape: 4 columns
101
+ :global(.omrs-breakpoint-lt-desktop) & {
102
+ grid-template-columns: repeat(4, 1fr);
103
+ gap: layout.$spacing-04;
104
+ }
105
+
106
+ // Tablet portrait: 3 columns
107
+ :global(.omrs-breakpoint-lt-tablet) & {
108
+ grid-template-columns: repeat(3, 1fr);
109
+ gap: layout.$spacing-03;
110
+ }
111
+
112
+ // Mobile: 2 columns
113
+ @media (max-width: 480px) {
114
+ grid-template-columns: repeat(2, 1fr);
115
+ gap: layout.$spacing-03;
116
+ }
117
+ }
118
+
119
+ // When workspace is open, force smaller grid
120
+ .workspaceOpen {
121
+ .row {
122
+ grid-template-columns: repeat(4, 1fr);
123
+ gap: layout.$spacing-04;
124
+
125
+ // Tablet: 3 columns
126
+ :global(.omrs-breakpoint-lt-tablet) & {
127
+ grid-template-columns: repeat(3, 1fr);
128
+ gap: layout.$spacing-03;
129
+ }
130
+
131
+ // Mobile: 2 columns
132
+ @media (max-width: 480px) {
133
+ grid-template-columns: repeat(2, 1fr);
134
+ gap: layout.$spacing-03;
135
+ }
136
+ }
137
+ }
138
+
139
+ .loading {
140
+ display: flex;
141
+ background-color: $openmrs-background-grey;
142
+ justify-content: center;
143
+ align-items: center;
144
+ min-height: layout.$spacing-09;
145
+ padding: layout.$spacing-05;
146
+ }
147
+
148
+ .tag {
149
+ margin-right: 0;
150
+
151
+ :global(.omrs-breakpoint-lt-tablet) & {
152
+ width: 100%;
153
+ margin-top: layout.$spacing-02;
154
+ }
155
+ }
156
+
157
+ .overdueIndicator {
158
+ display: flex;
159
+ align-items: center;
160
+ min-inline-size: max-content;
161
+ color: colors.$red-70;
162
+
163
+ svg {
164
+ margin-right: layout.$spacing-02;
165
+ }
166
+
167
+ :global(.omrs-breakpoint-lt-tablet) & {
168
+ font-size: 0.875rem;
169
+ }
170
+ }
171
+
172
+ .buttonContainer {
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: right;
176
+
177
+ :global(.omrs-breakpoint-lt-tablet) & {
178
+ width: 100%;
179
+ margin-top: layout.$spacing-03;
180
+ }
181
+ }
182
+
183
+ .recordVitalsButton {
184
+ @include type.type-style('body-compact-01');
185
+
186
+ &:not([disabled]) svg {
187
+ fill: $color-blue-60-2;
188
+ }
189
+ }
190
+
191
+ .recordVitalsIconButton {
192
+ display: flex;
193
+ align-items: baseline;
194
+ justify-content: center;
195
+ cursor: pointer;
196
+ margin-left: layout.$spacing-03;
197
+ }
198
+
199
+ .bodyText {
200
+ @include type.type-style('label-01');
201
+ color: $text-02;
202
+ }
203
+
204
+ .link {
205
+ @extend .bodyText;
206
+ text-decoration-line: none;
207
+ color: $interactive-01;
208
+ }
209
+
210
+ .backgroundDataFetchingIndicator {
211
+ :global(.omrs-breakpoint-lt-tablet) & {
212
+ margin-top: layout.$spacing-02;
213
+ }
214
+ }
215
+
216
+ // RTL support
217
+ html[dir='rtl'] {
218
+ .container {
219
+ & > span:nth-child(2) {
220
+ margin-right: layout.$spacing-03;
221
+ }
222
+ }
223
+
224
+ .overdueIndicator svg {
225
+ margin-right: 0;
226
+ margin-left: layout.$spacing-02;
227
+ }
228
+
229
+ .recordVitalsIconButton {
230
+ margin-left: 0;
231
+ margin-right: layout.$spacing-03;
232
+ }
233
+ }
@@ -0,0 +1,380 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import {
3
+ DataTable,
4
+ Table,
5
+ TableHead,
6
+ TableRow,
7
+ TableHeader,
8
+ TableBody,
9
+ TableCell,
10
+ Button,
11
+ TableContainer,
12
+ Tile,
13
+ Pagination,
14
+ Tag,
15
+ InlineLoading,
16
+ Search,
17
+ } from '@carbon/react';
18
+ import { Activity, CheckmarkFilled, Time } from '@carbon/react/icons';
19
+ import { useTranslation } from 'react-i18next';
20
+ import { isDesktop, launchWorkspace, navigate, showSnackbar, useConfig, useLayoutType } from '@openmrs/esm-framework';
21
+ import { useQueueEntries } from './queue-room-table.resource';
22
+ import { useQueueActions } from './useQueueActions';
23
+ import styles from './queue-room-table.scss';
24
+ import { type QueueEntry } from './type';
25
+ import capitalize from 'lodash/capitalize';
26
+
27
+ interface QueueRoomTableProps {
28
+ queueRoomUuid: string;
29
+ queueUuid: string;
30
+ }
31
+
32
+ export const QueueRoomTable: React.FC<QueueRoomTableProps> = ({ queueRoomUuid, queueUuid }) => {
33
+ const { t } = useTranslation();
34
+ const config = useConfig();
35
+ const layout = useLayoutType();
36
+ const { servePatient, completeService } = useQueueActions();
37
+
38
+ const [page, setPage] = useState(1);
39
+ const [pageSize, setPageSize] = useState(10);
40
+ const [searchTerm, setSearchTerm] = useState('');
41
+ const [processingUuid, setProcessingUuid] = useState<string | null>(null);
42
+
43
+ const { queueEntries, isLoading, mutate } = useQueueEntries(queueRoomUuid, queueUuid);
44
+
45
+ const filteredEntries = useMemo(() => {
46
+ if (!queueEntries) {
47
+ return [];
48
+ }
49
+ if (!searchTerm) {
50
+ return queueEntries;
51
+ }
52
+
53
+ const search = searchTerm.toLowerCase();
54
+ return queueEntries?.filter(
55
+ (entry) =>
56
+ entry?.patient?.display.toLowerCase().includes(search) || entry?.display.toLowerCase().includes(search),
57
+ );
58
+ }, [queueEntries, searchTerm]);
59
+
60
+ const paginatedEntries = useMemo(() => {
61
+ const start = (page - 1) * pageSize;
62
+ return filteredEntries.slice(start, start + pageSize);
63
+ }, [filteredEntries, page, pageSize]);
64
+
65
+ const calculateWaitTime = (startedAt: string) => {
66
+ const start = new Date(startedAt);
67
+ const now = new Date();
68
+ const diffMs = now.getTime() - start.getTime();
69
+ const diffMins = Math.floor(diffMs / 60000);
70
+ return diffMins;
71
+ };
72
+
73
+ const getPriorityColor = (priority: string) => {
74
+ const lower = priority.toLowerCase();
75
+ if (lower.includes('emergency')) {
76
+ return 'red';
77
+ }
78
+ if (lower.includes('urgent')) {
79
+ return 'magenta';
80
+ }
81
+ if (lower.includes('priority')) {
82
+ return 'purple';
83
+ }
84
+ return 'gray';
85
+ };
86
+
87
+ const handleServePatient = async (entry: QueueEntry) => {
88
+ setProcessingUuid(entry.uuid);
89
+
90
+ try {
91
+ await servePatient(entry.uuid);
92
+
93
+ showSnackbar({
94
+ title: t('patientServed', 'Patient Served'),
95
+ subtitle: t('patientServedMessage', `${entry?.patient?.display} is now being served`),
96
+ kind: 'success',
97
+ timeoutInMs: 3000,
98
+ });
99
+
100
+ mutate();
101
+
102
+ launchWorkspace('record-vitals-workspace', {
103
+ workspaceTitle: t('recordVitals', 'Record Vitals'),
104
+ patientUuid: entry?.patient?.uuid,
105
+ queueEntryUuid: entry.uuid,
106
+ completeServiceCallback: async () => {
107
+ // This callback will be executed after vitals are saved and room transition is complete
108
+ try {
109
+ await completeService(entry.uuid);
110
+
111
+ showSnackbar({
112
+ title: t('serviceCompleted', 'Service Completed'),
113
+ subtitle: t('serviceCompletedMessage', `Service completed for ${entry.patient.display}`),
114
+ kind: 'success',
115
+ timeoutInMs: 3000,
116
+ });
117
+
118
+ mutate();
119
+ } catch (error) {
120
+ console.error('Error completing service:', error);
121
+ showSnackbar({
122
+ title: t('error', 'Error'),
123
+ subtitle: t('completeServiceError', 'Failed to complete service'),
124
+ kind: 'error',
125
+ timeoutInMs: 5000,
126
+ });
127
+ }
128
+ },
129
+ });
130
+ } catch (error) {
131
+ console.error('Error serving patient:', error);
132
+
133
+ showSnackbar({
134
+ title: t('error', 'Error'),
135
+ subtitle: t('servePatientError', 'Failed to serve patient. Please try again.'),
136
+ kind: 'error',
137
+ timeoutInMs: 5000,
138
+ });
139
+ } finally {
140
+ setProcessingUuid(null);
141
+ }
142
+ };
143
+
144
+ const handleViewPastVitals = (patientUuid: string) => {
145
+ navigate({
146
+ to: `${window.spaBase}/home/triage/patient/${patientUuid}/vitals`,
147
+ });
148
+ };
149
+
150
+ const handleCompleteService = async (entry: QueueEntry) => {
151
+ setProcessingUuid(entry.uuid);
152
+
153
+ try {
154
+ await completeService(entry.uuid);
155
+
156
+ showSnackbar({
157
+ title: t('serviceCompleted', 'Service Completed'),
158
+ subtitle: t('patientServedMessage', `${entry?.patient?.person?.display} is now being served`),
159
+ kind: 'success',
160
+ timeoutInMs: 3000,
161
+ });
162
+
163
+ mutate();
164
+ } catch (error) {
165
+ console.error('Error completing service:', error);
166
+
167
+ showSnackbar({
168
+ title: t('error', 'Error'),
169
+ subtitle: t('completeServiceError', 'Failed to complete service. Please try again.'),
170
+ kind: 'error',
171
+ timeoutInMs: 5000,
172
+ });
173
+ } finally {
174
+ setProcessingUuid(null);
175
+ }
176
+ };
177
+
178
+ const headers = [
179
+ { key: 'position', header: '#' },
180
+ { key: 'patientName', header: t('patient', 'Patient') },
181
+ { key: 'age', header: t('age', 'Age') },
182
+ { key: 'gender', header: t('gender', 'Gender') },
183
+ { key: 'waitTime', header: t('waitTime', 'Wait Time') },
184
+ { key: 'priority', header: t('priority', 'Priority') },
185
+ { key: 'status', header: t('status', 'Status') },
186
+ { key: 'actions', header: t('actions', 'Actions') },
187
+ { key: 'statusUuid', header: '' },
188
+ { key: 'patientUuid', header: '' },
189
+ { key: 'entryUuid', header: '' },
190
+ { key: 'priorityColor', header: '' },
191
+ { key: 'entry', header: '' },
192
+ ];
193
+
194
+ const rows = paginatedEntries?.map((entry, index) => {
195
+ const patientUuid = entry?.patient?.uuid || '';
196
+ const statusUuid = entry?.status?.uuid || '';
197
+ const entryUuid = entry?.uuid || '';
198
+
199
+ return {
200
+ id: entryUuid,
201
+ position: (page - 1) * pageSize + index + 1,
202
+ patientName: capitalize(entry?.patient?.person?.display || ''),
203
+ patientUuid: patientUuid,
204
+ age: entry?.patient?.person?.age,
205
+ gender: entry?.patient?.person?.gender === 'M' ? t('male', 'Male') : t('female', 'Female'),
206
+ priority: entry?.priority?.display || t('normal', 'Normal'),
207
+ priorityColor: getPriorityColor(entry?.priority?.display || ''),
208
+ waitTime: calculateWaitTime(entry?.startedAt),
209
+ status: entry?.status?.display,
210
+ statusUuid: statusUuid,
211
+ entryUuid: entryUuid,
212
+ entry: entry,
213
+ };
214
+ });
215
+
216
+ if (isLoading) {
217
+ return (
218
+ <div className={styles.loadingContainer}>
219
+ <InlineLoading description={t('loadingQueue', 'Loading queue...')} />
220
+ </div>
221
+ );
222
+ }
223
+
224
+ if (!queueEntries || queueEntries?.length === 0) {
225
+ return (
226
+ <div className={styles.emptyStateContainer}>
227
+ <Tile className={styles.tile}>
228
+ <div className={styles.tileContent}>
229
+ <p className={styles.content}>{t('noPatients', 'No patients in queue')}</p>
230
+ <p className={styles.emptyStateHelperText}>
231
+ {t('noPatientsDescription', 'There are currently no patients waiting in this queue room.')}{' '}
232
+ </p>
233
+ </div>
234
+ </Tile>
235
+ </div>
236
+ );
237
+ }
238
+
239
+ return (
240
+ <div className={styles.tableContainer}>
241
+ <Search
242
+ labelText={t('filterPatients', 'Filter patients')}
243
+ placeholder={t('filterPatients', 'Filter patients')}
244
+ onChange={(e) => setSearchTerm(e.target.value)}
245
+ size={isDesktop(layout) ? 'sm' : 'lg'}
246
+ />
247
+ <DataTable rows={rows} headers={headers} size={isDesktop(layout) ? 'sm' : 'lg'} useZebraStyles={rows?.length > 1}>
248
+ {({ rows, headers, getHeaderProps, getRowProps, getTableProps, getTableContainerProps }) => (
249
+ <TableContainer className={styles.tableContainer}>
250
+ <Table className={styles.activeVisitsTable} {...getTableProps()}>
251
+ <TableHead>
252
+ <TableRow>
253
+ {headers.map(
254
+ (header) =>
255
+ header.header && (
256
+ <TableHeader key={header.key} {...getHeaderProps({ header })}>
257
+ {header.header}
258
+ </TableHeader>
259
+ ),
260
+ )}
261
+ </TableRow>
262
+ </TableHead>
263
+ <TableBody>
264
+ {rows.map((row) => (
265
+ <TableRow key={row.id} {...getRowProps({ row })}>
266
+ {row.cells.map((cell) => {
267
+ if (
268
+ ['statusUuid', 'patientUuid', 'entryUuid', 'priorityColor', 'entry'].includes(cell.info.header)
269
+ ) {
270
+ return null;
271
+ }
272
+
273
+ if (cell.info.header === 'priority') {
274
+ return (
275
+ <TableCell key={cell.id}>
276
+ <Tag type={row.cells.find((c) => c.info.header === 'priorityColor')?.value}>
277
+ {cell.value}
278
+ </Tag>
279
+ </TableCell>
280
+ );
281
+ }
282
+
283
+ if (cell.info.header === 'waitTime') {
284
+ const mins = cell.value;
285
+ const hours = Math.floor(mins / 60);
286
+ const minutes = mins % 60;
287
+ const displayTime = hours > 0 ? `${hours}h ${minutes}min` : `${minutes}min`;
288
+
289
+ return (
290
+ <TableCell key={cell.id}>
291
+ <div className={styles.waitTime}>
292
+ <Time size={16} />
293
+ <span className={mins > 60 ? styles.longWait : ''}>{displayTime}</span>
294
+ </div>
295
+ </TableCell>
296
+ );
297
+ }
298
+
299
+ if (cell.info.header === 'status') {
300
+ const statusUuid = row.cells.find((c) => c.info.header === 'statusUuid')?.value;
301
+ const isWaiting = statusUuid === config.concepts.waitingStatusUuid;
302
+ const isInService = statusUuid === config.concepts.inServiceStatusUuid;
303
+
304
+ return (
305
+ <TableCell key={cell.id}>
306
+ <Tag type={isWaiting ? 'blue' : isInService ? 'green' : 'gray'}>{cell.value}</Tag>
307
+ </TableCell>
308
+ );
309
+ }
310
+
311
+ if (cell.info.header === 'actions') {
312
+ const entryUuid = row.cells.find((c) => c.info.header === 'entryUuid')?.value as string;
313
+ const patientUuid = row.cells.find((c) => c.info.header === 'patientUuid')?.value as string;
314
+ const statusUuid = row.cells.find((c) => c.info.header === 'statusUuid')?.value as string;
315
+ const entry = row.cells.find((c) => c.info.header === 'entry')?.value as QueueEntry;
316
+
317
+ const isProcessing = processingUuid === entryUuid;
318
+ const isWaiting = statusUuid === config.concepts.waitingStatusUuid;
319
+ const isInService = statusUuid === config.concepts.inServiceStatusUuid;
320
+ return (
321
+ <TableCell key={cell.id}>
322
+ <div className={styles.actions}>
323
+ {(isWaiting || isInService) && (
324
+ <Button
325
+ kind="primary"
326
+ size="sm"
327
+ renderIcon={CheckmarkFilled}
328
+ onClick={() => handleServePatient(entry)}
329
+ disabled={isProcessing}>
330
+ {isProcessing
331
+ ? t('recordVitals', 'Record Vitals...')
332
+ : t('recordVitals', 'Record Vitals')}
333
+ </Button>
334
+ )}
335
+ {/* {isInService && (
336
+ <Button
337
+ kind="tertiary"
338
+ size="sm"
339
+ renderIcon={CheckmarkFilled}
340
+ onClick={() => handleCompleteService(entry)}
341
+ disabled={isProcessing}>
342
+ {isProcessing ? t('completing', 'Completing...') : t('complete', 'Complete')}
343
+ </Button>
344
+ )} */}
345
+ <Button
346
+ kind="secondary"
347
+ size="sm"
348
+ renderIcon={Activity}
349
+ onClick={() => handleViewPastVitals(patientUuid)}
350
+ disabled={isProcessing}>
351
+ {isProcessing ? t('pastVitals', 'Past Vitals...') : t('pastVitals', 'Past Vitals')}
352
+ </Button>
353
+ </div>
354
+ </TableCell>
355
+ );
356
+ }
357
+
358
+ return <TableCell key={cell.id}>{cell.value}</TableCell>;
359
+ })}
360
+ </TableRow>
361
+ ))}
362
+ </TableBody>
363
+ </Table>
364
+ </TableContainer>
365
+ )}
366
+ </DataTable>
367
+ <Pagination
368
+ className={styles.pagination}
369
+ page={page}
370
+ pageSize={pageSize}
371
+ pageSizes={[10, 20, 30, 50]}
372
+ totalItems={filteredEntries.length}
373
+ onChange={({ page, pageSize }) => {
374
+ setPage(page);
375
+ setPageSize(pageSize);
376
+ }}
377
+ />
378
+ </div>
379
+ );
380
+ };
@@ -0,0 +1,73 @@
1
+ import { FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import useSWR from 'swr';
3
+ import { type QueueEntry } from './type';
4
+
5
+ export const useQueueEntries = (queueRoomUuid: string, queueUuid: string) => {
6
+ const customRepresentation =
7
+ 'custom:(uuid,display,patient:(uuid,display,person:(display,age,gender)),priority:(uuid,display),status:(uuid,display),visit:(uuid),startedAt,sortWeight,queueRoom:(uuid,display))';
8
+
9
+ const params = new URLSearchParams();
10
+ params.append('v', customRepresentation);
11
+ params.append('queue', queueUuid);
12
+ params.append('queueRoom', queueRoomUuid);
13
+ params.append('isEnded', 'false');
14
+
15
+ const apiUrl = `${restBaseUrl}/queue-entry?${params.toString()}`;
16
+
17
+ const { data, error, mutate, isLoading, isValidating } = useSWR<FetchResponse<{ results: Array<QueueEntry> }>>(
18
+ queueRoomUuid ? apiUrl : null,
19
+ openmrsFetch,
20
+ {
21
+ refreshInterval: 30000,
22
+ revalidateOnFocus: true,
23
+ },
24
+ );
25
+
26
+ const sortedEntries = data?.data?.results?.sort((a, b) => {
27
+ const priorityOrder: Record<string, number> = {
28
+ emergency: 0,
29
+ urgent: 1,
30
+ priority: 2,
31
+ normal: 3,
32
+ };
33
+
34
+ const aPriority = a.priority?.display?.toLowerCase() || 'normal';
35
+ const bPriority = b.priority?.display?.toLowerCase() || 'normal';
36
+
37
+ const aOrder = priorityOrder[aPriority] ?? 3;
38
+ const bOrder = priorityOrder[bPriority] ?? 3;
39
+
40
+ if (aOrder !== bOrder) {
41
+ return aOrder - bOrder;
42
+ }
43
+
44
+ return (a.sortWeight || 0) - (b.sortWeight || 0);
45
+ });
46
+
47
+ return {
48
+ queueEntries: sortedEntries || [],
49
+ isLoading,
50
+ error,
51
+ mutate,
52
+ isValidating,
53
+ };
54
+ };
55
+
56
+ export const updateQueueEntryStatus = async (
57
+ queueEntryUuid: string,
58
+ statusUuid: string,
59
+ completedStatusUuid: string,
60
+ ) => {
61
+ const url = `${restBaseUrl}/queue-entry/${queueEntryUuid}`;
62
+
63
+ return openmrsFetch(url, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ },
68
+ body: JSON.stringify({
69
+ status: statusUuid,
70
+ endedAt: statusUuid === completedStatusUuid ? new Date().toISOString() : undefined,
71
+ }),
72
+ });
73
+ };