@openmrs/esm-patient-vitals-app 11.3.1-pre.9452 → 11.3.1-pre.9455

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