@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,61 @@
|
|
|
1
|
+
import React, { useId } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
|
|
5
|
+
import styles from './vitals-header-item.scss';
|
|
6
|
+
import { ObservationInterpretation } from '../../record-vitals-workspace/type';
|
|
7
|
+
|
|
8
|
+
interface VitalsHeaderItemProps {
|
|
9
|
+
interpretation?: ObservationInterpretation;
|
|
10
|
+
unitName: string;
|
|
11
|
+
unitSymbol: React.ReactNode;
|
|
12
|
+
value: string | number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const VitalsHeaderItem: React.FC<VitalsHeaderItemProps> = ({ interpretation, value, unitName, unitSymbol }) => {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const flaggedCritical = interpretation && interpretation.includes('critically');
|
|
18
|
+
const flaggedAbnormal = interpretation && interpretation !== 'normal';
|
|
19
|
+
|
|
20
|
+
const generatedId = useId();
|
|
21
|
+
|
|
22
|
+
const trimmedUnitName = unitName.trim().toLowerCase();
|
|
23
|
+
const unitNameWithoutSpaces = trimmedUnitName.replace(/\s+/g, '-');
|
|
24
|
+
|
|
25
|
+
const labelId = `omrs-patient-chart-label-${unitNameWithoutSpaces}-${generatedId}`;
|
|
26
|
+
const valueId = `omrs-patient-chart-value-${unitNameWithoutSpaces}-${generatedId}`;
|
|
27
|
+
const unitId = `omrs-patient-chart-unit-${unitNameWithoutSpaces}-${generatedId}`;
|
|
28
|
+
|
|
29
|
+
const displayValue = Boolean(value) ? value : t('notAvailable', 'Not available');
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<section className={styles.container}>
|
|
33
|
+
<div
|
|
34
|
+
className={classNames({
|
|
35
|
+
[styles['critical-value']]: flaggedCritical,
|
|
36
|
+
[styles['abnormal-value']]: flaggedAbnormal,
|
|
37
|
+
})}>
|
|
38
|
+
<div className={styles['label-container']}>
|
|
39
|
+
<span id={labelId} className={styles.label}>
|
|
40
|
+
{unitName}
|
|
41
|
+
</span>
|
|
42
|
+
{flaggedAbnormal ? (
|
|
43
|
+
<span className={styles[interpretation.replace('_', '-')]} title={t('abnormalValue', 'Abnormal value')} />
|
|
44
|
+
) : null}
|
|
45
|
+
</div>
|
|
46
|
+
<div className={styles['value-container']}>
|
|
47
|
+
<div className={styles['pad-right']}>
|
|
48
|
+
<span id={valueId} aria-labelledby={`${labelId} ${unitId}`} className={styles.value}>
|
|
49
|
+
{displayValue}
|
|
50
|
+
</span>{' '}
|
|
51
|
+
<span id={unitId} className={styles.units}>
|
|
52
|
+
{value ? ` ${''} ${unitSymbol}` : ''}
|
|
53
|
+
</span>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default VitalsHeaderItem;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// vitals-header-item.scss
|
|
2
|
+
@use '@carbon/colors';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
@use '@carbon/layout';
|
|
5
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
6
|
+
|
|
7
|
+
.container {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
width: 100%;
|
|
11
|
+
min-width: 0;
|
|
12
|
+
height: 100%;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.abnormal-value {
|
|
16
|
+
border: solid 1px colors.$orange-20-hover;
|
|
17
|
+
background-color: colors.$orange-10;
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
width: 100%;
|
|
21
|
+
gap: layout.$spacing-01;
|
|
22
|
+
padding: layout.$spacing-02 layout.$spacing-03;
|
|
23
|
+
border-radius: 2px;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
height: 100%;
|
|
26
|
+
|
|
27
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
28
|
+
padding: layout.$spacing-02;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.critical-value {
|
|
33
|
+
@extend .abnormal-value;
|
|
34
|
+
border: 1px solid colors.$red-60;
|
|
35
|
+
background-color: colors.$red-20;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.label-container {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: row;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: layout.$spacing-02;
|
|
43
|
+
min-width: 0;
|
|
44
|
+
flex-wrap: wrap;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.value-container {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: row;
|
|
50
|
+
align-items: baseline;
|
|
51
|
+
gap: layout.$spacing-02; // Add gap between value and units
|
|
52
|
+
min-width: 0;
|
|
53
|
+
margin-top: layout.$spacing-01;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.label {
|
|
57
|
+
@include type.type-style('label-01');
|
|
58
|
+
color: $text-02;
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
text-overflow: ellipsis;
|
|
62
|
+
line-height: 1.2;
|
|
63
|
+
|
|
64
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
65
|
+
font-size: 0.75rem;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.danger-icon {
|
|
70
|
+
color: $danger;
|
|
71
|
+
height: layout.$spacing-05;
|
|
72
|
+
width: layout.$spacing-05;
|
|
73
|
+
flex-shrink: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.value {
|
|
77
|
+
@include type.type-style('body-compact-02');
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
word-break: break-word;
|
|
80
|
+
line-height: 1.3;
|
|
81
|
+
|
|
82
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
83
|
+
font-size: 0.875rem;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.units {
|
|
88
|
+
@include type.type-style('body-compact-01');
|
|
89
|
+
color: $text-02;
|
|
90
|
+
white-space: nowrap;
|
|
91
|
+
flex-shrink: 0;
|
|
92
|
+
margin-left: layout.$spacing-01;
|
|
93
|
+
|
|
94
|
+
:global(.omrs-breakpoint-lt-tablet) & {
|
|
95
|
+
font-size: 0.75rem;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.pad-right {
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: baseline;
|
|
102
|
+
min-width: 0;
|
|
103
|
+
flex-wrap: nowrap;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.critically-low,
|
|
107
|
+
.critically-high,
|
|
108
|
+
.low,
|
|
109
|
+
.high {
|
|
110
|
+
position: relative;
|
|
111
|
+
display: inline-flex;
|
|
112
|
+
align-items: baseline;
|
|
113
|
+
flex-shrink: 0;
|
|
114
|
+
|
|
115
|
+
&::after {
|
|
116
|
+
@include type.type-style('heading-compact-01');
|
|
117
|
+
color: $text-02;
|
|
118
|
+
margin-left: 2px;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.arrow {
|
|
123
|
+
color: colors.$gray-100;
|
|
124
|
+
font-weight: 500;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.low::after {
|
|
128
|
+
@extend .arrow;
|
|
129
|
+
content: '↓';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.critically-low::after {
|
|
133
|
+
@extend .arrow;
|
|
134
|
+
content: '↓↓';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.high::after {
|
|
138
|
+
@extend .arrow;
|
|
139
|
+
content: '↑';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.critically-high::after {
|
|
143
|
+
@extend .arrow;
|
|
144
|
+
content: '↑↑';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Ensure arrows are visible on all screen sizes
|
|
148
|
+
:global(.omrs-breakpoint-lt-tablet) {
|
|
149
|
+
.critically-low::after,
|
|
150
|
+
.critically-high::after,
|
|
151
|
+
.low::after,
|
|
152
|
+
.high::after {
|
|
153
|
+
font-size: 0.875rem;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// RTL support
|
|
158
|
+
html[dir='rtl'] {
|
|
159
|
+
.units {
|
|
160
|
+
margin-left: 0;
|
|
161
|
+
margin-right: layout.$spacing-01;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.critically-low::after,
|
|
165
|
+
.critically-high::after,
|
|
166
|
+
.low::after,
|
|
167
|
+
.high::after {
|
|
168
|
+
margin-left: 0;
|
|
169
|
+
margin-right: 2px;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import VitalsHeaderItem from './vitals-header-item.component';
|
|
4
|
+
|
|
5
|
+
const testProps = { unitName: 'Temp', value: '36.5', unitSymbol: '°C' };
|
|
6
|
+
|
|
7
|
+
describe('VitalsHeaderItem', () => {
|
|
8
|
+
it('renders a vital sign in the vitals header', () => {
|
|
9
|
+
render(<VitalsHeaderItem {...testProps} />);
|
|
10
|
+
|
|
11
|
+
expect(screen.getByText('Temp')).toBeInTheDocument();
|
|
12
|
+
expect(screen.getByText('°C')).toBeInTheDocument();
|
|
13
|
+
expect(screen.getByText('36.5')).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('handles empty unit symbol gracefully', () => {
|
|
17
|
+
const propsWithEmptyUnit = { ...testProps, unitSymbol: '' };
|
|
18
|
+
render(<VitalsHeaderItem {...propsWithEmptyUnit} />);
|
|
19
|
+
|
|
20
|
+
expect(screen.getByText('Temp')).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByText('36.5')).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('handles undefined unit symbol gracefully', () => {
|
|
25
|
+
const propsWithUndefinedUnit = { ...testProps, unitSymbol: undefined };
|
|
26
|
+
render(<VitalsHeaderItem {...propsWithUndefinedUnit} />);
|
|
27
|
+
|
|
28
|
+
expect(screen.getByText('Temp')).toBeInTheDocument();
|
|
29
|
+
expect(screen.getByText('36.5')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
import duration from 'dayjs/plugin/duration';
|
|
5
|
+
import isToday from 'dayjs/plugin/isToday';
|
|
6
|
+
dayjs.extend(isToday);
|
|
7
|
+
dayjs.extend(duration);
|
|
8
|
+
import { Trans, useTranslation } from 'react-i18next';
|
|
9
|
+
import { Button, InlineLoading, Tag } from '@carbon/react';
|
|
10
|
+
import { ArrowRight } from '@carbon/react/icons';
|
|
11
|
+
import { ConfigurableLink, formatDate, parseDate, useConfig, useVisit, useWorkspaces } from '@openmrs/esm-framework';
|
|
12
|
+
|
|
13
|
+
import VitalsHeaderItem from './vitals-header-item.component';
|
|
14
|
+
import styles from './vitals-header.scss';
|
|
15
|
+
import { ConfigObject } from '../../config-schema';
|
|
16
|
+
import {
|
|
17
|
+
assessValue,
|
|
18
|
+
getReferenceRangesForConcept,
|
|
19
|
+
interpretBloodPressure,
|
|
20
|
+
useConceptUnits,
|
|
21
|
+
useVitalsAndBiometrics,
|
|
22
|
+
useVitalsConceptMetadata,
|
|
23
|
+
} from '../../record-vitals-workspace/common';
|
|
24
|
+
|
|
25
|
+
interface VitalsHeaderProps {
|
|
26
|
+
patientUuid: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* This is useful for extensions slots using the Vitals Header
|
|
30
|
+
*/
|
|
31
|
+
hideLinks?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const VitalsHeader: React.FC<VitalsHeaderProps> = ({ patientUuid, hideLinks = false }) => {
|
|
35
|
+
const { t } = useTranslation();
|
|
36
|
+
const config = useConfig<ConfigObject>();
|
|
37
|
+
const { conceptUnits } = useConceptUnits();
|
|
38
|
+
const { data: vitals, isLoading, isValidating } = useVitalsAndBiometrics(patientUuid, 'both');
|
|
39
|
+
const { conceptRanges } = useVitalsConceptMetadata(patientUuid);
|
|
40
|
+
const latestVitals = vitals?.[0];
|
|
41
|
+
const [showDetailsPanel, setShowDetailsPanel] = useState(false);
|
|
42
|
+
const toggleDetailsPanel = () => setShowDetailsPanel(!showDetailsPanel);
|
|
43
|
+
const { activeVisit } = useVisit(patientUuid);
|
|
44
|
+
const { workspaces } = useWorkspaces();
|
|
45
|
+
|
|
46
|
+
const isWorkspaceOpen = useCallback(() => Boolean(workspaces?.length), [workspaces]);
|
|
47
|
+
|
|
48
|
+
if (isLoading) {
|
|
49
|
+
return (
|
|
50
|
+
<InlineLoading role="progressbar" className={styles.loading} description={`${t('loading', 'Loading')} ...`} />
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (latestVitals && Object.keys(latestVitals)?.length && conceptRanges?.length) {
|
|
55
|
+
const hasActiveVisit = Boolean(activeVisit?.uuid);
|
|
56
|
+
const now = dayjs();
|
|
57
|
+
const vitalsTakenTimeAgo = dayjs.duration(now.diff(latestVitals?.date));
|
|
58
|
+
const vitalsOverdueThresholdHours = config.vitals.vitalsOverdueThresholdHours;
|
|
59
|
+
const areVitalsOverdue = hasActiveVisit && vitalsTakenTimeAgo.asHours() >= vitalsOverdueThresholdHours;
|
|
60
|
+
const vitalsOverdueDayCount = Math.round(vitalsTakenTimeAgo.asDays());
|
|
61
|
+
const hoursSinceVitalsTaken = Math.round(vitalsTakenTimeAgo.asHours());
|
|
62
|
+
let overdueVitalsTagContent: React.ReactNode = null;
|
|
63
|
+
|
|
64
|
+
if (vitalsOverdueDayCount < 1) {
|
|
65
|
+
overdueVitalsTagContent = (
|
|
66
|
+
<Trans i18nKey="hoursOldVitals" count={hoursSinceVitalsTaken}>
|
|
67
|
+
<span>
|
|
68
|
+
{/* @ts-ignore: See comment below */}
|
|
69
|
+
These vitals are <strong>{{ count: hoursSinceVitalsTaken }} hour old</strong>
|
|
70
|
+
</span>
|
|
71
|
+
</Trans>
|
|
72
|
+
);
|
|
73
|
+
} else if (vitalsOverdueDayCount >= 1 && vitalsOverdueDayCount < 7) {
|
|
74
|
+
overdueVitalsTagContent = (
|
|
75
|
+
<Trans i18nKey="daysOldVitals" count={vitalsOverdueDayCount}>
|
|
76
|
+
<span>
|
|
77
|
+
{/* @ts-ignore Workaround for i18next types issue (see https://github.com/i18next/react-i18next/issues/1543 and https://github.com/i18next/react-i18next/issues/465). Additionally, I can't find a way to get the proper plural suffix to be used in the translation file without amending the translation file by hand. */}
|
|
78
|
+
These vitals are <strong>{{ count: vitalsOverdueDayCount }} day old</strong>
|
|
79
|
+
</span>
|
|
80
|
+
</Trans>
|
|
81
|
+
);
|
|
82
|
+
} else if (vitalsOverdueDayCount >= 8 && vitalsOverdueDayCount <= 14) {
|
|
83
|
+
overdueVitalsTagContent = (
|
|
84
|
+
<Trans i18nKey="overOneWeekOldVitals">
|
|
85
|
+
<span>
|
|
86
|
+
These vitals are <strong>over one week old</strong>
|
|
87
|
+
</span>
|
|
88
|
+
</Trans>
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
overdueVitalsTagContent = (
|
|
92
|
+
<Trans i18nKey="outOfDateVitals">
|
|
93
|
+
<span>
|
|
94
|
+
These vitals are <strong>out of date</strong>
|
|
95
|
+
</span>
|
|
96
|
+
</Trans>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className={styles.container}>
|
|
102
|
+
<div className={styles.vitalsHeader} role="button" tabIndex={0} onClick={toggleDetailsPanel}>
|
|
103
|
+
<div className={styles.headerItems}>
|
|
104
|
+
<span className={styles.heading}>{t('vitalsAndAnthropometrics', 'Vitals and anthropometrics')}</span>
|
|
105
|
+
<span className={styles.bodyText}>
|
|
106
|
+
{formatDate(parseDate(latestVitals?.date), { day: true, time: true })}
|
|
107
|
+
</span>
|
|
108
|
+
{areVitalsOverdue ? (
|
|
109
|
+
<Tag className={styles.tag} type="red">
|
|
110
|
+
<span className={styles.overdueIndicator}>{overdueVitalsTagContent}</span>
|
|
111
|
+
</Tag>
|
|
112
|
+
) : null}
|
|
113
|
+
</div>
|
|
114
|
+
{isValidating ? (
|
|
115
|
+
<div className={styles.backgroundDataFetchingIndicator}>
|
|
116
|
+
<span>{isValidating ? <InlineLoading /> : null}</span>
|
|
117
|
+
</div>
|
|
118
|
+
) : null}
|
|
119
|
+
</div>
|
|
120
|
+
<div
|
|
121
|
+
className={classNames(styles.rowContainer, {
|
|
122
|
+
[styles.workspaceOpen]: isWorkspaceOpen(),
|
|
123
|
+
})}>
|
|
124
|
+
<div className={styles.row}>
|
|
125
|
+
<VitalsHeaderItem
|
|
126
|
+
interpretation={interpretBloodPressure(
|
|
127
|
+
latestVitals?.systolic,
|
|
128
|
+
latestVitals?.diastolic,
|
|
129
|
+
config?.concepts,
|
|
130
|
+
conceptRanges,
|
|
131
|
+
latestVitals?.systolicRenderInterpretation,
|
|
132
|
+
latestVitals?.diastolicRenderInterpretation,
|
|
133
|
+
)}
|
|
134
|
+
unitName={t('bp', 'BP')}
|
|
135
|
+
unitSymbol={(latestVitals?.systolic && conceptUnits.get(config.concepts.systolicBloodPressureUuid)) ?? ''}
|
|
136
|
+
value={`${latestVitals?.systolic ?? '--'} / ${latestVitals?.diastolic ?? '--'}`}
|
|
137
|
+
/>
|
|
138
|
+
<VitalsHeaderItem
|
|
139
|
+
interpretation={
|
|
140
|
+
latestVitals?.pulseRenderInterpretation ??
|
|
141
|
+
assessValue(latestVitals?.pulse, getReferenceRangesForConcept(config.concepts.pulseUuid, conceptRanges))
|
|
142
|
+
}
|
|
143
|
+
unitName={t('heartRate', 'Heart rate')}
|
|
144
|
+
unitSymbol={(latestVitals?.pulse && conceptUnits.get(config.concepts.pulseUuid)) ?? ''}
|
|
145
|
+
value={latestVitals?.pulse ?? '--'}
|
|
146
|
+
/>
|
|
147
|
+
<VitalsHeaderItem
|
|
148
|
+
interpretation={
|
|
149
|
+
latestVitals?.respiratoryRateRenderInterpretation ??
|
|
150
|
+
assessValue(
|
|
151
|
+
latestVitals?.respiratoryRate,
|
|
152
|
+
getReferenceRangesForConcept(config.concepts.respiratoryRateUuid, conceptRanges),
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
unitName={t('respiratoryRate', 'R. rate')}
|
|
156
|
+
unitSymbol={
|
|
157
|
+
(latestVitals?.respiratoryRate && conceptUnits.get(config.concepts.respiratoryRateUuid)) ?? ''
|
|
158
|
+
}
|
|
159
|
+
value={latestVitals?.respiratoryRate ?? '--'}
|
|
160
|
+
/>
|
|
161
|
+
<VitalsHeaderItem
|
|
162
|
+
interpretation={
|
|
163
|
+
latestVitals?.spo2RenderInterpretation ??
|
|
164
|
+
assessValue(
|
|
165
|
+
latestVitals?.spo2,
|
|
166
|
+
getReferenceRangesForConcept(config.concepts.oxygenSaturationUuid, conceptRanges),
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
unitName={t('spo2', 'SpO2')}
|
|
170
|
+
unitSymbol={(latestVitals?.spo2 && conceptUnits.get(config.concepts.oxygenSaturationUuid)) ?? ''}
|
|
171
|
+
value={latestVitals?.spo2 ?? '--'}
|
|
172
|
+
/>
|
|
173
|
+
<VitalsHeaderItem
|
|
174
|
+
interpretation={
|
|
175
|
+
latestVitals?.temperatureRenderInterpretation ??
|
|
176
|
+
assessValue(
|
|
177
|
+
latestVitals?.temperature,
|
|
178
|
+
getReferenceRangesForConcept(config.concepts.temperatureUuid, conceptRanges),
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
unitName={t('temperatureAbbreviated', 'Temp')}
|
|
182
|
+
unitSymbol={(latestVitals?.temperature && conceptUnits.get(config.concepts.temperatureUuid)) ?? ''}
|
|
183
|
+
value={latestVitals?.temperature ?? '--'}
|
|
184
|
+
/>
|
|
185
|
+
<VitalsHeaderItem
|
|
186
|
+
unitName={t('weight', 'Weight')}
|
|
187
|
+
unitSymbol={(latestVitals?.weight && conceptUnits.get(config.concepts.weightUuid)) ?? ''}
|
|
188
|
+
value={latestVitals?.weight ?? '--'}
|
|
189
|
+
/>
|
|
190
|
+
<VitalsHeaderItem
|
|
191
|
+
unitName={t('height', 'Height')}
|
|
192
|
+
unitSymbol={(latestVitals?.height && conceptUnits.get(config.concepts.heightUuid)) ?? ''}
|
|
193
|
+
value={latestVitals?.height ?? '--'}
|
|
194
|
+
/>
|
|
195
|
+
<VitalsHeaderItem
|
|
196
|
+
unitName={t('bmi', 'BMI')}
|
|
197
|
+
unitSymbol={(latestVitals?.bmi && config.biometrics['bmiUnit']) ?? ''}
|
|
198
|
+
value={latestVitals?.bmi ?? '--'}
|
|
199
|
+
/>
|
|
200
|
+
{latestVitals?.muac && (
|
|
201
|
+
<VitalsHeaderItem
|
|
202
|
+
unitName={t('muac', 'MUAC')}
|
|
203
|
+
unitSymbol={
|
|
204
|
+
(latestVitals?.muac && conceptUnits.get(config.concepts.midUpperArmCircumferenceUuid)) ?? ''
|
|
205
|
+
}
|
|
206
|
+
value={latestVitals?.muac ?? '--'}
|
|
207
|
+
/>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div className={styles.emptyStateVitalsHeader}>
|
|
217
|
+
<div className={styles.container}>
|
|
218
|
+
<span className={styles.heading}>{t('vitalsAndAnthropometrics', 'Vitals and anthropometrics')}</span>
|
|
219
|
+
<span className={styles.bodyText}>{t('noDataRecorded', 'No data has been recorded for this patient')}</span>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export default VitalsHeader;
|