@openmrs/esm-patient-vitals-app 9.2.3-pre.7182 → 9.2.3-pre.7191
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +19 -19
- package/dist/101.js +2 -0
- package/dist/101.js.map +1 -0
- package/dist/{9057.js → 1423.js} +1 -1
- package/dist/1423.js.map +1 -0
- package/dist/4300.js +1 -1
- package/dist/5207.js +1 -0
- package/dist/5207.js.map +1 -0
- package/dist/5387.js +1 -0
- package/dist/5387.js.map +1 -0
- package/dist/5395.js +2 -0
- package/dist/{3368.js.LICENSE.txt → 5395.js.LICENSE.txt} +19 -1
- package/dist/5395.js.map +1 -0
- package/dist/8295.js +2 -0
- package/dist/8295.js.LICENSE.txt +7 -0
- package/dist/8295.js.map +1 -0
- package/dist/8953.js +1 -0
- package/dist/8953.js.map +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-patient-vitals-app.js +1 -1
- package/dist/openmrs-esm-patient-vitals-app.js.buildmanifest.json +152 -128
- package/dist/openmrs-esm-patient-vitals-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/biometrics/biometrics-base.component.tsx +0 -1
- package/src/biometrics/biometrics-overview.test.tsx +26 -5
- package/src/biometrics/paginated-biometrics.component.tsx +5 -0
- package/src/common/data.resource.ts +128 -75
- package/src/common/helpers.ts +50 -0
- package/src/common/index.ts +3 -2
- package/src/common/types.ts +2 -1
- package/src/components/action-menu/vitals-biometrics-action-menu.component.tsx +60 -0
- package/src/components/action-menu/vitals-biometrics-action-menu.scss +11 -0
- package/src/components/delete-vitals-biometrics-modal/delete-vitals-biometrics.modal.tsx +84 -0
- package/src/{weight-tile → components/weight-tile}/weight-tile.component.tsx +2 -2
- package/src/{weight-tile → components/weight-tile}/weight-tile.test.tsx +4 -4
- package/src/index.ts +6 -1
- package/src/routes.json +6 -0
- package/src/vitals/paginated-vitals.component.tsx +5 -0
- package/src/vitals/vitals-overview.component.tsx +3 -2
- package/src/vitals-biometrics-form/schema.ts +28 -0
- package/src/vitals-biometrics-form/vitals-biometrics-form.test.tsx +203 -21
- package/src/vitals-biometrics-form/vitals-biometrics-form.workspace.tsx +78 -60
- package/src/vitals-biometrics-form/vitals-biometrics-input.component.tsx +1 -1
- package/translations/en.json +14 -1
- package/dist/3368.js +0 -2
- package/dist/3368.js.map +0 -1
- package/dist/4716.js +0 -1
- package/dist/4716.js.map +0 -1
- package/dist/7738.js +0 -2
- package/dist/7738.js.LICENSE.txt +0 -25
- package/dist/7738.js.map +0 -1
- package/dist/8895.js +0 -2
- package/dist/8895.js.map +0 -1
- package/dist/8957.js +0 -1
- package/dist/8957.js.map +0 -1
- package/dist/9057.js.map +0 -1
- /package/dist/{8895.js.LICENSE.txt → 101.js.LICENSE.txt} +0 -0
- /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(
|
|
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
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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(
|
|
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(([
|
|
252
|
+
return Array.from(vitalsHashTable ?? []).map(([encounterId, vitalSigns]) => {
|
|
226
253
|
const result = {
|
|
227
|
-
id:
|
|
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
|
|
306
|
-
encounterTypeUuid: string,
|
|
307
|
-
formUuid: string,
|
|
308
|
-
concepts: ConfigObject['concepts'],
|
|
381
|
+
export function createOrUpdateVitalsAndBiometrics(
|
|
309
382
|
patientUuid: string,
|
|
310
|
-
|
|
311
|
-
|
|
383
|
+
encounterTypeUuid: string,
|
|
384
|
+
encounterUuid: string | null,
|
|
312
385
|
location: string,
|
|
386
|
+
vitalsAndBiometricsObs: Array<OpenmrsResource>,
|
|
387
|
+
abortController: AbortController,
|
|
313
388
|
) {
|
|
314
|
-
|
|
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
|
|
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: '
|
|
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
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return
|
|
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
|
/**
|
package/src/common/helpers.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/common/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export {
|
|
2
|
+
deleteEncounter,
|
|
2
3
|
invalidateCachedVitalsAndBiometrics,
|
|
3
4
|
useVitalsAndBiometrics,
|
|
4
5
|
useVitalsConceptMetadata,
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
createOrUpdateVitalsAndBiometrics,
|
|
7
|
+
useEncounterVitalsAndBiometrics,
|
|
7
8
|
withUnit,
|
|
8
9
|
type ConceptMetadata,
|
|
9
10
|
} from './data.resource';
|
package/src/common/types.ts
CHANGED
|
@@ -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,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 '
|
|
6
|
-
import { type ConfigObject } from '
|
|
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 '
|
|
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 '
|
|
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('
|
|
17
|
-
const originalModule = jest.requireActual('
|
|
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
|
+
);
|