@openmrs/esm-patient-vitals-app 11.3.1-patch.9064 → 11.3.1-patch.9310

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 (55) hide show
  1. package/.turbo/turbo-build.log +21 -18
  2. package/dist/3174.js +2 -0
  3. package/dist/3174.js.map +1 -0
  4. package/dist/4341.js +1 -0
  5. package/dist/4341.js.map +1 -0
  6. package/dist/5652.js +1 -0
  7. package/dist/5652.js.map +1 -0
  8. package/dist/5670.js +1 -1
  9. package/dist/5670.js.map +1 -1
  10. package/dist/6336.js +1 -0
  11. package/dist/6336.js.map +1 -0
  12. package/dist/7299.js +1 -1
  13. package/dist/7437.js +1 -0
  14. package/dist/7437.js.map +1 -0
  15. package/dist/8953.js +1 -1
  16. package/dist/9228.js +1 -0
  17. package/dist/9228.js.map +1 -0
  18. package/dist/main.js +1 -1
  19. package/dist/main.js.map +1 -1
  20. package/dist/openmrs-esm-patient-vitals-app.js +1 -1
  21. package/dist/openmrs-esm-patient-vitals-app.js.buildmanifest.json +155 -130
  22. package/dist/openmrs-esm-patient-vitals-app.js.map +1 -1
  23. package/dist/routes.json +1 -1
  24. package/package.json +4 -3
  25. package/src/biometrics/biometrics-base.component.tsx +4 -2
  26. package/src/biometrics/biometrics-main.component.tsx +11 -2
  27. package/src/biometrics/biometrics-overview.component.tsx +11 -2
  28. package/src/biometrics/biometrics-overview.test.tsx +3 -0
  29. package/src/biometrics/paginated-biometrics.component.tsx +3 -1
  30. package/src/common/data.resource.ts +20 -18
  31. package/src/common/helpers.ts +38 -9
  32. package/src/common/types.ts +13 -1
  33. package/src/components/action-menu/vitals-biometrics-action-menu.component.tsx +5 -5
  34. package/src/index.ts +6 -2
  35. package/src/routes.json +10 -4
  36. package/src/utils.ts +2 -1
  37. package/src/vitals/paginated-vitals.component.tsx +3 -1
  38. package/src/vitals/vitals-overview.component.tsx +2 -1
  39. package/src/vitals-and-biometrics-header/{vitals-header.component.tsx → vitals-header.extension.tsx} +31 -21
  40. package/src/vitals-and-biometrics-header/vitals-header.test.tsx +107 -11
  41. package/src/vitals-biometrics-form/exported-vitals-biometrics-form.workspace.tsx +640 -0
  42. package/src/vitals-biometrics-form/vitals-biometrics-form.test.tsx +38 -17
  43. package/src/vitals-biometrics-form/vitals-biometrics-form.workspace.tsx +19 -604
  44. package/src/vitals-biometrics-form/vitals-biometrics-input.component.tsx +4 -1
  45. package/dist/5415.js +0 -1
  46. package/dist/5415.js.map +0 -1
  47. package/dist/5639.js +0 -1
  48. package/dist/5639.js.map +0 -1
  49. package/dist/5810.js +0 -1
  50. package/dist/5810.js.map +0 -1
  51. package/dist/6712.js +0 -2
  52. package/dist/6712.js.map +0 -1
  53. package/dist/8803.js +0 -1
  54. package/dist/8803.js.map +0 -1
  55. /package/dist/{6712.js.LICENSE.txt → 3174.js.LICENSE.txt} +0 -0
@@ -1,614 +1,29 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
- import { useForm } from 'react-hook-form';
3
- import { useTranslation } from 'react-i18next';
4
- import { zodResolver } from '@hookform/resolvers/zod';
5
- import {
6
- Button,
7
- ButtonSkeleton,
8
- ButtonSet,
9
- Column,
10
- Form,
11
- InlineNotification,
12
- NumberInputSkeleton,
13
- Row,
14
- Stack,
15
- } from '@carbon/react';
16
- import {
17
- age,
18
- ExtensionSlot,
19
- showSnackbar,
20
- useAbortController,
21
- useConfig,
22
- useLayoutType,
23
- useSession,
24
- useVisit,
25
- } from '@openmrs/esm-framework';
26
- import { type DefaultPatientWorkspaceProps, useOptimisticVisitMutations } from '@openmrs/esm-patient-common-lib';
27
- import { type ConfigObject } from '../config-schema';
28
- import {
29
- calculateBodyMassIndex,
30
- extractNumbers,
31
- getMuacColorCode,
32
- isValueWithinReferenceRange,
33
- } from './vitals-biometrics-form.utils';
34
- import {
35
- assessValue,
36
- createOrUpdateVitalsAndBiometrics,
37
- getReferenceRangesForConcept,
38
- interpretBloodPressure,
39
- invalidateCachedVitalsAndBiometrics,
40
- useConceptUnits,
41
- useEncounterVitalsAndBiometrics,
42
- } from '../common';
43
- import { prepareObsForSubmission } from '../common/helpers';
44
- import { useVitalsConceptMetadata } from '../common/data.resource';
45
- import { VitalsAndBiometricsFormSchema, type VitalsBiometricsFormData } from './schema';
46
- import VitalsAndBiometricsInput from './vitals-biometrics-input.component';
47
- import styles from './vitals-biometrics-form.scss';
1
+ import React from 'react';
2
+ import { type PatientWorkspace2DefinitionProps } from '@openmrs/esm-patient-common-lib';
3
+ import ExportedVitalsAndBiometricsForm from './exported-vitals-biometrics-form.workspace';
48
4
 
49
- interface VitalsAndBiometricsFormProps extends DefaultPatientWorkspaceProps {
5
+ export interface VitalsAndBiometricsFormProps {
50
6
  formContext: 'creating' | 'editing';
51
7
  editEncounterUuid?: string;
52
8
  }
53
9
 
54
- const VitalsAndBiometricsForm: React.FC<VitalsAndBiometricsFormProps> = ({
55
- patientUuid,
56
- patient,
57
- editEncounterUuid,
58
- formContext = 'creating',
59
- closeWorkspace,
60
- closeWorkspaceWithSavedChanges,
61
- promptBeforeClosing,
10
+ /**
11
+ * This workspace displays the form to input patient vitals and biometrics.
12
+ * This workspace should only be used within the patient chart. Use ExportedVitalsAndBiometricsForm
13
+ * for use cases outside the patient chart.
14
+ */
15
+ const VitalsAndBiometricsForm: React.FC<PatientWorkspace2DefinitionProps<VitalsAndBiometricsFormProps, {}>> = ({
16
+ workspaceProps: { editEncounterUuid, formContext = 'creating' },
17
+ groupProps: { patientUuid, patient, visitContext },
18
+ ...rest
62
19
  }) => {
63
- const { t } = useTranslation();
64
- const isTablet = useLayoutType() === 'tablet';
65
- const config = useConfig<ConfigObject>();
66
- const biometricsUnitsSymbols = config.biometrics;
67
- const useMuacColorStatus = config.vitals.useMuacColors;
68
-
69
- const session = useSession();
70
- const { currentVisit } = useVisit(patientUuid);
71
- const { conceptUnits, isLoading: isLoadingConceptUnits } = useConceptUnits();
72
- const { conceptRanges, conceptRangeMap } = useVitalsConceptMetadata(patientUuid);
73
- const {
74
- getRefinedInitialValues,
75
- isLoading: isLoadingEncounter,
76
- mutate: mutateEncounter,
77
- vitalsAndBiometrics: initialFieldValuesMap,
78
- } = useEncounterVitalsAndBiometrics(formContext === 'editing' ? editEncounterUuid : null);
79
- const [hasInvalidVitals, setHasInvalidVitals] = useState(false);
80
- const [muacColorCode, setMuacColorCode] = useState('');
81
- const [showErrorNotification, setShowErrorNotification] = useState(false);
82
- const [showErrorMessage, setShowErrorMessage] = useState(false);
83
- const abortController = useAbortController();
84
- const { invalidateVisitRelatedData } = useOptimisticVisitMutations(patientUuid);
85
-
86
- const isLoadingInitialValues = useMemo(
87
- () => (formContext === 'creating' ? false : isLoadingEncounter),
88
- [formContext, isLoadingEncounter],
89
- );
90
-
91
- const {
92
- control,
93
- handleSubmit,
94
- watch,
95
- setValue,
96
- formState: { isDirty, isSubmitting, dirtyFields },
97
- reset,
98
- } = useForm<VitalsBiometricsFormData>({
99
- mode: 'all',
100
- resolver: zodResolver(VitalsAndBiometricsFormSchema),
101
- });
102
-
103
- useEffect(() => {
104
- if (formContext === 'editing' && !isLoadingInitialValues && initialFieldValuesMap) {
105
- reset(getRefinedInitialValues());
106
- }
107
- }, [formContext, isLoadingInitialValues, initialFieldValuesMap, getRefinedInitialValues, reset]);
108
-
109
- useEffect(() => promptBeforeClosing(() => isDirty), [isDirty, promptBeforeClosing]);
110
-
111
- const encounterUuid = currentVisit?.encounters?.find(
112
- (encounter) => encounter?.form?.uuid === config.vitals.formUuid,
113
- )?.uuid;
114
-
115
- const midUpperArmCircumference = watch('midUpperArmCircumference');
116
- const systolicBloodPressure = watch('systolicBloodPressure');
117
- const diastolicBloodPressure = watch('diastolicBloodPressure');
118
- const respiratoryRate = watch('respiratoryRate');
119
- const oxygenSaturation = watch('oxygenSaturation');
120
- const temperature = watch('temperature');
121
- const pulse = watch('pulse');
122
- const weight = watch('weight');
123
- const height = watch('height');
124
-
125
- useEffect(() => {
126
- const patientBirthDate = patient?.birthDate;
127
- if (patientBirthDate && midUpperArmCircumference) {
128
- const patientAge = extractNumbers(age(patientBirthDate));
129
- getMuacColorCode(patientAge, midUpperArmCircumference, setMuacColorCode);
130
- }
131
- }, [watch, patient?.birthDate, midUpperArmCircumference]);
132
-
133
- useEffect(() => {
134
- if (height && weight) {
135
- const computedBodyMassIndex = calculateBodyMassIndex(
136
- weight,
137
- height,
138
- conceptUnits.get(config.concepts.weightUuid) as 'lb' | 'lbs' | 'g',
139
- conceptUnits.get(config.concepts.heightUuid) as 'm' | 'cm' | 'in',
140
- );
141
- setValue('computedBodyMassIndex', computedBodyMassIndex);
142
- }
143
- }, [weight, height, setValue, conceptUnits, config.concepts.weightUuid, config.concepts.heightUuid]);
144
-
145
- function onError(err) {
146
- if (err?.oneFieldRequired) {
147
- setShowErrorNotification(true);
148
- }
149
- }
150
-
151
- const concepts = useMemo(
152
- () => ({
153
- midUpperArmCircumferenceRange: conceptRangeMap.get(config.concepts.midUpperArmCircumferenceUuid),
154
- diastolicBloodPressureRange: conceptRangeMap.get(config.concepts.diastolicBloodPressureUuid),
155
- systolicBloodPressureRange: conceptRangeMap.get(config.concepts.systolicBloodPressureUuid),
156
- oxygenSaturationRange: conceptRangeMap.get(config.concepts.oxygenSaturationUuid),
157
- respiratoryRateRange: conceptRangeMap.get(config.concepts.respiratoryRateUuid),
158
- temperatureRange: conceptRangeMap.get(config.concepts.temperatureUuid),
159
- weightRange: conceptRangeMap.get(config.concepts.weightUuid),
160
- heightRange: conceptRangeMap.get(config.concepts.heightUuid),
161
- pulseRange: conceptRangeMap.get(config.concepts.pulseUuid),
162
- }),
163
- [conceptRangeMap, config.concepts],
164
- );
165
-
166
- const savePatientVitalsAndBiometrics = useCallback(
167
- (data: VitalsBiometricsFormData) => {
168
- const formData = data;
169
- setShowErrorMessage(true);
170
- setShowErrorNotification(false);
171
-
172
- data?.computedBodyMassIndex && delete data.computedBodyMassIndex;
173
-
174
- const allFieldsAreValid = Object.entries(formData)
175
- .filter(([, value]) => Boolean(value))
176
- .every(([key, value]) => isValueWithinReferenceRange(conceptRanges, config.concepts[`${key}Uuid`], value));
177
-
178
- if (allFieldsAreValid) {
179
- setShowErrorMessage(false);
180
- const { newObs, toBeVoided } = prepareObsForSubmission(
181
- formData,
182
- dirtyFields,
183
- formContext,
184
- initialFieldValuesMap,
185
- config.concepts,
186
- );
187
-
188
- createOrUpdateVitalsAndBiometrics(
189
- patientUuid,
190
- config.vitals.encounterTypeUuid,
191
- editEncounterUuid,
192
- session?.sessionLocation?.uuid,
193
- [...newObs, ...toBeVoided],
194
- abortController,
195
- )
196
- .then(() => {
197
- if (mutateEncounter) {
198
- mutateEncounter();
199
- }
200
- // Only invalidate observations data since we created new vitals/biometrics observations
201
- invalidateVisitRelatedData({ observations: true, encounters: true });
202
- invalidateCachedVitalsAndBiometrics();
203
- closeWorkspaceWithSavedChanges();
204
- showSnackbar({
205
- isLowContrast: true,
206
- kind: 'success',
207
- title:
208
- formContext === 'creating'
209
- ? t('vitalsAndBiometricsSaved', 'Vitals and Biometrics saved')
210
- : t('vitalsAndBiometricsUpdated', 'Vitals and Biometrics updated'),
211
- subtitle: t('vitalsAndBiometricsNowAvailable', 'They are now visible on the Vitals and Biometrics page'),
212
- });
213
- })
214
- .catch(() => {
215
- showSnackbar({
216
- title:
217
- formContext === 'creating'
218
- ? t('vitalsAndBiometricsSaveError', 'Error saving Vitals and Biometrics')
219
- : t('vitalsAndBiometricsUpdateError', 'Error updating Vitals and Biometrics'),
220
- kind: 'error',
221
- isLowContrast: false,
222
- subtitle: t('checkForValidity', 'Some of the values entered are invalid'),
223
- });
224
- });
225
- } else {
226
- setHasInvalidVitals(true);
227
- }
228
- },
229
- [
230
- abortController,
231
- closeWorkspaceWithSavedChanges,
232
- config.concepts,
233
- config.vitals.encounterTypeUuid,
234
- dirtyFields,
235
- editEncounterUuid,
236
- conceptRanges,
237
- formContext,
238
- initialFieldValuesMap,
239
- mutateEncounter,
240
- invalidateVisitRelatedData,
241
- patientUuid,
242
- session?.sessionLocation?.uuid,
243
- t,
244
- ],
245
- );
246
-
247
- if (config.vitals.useFormEngine) {
248
- return (
249
- <ExtensionSlot
250
- name="form-widget-slot"
251
- state={{
252
- view: 'form',
253
- formUuid: config.vitals.formUuid,
254
- visitUuid: currentVisit?.uuid,
255
- visitTypeUuid: currentVisit?.visitType?.uuid,
256
- patientUuid: patientUuid ?? null,
257
- patient,
258
- encounterUuid,
259
- closeWorkspaceWithSavedChanges,
260
- }}
261
- />
262
- );
263
- }
264
-
265
- if (isLoadingConceptUnits || isLoadingInitialValues) {
266
- return (
267
- <Form className={styles.form}>
268
- <ExtensionSlot name="visit-context-header-slot" state={{ patientUuid }} />
269
- <div className={styles.grid}>
270
- <Stack>
271
- <Column>
272
- <p className={styles.title}>{t('recordVitals', 'Record vitals')}</p>
273
- </Column>
274
- <Row className={styles.row}>
275
- <Column>
276
- <NumberInputSkeleton />
277
- </Column>
278
- <Column>
279
- <NumberInputSkeleton />
280
- </Column>
281
- <Column>
282
- <NumberInputSkeleton />
283
- </Column>
284
- <Column>
285
- <NumberInputSkeleton />
286
- </Column>
287
- </Row>
288
- </Stack>
289
- </div>
290
- <ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
291
- <ButtonSkeleton className={styles.button} />
292
- <ButtonSkeleton className={styles.button} />
293
- </ButtonSet>
294
- </Form>
295
- );
296
- }
297
-
298
20
  return (
299
- <Form className={styles.form} data-openmrs-role="Vitals and Biometrics Form">
300
- <ExtensionSlot name="visit-context-header-slot" state={{ patientUuid }} />
301
- <div className={styles.grid}>
302
- <Stack>
303
- <Column>
304
- <p className={styles.title}>{t('recordVitals', 'Record vitals')}</p>
305
- </Column>
306
- <Row className={styles.row}>
307
- <Column>
308
- <VitalsAndBiometricsInput
309
- control={control}
310
- fieldProperties={[
311
- {
312
- id: 'temperature',
313
- max: concepts.temperatureRange?.hiAbsolute,
314
- min: concepts.temperatureRange?.lowAbsolute,
315
- name: t('temperature', 'Temperature'),
316
- type: 'number',
317
- },
318
- ]}
319
- interpretation={
320
- temperature &&
321
- assessValue(temperature, getReferenceRangesForConcept(config.concepts.temperatureUuid, conceptRanges))
322
- }
323
- isValueWithinReferenceRange={
324
- temperature
325
- ? isValueWithinReferenceRange(conceptRanges, config.concepts['temperatureUuid'], temperature)
326
- : true
327
- }
328
- showErrorMessage={showErrorMessage}
329
- label={t('temperature', 'Temperature')}
330
- unitSymbol={conceptUnits.get(config.concepts.temperatureUuid) ?? ''}
331
- />
332
- </Column>
333
- <Column>
334
- <VitalsAndBiometricsInput
335
- control={control}
336
- fieldProperties={[
337
- {
338
- name: t('systolic', 'systolic'),
339
- separator: '/',
340
- type: 'number',
341
- min: concepts.systolicBloodPressureRange?.lowAbsolute,
342
- max: concepts.systolicBloodPressureRange?.hiAbsolute,
343
- id: 'systolicBloodPressure',
344
- },
345
- {
346
- name: t('diastolic', 'diastolic'),
347
- type: 'number',
348
- min: concepts.diastolicBloodPressureRange?.lowAbsolute,
349
- max: concepts.diastolicBloodPressureRange?.hiAbsolute,
350
- id: 'diastolicBloodPressure',
351
- },
352
- ]}
353
- interpretation={
354
- systolicBloodPressure &&
355
- diastolicBloodPressure &&
356
- interpretBloodPressure(systolicBloodPressure, diastolicBloodPressure, config.concepts, conceptRanges)
357
- }
358
- isValueWithinReferenceRange={
359
- systolicBloodPressure &&
360
- diastolicBloodPressure &&
361
- isValueWithinReferenceRange(
362
- conceptRanges,
363
- config.concepts.systolicBloodPressureUuid,
364
- systolicBloodPressure,
365
- ) &&
366
- isValueWithinReferenceRange(
367
- conceptRanges,
368
- config.concepts.diastolicBloodPressureUuid,
369
- diastolicBloodPressure,
370
- )
371
- }
372
- showErrorMessage={showErrorMessage}
373
- label={t('bloodPressure', 'Blood pressure')}
374
- unitSymbol={conceptUnits.get(config.concepts.systolicBloodPressureUuid) ?? ''}
375
- />
376
- </Column>
377
- <Column>
378
- <VitalsAndBiometricsInput
379
- control={control}
380
- fieldProperties={[
381
- {
382
- name: t('pulse', 'Pulse'),
383
- type: 'number',
384
- min: concepts.pulseRange?.lowAbsolute,
385
- max: concepts.pulseRange?.hiAbsolute,
386
- id: 'pulse',
387
- },
388
- ]}
389
- interpretation={
390
- pulse && assessValue(pulse, getReferenceRangesForConcept(config.concepts.pulseUuid, conceptRanges))
391
- }
392
- isValueWithinReferenceRange={
393
- pulse && isValueWithinReferenceRange(conceptRanges, config.concepts['pulseUuid'], pulse)
394
- }
395
- label={t('heartRate', 'Heart rate')}
396
- showErrorMessage={showErrorMessage}
397
- unitSymbol={conceptUnits.get(config.concepts.pulseUuid) ?? ''}
398
- />
399
- </Column>
400
- <Column>
401
- <VitalsAndBiometricsInput
402
- control={control}
403
- fieldProperties={[
404
- {
405
- name: t('respirationRate', 'Respiration rate'),
406
- type: 'number',
407
- min: concepts.respiratoryRateRange?.lowAbsolute,
408
- max: concepts.respiratoryRateRange?.hiAbsolute,
409
- id: 'respiratoryRate',
410
- },
411
- ]}
412
- interpretation={
413
- respiratoryRate &&
414
- assessValue(
415
- respiratoryRate,
416
- getReferenceRangesForConcept(config.concepts.respiratoryRateUuid, conceptRanges),
417
- )
418
- }
419
- isValueWithinReferenceRange={
420
- respiratoryRate &&
421
- isValueWithinReferenceRange(conceptRanges, config.concepts['respiratoryRateUuid'], respiratoryRate)
422
- }
423
- showErrorMessage={showErrorMessage}
424
- label={t('respirationRate', 'Respiration rate')}
425
- unitSymbol={conceptUnits.get(config.concepts.respiratoryRateUuid) ?? ''}
426
- />
427
- </Column>
428
- <Column>
429
- <VitalsAndBiometricsInput
430
- control={control}
431
- fieldProperties={[
432
- {
433
- name: t('oxygenSaturation', 'Oxygen saturation'),
434
- type: 'number',
435
- min: concepts.oxygenSaturationRange?.lowAbsolute,
436
- max: concepts.oxygenSaturationRange?.hiAbsolute,
437
- id: 'oxygenSaturation',
438
- },
439
- ]}
440
- interpretation={
441
- oxygenSaturation &&
442
- assessValue(
443
- oxygenSaturation,
444
- getReferenceRangesForConcept(config.concepts.oxygenSaturationUuid, conceptRanges),
445
- )
446
- }
447
- isValueWithinReferenceRange={
448
- oxygenSaturation &&
449
- isValueWithinReferenceRange(conceptRanges, config.concepts['oxygenSaturationUuid'], oxygenSaturation)
450
- }
451
- showErrorMessage={showErrorMessage}
452
- label={t('spo2', 'SpO2')}
453
- unitSymbol={conceptUnits.get(config.concepts.oxygenSaturationUuid) ?? ''}
454
- />
455
- </Column>
456
- </Row>
457
-
458
- <Row className={styles.row}>
459
- <Column className={styles.noteInput}>
460
- <VitalsAndBiometricsInput
461
- control={control}
462
- fieldWidth={isTablet ? '70%' : '100%'}
463
- fieldProperties={[
464
- {
465
- name: t('notes', 'Notes'),
466
- type: 'textarea',
467
- id: 'generalPatientNote',
468
- },
469
- ]}
470
- placeholder={t('additionalNoteText', 'Type any additional notes here')}
471
- label={t('notes', 'Notes')}
472
- />
473
- </Column>
474
- </Row>
475
- </Stack>
476
- <Stack className={styles.spacer}>
477
- <Column>
478
- <p className={styles.title}>{t('recordBiometrics', 'Record biometrics')}</p>
479
- </Column>
480
- <Row className={styles.row}>
481
- <Column>
482
- <VitalsAndBiometricsInput
483
- control={control}
484
- fieldProperties={[
485
- {
486
- name: t('weight', 'Weight'),
487
- type: 'number',
488
- min: concepts.weightRange?.lowAbsolute,
489
- max: concepts.weightRange?.hiAbsolute,
490
- id: 'weight',
491
- },
492
- ]}
493
- interpretation={
494
- weight && assessValue(weight, getReferenceRangesForConcept(config.concepts.weightUuid, conceptRanges))
495
- }
496
- isValueWithinReferenceRange={
497
- height && isValueWithinReferenceRange(conceptRanges, config.concepts['weightUuid'], weight)
498
- }
499
- showErrorMessage={showErrorMessage}
500
- label={t('weight', 'Weight')}
501
- unitSymbol={conceptUnits.get(config.concepts.weightUuid) ?? ''}
502
- />
503
- </Column>
504
- <Column>
505
- <VitalsAndBiometricsInput
506
- control={control}
507
- fieldProperties={[
508
- {
509
- name: t('height', 'Height'),
510
- type: 'number',
511
- min: concepts.heightRange?.lowAbsolute,
512
- max: concepts.heightRange?.hiAbsolute,
513
- id: 'height',
514
- },
515
- ]}
516
- interpretation={
517
- height && assessValue(height, getReferenceRangesForConcept(config.concepts.heightUuid, conceptRanges))
518
- }
519
- isValueWithinReferenceRange={
520
- weight && isValueWithinReferenceRange(conceptRanges, config.concepts['heightUuid'], height)
521
- }
522
- showErrorMessage={showErrorMessage}
523
- label={t('height', 'Height')}
524
- unitSymbol={conceptUnits.get(config.concepts.heightUuid) ?? ''}
525
- />
526
- </Column>
527
- <Column>
528
- <VitalsAndBiometricsInput
529
- control={control}
530
- fieldProperties={[
531
- {
532
- name: t('bmi', 'BMI'),
533
- type: 'number',
534
- id: 'computedBodyMassIndex',
535
- },
536
- ]}
537
- readOnly
538
- label={t('calculatedBmi', 'BMI (calc.)')}
539
- unitSymbol={biometricsUnitsSymbols['bmiUnit']}
540
- />
541
- </Column>
542
- <Column>
543
- <VitalsAndBiometricsInput
544
- control={control}
545
- fieldProperties={[
546
- {
547
- name: t('muac', 'MUAC'),
548
- type: 'number',
549
- min: concepts.midUpperArmCircumferenceRange?.lowAbsolute,
550
- max: concepts.midUpperArmCircumferenceRange?.hiAbsolute,
551
- id: 'midUpperArmCircumference',
552
- },
553
- ]}
554
- muacColorCode={muacColorCode}
555
- isValueWithinReferenceRange={
556
- height &&
557
- weight &&
558
- isValueWithinReferenceRange(
559
- conceptRanges,
560
- config.concepts['midUpperArmCircumferenceUuid'],
561
- midUpperArmCircumference,
562
- )
563
- }
564
- showErrorMessage={showErrorMessage}
565
- label={t('muac', 'MUAC')}
566
- unitSymbol={conceptUnits.get(config.concepts.midUpperArmCircumferenceUuid) ?? ''}
567
- useMuacColors={useMuacColorStatus}
568
- />
569
- </Column>
570
- </Row>
571
- </Stack>
572
- </div>
573
-
574
- {showErrorNotification && (
575
- <Column className={styles.errorContainer}>
576
- <InlineNotification
577
- lowContrast
578
- title={t('error', 'Error')}
579
- subtitle={t('pleaseFillField', 'Please fill at least one field') + '.'}
580
- onClose={() => setShowErrorNotification(false)}
581
- />
582
- </Column>
583
- )}
584
-
585
- {hasInvalidVitals && (
586
- <Column className={styles.errorContainer}>
587
- <InlineNotification
588
- className={styles.errorNotification}
589
- lowContrast={false}
590
- onClose={() => setHasInvalidVitals(false)}
591
- title={t('vitalsAndBiometricsSaveError', 'Error saving Vitals and Biometrics')}
592
- subtitle={t('checkForValidity', 'Some of the values entered are invalid')}
593
- />
594
- </Column>
595
- )}
596
-
597
- <ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
598
- <Button className={styles.button} kind="secondary" onClick={() => closeWorkspace()}>
599
- {t('discard', 'Discard')}
600
- </Button>
601
- <Button
602
- className={styles.button}
603
- kind="primary"
604
- onClick={handleSubmit(savePatientVitalsAndBiometrics, onError)}
605
- disabled={!isDirty || isSubmitting}
606
- type="submit"
607
- >
608
- {t('saveAndClose', 'Save and close')}
609
- </Button>
610
- </ButtonSet>
611
- </Form>
21
+ <ExportedVitalsAndBiometricsForm
22
+ workspaceProps={{ editEncounterUuid, formContext, patientUuid, patient, visitContext }}
23
+ windowProps={null}
24
+ groupProps={null}
25
+ {...rest}
26
+ />
612
27
  );
613
28
  };
614
29
 
@@ -107,7 +107,9 @@ const VitalsAndBiometricsInput: React.FC<VitalsAndBiometricsInputProps> = ({
107
107
  <>
108
108
  <div className={containerClasses} style={{ width: fieldWidth }}>
109
109
  <section className={styles.labelContainer}>
110
- <span className={styles.label}>{label}</span>
110
+ <span className={styles.label} id={`${fieldId}-label`}>
111
+ {label}
112
+ </span>
111
113
 
112
114
  {Boolean(hasAbnormalValue) ? (
113
115
  <span className={styles[interpretation.replace('_', '-')]} title={t('abnormalValue', 'Abnormal value')} />
@@ -178,6 +180,7 @@ const VitalsAndBiometricsInput: React.FC<VitalsAndBiometricsInputProps> = ({
178
180
  className={styles.textarea}
179
181
  id={`${fieldId}-${fieldProperty.id}`}
180
182
  labelText={''}
183
+ aria-labelledby={`${fieldId}-label`}
181
184
  maxCount={100}
182
185
  name={fieldProperty.name}
183
186
  onBlur={() => handleFocusChange(false)}