@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.
- package/README.md +4 -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/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/183.js +66 -0
- package/dist/183.js.map +1 -0
- package/dist/209.js +1 -0
- package/dist/209.js.map +1 -0
- package/dist/22.js +1 -0
- package/dist/22.js.map +1 -0
- package/dist/222.js +1 -0
- package/dist/222.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/508.js +1 -0
- package/dist/508.js.map +1 -0
- package/dist/61.js +1 -0
- package/dist/61.js.map +1 -0
- package/dist/640.js +1 -0
- package/dist/640.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/709.js +1 -0
- package/dist/709.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/737.js +12 -0
- package/dist/737.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/809.js +1 -0
- package/dist/809.js.map +1 -0
- package/dist/9.js +15 -0
- package/dist/9.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/974.js +1 -0
- package/dist/974.js.map +1 -0
- package/dist/976.js +1 -0
- package/dist/976.js.map +1 -0
- package/dist/digihmis-esm-triage-app.js +6 -0
- package/dist/digihmis-esm-triage-app.js.buildmanifest.json +905 -0
- package/dist/digihmis-esm-triage-app.js.map +1 -0
- package/dist/main.js +90 -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 +142 -0
- package/src/dashboard.meta.ts +16 -0
- package/src/declarations.d.ts +6 -0
- package/src/icon-wrapper/icon-wrapper.extension.tsx +17 -0
- package/src/index.ts +41 -0
- package/src/left-panel/left-panel.component.tsx +100 -0
- package/src/left-panel/left-panel.scss +13 -0
- package/src/page-header/page-header.component.tsx +35 -0
- package/src/page-header/page-header.scss +76 -0
- package/src/past-vitals/action-menu/vitals-anthropometrics.component.tsx +44 -0
- package/src/past-vitals/action-menu/vitals-anthropometrics.scss +0 -0
- package/src/past-vitals/paginated-antropometrics/paginated-antropometrics.component.tsx +132 -0
- package/src/past-vitals/paginated-antropometrics/paginated-antropometrics.scss +9 -0
- package/src/past-vitals/paginated-vitals/paginated-vitals.component.tsx +157 -0
- package/src/past-vitals/paginated-vitals/paginated-vitals.scss +94 -0
- package/src/past-vitals/paginated-vitals/type/index.ts +1 -0
- package/src/past-vitals/patient-banner/patient-banner.component.tsx +60 -0
- package/src/past-vitals/patient-banner/patient-banner.scss +72 -0
- package/src/past-vitals/patient-vitals-history.component.tsx +85 -0
- package/src/past-vitals/patient-vitals-history.resource.ts +30 -0
- package/src/past-vitals/patient-vitals-history.scss +75 -0
- package/src/past-vitals/triage-anthropometrics/antropometrics-chart.component.tsx +170 -0
- package/src/past-vitals/triage-anthropometrics/antropometrics-chart.scss +70 -0
- package/src/past-vitals/triage-anthropometrics/triage-antropometrics.component.tsx +124 -0
- package/src/past-vitals/triage-anthropometrics/triage-antropometrics.scss +37 -0
- package/src/past-vitals/triage-anthropometrics/type/index.ts +17 -0
- package/src/past-vitals/triage-notes/triage-note.scss +94 -0
- package/src/past-vitals/triage-notes/triage-notes.component.tsx +75 -0
- package/src/past-vitals/triage-notes/triage-notes.resource.ts +72 -0
- package/src/past-vitals/triage-notes/type/index.ts +50 -0
- package/src/past-vitals/triage-vitals/triage-vitals-history.component.tsx +229 -0
- package/src/past-vitals/triage-vitals/triage-vitals-history.scss +48 -0
- package/src/past-vitals/triage-vitals/type/index.ts +32 -0
- package/src/past-vitals/triage-vitals/vital-chart.component.tsx +198 -0
- package/src/past-vitals/triage-vitals/vitals-chart.scss +70 -0
- package/src/past-vitals/utils/index.ts +5 -0
- package/src/past-vitals/vitals-and-biometrics-header/vitals-header-item.component.tsx +61 -0
- package/src/past-vitals/vitals-and-biometrics-header/vitals-header-item.scss +171 -0
- package/src/past-vitals/vitals-and-biometrics-header/vitals-header-item.test.tsx +31 -0
- package/src/past-vitals/vitals-and-biometrics-header/vitals-header.extension.tsx +225 -0
- package/src/past-vitals/vitals-and-biometrics-header/vitals-header.scss +233 -0
- package/src/queue-room-table/queue-room-table.component.tsx +380 -0
- package/src/queue-room-table/queue-room-table.resource.ts +73 -0
- package/src/queue-room-table/queue-room-table.scss +193 -0
- package/src/queue-room-table/type/index.ts +30 -0
- package/src/queue-room-table/useQueueActions.ts +24 -0
- package/src/record-vitals-workspace/common/index.ts +19 -0
- package/src/record-vitals-workspace/component/vitals-biometrics-input.component.tsx +433 -0
- package/src/record-vitals-workspace/component/vitals-biometrics-input.scss +358 -0
- package/src/record-vitals-workspace/helper/index.ts +200 -0
- package/src/record-vitals-workspace/record-vitals.resource.ts +570 -0
- package/src/record-vitals-workspace/record-vitals.scss +140 -0
- package/src/record-vitals-workspace/record-vitals.workspace.tsx +612 -0
- package/src/record-vitals-workspace/schema/index.ts +28 -0
- package/src/record-vitals-workspace/type/index.ts +170 -0
- package/src/record-vitals-workspace/utils/index.ts +100 -0
- package/src/routes.json +70 -0
- package/src/setup-tests.ts +1 -0
- package/src/triage-queue-dashboard/triage-dashboard.resource.ts +75 -0
- package/src/triage-queue-dashboard/triage-queue-dashboard.component.tsx +261 -0
- package/src/triage-queue-dashboard/triage-queue-dashboard.scss +166 -0
- package/src/triage-queue-dashboard/type/index.ts +31 -0
- package/translations/am.json +92 -0
- package/translations/ar.json +92 -0
- package/translations/en.json +92 -0
- package/translations/es.json +92 -0
- package/translations/fr.json +92 -0
- package/translations/he.json +92 -0
- package/translations/km.json +92 -0
- package/translations/pt.json +92 -0
- package/translations/sw.json +92 -0
- package/translations/vi.json +92 -0
- package/translations/zh.json +92 -0
- 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
|
+
}
|
package/src/routes.json
ADDED
|
@@ -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
|
+
};
|