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