@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,170 @@
1
+ import type { FetchResponse, FHIRResource } from '@openmrs/esm-framework';
2
+
3
+ export interface ConceptAnswers {
4
+ display: string;
5
+ uuid: string;
6
+ }
7
+
8
+ export interface ConceptResponse {
9
+ uuid: string;
10
+ display: string;
11
+ datatype: {
12
+ uuid: string;
13
+ display: string;
14
+ };
15
+ answers: Array<ConceptAnswers>;
16
+ setMembers: Array<ConceptAnswers>;
17
+ }
18
+ export interface Identifier {
19
+ identifierType: { uuid: string; display: string };
20
+ identifier: string;
21
+ }
22
+ export interface Person {
23
+ uuid: string;
24
+ display: string;
25
+ identifiers?: Array<Identifier>;
26
+ person?: Person;
27
+ gender: string;
28
+ age: number;
29
+ birthdate: string;
30
+ birthdateEstimated: boolean;
31
+ }
32
+ export interface Patient {
33
+ uuid: string;
34
+ display: string;
35
+ identifiers: Array<Identifier>;
36
+ person: Person;
37
+ }
38
+
39
+ type ReferenceRangeValue = number | null | undefined;
40
+
41
+ export type FHIRSearchBundleResponse = FetchResponse<{
42
+ entry: Array<FHIRResource>;
43
+ link: Array<{ relation: string; url: string }>;
44
+ }>;
45
+
46
+ export interface ObsReferenceRanges {
47
+ hiAbsolute: ReferenceRangeValue;
48
+ hiCritical: ReferenceRangeValue;
49
+ hiNormal: ReferenceRangeValue;
50
+ lowNormal: ReferenceRangeValue;
51
+ lowCritical: ReferenceRangeValue;
52
+ lowAbsolute: ReferenceRangeValue;
53
+ }
54
+
55
+ export type ObservationInterpretation = 'critically_low' | 'critically_high' | 'high' | 'low' | 'normal';
56
+
57
+ export type MappedVitals = {
58
+ code: string;
59
+ interpretation: ObservationInterpretation;
60
+ recordedDate: string | Date;
61
+ value: number;
62
+ encounterId: string;
63
+ };
64
+
65
+ export type FHIRInterpretation = 'Critically Low' | 'Critically High' | 'High' | 'Low' | 'Normal';
66
+
67
+ export interface FHIRObservationResource {
68
+ resourceType: string;
69
+ id: string;
70
+ category: Array<{
71
+ coding: Array<{
72
+ system: string;
73
+ code: string;
74
+ display: string;
75
+ }>;
76
+ }>;
77
+ code: {
78
+ coding: Array<{
79
+ code: string;
80
+ display: string;
81
+ }>;
82
+ text: string;
83
+ };
84
+ encounter?: {
85
+ reference: string;
86
+ type: string;
87
+ };
88
+ effectiveDateTime: string;
89
+ issued: string;
90
+ valueString?: string;
91
+ valueQuantity?: {
92
+ value: number;
93
+ unit: string;
94
+ system: string;
95
+ code: string;
96
+ };
97
+ valueCodeableConcept?: {
98
+ coding: [
99
+ {
100
+ code: string;
101
+ display: string;
102
+ },
103
+ ];
104
+ text: string;
105
+ };
106
+ referenceRange: Array<{
107
+ low?: {
108
+ value: number;
109
+ };
110
+ high?: {
111
+ value: number;
112
+ };
113
+ type: {
114
+ coding: Array<{
115
+ system: string;
116
+ code: string;
117
+ }>;
118
+ };
119
+ }>;
120
+ hasMember?: Array<{
121
+ reference: string;
122
+ }>;
123
+ interpretation?: Array<{
124
+ coding: Array<{
125
+ code: string;
126
+ display: FHIRInterpretation;
127
+ system: string;
128
+ }>;
129
+ text: string;
130
+ }>;
131
+ }
132
+
133
+ export interface PatientVitalsAndBiometrics {
134
+ id: string;
135
+ date: string;
136
+ systolic?: number;
137
+ systolicRenderInterpretation?: ObservationInterpretation;
138
+ diastolic?: number;
139
+ diastolicRenderInterpretation?: ObservationInterpretation;
140
+ bloodPressureRenderInterpretation?: ObservationInterpretation;
141
+ pulse?: number;
142
+ pulseRenderInterpretation?: ObservationInterpretation;
143
+ temperature?: number;
144
+ temperatureRenderInterpretation?: ObservationInterpretation;
145
+ spo2?: number;
146
+ spo2RenderInterpretation?: ObservationInterpretation;
147
+ height?: number;
148
+ weight?: number;
149
+ bmi?: number | null;
150
+ respiratoryRate?: number;
151
+ respiratoryRateRenderInterpretation?: ObservationInterpretation;
152
+ muac?: number;
153
+ }
154
+
155
+ export interface VitalsResponse {
156
+ entry: Array<{
157
+ resource: FHIRObservationResource;
158
+ }>;
159
+ id: string;
160
+ meta: {
161
+ lastUpdated: string;
162
+ };
163
+ link: Array<{
164
+ relation: string;
165
+ url: string;
166
+ }>;
167
+ resourceType: string;
168
+ total: number;
169
+ type: string;
170
+ }
@@ -0,0 +1,100 @@
1
+ import isNumber from 'lodash/isNumber';
2
+ import { type ConceptMetadata } from '../common';
3
+
4
+ export function calculateBodyMassIndex(
5
+ weight: number,
6
+ height: number,
7
+ weightUnit: 'lb' | 'lbs' | 'g',
8
+ heightUnit: 'm' | 'cm' | 'in',
9
+ ): number {
10
+ if (!weight || !height) {
11
+ return;
12
+ }
13
+
14
+ if (weight > 0 && height > 0) {
15
+ if (weightUnit === 'lb' || weightUnit === 'lbs') {
16
+ weight = weight * 0.45359237;
17
+ }
18
+ if (weightUnit === 'g') {
19
+ weight = weight / 1000;
20
+ }
21
+ if (heightUnit === 'cm') {
22
+ height = height / 100;
23
+ }
24
+ if (heightUnit === 'in') {
25
+ height = height * 0.0254;
26
+ }
27
+ return Number((weight / height ** 2).toFixed(1));
28
+ }
29
+ }
30
+
31
+ export function isValueWithinReferenceRange(
32
+ conceptMetadata: Array<ConceptMetadata>,
33
+ conceptUuid: string,
34
+ value: string | number,
35
+ ) {
36
+ const concept = conceptMetadata?.find((c) => c.uuid === conceptUuid);
37
+
38
+ if (value === undefined || value === '' || concept === undefined) {
39
+ return true;
40
+ }
41
+
42
+ return isNumber(concept?.lowAbsolute) && isNumber(concept?.hiAbsolute)
43
+ ? Number(value) >= Number(concept.lowAbsolute) && Number(value) <= Number(concept.hiAbsolute)
44
+ : true;
45
+ }
46
+
47
+ // Convert age into an integer (whole number)
48
+ export function extractNumbers(str: string) {
49
+ const regex = /\d+/g;
50
+ const match = str.match(regex);
51
+ if (!match) {
52
+ return null;
53
+ }
54
+ return parseInt(match[0], 10);
55
+ }
56
+
57
+ export function getMuacColorCode(age: number, muac: number, setColorCode: (color) => void) {
58
+ switch (true) {
59
+ // children 5 years and below with a muac equal to 14
60
+ case age <= 5 && muac <= 11.5 && muac > 0:
61
+ setColorCode('red');
62
+ break;
63
+ case age < 5 && muac > 11.5 && muac < 12.5:
64
+ setColorCode('yellow');
65
+ break;
66
+ case age < 5 && muac > 12.5:
67
+ setColorCode('green');
68
+ break;
69
+ // above 5 but less than 10
70
+ case age > 5 && age < 10 && muac <= 13.5 && muac > 0:
71
+ setColorCode('red');
72
+ break;
73
+ case age > 5 && age < 10 && muac > 13.5 && muac < 14.5:
74
+ setColorCode('yellow');
75
+ break;
76
+ case age > 5 && age < 10 && muac > 14.5:
77
+ setColorCode('green');
78
+ break;
79
+ //above 10 but less than 18
80
+ case age > 10 && age < 18 && muac <= 16.5 && muac > 0:
81
+ setColorCode('red');
82
+ break;
83
+ case age > 10 && age < 18 && muac > 16.5 && muac < 19.0:
84
+ setColorCode('yellow');
85
+ break;
86
+ case age > 10 && age < 18 && muac > 19.0:
87
+ setColorCode('green');
88
+ break;
89
+ // above 18
90
+ case age > 18 && muac <= 19.5 && muac > 0:
91
+ setColorCode('red');
92
+ break;
93
+ case age > 18 && muac > 19.0 && muac < 22.0:
94
+ setColorCode('yellow');
95
+ break;
96
+ case age > 18 && muac > 22.0:
97
+ setColorCode('green');
98
+ break;
99
+ }
100
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "$schema": "https://json.openmrs.org/routes.schema.json",
3
+ "backendDependencies": {
4
+ "fhir2": ">=1.2",
5
+ "webservices.rest": "^2.24.0"
6
+ },
7
+ "extensions": [
8
+ {
9
+ "name": "triage-dashboard-header",
10
+ "slot": "triage-sidebar-slot",
11
+ "component": "triageDashboardHeader",
12
+ "order": 0,
13
+ "online": true,
14
+ "offline": true
15
+ },
16
+ {
17
+ "name": "triage-db-link",
18
+ "slot": "triage-dashboard-slot",
19
+ "component": "leftPanelLink",
20
+ "meta": {
21
+ "name": "triage",
22
+ "slot": "triage-homepage-dashboard-slot",
23
+ "title": "Triage Queue"
24
+ },
25
+ "order": 1,
26
+ "online": true,
27
+ "offline": true
28
+ },
29
+ {
30
+ "name": "triage-queue-dashboard",
31
+ "slot": "triage-homepage-dashboard-slot",
32
+ "component": "triageQueueDashboard"
33
+ },
34
+ {
35
+ "name": "patient-vitals-link",
36
+ "slot": "triage-dashboard-slot",
37
+ "component": "patientVitalsLink",
38
+ "meta": {
39
+ "name": "triage/:patientUuid/vitals",
40
+ "slot": "triage-vitals-dashboard-slot",
41
+ "title": "Patient Vitals",
42
+ "showOnlyOnRoute": "/vitals"
43
+ },
44
+ "order": 2,
45
+ "online": true,
46
+ "offline": true
47
+ },
48
+ {
49
+ "name": "patient-vitals-history",
50
+ "slot": "triage-vitals-dashboard-slot",
51
+ "component": "patientVitalsHistory"
52
+ },{
53
+ "name": "vitals-header",
54
+ "slot": "patient-vitals-header-slot",
55
+ "component": "vitalsHeader",
56
+ "online": true,
57
+ "offline": true
58
+ }
59
+ ],
60
+ "workspaces": [
61
+ {
62
+ "name": "record-vitals-workspace",
63
+ "component": "recordVitalsWorkspace",
64
+ "title": "Record Vitals",
65
+ "type": "other-form",
66
+ "canMaximize": true,
67
+ "width": "extra-wide"
68
+ }
69
+ ]
70
+ }
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom/extend-expect';
@@ -0,0 +1,75 @@
1
+ import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import useSWR from 'swr';
3
+ import { type ProviderSession, type QueueMetricsResponse } from './type';
4
+
5
+ // Clock out API call
6
+ export const clockOutProvider = async (sessionUuid?: string, providerUuid?: string) => {
7
+ const postUrl = `${restBaseUrl}/provider-clock/clock-out`;
8
+
9
+ return openmrsFetch(postUrl, {
10
+ method: 'POST',
11
+ headers: {
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ body: JSON.stringify({
15
+ ...(sessionUuid && { sessionUuid }),
16
+ ...(providerUuid && { providerUuid }),
17
+ }),
18
+ });
19
+ };
20
+
21
+ // Get active sessions for provider
22
+ export const useActiveSessions = (providerUuid: string, shouldRefresh: boolean = false) => {
23
+ // ✅ Remove the custom representation parameter since backend now handles it by default
24
+ const apiUrl = providerUuid ? `${restBaseUrl}/provider-clock/active-sessions?provider=${providerUuid}` : null;
25
+
26
+ const { data, error, isLoading, mutate } = useSWR<FetchResponse<Array<ProviderSession>>>(apiUrl, openmrsFetch, {
27
+ refreshInterval: shouldRefresh ? 60000 : 0,
28
+ revalidateOnFocus: true,
29
+ dedupingInterval: 30000,
30
+ });
31
+
32
+ return {
33
+ activeSessions: data?.data ?? [],
34
+ error,
35
+ isLoading,
36
+ mutate,
37
+ };
38
+ };
39
+
40
+ // Get queue metrics
41
+ export const useQueueMetrics = (
42
+ queueUuid?: string,
43
+ queueRoomUuid?: string,
44
+ providerUuid?: string,
45
+ isActive: boolean = false,
46
+ ) => {
47
+ const params = new URLSearchParams();
48
+ if (queueUuid) {
49
+ params.append('queue', queueUuid);
50
+ }
51
+ if (queueRoomUuid) {
52
+ params.append('queueRoom', queueRoomUuid);
53
+ }
54
+ if (providerUuid) {
55
+ params.append('provider', providerUuid);
56
+ }
57
+
58
+ const apiUrl = isActive
59
+ ? `${restBaseUrl}/provider-clock/metrics${params.toString() ? `?${params.toString()}` : ''}`
60
+ : null;
61
+
62
+ const { data, error, isLoading, mutate } = useSWR<FetchResponse<QueueMetricsResponse>>(apiUrl, openmrsFetch, {
63
+ refreshInterval: isActive ? 60000 : 0,
64
+ revalidateOnFocus: true,
65
+ revalidateOnReconnect: true,
66
+ dedupingInterval: 30000,
67
+ });
68
+
69
+ return {
70
+ metrics: data?.data,
71
+ error,
72
+ isLoading,
73
+ mutate,
74
+ };
75
+ };
@@ -0,0 +1,261 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { PageHeader } from '../page-header/page-header.component';
3
+ import { Button, Tile, InlineLoading } from '@carbon/react';
4
+ import { ConnectionTwoWay, Login, Logout, Alarm } from '@carbon/react/icons';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { CardHeader } from '@openmrs/esm-patient-common-lib';
7
+ import { useSession, showModal, formatDate, showSnackbar, useConfig } from '@openmrs/esm-framework';
8
+ import styles from './triage-queue-dashboard.scss';
9
+
10
+ import { type ProviderSession } from './type';
11
+ import { clockOutProvider, useActiveSessions, useQueueMetrics } from './triage-dashboard.resource';
12
+ import { ConfigObject } from '../config-schema';
13
+ import { QueueRoomTable } from '../queue-room-table/queue-room-table.component';
14
+
15
+ export const TriageQueueDashboard: React.FC = () => {
16
+ const { t } = useTranslation();
17
+ const { currentProvider } = useSession();
18
+ const { triageQueueUuid } = useConfig<ConfigObject>();
19
+
20
+ const [isProcessing, setIsProcessing] = useState(false);
21
+ const [activeSession, setActiveSession] = useState<ProviderSession | null>(null);
22
+
23
+ const isClockedIn = activeSession !== null;
24
+
25
+ const {
26
+ activeSessions,
27
+ mutate: mutateSessions,
28
+ isLoading: isLoadingSessions,
29
+ } = useActiveSessions(currentProvider?.uuid || '');
30
+
31
+ const {
32
+ metrics,
33
+ mutate: mutateMetrics,
34
+ isLoading: isLoadingMetrics,
35
+ } = useQueueMetrics(triageQueueUuid, activeSession?.queueRoom?.uuid, currentProvider?.uuid, isClockedIn);
36
+
37
+ useEffect(() => {
38
+ if (activeSessions && activeSessions.length > 0) {
39
+ const triageSession = activeSessions.find((session) => session.queueRoom?.queue?.uuid === triageQueueUuid);
40
+ setActiveSession(triageSession || null);
41
+ } else {
42
+ setActiveSession(null);
43
+ }
44
+ }, [activeSessions, triageQueueUuid]);
45
+
46
+ const handleClockIn = () => {
47
+ const dispose = showModal('generic-clock-in-modal', {
48
+ moduleName: 'Triage',
49
+ url: window.location.pathname,
50
+ queueUuid: triageQueueUuid,
51
+ closeModal: () => dispose(),
52
+ onClockInSuccess: (sessionData: ProviderSession) => {
53
+ setActiveSession(sessionData);
54
+ mutateSessions();
55
+ mutateMetrics();
56
+ },
57
+ });
58
+ };
59
+
60
+ const handleClockOut = async () => {
61
+ if (!activeSession?.uuid) {
62
+ showSnackbar({
63
+ title: t('error', 'Error'),
64
+ subtitle: t('noActiveSession', 'No active session found'),
65
+ kind: 'error',
66
+ });
67
+ return;
68
+ }
69
+
70
+ setIsProcessing(true);
71
+
72
+ try {
73
+ await clockOutProvider(activeSession.uuid);
74
+
75
+ showSnackbar({
76
+ title: t('clockOutSuccess', 'Clock Out Successful'),
77
+ subtitle: t('clockOutSuccessMessage', 'You have been clocked out successfully'),
78
+ kind: 'success',
79
+ timeoutInMs: 3000,
80
+ });
81
+
82
+ setActiveSession(null);
83
+ mutateSessions();
84
+ mutateMetrics();
85
+ } catch (error) {
86
+ console.error('Clock out error:', error);
87
+
88
+ const errorMessage = error?.responseBody?.error || t('clockOutFailed', 'Failed to clock out');
89
+
90
+ showSnackbar({
91
+ title: t('clockOutError', 'Clock Out Failed'),
92
+ subtitle: errorMessage,
93
+ kind: 'error',
94
+ timeoutInMs: 5000,
95
+ });
96
+ } finally {
97
+ setIsProcessing(false);
98
+ }
99
+ };
100
+
101
+ const handleSwitchServicePoint = async () => {
102
+ if (!activeSession?.uuid) {
103
+ showSnackbar({
104
+ title: t('error', 'Error'),
105
+ subtitle: t('noActiveSession', 'No active session found'),
106
+ kind: 'error',
107
+ });
108
+ return;
109
+ }
110
+
111
+ setIsProcessing(true);
112
+
113
+ try {
114
+ await clockOutProvider(activeSession.uuid);
115
+
116
+ showSnackbar({
117
+ title: t('switchingStation', 'Switching Station'),
118
+ subtitle: t('clockedOutFromCurrent', 'Clocked out from current station'),
119
+ kind: 'info',
120
+ timeoutInMs: 2000,
121
+ });
122
+
123
+ await mutateSessions();
124
+ setActiveSession(null);
125
+
126
+ const dispose = showModal('generic-clock-in-modal', {
127
+ moduleName: 'Triage',
128
+ url: window.location.pathname,
129
+ queueUuid: triageQueueUuid,
130
+ closeModal: () => dispose(),
131
+ onClockInSuccess: (sessionData: ProviderSession) => {
132
+ setActiveSession(sessionData);
133
+ mutateSessions();
134
+ mutateMetrics();
135
+ },
136
+ });
137
+ } catch (error) {
138
+ const errorMessage = error?.responseBody?.error || t('switchStationFailed', 'Failed to switch station');
139
+
140
+ showSnackbar({
141
+ title: t('switchStationError', 'Switch Station Failed'),
142
+ subtitle: errorMessage,
143
+ kind: 'error',
144
+ timeoutInMs: 5000,
145
+ });
146
+ } finally {
147
+ setIsProcessing(false);
148
+ }
149
+ };
150
+
151
+ const isLoading = isLoadingSessions || isLoadingMetrics;
152
+
153
+ return (
154
+ <div>
155
+ <PageHeader />
156
+ <div className={styles.dashboardContainer}>
157
+ <Tile className={styles.tileContainer}>
158
+ <div className={styles.displayDetails}>
159
+ <div className={styles.countLabel}>{t('myQueueWaiting', 'My Queue - Waiting')}</div>
160
+ <div className={styles.displayData}>
161
+ {isLoading ? <InlineLoading description="" /> : metrics?.myQueueWaiting || 0}
162
+ </div>
163
+ </div>
164
+ <div className={styles.displayDetails}>
165
+ <div className={styles.countLabel}>{t('myQueueInservice', 'My Queue - In Service')}</div>
166
+ <div className={styles.displayData}>
167
+ {isLoading ? <InlineLoading description="" /> : metrics?.inProgress || 0}
168
+ </div>
169
+ </div>
170
+ <div className={styles.displayDetails}>
171
+ <div className={styles.countLabel}>{t('avgWaitTime', 'Avg. Wait Time')}</div>
172
+ <div className={styles.displayData}>
173
+ {isLoading ? (
174
+ <InlineLoading description="" />
175
+ ) : (
176
+ (() => {
177
+ const totalMinutes = metrics?.averageWaitTime || 0;
178
+ const hours = Math.floor(totalMinutes / 60);
179
+ const minutes = totalMinutes % 60;
180
+ return hours > 0 ? `${hours}h ${minutes}min` : `${minutes}min`;
181
+ })()
182
+ )}
183
+ </div>
184
+ </div>
185
+
186
+ <div className={styles.displayDetails}>
187
+ <div className={styles.countLabel}>{t('completedToday', 'Completed Today')}</div>
188
+ <div className={styles.displayData}>
189
+ {isLoading ? <InlineLoading description="" /> : metrics?.completedToday || 0}
190
+ </div>
191
+ </div>
192
+ </Tile>
193
+
194
+ {isClockedIn && activeSession && (
195
+ <Tile className={styles.clockInBanner}>
196
+ <div className={styles.clockInInfo}>
197
+ <Alarm size={20} className={styles.alarmIcon} />
198
+ <div className={styles.clockInText}>
199
+ <strong>{t('clockedInTo', 'Clocked in to')}: </strong>
200
+ {activeSession?.queueRoom?.display}
201
+ <span className={styles.clockInTime}>
202
+ {' '}
203
+ ({t('since', 'since')} {formatDate(new Date(activeSession.clockInTime), { mode: 'wide' })})
204
+ </span>
205
+ </div>
206
+ </div>
207
+ </Tile>
208
+ )}
209
+
210
+ <div className={styles.cardHeaderContainer}>
211
+ <CardHeader title={activeSession?.queueRoom?.display || t('triageQueue', 'Triage Queue')}>
212
+ <div className={styles.headerActions}>
213
+ <Button
214
+ kind={isClockedIn ? 'danger' : 'primary'}
215
+ size="sm"
216
+ renderIcon={(props) => (isClockedIn ? <Logout size={16} {...props} /> : <Login size={16} {...props} />)}
217
+ onClick={isClockedIn ? handleClockOut : handleClockIn}
218
+ disabled={isProcessing}>
219
+ {isProcessing ? (
220
+ <InlineLoading
221
+ description={isClockedIn ? t('clockingOut', 'Clocking out') : t('clockingIn', 'Clocking in')}
222
+ />
223
+ ) : isClockedIn ? (
224
+ t('clockOut', 'Clock Out')
225
+ ) : (
226
+ t('clockIn', 'Clock In')
227
+ )}
228
+ </Button>
229
+
230
+ {isClockedIn && (
231
+ <Button
232
+ kind="secondary"
233
+ size="sm"
234
+ renderIcon={(props) => <ConnectionTwoWay size={16} {...props} />}
235
+ onClick={handleSwitchServicePoint}
236
+ disabled={isProcessing}>
237
+ {t('changeStation', 'Change Station')}
238
+ </Button>
239
+ )}
240
+ </div>
241
+ </CardHeader>
242
+ </div>
243
+
244
+ {isClockedIn && activeSession ? (
245
+ <QueueRoomTable queueRoomUuid={activeSession.queueRoom.uuid} queueUuid={triageQueueUuid} />
246
+ ) : (
247
+ <div className={styles.emptyStateContainer}>
248
+ <Tile className={styles.tile}>
249
+ <div className={styles.tileContent}>
250
+ <p className={styles.content}>{t('pleaseClockIn', 'Please clock in to view the queue')}</p>
251
+ <p className={styles.emptyStateHelperText}>
252
+ {t('clockInPrompt', 'Click the "Clock In" button above to start managing patients')}
253
+ </p>
254
+ </div>
255
+ </Tile>
256
+ </div>
257
+ )}
258
+ </div>
259
+ </div>
260
+ );
261
+ };