@openmrs/esm-patient-vitals-app 9.2.3-pre.7182 → 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.
Files changed (60) hide show
  1. package/.turbo/turbo-build.log +19 -19
  2. package/dist/101.js +2 -0
  3. package/dist/101.js.map +1 -0
  4. package/dist/{9057.js → 1423.js} +1 -1
  5. package/dist/1423.js.map +1 -0
  6. package/dist/4300.js +1 -1
  7. package/dist/5207.js +1 -0
  8. package/dist/5207.js.map +1 -0
  9. package/dist/5387.js +1 -0
  10. package/dist/5387.js.map +1 -0
  11. package/dist/5395.js +2 -0
  12. package/dist/{3368.js.LICENSE.txt → 5395.js.LICENSE.txt} +19 -1
  13. package/dist/5395.js.map +1 -0
  14. package/dist/8295.js +2 -0
  15. package/dist/8295.js.LICENSE.txt +7 -0
  16. package/dist/8295.js.map +1 -0
  17. package/dist/8953.js +1 -0
  18. package/dist/8953.js.map +1 -0
  19. package/dist/main.js +1 -1
  20. package/dist/main.js.map +1 -1
  21. package/dist/openmrs-esm-patient-vitals-app.js +1 -1
  22. package/dist/openmrs-esm-patient-vitals-app.js.buildmanifest.json +152 -128
  23. package/dist/openmrs-esm-patient-vitals-app.js.map +1 -1
  24. package/dist/routes.json +1 -1
  25. package/package.json +2 -2
  26. package/src/biometrics/biometrics-base.component.tsx +0 -1
  27. package/src/biometrics/biometrics-overview.test.tsx +26 -5
  28. package/src/biometrics/paginated-biometrics.component.tsx +5 -0
  29. package/src/common/data.resource.ts +128 -75
  30. package/src/common/helpers.ts +50 -0
  31. package/src/common/index.ts +3 -2
  32. package/src/common/types.ts +2 -1
  33. package/src/components/action-menu/vitals-biometrics-action-menu.component.tsx +60 -0
  34. package/src/components/action-menu/vitals-biometrics-action-menu.scss +11 -0
  35. package/src/components/delete-vitals-biometrics-modal/delete-vitals-biometrics.modal.tsx +84 -0
  36. package/src/{weight-tile → components/weight-tile}/weight-tile.component.tsx +2 -2
  37. package/src/{weight-tile → components/weight-tile}/weight-tile.test.tsx +4 -4
  38. package/src/index.ts +6 -1
  39. package/src/routes.json +6 -0
  40. package/src/vitals/paginated-vitals.component.tsx +5 -0
  41. package/src/vitals/vitals-overview.component.tsx +3 -2
  42. package/src/vitals-biometrics-form/schema.ts +28 -0
  43. package/src/vitals-biometrics-form/vitals-biometrics-form.test.tsx +203 -21
  44. package/src/vitals-biometrics-form/vitals-biometrics-form.workspace.tsx +78 -60
  45. package/src/vitals-biometrics-form/vitals-biometrics-input.component.tsx +1 -1
  46. package/translations/en.json +14 -1
  47. package/dist/3368.js +0 -2
  48. package/dist/3368.js.map +0 -1
  49. package/dist/4716.js +0 -1
  50. package/dist/4716.js.map +0 -1
  51. package/dist/7738.js +0 -2
  52. package/dist/7738.js.LICENSE.txt +0 -25
  53. package/dist/7738.js.map +0 -1
  54. package/dist/8895.js +0 -2
  55. package/dist/8895.js.map +0 -1
  56. package/dist/8957.js +0 -1
  57. package/dist/8957.js.map +0 -1
  58. package/dist/9057.js.map +0 -1
  59. /package/dist/{8895.js.LICENSE.txt → 101.js.LICENSE.txt} +0 -0
  60. /package/src/{weight-tile → components/weight-tile}/weight-tile.scss +0 -0
package/src/routes.json CHANGED
@@ -73,5 +73,11 @@
73
73
  "title": "recordVitalsAndBiometrics",
74
74
  "component": "vitalsBiometricsFormWorkspace"
75
75
  }
76
+ ],
77
+ "modals": [
78
+ {
79
+ "name": "vitals-biometrics-delete-confirmation-modal",
80
+ "component": "vitalsAndBiometricsDeleteConfirmationModal"
81
+ }
76
82
  ]
77
83
  }
@@ -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, index) => {
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 { saveVitalsAndBiometrics } from '../common';
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 mockSavePatientVitals = jest.mocked(saveVitalsAndBiometrics);
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
- saveVitalsAndBiometrics: jest.fn(),
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
- mockSavePatientVitals.mockResolvedValue(response as ReturnType<typeof saveVitalsAndBiometrics>);
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(mockSavePatientVitals).toHaveBeenCalledTimes(1);
145
- expect(mockSavePatientVitals).toHaveBeenCalledWith(
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
- height: heightValue,
152
- midUpperArmCircumference: muacValue,
153
- oxygenSaturation: oxygenSaturationValue,
154
- pulse: pulseValue,
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
- mockSavePatientVitals.mockRejectedValueOnce(error);
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 vitals and biometrics',
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
- const VitalsAndBiometricFormSchema = z
47
- .object({
48
- systolicBloodPressure: z.number(),
49
- diastolicBloodPressure: z.number(),
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
- export type VitalsBiometricsFormData = z.infer<typeof VitalsAndBiometricFormSchema>;
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 { data: conceptUnits, conceptMetadata, conceptRanges, isLoading } = useVitalsConceptMetadata();
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(VitalsAndBiometricFormSchema),
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 abortController = new AbortController();
185
-
186
- savePatientVitals(
187
- config.vitals.encounterTypeUuid,
188
- config.vitals.formUuid,
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
- formData,
192
- abortController,
201
+ config.vitals.encounterTypeUuid,
202
+ editEncounterUuid,
193
203
  session?.sessionLocation?.uuid,
204
+ [...newObs, ...toBeVoided],
205
+ abortController,
194
206
  )
195
- .then((response) => {
196
- if (response.status === 201) {
197
- invalidateCachedVitalsAndBiometrics();
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: t('vitalsAndBiometricsSaveError', 'Error saving vitals and biometrics'),
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
- closeWorkspaceWithSavedChanges,
240
+ abortController,
228
241
  conceptMetadata,
229
242
  config.concepts,
230
243
  config.vitals.encounterTypeUuid,
231
- config.vitals.formUuid,
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 (isLoading) {
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'
@@ -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
- "vitalsAndBiometricsRecorded": "Vitals and Biometrics saved",
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",