@openmrs/esm-patient-vitals-app 9.2.3-pre.7186 → 9.2.3-pre.7191
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/.turbo/turbo-build.log +19 -19
- package/dist/101.js +2 -0
- package/dist/101.js.map +1 -0
- package/dist/{9057.js → 1423.js} +1 -1
- package/dist/1423.js.map +1 -0
- package/dist/4300.js +1 -1
- package/dist/5207.js +1 -0
- package/dist/5207.js.map +1 -0
- package/dist/5387.js +1 -0
- package/dist/5387.js.map +1 -0
- package/dist/5395.js +2 -0
- package/dist/{3368.js.LICENSE.txt → 5395.js.LICENSE.txt} +19 -1
- package/dist/5395.js.map +1 -0
- package/dist/8295.js +2 -0
- package/dist/8295.js.LICENSE.txt +7 -0
- package/dist/8295.js.map +1 -0
- package/dist/8953.js +1 -0
- package/dist/8953.js.map +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-patient-vitals-app.js +1 -1
- package/dist/openmrs-esm-patient-vitals-app.js.buildmanifest.json +152 -128
- package/dist/openmrs-esm-patient-vitals-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/biometrics/biometrics-base.component.tsx +0 -1
- package/src/biometrics/biometrics-overview.test.tsx +26 -5
- package/src/biometrics/paginated-biometrics.component.tsx +5 -0
- package/src/common/data.resource.ts +128 -75
- package/src/common/helpers.ts +50 -0
- package/src/common/index.ts +3 -2
- package/src/common/types.ts +2 -1
- package/src/components/action-menu/vitals-biometrics-action-menu.component.tsx +60 -0
- package/src/components/action-menu/vitals-biometrics-action-menu.scss +11 -0
- package/src/components/delete-vitals-biometrics-modal/delete-vitals-biometrics.modal.tsx +84 -0
- package/src/{weight-tile → components/weight-tile}/weight-tile.component.tsx +2 -2
- package/src/{weight-tile → components/weight-tile}/weight-tile.test.tsx +4 -4
- package/src/index.ts +6 -1
- package/src/routes.json +6 -0
- package/src/vitals/paginated-vitals.component.tsx +5 -0
- package/src/vitals/vitals-overview.component.tsx +3 -2
- package/src/vitals-biometrics-form/schema.ts +28 -0
- package/src/vitals-biometrics-form/vitals-biometrics-form.test.tsx +203 -21
- package/src/vitals-biometrics-form/vitals-biometrics-form.workspace.tsx +78 -60
- package/src/vitals-biometrics-form/vitals-biometrics-input.component.tsx +1 -1
- package/translations/en.json +14 -1
- package/dist/3368.js +0 -2
- package/dist/3368.js.map +0 -1
- package/dist/4716.js +0 -1
- package/dist/4716.js.map +0 -1
- package/dist/7738.js +0 -2
- package/dist/7738.js.LICENSE.txt +0 -25
- package/dist/7738.js.map +0 -1
- package/dist/8895.js +0 -2
- package/dist/8895.js.map +0 -1
- package/dist/8957.js +0 -1
- package/dist/8957.js.map +0 -1
- package/dist/9057.js.map +0 -1
- /package/dist/{8895.js.LICENSE.txt → 101.js.LICENSE.txt} +0 -0
- /package/src/{weight-tile → components/weight-tile}/weight-tile.scss +0 -0
package/src/routes.json
CHANGED
|
@@ -13,6 +13,7 @@ import { useLayoutType, usePagination } from '@openmrs/esm-framework';
|
|
|
13
13
|
import { PatientChartPagination } from '@openmrs/esm-patient-common-lib';
|
|
14
14
|
import type { VitalsTableHeader, VitalsTableRow } from './types';
|
|
15
15
|
import styles from './paginated-vitals.scss';
|
|
16
|
+
import { VitalsAndBiometricsActionMenu } from '../components/action-menu/vitals-biometrics-action-menu.component';
|
|
16
17
|
|
|
17
18
|
interface PaginatedVitalsProps {
|
|
18
19
|
isPrinting?: boolean;
|
|
@@ -108,6 +109,7 @@ const PaginatedVitals: React.FC<PaginatedVitalsProps> = ({
|
|
|
108
109
|
{header.header?.content ?? header.header}
|
|
109
110
|
</TableHeader>
|
|
110
111
|
))}
|
|
112
|
+
<TableHeader />
|
|
111
113
|
</TableRow>
|
|
112
114
|
</TableHead>
|
|
113
115
|
<TableBody>
|
|
@@ -123,6 +125,9 @@ const PaginatedVitals: React.FC<PaginatedVitalsProps> = ({
|
|
|
123
125
|
</StyledTableCell>
|
|
124
126
|
);
|
|
125
127
|
})}
|
|
128
|
+
<TableCell className="cds--table-column-menu" id="actions">
|
|
129
|
+
<VitalsAndBiometricsActionMenu encounterUuid={row.id} />
|
|
130
|
+
</TableCell>
|
|
126
131
|
</TableRow>
|
|
127
132
|
))}
|
|
128
133
|
</TableBody>
|
|
@@ -22,6 +22,7 @@ import PaginatedVitals from './paginated-vitals.component';
|
|
|
22
22
|
import PrintComponent from './print/print.component';
|
|
23
23
|
import VitalsChart from './vitals-chart.component';
|
|
24
24
|
import styles from './vitals-overview.scss';
|
|
25
|
+
import { useEncounterVitalsAndBiometrics } from '../common/data.resource';
|
|
25
26
|
|
|
26
27
|
interface VitalsOverviewProps {
|
|
27
28
|
patientUuid: string;
|
|
@@ -50,6 +51,7 @@ const VitalsOverview: React.FC<VitalsOverviewProps> = ({ patientUuid, patient, p
|
|
|
50
51
|
launchVitalsAndBiometricsForm(currentVisit, config);
|
|
51
52
|
}, [config, currentVisit]);
|
|
52
53
|
|
|
54
|
+
useEncounterVitalsAndBiometrics('771bbc44-8d45-4ac3-af6e-059814dd7cde');
|
|
53
55
|
const patientDetails = useMemo(() => {
|
|
54
56
|
const getGender = (gender: string): string => {
|
|
55
57
|
switch (gender) {
|
|
@@ -139,10 +141,9 @@ const VitalsOverview: React.FC<VitalsOverviewProps> = ({ patientUuid, patient, p
|
|
|
139
141
|
|
|
140
142
|
const tableRows: Array<VitalsTableRow> = useMemo(
|
|
141
143
|
() =>
|
|
142
|
-
vitals?.map((vitalSigns
|
|
144
|
+
vitals?.map((vitalSigns) => {
|
|
143
145
|
return {
|
|
144
146
|
...vitalSigns,
|
|
145
|
-
id: `${index}`,
|
|
146
147
|
dateRender: formatDate(parseDate(vitalSigns.date.toString()), { mode: 'wide', time: true }),
|
|
147
148
|
bloodPressureRender: `${vitalSigns.systolic ?? '--'} / ${vitalSigns.diastolic ?? '--'}`,
|
|
148
149
|
pulseRender: vitalSigns.pulse ?? '--',
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const VitalsAndBiometricsFormSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
systolicBloodPressure: z.number(),
|
|
6
|
+
diastolicBloodPressure: z.number(),
|
|
7
|
+
respiratoryRate: z.number(),
|
|
8
|
+
oxygenSaturation: z.number(),
|
|
9
|
+
pulse: z.number(),
|
|
10
|
+
temperature: z.number(),
|
|
11
|
+
generalPatientNote: z.string(),
|
|
12
|
+
weight: z.number(),
|
|
13
|
+
height: z.number(),
|
|
14
|
+
midUpperArmCircumference: z.number(),
|
|
15
|
+
computedBodyMassIndex: z.number(),
|
|
16
|
+
})
|
|
17
|
+
.partial()
|
|
18
|
+
.refine(
|
|
19
|
+
(fields) => {
|
|
20
|
+
return Object.values(fields).some((value) => Boolean(value));
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
message: 'Please fill at least one field',
|
|
24
|
+
path: ['oneFieldRequired'],
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export type VitalsBiometricsFormData = z.infer<typeof VitalsAndBiometricsFormSchema>;
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { screen, render } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
import { type FetchResponse, showSnackbar, useConfig, getDefaultsFromConfigSchema } from '@openmrs/esm-framework';
|
|
5
|
-
import {
|
|
5
|
+
import { createOrUpdateVitalsAndBiometrics, useEncounterVitalsAndBiometrics } from '../common';
|
|
6
6
|
import { type ConfigObject, configSchema } from '../config-schema';
|
|
7
7
|
import { mockConceptMetadata, mockConceptRanges, mockConceptUnits, mockVitalsConfig } from '__mocks__';
|
|
8
8
|
import { mockPatient } from 'tools';
|
|
@@ -28,8 +28,9 @@ const testProps = {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
const mockShowSnackbar = jest.mocked(showSnackbar);
|
|
31
|
-
const
|
|
31
|
+
const mockCreateOrUpdateVitalsAndBiometrics = jest.mocked(createOrUpdateVitalsAndBiometrics);
|
|
32
32
|
const mockUseConfig = jest.mocked(useConfig<ConfigObject>);
|
|
33
|
+
const mockUseEncounterVitalsAndBiometrics = jest.mocked(useEncounterVitalsAndBiometrics);
|
|
33
34
|
|
|
34
35
|
jest.mock('../common', () => ({
|
|
35
36
|
assessValue: jest.fn(),
|
|
@@ -37,13 +38,18 @@ jest.mock('../common', () => ({
|
|
|
37
38
|
generatePlaceholder: jest.fn(),
|
|
38
39
|
interpretBloodPressure: jest.fn(),
|
|
39
40
|
invalidateCachedVitalsAndBiometrics: jest.fn(),
|
|
40
|
-
|
|
41
|
+
createOrUpdateVitalsAndBiometrics: jest.fn(),
|
|
41
42
|
useVitalsAndBiometrics: jest.fn(),
|
|
42
43
|
useVitalsConceptMetadata: jest.fn().mockImplementation(() => ({
|
|
43
44
|
data: mockConceptUnits,
|
|
44
45
|
conceptMetadata: mockConceptMetadata,
|
|
45
46
|
conceptRanges: mockConceptRanges,
|
|
46
47
|
})),
|
|
48
|
+
useEncounterVitalsAndBiometrics: jest.fn().mockImplementation(() => ({
|
|
49
|
+
isLoading: false,
|
|
50
|
+
vitalsAndBiometrics: null,
|
|
51
|
+
mutate: jest.fn(),
|
|
52
|
+
})),
|
|
47
53
|
}));
|
|
48
54
|
|
|
49
55
|
mockUseConfig.mockReturnValue({
|
|
@@ -51,6 +57,91 @@ mockUseConfig.mockReturnValue({
|
|
|
51
57
|
...mockVitalsConfig,
|
|
52
58
|
});
|
|
53
59
|
|
|
60
|
+
function setupMockUseEncounterVitalsAndBiometrics() {
|
|
61
|
+
mockUseEncounterVitalsAndBiometrics.mockReturnValue({
|
|
62
|
+
isLoading: false,
|
|
63
|
+
vitalsAndBiometrics: new Map([
|
|
64
|
+
[
|
|
65
|
+
'systolicBloodPressure',
|
|
66
|
+
{
|
|
67
|
+
value: 120,
|
|
68
|
+
obs: { uuid: '123e4567-e89b-12d3-a456-426614174001', display: 'Systolic Blood Pressure: 120' },
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
'diastolicBloodPressure',
|
|
73
|
+
{
|
|
74
|
+
value: 80,
|
|
75
|
+
obs: { uuid: '123e4567-e89b-12d3-a456-426614174002', display: 'Diastolic Blood Pressure: 80' },
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
'pulse',
|
|
80
|
+
{
|
|
81
|
+
value: 75,
|
|
82
|
+
obs: { uuid: '123e4567-e89b-12d3-a456-426614174003', display: 'Pulse Rate: 75' },
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
[
|
|
86
|
+
'temperature',
|
|
87
|
+
{
|
|
88
|
+
value: 36.5,
|
|
89
|
+
obs: { uuid: '123e4567-e89b-12d3-a456-426614174004', display: 'Body Temperature: 36.5°C' },
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
'oxygenSaturation',
|
|
94
|
+
{
|
|
95
|
+
value: 98,
|
|
96
|
+
obs: { uuid: '123e4567-e89b-12d3-a456-426614174005', display: 'Oxygen Saturation: 98%' },
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
'height',
|
|
101
|
+
{
|
|
102
|
+
value: 170,
|
|
103
|
+
obs: { uuid: '123e4567-e89b-12d3-a456-426614174006', display: 'Height: 170 cm' },
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
[
|
|
107
|
+
'weight',
|
|
108
|
+
{
|
|
109
|
+
value: 65,
|
|
110
|
+
obs: { uuid: '123e4567-e89b-12d3-a456-426614174007', display: 'Weight: 65 kg' },
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
[
|
|
114
|
+
'respiratoryRate',
|
|
115
|
+
{
|
|
116
|
+
value: 16,
|
|
117
|
+
obs: { uuid: '123e4567-e89b-12d3-a456-426614174008', display: 'Respiratory Rate: 16 breaths/min' },
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
[
|
|
121
|
+
'midUpperArmCircumference',
|
|
122
|
+
{
|
|
123
|
+
value: 25,
|
|
124
|
+
obs: { uuid: '123e4567-e89b-12d3-a456-426614174009', display: 'Mid-Upper Arm Circumference: 25 cm' },
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
]),
|
|
128
|
+
encounter: null,
|
|
129
|
+
error: null,
|
|
130
|
+
mutate: jest.fn(),
|
|
131
|
+
getRefinedInitialValues: () => ({
|
|
132
|
+
height: 170,
|
|
133
|
+
weight: 65,
|
|
134
|
+
systolicBloodPressure: 120,
|
|
135
|
+
diastolicBloodPressure: 80,
|
|
136
|
+
pulse: 75,
|
|
137
|
+
oxygenSaturation: 98,
|
|
138
|
+
respiratoryRate: 16,
|
|
139
|
+
temperature: 36.5,
|
|
140
|
+
midUpperArmCircumference: 25,
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
54
145
|
describe('VitalsBiometricsForm', () => {
|
|
55
146
|
it('renders the vitals and biometrics form', async () => {
|
|
56
147
|
render(<VitalsAndBiometricsForm {...testProps} />);
|
|
@@ -107,7 +198,9 @@ describe('VitalsBiometricsForm', () => {
|
|
|
107
198
|
data: [],
|
|
108
199
|
};
|
|
109
200
|
|
|
110
|
-
|
|
201
|
+
mockCreateOrUpdateVitalsAndBiometrics.mockResolvedValue(
|
|
202
|
+
response as ReturnType<typeof createOrUpdateVitalsAndBiometrics>,
|
|
203
|
+
);
|
|
111
204
|
|
|
112
205
|
render(<VitalsAndBiometricsForm {...testProps} />);
|
|
113
206
|
|
|
@@ -141,24 +234,28 @@ describe('VitalsBiometricsForm', () => {
|
|
|
141
234
|
|
|
142
235
|
await user.click(saveButton);
|
|
143
236
|
|
|
144
|
-
expect(
|
|
145
|
-
expect(
|
|
146
|
-
mockVitalsConfig.vitals.encounterTypeUuid,
|
|
147
|
-
mockVitalsConfig.vitals.formUuid,
|
|
148
|
-
mockVitalsConfig.concepts,
|
|
237
|
+
expect(mockCreateOrUpdateVitalsAndBiometrics).toHaveBeenCalledTimes(1);
|
|
238
|
+
expect(mockCreateOrUpdateVitalsAndBiometrics).toHaveBeenCalledWith(
|
|
149
239
|
mockPatient.id,
|
|
240
|
+
mockVitalsConfig.vitals.encounterTypeUuid,
|
|
241
|
+
undefined,
|
|
242
|
+
undefined,
|
|
243
|
+
expect.arrayContaining([
|
|
244
|
+
{ concept: '5085AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', value: 120 },
|
|
245
|
+
{ concept: '5242AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', value: 16 },
|
|
246
|
+
{ concept: '5092AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', value: 100 },
|
|
247
|
+
{ concept: '5087AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', value: 80 },
|
|
248
|
+
{ concept: '5088AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', value: 37 },
|
|
249
|
+
{ concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', value: 62 },
|
|
250
|
+
{ concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', value: 180 },
|
|
251
|
+
{ concept: '1343AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', value: 23 },
|
|
252
|
+
]),
|
|
150
253
|
expect.objectContaining({
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
respiratoryRate: respiratoryRateValue,
|
|
156
|
-
systolicBloodPressure: systolicBloodPressureValue,
|
|
157
|
-
temperature: temperatureValue,
|
|
158
|
-
weight: weightValue,
|
|
254
|
+
signal: {
|
|
255
|
+
aborted: false,
|
|
256
|
+
},
|
|
257
|
+
abort: expect.any(Function),
|
|
159
258
|
}),
|
|
160
|
-
new AbortController(),
|
|
161
|
-
undefined,
|
|
162
259
|
);
|
|
163
260
|
|
|
164
261
|
expect(mockShowSnackbar).toHaveBeenCalledTimes(1);
|
|
@@ -172,6 +269,91 @@ describe('VitalsBiometricsForm', () => {
|
|
|
172
269
|
);
|
|
173
270
|
});
|
|
174
271
|
|
|
272
|
+
it('correctly initializes the form with existing vitals and biometrics data while in edit mode', async () => {
|
|
273
|
+
setupMockUseEncounterVitalsAndBiometrics();
|
|
274
|
+
render(<VitalsAndBiometricsForm {...testProps} formContext="editing" editEncounterUuid="encounter-uuid" />);
|
|
275
|
+
|
|
276
|
+
expect(screen.getByRole('spinbutton', { name: /height/i })).toHaveValue(170);
|
|
277
|
+
expect(screen.getByRole('spinbutton', { name: /weight/i })).toHaveValue(65);
|
|
278
|
+
expect(screen.getByRole('spinbutton', { name: /systolic/i })).toHaveValue(120);
|
|
279
|
+
expect(screen.getByRole('spinbutton', { name: /diastolic/i })).toHaveValue(80);
|
|
280
|
+
expect(screen.getByRole('spinbutton', { name: /pulse/i })).toHaveValue(75);
|
|
281
|
+
expect(screen.getByRole('spinbutton', { name: /oxygen saturation/i })).toHaveValue(98);
|
|
282
|
+
expect(screen.getByRole('spinbutton', { name: /respiration rate/i })).toHaveValue(16);
|
|
283
|
+
expect(screen.getByRole('spinbutton', { name: /temperature/i })).toHaveValue(36.5);
|
|
284
|
+
expect(screen.getByRole('spinbutton', { name: /muac/i })).toHaveValue(25);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('edits patient vitals and biometrics', async () => {
|
|
288
|
+
const user = userEvent.setup();
|
|
289
|
+
setupMockUseEncounterVitalsAndBiometrics();
|
|
290
|
+
|
|
291
|
+
const response: Partial<FetchResponse> = {
|
|
292
|
+
statusText: 'created',
|
|
293
|
+
status: 201,
|
|
294
|
+
data: [],
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
mockCreateOrUpdateVitalsAndBiometrics.mockResolvedValue(
|
|
298
|
+
response as ReturnType<typeof createOrUpdateVitalsAndBiometrics>,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
render(<VitalsAndBiometricsForm {...testProps} formContext="editing" editEncounterUuid="encounter-uuid" />);
|
|
302
|
+
|
|
303
|
+
const weightInput = screen.getByRole('spinbutton', { name: /weight/i });
|
|
304
|
+
const systolicInput = screen.getByRole('spinbutton', { name: /systolic/i });
|
|
305
|
+
const pulseInput = screen.getByRole('spinbutton', { name: /pulse/i });
|
|
306
|
+
const temperatureInput = screen.getByRole('spinbutton', { name: /temperature/i });
|
|
307
|
+
const saveButton = screen.getByRole('button', { name: /Save and close/i });
|
|
308
|
+
|
|
309
|
+
// the save button should be disabled until the user makes a change
|
|
310
|
+
expect(saveButton).toBeDisabled();
|
|
311
|
+
await user.clear(weightInput);
|
|
312
|
+
await user.type(weightInput, '70');
|
|
313
|
+
await user.clear(systolicInput);
|
|
314
|
+
await user.type(systolicInput, '130');
|
|
315
|
+
await user.clear(temperatureInput);
|
|
316
|
+
await user.type(temperatureInput, '37.5');
|
|
317
|
+
// delete the pulse value
|
|
318
|
+
await user.clear(pulseInput);
|
|
319
|
+
|
|
320
|
+
expect(saveButton).toBeEnabled();
|
|
321
|
+
await user.click(saveButton);
|
|
322
|
+
|
|
323
|
+
expect(mockCreateOrUpdateVitalsAndBiometrics).toHaveBeenCalledTimes(1);
|
|
324
|
+
expect(mockCreateOrUpdateVitalsAndBiometrics).toHaveBeenCalledWith(
|
|
325
|
+
mockPatient.id,
|
|
326
|
+
mockVitalsConfig.vitals.encounterTypeUuid,
|
|
327
|
+
'encounter-uuid',
|
|
328
|
+
undefined,
|
|
329
|
+
expect.arrayContaining([
|
|
330
|
+
{ concept: '5085AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', value: 130 },
|
|
331
|
+
{ concept: '5088AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', value: 37.5 },
|
|
332
|
+
{ concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', value: 70 },
|
|
333
|
+
{ uuid: '123e4567-e89b-12d3-a456-426614174001', voided: true },
|
|
334
|
+
{ uuid: '123e4567-e89b-12d3-a456-426614174003', voided: true },
|
|
335
|
+
{ uuid: '123e4567-e89b-12d3-a456-426614174004', voided: true },
|
|
336
|
+
{ uuid: '123e4567-e89b-12d3-a456-426614174007', voided: true },
|
|
337
|
+
]),
|
|
338
|
+
expect.objectContaining({
|
|
339
|
+
signal: {
|
|
340
|
+
aborted: false,
|
|
341
|
+
},
|
|
342
|
+
abort: expect.any(Function),
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
expect(mockShowSnackbar).toHaveBeenCalledTimes(1);
|
|
347
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
348
|
+
expect.objectContaining({
|
|
349
|
+
isLowContrast: true,
|
|
350
|
+
kind: 'success',
|
|
351
|
+
subtitle: 'They are now visible on the Vitals and Biometrics page',
|
|
352
|
+
title: 'Vitals and Biometrics updated',
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
|
|
175
357
|
it('renders an error snackbar if there was a problem saving vitals and biometrics', async () => {
|
|
176
358
|
const user = userEvent.setup();
|
|
177
359
|
|
|
@@ -183,7 +365,7 @@ describe('VitalsBiometricsForm', () => {
|
|
|
183
365
|
},
|
|
184
366
|
};
|
|
185
367
|
|
|
186
|
-
|
|
368
|
+
mockCreateOrUpdateVitalsAndBiometrics.mockRejectedValueOnce(error);
|
|
187
369
|
|
|
188
370
|
render(<VitalsAndBiometricsForm {...testProps} />);
|
|
189
371
|
|
|
@@ -214,7 +396,7 @@ describe('VitalsBiometricsForm', () => {
|
|
|
214
396
|
isLowContrast: false,
|
|
215
397
|
kind: 'error',
|
|
216
398
|
subtitle: 'Some of the values entered are invalid',
|
|
217
|
-
title: 'Error saving
|
|
399
|
+
title: 'Error saving Vitals and Biometrics',
|
|
218
400
|
});
|
|
219
401
|
});
|
|
220
402
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { useForm } from 'react-hook-form';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { z } from 'zod';
|
|
5
4
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
6
5
|
import {
|
|
7
6
|
Button,
|
|
@@ -23,6 +22,7 @@ import {
|
|
|
23
22
|
useSession,
|
|
24
23
|
ExtensionSlot,
|
|
25
24
|
useVisit,
|
|
25
|
+
useAbortController,
|
|
26
26
|
} from '@openmrs/esm-framework';
|
|
27
27
|
import { type DefaultPatientWorkspaceProps } from '@openmrs/esm-patient-common-lib';
|
|
28
28
|
import type { ConfigObject } from '../config-schema';
|
|
@@ -37,42 +37,25 @@ import {
|
|
|
37
37
|
getReferenceRangesForConcept,
|
|
38
38
|
interpretBloodPressure,
|
|
39
39
|
invalidateCachedVitalsAndBiometrics,
|
|
40
|
-
saveVitalsAndBiometrics as savePatientVitals,
|
|
41
40
|
useVitalsConceptMetadata,
|
|
41
|
+
createOrUpdateVitalsAndBiometrics,
|
|
42
|
+
useEncounterVitalsAndBiometrics,
|
|
42
43
|
} from '../common';
|
|
43
44
|
import VitalsAndBiometricsInput from './vitals-biometrics-input.component';
|
|
44
45
|
import styles from './vitals-biometrics-form.scss';
|
|
46
|
+
import { VitalsAndBiometricsFormSchema, type VitalsBiometricsFormData } from './schema';
|
|
47
|
+
import { prepareObsForSubmission } from '../common/helpers';
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
respiratoryRate: z.number(),
|
|
51
|
-
oxygenSaturation: z.number(),
|
|
52
|
-
pulse: z.number(),
|
|
53
|
-
temperature: z.number(),
|
|
54
|
-
generalPatientNote: z.string(),
|
|
55
|
-
weight: z.number(),
|
|
56
|
-
height: z.number(),
|
|
57
|
-
midUpperArmCircumference: z.number(),
|
|
58
|
-
computedBodyMassIndex: z.number(),
|
|
59
|
-
})
|
|
60
|
-
.partial()
|
|
61
|
-
.refine(
|
|
62
|
-
(fields) => {
|
|
63
|
-
return Object.values(fields).some((value) => Boolean(value));
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
message: 'Please fill at least one field',
|
|
67
|
-
path: ['oneFieldRequired'],
|
|
68
|
-
},
|
|
69
|
-
);
|
|
49
|
+
interface VitalsAndBiometricsFormProps extends DefaultPatientWorkspaceProps {
|
|
50
|
+
formContext: 'creating' | 'editing';
|
|
51
|
+
editEncounterUuid?: string;
|
|
52
|
+
}
|
|
70
53
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const VitalsAndBiometricsForm: React.FC<DefaultPatientWorkspaceProps> = ({
|
|
54
|
+
const VitalsAndBiometricsForm: React.FC<VitalsAndBiometricsFormProps> = ({
|
|
74
55
|
patientUuid,
|
|
75
56
|
patient,
|
|
57
|
+
editEncounterUuid,
|
|
58
|
+
formContext = 'creating',
|
|
76
59
|
closeWorkspace,
|
|
77
60
|
closeWorkspaceWithSavedChanges,
|
|
78
61
|
promptBeforeClosing,
|
|
@@ -85,23 +68,47 @@ const VitalsAndBiometricsForm: React.FC<DefaultPatientWorkspaceProps> = ({
|
|
|
85
68
|
|
|
86
69
|
const session = useSession();
|
|
87
70
|
const { currentVisit } = useVisit(patientUuid);
|
|
88
|
-
const {
|
|
71
|
+
const {
|
|
72
|
+
data: conceptUnits,
|
|
73
|
+
conceptMetadata,
|
|
74
|
+
conceptRanges,
|
|
75
|
+
isLoading: isLoadingConceptMetadata,
|
|
76
|
+
} = useVitalsConceptMetadata();
|
|
77
|
+
const {
|
|
78
|
+
isLoading: isLoadingEncounter,
|
|
79
|
+
vitalsAndBiometrics: initialFieldValuesMap,
|
|
80
|
+
getRefinedInitialValues,
|
|
81
|
+
mutate: mutateEncounter,
|
|
82
|
+
} = useEncounterVitalsAndBiometrics(formContext === 'editing' ? editEncounterUuid : null);
|
|
89
83
|
const [hasInvalidVitals, setHasInvalidVitals] = useState(false);
|
|
90
84
|
const [muacColorCode, setMuacColorCode] = useState('');
|
|
91
85
|
const [showErrorNotification, setShowErrorNotification] = useState(false);
|
|
92
86
|
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
|
87
|
+
const abortController = useAbortController();
|
|
88
|
+
|
|
89
|
+
const isLoadingInitialValues = useMemo(
|
|
90
|
+
() => (formContext === 'creating' ? false : isLoadingEncounter),
|
|
91
|
+
[formContext, isLoadingEncounter],
|
|
92
|
+
);
|
|
93
93
|
|
|
94
94
|
const {
|
|
95
95
|
control,
|
|
96
96
|
handleSubmit,
|
|
97
97
|
watch,
|
|
98
98
|
setValue,
|
|
99
|
-
formState: { isDirty, isSubmitting },
|
|
99
|
+
formState: { isDirty, isSubmitting, dirtyFields },
|
|
100
|
+
reset,
|
|
100
101
|
} = useForm<VitalsBiometricsFormData>({
|
|
101
102
|
mode: 'all',
|
|
102
|
-
resolver: zodResolver(
|
|
103
|
+
resolver: zodResolver(VitalsAndBiometricsFormSchema),
|
|
103
104
|
});
|
|
104
105
|
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (formContext === 'editing' && !isLoadingInitialValues && initialFieldValuesMap) {
|
|
108
|
+
reset(getRefinedInitialValues());
|
|
109
|
+
}
|
|
110
|
+
}, [formContext, isLoadingInitialValues, initialFieldValuesMap, getRefinedInitialValues, reset]);
|
|
111
|
+
|
|
105
112
|
useEffect(() => {
|
|
106
113
|
promptBeforeClosing(() => isDirty);
|
|
107
114
|
}, [isDirty, promptBeforeClosing]);
|
|
@@ -181,56 +188,67 @@ const VitalsAndBiometricsForm: React.FC<DefaultPatientWorkspaceProps> = ({
|
|
|
181
188
|
|
|
182
189
|
if (allFieldsAreValid) {
|
|
183
190
|
setShowErrorMessage(false);
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
const { newObs, toBeVoided } = prepareObsForSubmission(
|
|
192
|
+
formData,
|
|
193
|
+
dirtyFields,
|
|
194
|
+
formContext,
|
|
195
|
+
initialFieldValuesMap,
|
|
189
196
|
config.concepts,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
createOrUpdateVitalsAndBiometrics(
|
|
190
200
|
patientUuid,
|
|
191
|
-
|
|
192
|
-
|
|
201
|
+
config.vitals.encounterTypeUuid,
|
|
202
|
+
editEncounterUuid,
|
|
193
203
|
session?.sessionLocation?.uuid,
|
|
204
|
+
[...newObs, ...toBeVoided],
|
|
205
|
+
abortController,
|
|
194
206
|
)
|
|
195
|
-
.then((
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
closeWorkspaceWithSavedChanges();
|
|
199
|
-
showSnackbar({
|
|
200
|
-
isLowContrast: true,
|
|
201
|
-
kind: 'success',
|
|
202
|
-
title: t('vitalsAndBiometricsRecorded', 'Vitals and Biometrics saved'),
|
|
203
|
-
subtitle: t(
|
|
204
|
-
'vitalsAndBiometricsNowAvailable',
|
|
205
|
-
'They are now visible on the Vitals and Biometrics page',
|
|
206
|
-
),
|
|
207
|
-
});
|
|
207
|
+
.then(() => {
|
|
208
|
+
if (mutateEncounter) {
|
|
209
|
+
mutateEncounter();
|
|
208
210
|
}
|
|
211
|
+
invalidateCachedVitalsAndBiometrics();
|
|
212
|
+
closeWorkspaceWithSavedChanges();
|
|
213
|
+
showSnackbar({
|
|
214
|
+
isLowContrast: true,
|
|
215
|
+
kind: 'success',
|
|
216
|
+
title:
|
|
217
|
+
formContext === 'creating'
|
|
218
|
+
? t('vitalsAndBiometricsSaved', 'Vitals and Biometrics saved')
|
|
219
|
+
: t('vitalsAndBiometricsUpdated', 'Vitals and Biometrics updated'),
|
|
220
|
+
subtitle: t('vitalsAndBiometricsNowAvailable', 'They are now visible on the Vitals and Biometrics page'),
|
|
221
|
+
});
|
|
209
222
|
})
|
|
210
223
|
.catch(() => {
|
|
211
224
|
createErrorHandler();
|
|
212
225
|
showSnackbar({
|
|
213
|
-
title:
|
|
226
|
+
title:
|
|
227
|
+
formContext === 'creating'
|
|
228
|
+
? t('vitalsAndBiometricsSaveError', 'Error saving Vitals and Biometrics')
|
|
229
|
+
: t('vitalsAndBiometricsUpdateError', 'Error updating Vitals and Biometrics'),
|
|
214
230
|
kind: 'error',
|
|
215
231
|
isLowContrast: false,
|
|
216
232
|
subtitle: t('checkForValidity', 'Some of the values entered are invalid'),
|
|
217
233
|
});
|
|
218
|
-
})
|
|
219
|
-
.finally(() => {
|
|
220
|
-
abortController.abort();
|
|
221
234
|
});
|
|
222
235
|
} else {
|
|
223
236
|
setHasInvalidVitals(true);
|
|
224
237
|
}
|
|
225
238
|
},
|
|
226
239
|
[
|
|
227
|
-
|
|
240
|
+
abortController,
|
|
228
241
|
conceptMetadata,
|
|
229
242
|
config.concepts,
|
|
230
243
|
config.vitals.encounterTypeUuid,
|
|
231
|
-
|
|
244
|
+
dirtyFields,
|
|
245
|
+
editEncounterUuid,
|
|
246
|
+
formContext,
|
|
247
|
+
initialFieldValuesMap,
|
|
232
248
|
patientUuid,
|
|
233
249
|
session?.sessionLocation?.uuid,
|
|
250
|
+
closeWorkspaceWithSavedChanges,
|
|
251
|
+
mutateEncounter,
|
|
234
252
|
t,
|
|
235
253
|
],
|
|
236
254
|
);
|
|
@@ -253,7 +271,7 @@ const VitalsAndBiometricsForm: React.FC<DefaultPatientWorkspaceProps> = ({
|
|
|
253
271
|
);
|
|
254
272
|
}
|
|
255
273
|
|
|
256
|
-
if (
|
|
274
|
+
if (isLoadingConceptMetadata || isLoadingInitialValues) {
|
|
257
275
|
return (
|
|
258
276
|
<Form className={styles.form}>
|
|
259
277
|
<div className={styles.grid}>
|
|
@@ -605,7 +623,7 @@ const VitalsAndBiometricsForm: React.FC<DefaultPatientWorkspaceProps> = ({
|
|
|
605
623
|
className={styles.button}
|
|
606
624
|
kind="primary"
|
|
607
625
|
onClick={handleSubmit(savePatientVitalsAndBiometrics, onError)}
|
|
608
|
-
disabled={isSubmitting}
|
|
626
|
+
disabled={!isDirty || isSubmitting}
|
|
609
627
|
type="submit"
|
|
610
628
|
>
|
|
611
629
|
{t('saveAndClose', 'Save and close')}
|
|
@@ -6,8 +6,8 @@ import { Warning } from '@carbon/react/icons';
|
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { useLayoutType, ResponsiveWrapper } from '@openmrs/esm-framework';
|
|
8
8
|
import { generatePlaceholder } from '../common';
|
|
9
|
-
import { type VitalsBiometricsFormData } from './vitals-biometrics-form.workspace';
|
|
10
9
|
import styles from './vitals-biometrics-input.scss';
|
|
10
|
+
import { type VitalsBiometricsFormData } from './schema';
|
|
11
11
|
|
|
12
12
|
type fieldId =
|
|
13
13
|
| 'computedBodyMassIndex'
|
package/translations/en.json
CHANGED
|
@@ -10,14 +10,24 @@
|
|
|
10
10
|
"bmi": "BMI",
|
|
11
11
|
"bp": "BP",
|
|
12
12
|
"calculatedBmi": "BMI (calc.)",
|
|
13
|
+
"cancel": "Cancel",
|
|
13
14
|
"checkForValidity": "Some of the values entered are invalid",
|
|
14
15
|
"date": "Date",
|
|
15
16
|
"dateAndTime": "Date and time",
|
|
16
17
|
"daysOldVitals_one": "<0>These vitals are <1>{{count}} day old</1></0>",
|
|
17
18
|
"daysOldVitals_other": "<0>These vitals are <1>{{count}} days old</1></0>",
|
|
19
|
+
"delete": "Delete",
|
|
20
|
+
"deleteConfirmationText": "Note: Deleting these entries will also remove related vitals or biometrics data. Are you sure you want to continue?",
|
|
21
|
+
"deleteVitalsAndBiometrics": "Delete vitals and biometrics",
|
|
22
|
+
"deleting": "Deleting",
|
|
18
23
|
"diastolic": "diastolic",
|
|
19
24
|
"discard": "Discard",
|
|
25
|
+
"edit": "Edit",
|
|
26
|
+
"editOrDeleteVitalsAndBiometrics": "Edit or delete Vitals and Biometrics",
|
|
27
|
+
"editVitalsAndBiometrics": "Edit Vitals and Biometrics",
|
|
28
|
+
"encounterUuidRequired": "Encounter UUID is required to delete vitals or biometrics",
|
|
20
29
|
"error": "Error",
|
|
30
|
+
"errorDeleting": "Error deleting vitals and biometrics",
|
|
21
31
|
"female": "Female",
|
|
22
32
|
"goToSummary": "Go to Summary",
|
|
23
33
|
"heartRate": "Heart rate",
|
|
@@ -54,9 +64,12 @@
|
|
|
54
64
|
"vitals": "Vitals",
|
|
55
65
|
"Vitals & Biometrics": "Vitals & Biometrics",
|
|
56
66
|
"vitalsAndBiometrics": "Vitals and biometrics",
|
|
67
|
+
"vitalsAndBiometricsDeleted": "Vitals and biometrics deleted",
|
|
57
68
|
"vitalsAndBiometricsNowAvailable": "They are now visible on the Vitals and Biometrics page",
|
|
58
|
-
"
|
|
69
|
+
"vitalsAndBiometricsSaved": "Vitals and Biometrics saved",
|
|
59
70
|
"vitalsAndBiometricsSaveError": "Error saving vitals and biometrics",
|
|
71
|
+
"vitalsAndBiometricsUpdated": "Vitals and Biometrics updated",
|
|
72
|
+
"vitalsAndBiometricsUpdateError": "Error updating Vitals and Biometrics",
|
|
60
73
|
"vitalsHistory": "Vitals history",
|
|
61
74
|
"vitalSignDisplayed": "Vital sign displayed",
|
|
62
75
|
"vitalSigns": "Vital signs",
|