@openmrs/esm-patient-vitals-app 9.2.3-pre.7186 → 9.2.3-pre.7191

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -96,8 +96,6 @@ describe('BiometricsOverview', () => {
96
96
  expect(screen.getByRole('tab', { name: /chart view/i })).toBeInTheDocument();
97
97
  expect(screen.getByRole('link', { name: /see all/i })).toBeInTheDocument();
98
98
 
99
- const initialRowElements = screen.getAllByRole('row');
100
-
101
99
  const expectedColumnHeaders = [/date/, /weight/, /height/, /bmi/, /muac/];
102
100
  expectedColumnHeaders.map((header) =>
103
101
  expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument(),
@@ -114,20 +112,43 @@ describe('BiometricsOverview', () => {
114
112
 
115
113
  const sortRowsButton = screen.getByRole('button', { name: /date and time/i });
116
114
 
115
+ const expectedDescendingOrder = [
116
+ '12 — Aug — 2021, 12:00 AM',
117
+ '18 — Jun — 2021, 12:00 AM',
118
+ '10 — Jun — 2021, 12:00 AM',
119
+ '26 — May — 2021, 12:00 AM',
120
+ '10 — May — 2021, 12:00 AM',
121
+ ];
122
+ const expectedAscendingOrder = [
123
+ '08 — Dec — 2020, 12:00 AM',
124
+ '08 — Dec — 2020, 12:00 AM',
125
+ '08 — Dec — 2020, 12:00 AM',
126
+ '08 — Dec — 2020, 12:00 AM',
127
+ '09 — Dec — 2020, 12:00 AM',
128
+ ];
129
+
130
+ const getRowDates = () =>
131
+ screen
132
+ .getAllByRole('row')
133
+ .slice(1) // Exclude header row
134
+ .map((row) => row.textContent?.match(/\d{1,2} — \w{3} — \d{4}, \d{1,2}:\d{2} (AM|PM)/)?.[0] || '');
135
+
136
+ // Initial state should be descending
137
+ expect(getRowDates()).toEqual(expectedDescendingOrder);
138
+
117
139
  // Sorting in descending order
118
140
  // Since the date order is already in descending order, the rows should be the same
119
141
  await user.click(sortRowsButton);
120
142
  // Sorting in ascending order
121
143
  await user.click(sortRowsButton);
122
-
123
- expect(screen.getAllByRole('row')).not.toEqual(initialRowElements);
144
+ expect(getRowDates()).toEqual(expectedAscendingOrder);
124
145
 
125
146
  // Sorting order = NONE, hence it is still in the ascending order
126
147
  await user.click(sortRowsButton);
127
148
  // Sorting in descending order
128
149
  await user.click(sortRowsButton);
129
150
 
130
- expect(screen.getAllByRole('row')).toEqual(initialRowElements);
151
+ expect(getRowDates()).toEqual(expectedDescendingOrder);
131
152
  });
132
153
 
133
154
  it('toggles between rendering either a tabular view or a chart view', async () => {
@@ -12,6 +12,7 @@ import {
12
12
  import { useLayoutType, usePagination } from '@openmrs/esm-framework';
13
13
  import { PatientChartPagination } from '@openmrs/esm-patient-common-lib';
14
14
  import type { BiometricsTableHeader, BiometricsTableRow } from './types';
15
+ import { VitalsAndBiometricsActionMenu } from '../components/action-menu/vitals-biometrics-action-menu.component';
15
16
  import styles from './paginated-biometrics.scss';
16
17
 
17
18
  interface PaginatedBiometricsProps {
@@ -94,6 +95,7 @@ const PaginatedBiometrics: React.FC<PaginatedBiometricsProps> = ({
94
95
  {header.header?.content ?? header.header}
95
96
  </TableHeader>
96
97
  ))}
98
+ <TableHeader />
97
99
  </TableRow>
98
100
  </TableHead>
99
101
  <TableBody>
@@ -102,6 +104,9 @@ const PaginatedBiometrics: React.FC<PaginatedBiometricsProps> = ({
102
104
  {row.cells.map((cell) => (
103
105
  <TableCell key={cell.id}>{cell.value?.content ?? cell.value}</TableCell>
104
106
  ))}
107
+ <TableCell className="cds--table-column-menu" id="actions">
108
+ <VitalsAndBiometricsActionMenu encounterUuid={row.id} />
109
+ </TableCell>
105
110
  </TableRow>
106
111
  ))}
107
112
  </TableBody>
@@ -6,18 +6,20 @@ import {
6
6
  restBaseUrl,
7
7
  openmrsFetch,
8
8
  useConfig,
9
+ type OpenmrsResource,
9
10
  } from '@openmrs/esm-framework';
10
11
  import useSWRImmutable from 'swr/immutable';
11
12
  import useSWRInfinite from 'swr/infinite';
12
13
  import { type ConfigObject } from '../config-schema';
13
14
  import { assessValue, calculateBodyMassIndex, getReferenceRangesForConcept, interpretBloodPressure } from './helpers';
14
15
  import type { FHIRSearchBundleResponse, MappedVitals, PatientVitalsAndBiometrics, VitalsResponse } from './types';
15
- import { type VitalsBiometricsFormData } from '../vitals-biometrics-form/vitals-biometrics-form.workspace';
16
16
 
17
17
  const pageSize = 100;
18
18
 
19
19
  /** We use this as the first value to the SWR key to be able to invalidate all relevant cached entries */
20
20
  const swrKeyNeedle = Symbol('vitalsAndBiometrics');
21
+ const encounterRepresentation =
22
+ 'custom:(uuid,encounterDatetime,encounterType:(uuid,display),obs:(uuid,concept:(uuid,display),value,interpretation))';
21
23
 
22
24
  type VitalsAndBiometricsMode = 'vitals' | 'biometrics' | 'both';
23
25
 
@@ -48,6 +50,14 @@ interface VitalsConceptMetadataResponse {
48
50
  setMembers: Array<ConceptMetadata>;
49
51
  }
50
52
 
53
+ export type VitalsAndBiometricsFieldValuesMap = Map<string, { value: number | string; obs: OpenmrsResource }>;
54
+
55
+ interface PartialEncounter extends OpenmrsResource {
56
+ encounterType: OpenmrsResource;
57
+ encounterDatetime: string;
58
+ obs: Array<OpenmrsResource>;
59
+ }
60
+
51
61
  function getInterpretationKey(header: string) {
52
62
  // Reason for `Render` string is to match the column header in the table
53
63
  return `${header}RenderInterpretation`;
@@ -105,6 +115,36 @@ export const withUnit = (label: string, unit: string | null | undefined) => {
105
115
  let vitalsHooksCounter = 0;
106
116
  const vitalsHooksMutates = new Map<number, ReturnType<typeof useSWRInfinite<VitalsFetchResponse>>['mutate']>();
107
117
 
118
+ /**
119
+ * Hook to get concepts for vitals, biometrics or both
120
+ */
121
+ export function useVitalsOrBiometricsConcepts(mode: VitalsAndBiometricsMode) {
122
+ const { concepts } = useConfig<ConfigObject>();
123
+
124
+ const conceptUuids = useMemo(() => {
125
+ const biometricsKeys = ['heightUuid', 'midUpperArmCircumferenceUuid', 'weightUuid'];
126
+
127
+ if (!concepts) {
128
+ return [];
129
+ }
130
+
131
+ return Object.entries(concepts)
132
+ .filter(([key, conceptUuid]) => {
133
+ if (!conceptUuid) {
134
+ console.warn(`Missing UUID for concept ${key}`);
135
+ return false;
136
+ }
137
+ if (mode === 'both') {
138
+ return true;
139
+ }
140
+ return mode === 'vitals' ? !biometricsKeys.includes(key) : biometricsKeys.includes(key);
141
+ })
142
+ .map(([_, conceptUuid]) => conceptUuid);
143
+ }, [concepts, mode]);
144
+
145
+ return conceptUuids;
146
+ }
147
+
108
148
  /**
109
149
  * Hook to get the vitals and / or biometrics for a patient
110
150
  *
@@ -115,30 +155,14 @@ const vitalsHooksMutates = new Map<number, ReturnType<typeof useSWRInfinite<Vita
115
155
  export function useVitalsAndBiometrics(patientUuid: string, mode: VitalsAndBiometricsMode = 'vitals') {
116
156
  const { conceptMetadata } = useVitalsConceptMetadata();
117
157
  const { concepts } = useConfig<ConfigObject>();
118
- const biometricsConcepts = useMemo(
119
- () => [concepts.heightUuid, concepts.midUpperArmCircumferenceUuid, concepts.weightUuid],
120
- [concepts.heightUuid, concepts.midUpperArmCircumferenceUuid, concepts.weightUuid],
121
- );
122
-
123
- const conceptUuids = useMemo(
124
- () =>
125
- (mode === 'both'
126
- ? Object.values(concepts)
127
- : Object.values(concepts).filter(
128
- (uuid) =>
129
- (mode === 'vitals' && !biometricsConcepts.includes(uuid)) ||
130
- (mode === 'biometrics' && biometricsConcepts.includes(uuid)),
131
- )
132
- ).join(','),
133
- [concepts, biometricsConcepts, mode],
134
- );
158
+ const conceptUuids = useVitalsOrBiometricsConcepts(mode);
135
159
 
136
160
  const getPage = useCallback(
137
161
  (page: number, prevPageData: FHIRSearchBundleResponse): VitalsAndBiometricsSwrKey => ({
138
162
  swrKeyNeedle,
139
163
  mode,
140
164
  patientUuid,
141
- conceptUuids,
165
+ conceptUuids: conceptUuids.join(','),
142
166
  page,
143
167
  prevPageData,
144
168
  }),
@@ -203,17 +227,20 @@ export function useVitalsAndBiometrics(patientUuid: string, mode: VitalsAndBiome
203
227
  .filter(Boolean)
204
228
  .map(vitalsProperties(conceptMetadata))
205
229
  ?.reduce((vitalsHashTable, vitalSign) => {
206
- const recordedDate = new Date(new Date(vitalSign.recordedDate)).toISOString();
207
-
208
- if (vitalsHashTable.has(recordedDate) && vitalsHashTable.get(recordedDate)) {
209
- vitalsHashTable.set(recordedDate, {
210
- ...vitalsHashTable.get(recordedDate),
230
+ const encounterId = vitalSign.encounterId;
231
+ if (vitalsHashTable.has(encounterId) && vitalsHashTable.get(encounterId)) {
232
+ vitalsHashTable.set(encounterId, {
233
+ ...vitalsHashTable.get(encounterId),
211
234
  [getVitalsMapKey(vitalSign.code)]: vitalSign.value,
212
235
  [getInterpretationKey(getVitalsMapKey(vitalSign.code))]: vitalSign.interpretation,
213
236
  });
214
237
  } else {
215
238
  vitalSign.value &&
216
- vitalsHashTable.set(recordedDate, {
239
+ vitalsHashTable.set(encounterId, {
240
+ date:
241
+ typeof vitalSign.recordedDate === 'string'
242
+ ? vitalSign.recordedDate
243
+ : vitalSign.recordedDate.toDateString(),
217
244
  [getVitalsMapKey(vitalSign.code)]: vitalSign.value,
218
245
  [getInterpretationKey(getVitalsMapKey(vitalSign.code))]: vitalSign.interpretation,
219
246
  });
@@ -222,10 +249,10 @@ export function useVitalsAndBiometrics(patientUuid: string, mode: VitalsAndBiome
222
249
  return vitalsHashTable;
223
250
  }, new Map<string, Partial<PatientVitalsAndBiometrics>>());
224
251
 
225
- return Array.from(vitalsHashTable ?? []).map(([date, vitalSigns], index) => {
252
+ return Array.from(vitalsHashTable ?? []).map(([encounterId, vitalSigns]) => {
226
253
  const result = {
227
- id: index.toString(),
228
- date: date,
254
+ id: encounterId,
255
+ date: vitalSigns.date,
229
256
  ...vitalSigns,
230
257
  };
231
258
 
@@ -262,6 +289,54 @@ export function useVitalsAndBiometrics(patientUuid: string, mode: VitalsAndBiome
262
289
  };
263
290
  }
264
291
 
292
+ /**
293
+ * Hook to get the vitals and biometrics for a patient associated with a specific encounter
294
+ */
295
+ export function useEncounterVitalsAndBiometrics(encounterUuid: string) {
296
+ const { concepts } = useConfig<ConfigObject>();
297
+ const fieldNameSuffix = 'Uuid';
298
+ const url = encounterUuid ? `${restBaseUrl}/encounter/${encounterUuid}?v=${encounterRepresentation}` : null;
299
+
300
+ const { data, isLoading, error, mutate } = useSWRImmutable<FetchResponse<PartialEncounter>, Error>(url, openmrsFetch);
301
+
302
+ const vitalsAndBiometrics: VitalsAndBiometricsFieldValuesMap = useMemo(() => {
303
+ if (!isLoading && data && concepts) {
304
+ return data.data.obs.reduce((vitalsAndBiometrics, obs) => {
305
+ let fieldName = Object.entries(concepts).find(([, value]) => value === obs.concept.uuid)?.[0];
306
+
307
+ if (fieldName) {
308
+ fieldName = fieldName.endsWith(fieldNameSuffix) ? fieldName.replace(fieldNameSuffix, '') : fieldName;
309
+ vitalsAndBiometrics.set(fieldName, {
310
+ value: obs.value,
311
+ obs,
312
+ });
313
+ }
314
+ return vitalsAndBiometrics;
315
+ }, new Map<string, { value: string; obs: OpenmrsResource }>());
316
+ }
317
+ return null;
318
+ }, [isLoading, data, concepts]);
319
+
320
+ const getRefinedInitialValues = useCallback(() => {
321
+ const initialValues: Record<string, string | number> = {};
322
+ if (vitalsAndBiometrics) {
323
+ vitalsAndBiometrics.forEach((value, key) => {
324
+ initialValues[key] = value.value;
325
+ });
326
+ }
327
+ return initialValues;
328
+ }, [vitalsAndBiometrics]);
329
+
330
+ return {
331
+ isLoading,
332
+ vitalsAndBiometrics,
333
+ encounter: data?.data,
334
+ error,
335
+ mutate,
336
+ getRefinedInitialValues,
337
+ };
338
+ }
339
+
265
340
  /**
266
341
  * Fetcher for the useVitalsAndBiometricsHook
267
342
  * @internal
@@ -299,71 +374,49 @@ function vitalsProperties(conceptMetadata: Array<ConceptMetadata> | undefined) {
299
374
  ),
300
375
  recordedDate: resource?.effectiveDateTime,
301
376
  value: resource?.valueQuantity?.value,
377
+ encounterId: extractEncounterUuid(resource.encounter),
302
378
  });
303
379
  }
304
380
 
305
- export function saveVitalsAndBiometrics(
306
- encounterTypeUuid: string,
307
- formUuid: string,
308
- concepts: ConfigObject['concepts'],
381
+ export function createOrUpdateVitalsAndBiometrics(
309
382
  patientUuid: string,
310
- vitals: VitalsBiometricsFormData,
311
- abortController: AbortController,
383
+ encounterTypeUuid: string,
384
+ encounterUuid: string | null,
312
385
  location: string,
386
+ vitalsAndBiometricsObs: Array<OpenmrsResource>,
387
+ abortController: AbortController,
313
388
  ) {
314
- return openmrsFetch<unknown>(`${restBaseUrl}/encounter`, {
389
+ const url = encounterUuid ? `${restBaseUrl}/encounter/${encounterUuid}` : `${restBaseUrl}/encounter`;
390
+
391
+ const encounter = {
392
+ patient: patientUuid,
393
+ obs: vitalsAndBiometricsObs,
394
+ };
395
+ if (!encounterUuid) {
396
+ encounter['location'] = location;
397
+ encounter['encounterType'] = encounterTypeUuid;
398
+ }
399
+ return openmrsFetch(url, {
315
400
  method: 'POST',
316
401
  headers: {
317
402
  'Content-Type': 'application/json',
318
403
  },
319
404
  signal: abortController.signal,
320
- body: {
321
- patient: patientUuid,
322
- location: location,
323
- encounterType: encounterTypeUuid,
324
- form: formUuid,
325
- obs: createObsObject(vitals, concepts),
326
- },
405
+ body: encounter,
327
406
  });
328
407
  }
329
408
 
330
- export function updateVitalsAndBiometrics(
331
- concepts: ConfigObject['concepts'],
332
- patientUuid: string,
333
- vitals: VitalsBiometricsFormData,
334
- encounterDatetime: Date,
335
- abortController: AbortController,
336
- encounterUuid: string,
337
- location: string,
338
- ) {
409
+ export function deleteEncounter(encounterUuid: string) {
339
410
  return openmrsFetch(`${restBaseUrl}/encounter/${encounterUuid}`, {
340
- method: 'POST',
341
- headers: {
342
- 'Content-Type': 'application/json',
343
- },
344
- signal: abortController.signal,
345
- body: {
346
- encounterDatetime: encounterDatetime,
347
- location: location,
348
- patient: patientUuid,
349
- obs: createObsObject(vitals, concepts),
350
- orders: [],
351
- },
411
+ method: 'DELETE',
352
412
  });
353
413
  }
354
414
 
355
- function createObsObject(
356
- vitals: VitalsBiometricsFormData,
357
- concepts: ConfigObject['concepts'],
358
- ): Array<{ concept: string; value: string | number }> {
359
- return Object.entries(vitals)
360
- .filter(([_, result]) => Boolean(result))
361
- .map(([name, result]) => {
362
- return {
363
- concept: concepts[name + 'Uuid'],
364
- value: result,
365
- };
366
- });
415
+ function extractEncounterUuid(encounter: FHIRResource['resource']['encounter']): string {
416
+ if (!encounter || !encounter.reference) {
417
+ return '';
418
+ }
419
+ return encounter.reference.split('/')[1];
367
420
  }
368
421
 
369
422
  /**
@@ -1,4 +1,7 @@
1
+ import { type OpenmrsResource } from '@openmrs/esm-framework';
1
2
  import { type ConceptMetadata } from '../common';
3
+ import { type VitalsBiometricsFormData } from '../vitals-biometrics-form/schema';
4
+ import { type VitalsAndBiometricsFieldValuesMap } from './data.resource';
2
5
  import type { ObsReferenceRanges, ObservationInterpretation } from './types';
3
6
 
4
7
  export function calculateBodyMassIndex(weight: number, height: number) {
@@ -98,3 +101,50 @@ export function getReferenceRangesForConcept(
98
101
 
99
102
  return conceptMetadata?.find((metadata) => metadata.uuid === conceptUuid);
100
103
  }
104
+
105
+ export function prepareObsForSubmission(
106
+ formData: VitalsBiometricsFormData,
107
+ dirtyFields: Record<string, boolean>,
108
+ formContext: 'creating' | 'editing',
109
+ initialFieldValuesMap: VitalsAndBiometricsFieldValuesMap,
110
+ fieldToConceptMap: Record<string, string>,
111
+ ): {
112
+ toBeVoided: Array<OpenmrsResource>;
113
+ newObs: Array<OpenmrsResource>;
114
+ } {
115
+ return Object.entries(formData).reduce(
116
+ (obsForSubmission, [field, newValue]) => {
117
+ if (!fieldToConceptMap[`${field}Uuid`]) {
118
+ console.error(`Missing concept mapping for field: ${field}`);
119
+ return obsForSubmission;
120
+ }
121
+ if (formContext === 'editing' && initialFieldValuesMap.has(field) && dirtyFields[field]) {
122
+ // void old obs
123
+ const { obs } = initialFieldValuesMap.get(field);
124
+ obsForSubmission.toBeVoided.push({
125
+ uuid: obs.uuid,
126
+ voided: true,
127
+ });
128
+
129
+ if (newValue) {
130
+ obsForSubmission.newObs.push({
131
+ concept: fieldToConceptMap[`${field}Uuid`],
132
+ value: newValue,
133
+ });
134
+ }
135
+ } else if (dirtyFields[field] && newValue) {
136
+ // create new obs
137
+ obsForSubmission.newObs.push({
138
+ concept: fieldToConceptMap[`${field}Uuid`],
139
+ value: newValue,
140
+ });
141
+ }
142
+
143
+ return obsForSubmission;
144
+ },
145
+ {
146
+ toBeVoided: [],
147
+ newObs: [],
148
+ },
149
+ );
150
+ }
@@ -1,9 +1,10 @@
1
1
  export {
2
+ deleteEncounter,
2
3
  invalidateCachedVitalsAndBiometrics,
3
4
  useVitalsAndBiometrics,
4
5
  useVitalsConceptMetadata,
5
- updateVitalsAndBiometrics,
6
- saveVitalsAndBiometrics,
6
+ createOrUpdateVitalsAndBiometrics,
7
+ useEncounterVitalsAndBiometrics,
7
8
  withUnit,
8
9
  type ConceptMetadata,
9
10
  } from './data.resource';
@@ -21,8 +21,9 @@ export type ObservationInterpretation = 'critically_low' | 'critically_high' | '
21
21
  export type MappedVitals = {
22
22
  code: string;
23
23
  interpretation: string;
24
- recordedDate: Date;
24
+ recordedDate: string | Date;
25
25
  value: number;
26
+ encounterId: string;
26
27
  };
27
28
 
28
29
  export interface PatientVitalsAndBiometrics {
@@ -0,0 +1,60 @@
1
+ import React, { useCallback } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Layer, OverflowMenu, OverflowMenuItem } from '@carbon/react';
4
+ import { getPatientUuidFromStore } from '@openmrs/esm-patient-common-lib';
5
+ import { launchWorkspace, showModal, useLayoutType } from '@openmrs/esm-framework';
6
+ import styles from './vitals-biometrics-action-menu.scss';
7
+ import { patientVitalsBiometricsFormWorkspace } from '../../constants';
8
+
9
+ interface VitalsAndBiometricsActionMenuProps {
10
+ encounterUuid: string;
11
+ }
12
+
13
+ export const VitalsAndBiometricsActionMenu = ({ encounterUuid }: VitalsAndBiometricsActionMenuProps) => {
14
+ const { t } = useTranslation();
15
+ const patientUuid = getPatientUuidFromStore();
16
+ const isTablet = useLayoutType() === 'tablet';
17
+
18
+ const handleLaunchVitalsAndBiometricsForm = useCallback(() => {
19
+ launchWorkspace(patientVitalsBiometricsFormWorkspace, {
20
+ workspaceTitle: t('editVitalsAndBiometrics', 'Edit Vitals and Biometrics'),
21
+ editEncounterUuid: encounterUuid,
22
+ formContext: 'editing',
23
+ });
24
+ }, [encounterUuid, t]);
25
+
26
+ const handleLaunchDeleteVitalsAndBiometricsModal = useCallback(() => {
27
+ const dispose = showModal('vitals-biometrics-delete-confirmation-modal', {
28
+ closeDeleteModal: () => dispose(),
29
+ encounterUuid,
30
+ patientUuid,
31
+ });
32
+ }, [encounterUuid, patientUuid]);
33
+
34
+ return (
35
+ <Layer className={styles.layer}>
36
+ <OverflowMenu
37
+ aria-label={t('editOrDeleteVitalsAndBiometrics', 'Edit or delete Vitals and Biometrics')}
38
+ size={isTablet ? 'lg' : 'sm'}
39
+ flipped
40
+ align="left"
41
+ id={encounterUuid}
42
+ >
43
+ <OverflowMenuItem
44
+ className={styles.menuItem}
45
+ id="editVitalsAndBiometrics"
46
+ onClick={handleLaunchVitalsAndBiometricsForm}
47
+ itemText={t('edit', 'Edit')}
48
+ />
49
+ <OverflowMenuItem
50
+ className={styles.menuItem}
51
+ id="deleteVitalsAndBiometrics"
52
+ itemText={t('delete', 'Delete')}
53
+ onClick={handleLaunchDeleteVitalsAndBiometricsModal}
54
+ isDelete
55
+ hasDivider
56
+ />
57
+ </OverflowMenu>
58
+ </Layer>
59
+ );
60
+ };
@@ -0,0 +1,11 @@
1
+ .layer {
2
+ height: 100%;
3
+
4
+ :global(.cds--overflow-menu) {
5
+ min-height: unset;
6
+ }
7
+ }
8
+
9
+ .menuItem {
10
+ max-width: none;
11
+ }
@@ -0,0 +1,84 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import { showSnackbar } from '@openmrs/esm-framework';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { deleteEncounter, invalidateCachedVitalsAndBiometrics } from '../../common';
5
+ import { ModalHeader, ModalBody, ModalFooter, Button, InlineLoading } from '@carbon/react';
6
+
7
+ interface DeleteVitalsAndBiometricsModalProps {
8
+ patientUuid: string;
9
+ encounterUuid: string;
10
+ closeDeleteModal: () => void;
11
+ }
12
+
13
+ const DeleteVitalsAndBiometricsModal: React.FC<DeleteVitalsAndBiometricsModalProps> = ({
14
+ encounterUuid,
15
+ closeDeleteModal,
16
+ }) => {
17
+ const { t } = useTranslation();
18
+ const [isDeleting, setIsDeleting] = useState(false);
19
+
20
+ const handleDelete = useCallback(async () => {
21
+ if (!encounterUuid) {
22
+ showSnackbar({
23
+ isLowContrast: false,
24
+ kind: 'error',
25
+ title: t('errorDeleting', 'Error deleting vitals and biometrics'),
26
+ subtitle: t('encounterUuidRequired', 'Encounter UUID is required to delete vitals and biometrics'),
27
+ });
28
+ return;
29
+ }
30
+
31
+ setIsDeleting(true);
32
+ deleteEncounter(encounterUuid)
33
+ .then(() => {
34
+ invalidateCachedVitalsAndBiometrics();
35
+ closeDeleteModal();
36
+ showSnackbar({
37
+ isLowContrast: true,
38
+ kind: 'success',
39
+ title: t('vitalsAndBiometricsDeleted', 'Vitals and biometrics deleted'),
40
+ });
41
+ })
42
+ .catch((error) => {
43
+ console.error('Error deleting encounter: ', error);
44
+ showSnackbar({
45
+ isLowContrast: false,
46
+ kind: 'error',
47
+ title: t('errorDeleting', 'Error deleting vitals and biometrics'),
48
+ subtitle: error?.message,
49
+ });
50
+ })
51
+ .finally(() => setIsDeleting(false));
52
+ }, [encounterUuid, t, closeDeleteModal]);
53
+
54
+ return (
55
+ <>
56
+ <ModalHeader
57
+ closeModal={closeDeleteModal}
58
+ title={t('deleteVitalsAndBiometrics', 'Delete vitals and biometrics')}
59
+ />
60
+ <ModalBody>
61
+ <p>
62
+ {t(
63
+ 'deleteConfirmationText',
64
+ 'Note: Deleting these entries will also remove related vitals or biometrics data. Are you sure you want to continue?',
65
+ )}
66
+ </p>
67
+ </ModalBody>
68
+ <ModalFooter>
69
+ <Button kind="secondary" onClick={closeDeleteModal}>
70
+ {t('cancel', 'Cancel')}
71
+ </Button>
72
+ <Button kind="danger" onClick={handleDelete} disabled={isDeleting}>
73
+ {isDeleting ? (
74
+ <InlineLoading description={t('deleting', 'Deleting') + '...'} />
75
+ ) : (
76
+ <span>{t('delete', 'Delete')}</span>
77
+ )}
78
+ </Button>
79
+ </ModalFooter>
80
+ </>
81
+ );
82
+ };
83
+
84
+ export default DeleteVitalsAndBiometricsModal;
@@ -2,8 +2,8 @@ import React from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { InlineLoading } from '@carbon/react';
4
4
  import { useConfig } from '@openmrs/esm-framework';
5
- import { useVitalsAndBiometrics, useVitalsConceptMetadata } from '../common';
6
- import { type ConfigObject } from '../config-schema';
5
+ import { useVitalsAndBiometrics, useVitalsConceptMetadata } from '../../common';
6
+ import { type ConfigObject } from '../../config-schema';
7
7
  import styles from './weight-tile.scss';
8
8
 
9
9
  interface WeightTileInterface {
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
2
  import { screen } from '@testing-library/react';
3
3
  import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
4
- import { configSchema, type ConfigObject } from '../config-schema';
4
+ import { configSchema, type ConfigObject } from '../../config-schema';
5
5
  import { getByTextWithMarkup, mockPatient, renderWithSwr, waitForLoadingToFinish } from 'tools';
6
6
  import { formattedBiometrics, mockBiometricsConfig, mockConceptMetadata, mockVitalsSignsConcepts } from '__mocks__';
7
- import { useVitalsAndBiometrics } from '../common';
7
+ import { useVitalsAndBiometrics } from '../../common';
8
8
  import WeightTile from './weight-tile.component';
9
9
 
10
10
  const mockUseConfig = jest.mocked(useConfig<ConfigObject>);
@@ -13,8 +13,8 @@ const mockConceptUnits = new Map<string, string>(
13
13
  mockVitalsSignsConcepts.data.results[0].setMembers.map((concept) => [concept.uuid, concept.units]),
14
14
  );
15
15
 
16
- jest.mock('../common', () => {
17
- const originalModule = jest.requireActual('../common');
16
+ jest.mock('../../common', () => {
17
+ const originalModule = jest.requireActual('../../common');
18
18
 
19
19
  return {
20
20
  ...originalModule,
package/src/index.ts CHANGED
@@ -58,10 +58,15 @@ export const vitalsAndBiometricsDashboardLink =
58
58
  options,
59
59
  );
60
60
 
61
- export const weightTile = getAsyncLifecycle(() => import('./weight-tile/weight-tile.component'), options);
61
+ export const weightTile = getAsyncLifecycle(() => import('./components/weight-tile/weight-tile.component'), options);
62
62
 
63
63
  // t('recordVitalsAndBiometrics', 'Record Vitals and Biometrics')
64
64
  export const vitalsBiometricsFormWorkspace = getAsyncLifecycle(
65
65
  () => import('./vitals-biometrics-form/vitals-biometrics-form.workspace'),
66
66
  options,
67
67
  );
68
+
69
+ export const vitalsAndBiometricsDeleteConfirmationModal = getAsyncLifecycle(
70
+ () => import('./components/delete-vitals-biometrics-modal/delete-vitals-biometrics.modal'),
71
+ options,
72
+ );