@openmrs/esm-generic-patient-widgets-app 11.3.1-pre.9400 → 11.3.1-pre.9403
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 +16 -16
- package/dist/1936.js +1 -0
- package/dist/1936.js.map +1 -0
- package/dist/2606.js +2 -0
- package/dist/2606.js.map +1 -0
- package/dist/4300.js +1 -1
- package/dist/5670.js +1 -1
- package/dist/7545.js +2 -0
- package/dist/7545.js.map +1 -0
- package/dist/8803.js +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-generic-patient-widgets-app.js +1 -1
- package/dist/openmrs-esm-generic-patient-widgets-app.js.buildmanifest.json +91 -91
- package/dist/openmrs-esm-generic-patient-widgets-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/config-schema-obs-horizontal.ts +12 -0
- package/src/obs-graph/obs-graph.component.tsx +12 -12
- package/src/obs-switchable/obs-switchable.component.tsx +5 -9
- package/src/obs-switchable/obs-switchable.test.tsx +22 -12
- package/src/obs-table/obs-table.component.tsx +2 -1
- package/src/obs-table-horizontal/obs-table-horizontal.component.tsx +466 -56
- package/src/obs-table-horizontal/obs-table-horizontal.resource.ts +67 -0
- package/src/obs-table-horizontal/obs-table-horizontal.scss +47 -0
- package/src/obs-table-horizontal/obs-table-horizontal.test.tsx +912 -0
- package/src/resources/useConcepts.ts +14 -4
- package/src/resources/useEncounterTypes.ts +34 -0
- package/src/resources/useObs.ts +29 -47
- package/translations/en.json +6 -1
- package/dist/251.js +0 -2
- package/dist/251.js.map +0 -1
- package/dist/8743.js +0 -2
- package/dist/8743.js.map +0 -1
- package/dist/9351.js +0 -1
- package/dist/9351.js.map +0 -1
- /package/dist/{251.js.LICENSE.txt → 2606.js.LICENSE.txt} +0 -0
- /package/dist/{8743.js.LICENSE.txt → 7545.js.LICENSE.txt} +0 -0
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { usePagination, useConfig, formatDate, formatTime } from '@openmrs/esm-framework';
|
|
1
|
+
import React, { useState, useCallback, type ComponentProps, useMemo } from 'react';
|
|
3
2
|
import {
|
|
3
|
+
usePagination,
|
|
4
|
+
useConfig,
|
|
5
|
+
formatDate,
|
|
6
|
+
formatTime,
|
|
7
|
+
showSnackbar,
|
|
8
|
+
useSession,
|
|
9
|
+
useLayoutType,
|
|
10
|
+
isDesktop,
|
|
11
|
+
EditIcon,
|
|
12
|
+
AddIcon,
|
|
13
|
+
type Concept,
|
|
14
|
+
userHasAccess,
|
|
15
|
+
type Privilege,
|
|
16
|
+
getCoreTranslation,
|
|
17
|
+
} from '@openmrs/esm-framework';
|
|
18
|
+
import {
|
|
19
|
+
Button,
|
|
4
20
|
Table,
|
|
5
21
|
TableCell,
|
|
6
22
|
TableContainer,
|
|
@@ -9,29 +25,97 @@ import {
|
|
|
9
25
|
TableHeader,
|
|
10
26
|
TableRow,
|
|
11
27
|
InlineLoading,
|
|
28
|
+
TextInput,
|
|
29
|
+
NumberInput,
|
|
30
|
+
Select,
|
|
31
|
+
SelectItem,
|
|
32
|
+
IconButton,
|
|
12
33
|
} from '@carbon/react';
|
|
34
|
+
import { Checkmark, Close } from '@carbon/react/icons';
|
|
13
35
|
import { CardHeader, PatientChartPagination } from '@openmrs/esm-patient-common-lib';
|
|
14
36
|
import { useObs } from '../resources/useObs';
|
|
15
37
|
import styles from './obs-table-horizontal.scss';
|
|
16
38
|
import { useTranslation } from 'react-i18next';
|
|
17
39
|
import { type ConfigObjectHorizontal } from '../config-schema-obs-horizontal';
|
|
40
|
+
import { updateObservation, createObservationInEncounter, createEncounter } from './obs-table-horizontal.resource';
|
|
41
|
+
import classNames from 'classnames';
|
|
42
|
+
import { useEncounterTypes } from '../resources/useEncounterTypes';
|
|
18
43
|
|
|
19
44
|
interface ObsTableHorizontalProps {
|
|
20
45
|
patientUuid: string;
|
|
21
46
|
}
|
|
22
47
|
|
|
48
|
+
interface ColumnData {
|
|
49
|
+
id: string;
|
|
50
|
+
date: Date;
|
|
51
|
+
encounter: { value: string; editPrivilege: Privilege };
|
|
52
|
+
encounterReference: string;
|
|
53
|
+
encounterUuid: string | null; // null for temporary encounters
|
|
54
|
+
obs: Record<string, CellData>;
|
|
55
|
+
isTemporary?: boolean; // true for encounters that haven't been saved yet
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface CellData {
|
|
59
|
+
value: string | number;
|
|
60
|
+
obsUuid: string;
|
|
61
|
+
dataType: string;
|
|
62
|
+
display?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* This component displays a table of observations, where each column represents an encounter.
|
|
67
|
+
* It may be 'editable' or not. If it is editable, then
|
|
68
|
+
* - Individual observations can be edited by tapping them or clicking an edit button.
|
|
69
|
+
* - New encounters can be created by tapping a "plus" button. When the plus button is
|
|
70
|
+
* tapped, a temporary encounter is created on the front-end only. The new encounter is
|
|
71
|
+
* only saved to the backend when the first obs value for it is entered.
|
|
72
|
+
*/
|
|
23
73
|
const ObsTableHorizontal: React.FC<ObsTableHorizontalProps> = ({ patientUuid }) => {
|
|
24
74
|
const { t } = useTranslation();
|
|
25
75
|
const config = useConfig<ConfigObjectHorizontal>();
|
|
76
|
+
const isTablet = !isDesktop(useLayoutType());
|
|
26
77
|
const {
|
|
27
|
-
data: { observations, concepts },
|
|
78
|
+
data: { observations, concepts, encounters },
|
|
28
79
|
isValidating,
|
|
80
|
+
mutate,
|
|
29
81
|
} = useObs(patientUuid);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
82
|
+
|
|
83
|
+
const [temporaryEncounters, setTemporaryEncounters] = useState<Array<ColumnData>>([]);
|
|
84
|
+
|
|
85
|
+
let obssGroupedByEncounters = useMemo(
|
|
86
|
+
() =>
|
|
87
|
+
encounters?.length
|
|
88
|
+
? encounters.map((encounter) => observations.filter((o) => o.encounter.reference === encounter.reference))
|
|
89
|
+
: [],
|
|
90
|
+
[encounters, observations],
|
|
33
91
|
);
|
|
34
92
|
|
|
93
|
+
const { encounterTypes, isLoading: isLoadingEncounterTypes, error: errorEncounterTypes } = useEncounterTypes();
|
|
94
|
+
|
|
95
|
+
const encounterTypeToCreateEditPrivilege = useMemo(() => {
|
|
96
|
+
return encounterTypes.find((et) => et.uuid === config.encounterTypeToCreateUuid)?.editPrivilege;
|
|
97
|
+
}, [encounterTypes, config.encounterTypeToCreateUuid]);
|
|
98
|
+
|
|
99
|
+
const editPrivilegePerEncounterReference = useMemo(() => {
|
|
100
|
+
if (!encounters?.length || isLoadingEncounterTypes || errorEncounterTypes) {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
return encounters.reduce(
|
|
104
|
+
(acc, encounter) => {
|
|
105
|
+
const encounterType = encounterTypes.find((et) => et.uuid === encounter.encounterTypeUuid);
|
|
106
|
+
if (encounterType) {
|
|
107
|
+
acc[encounter.reference] = encounterType.editPrivilege;
|
|
108
|
+
}
|
|
109
|
+
return acc;
|
|
110
|
+
},
|
|
111
|
+
{} as Record<string, Privilege>,
|
|
112
|
+
);
|
|
113
|
+
}, [encounterTypes, encounters, isLoadingEncounterTypes, errorEncounterTypes]);
|
|
114
|
+
|
|
115
|
+
const conceptByUuid = useMemo(() => {
|
|
116
|
+
return Object.fromEntries(concepts.map((c) => [c.uuid, c]));
|
|
117
|
+
}, [concepts]);
|
|
118
|
+
|
|
35
119
|
if (config.oldestFirst) {
|
|
36
120
|
obssGroupedByEncounters.sort(
|
|
37
121
|
(a, b) => new Date(a[0].effectiveDateTime).getTime() - new Date(b[0].effectiveDateTime).getTime(),
|
|
@@ -51,50 +135,90 @@ const ObsTableHorizontal: React.FC<ObsTableHorizontalProps> = ({ patientUuid })
|
|
|
51
135
|
tableRowLabels = [{ key: 'encounter', header: t('encounterType', 'Encounter type') }, ...tableRowLabels];
|
|
52
136
|
}
|
|
53
137
|
|
|
54
|
-
const
|
|
55
|
-
()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
138
|
+
const handleAddEncounter = useCallback(() => {
|
|
139
|
+
const now = new Date();
|
|
140
|
+
const newTemporaryEncounter: ColumnData = {
|
|
141
|
+
id: `temp-${Date.now()}`,
|
|
142
|
+
date: now,
|
|
143
|
+
encounter: { value: '', editPrivilege: encounterTypeToCreateEditPrivilege },
|
|
144
|
+
encounterReference: '',
|
|
145
|
+
encounterUuid: null,
|
|
146
|
+
obs: {},
|
|
147
|
+
isTemporary: true,
|
|
148
|
+
};
|
|
149
|
+
setTemporaryEncounters((prev) => [...prev, newTemporaryEncounter]);
|
|
150
|
+
}, [encounterTypeToCreateEditPrivilege]);
|
|
151
|
+
|
|
152
|
+
const handleEncounterCreated = useCallback(
|
|
153
|
+
async (tempEncounterId: string, encounterUuid: string) => {
|
|
154
|
+
await mutate();
|
|
155
|
+
// Remove the temporary encounter from state since it's now in the real data
|
|
156
|
+
setTemporaryEncounters((prev) => prev.filter((enc) => enc.id !== tempEncounterId));
|
|
157
|
+
},
|
|
158
|
+
[mutate],
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const tableColumns = useMemo(() => {
|
|
162
|
+
const existingColumns = obssGroupedByEncounters?.map((obss, index) => {
|
|
163
|
+
const encounterReference = obss[0].encounter.reference;
|
|
164
|
+
const encounterUuid = encounterReference.split('/')[1];
|
|
165
|
+
const columnData: ColumnData = {
|
|
166
|
+
id: `${index}`,
|
|
167
|
+
date: new Date(obss[0].effectiveDateTime),
|
|
168
|
+
encounter: {
|
|
169
|
+
value: obss[0].encounter.name,
|
|
170
|
+
editPrivilege: editPrivilegePerEncounterReference[encounterReference],
|
|
171
|
+
},
|
|
172
|
+
encounterReference,
|
|
173
|
+
encounterUuid,
|
|
174
|
+
obs: {} as Record<string, CellData>,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
for (const obs of obss) {
|
|
178
|
+
switch (conceptByUuid[obs.conceptUuid]?.dataType) {
|
|
179
|
+
case 'Text':
|
|
180
|
+
columnData.obs[obs.conceptUuid] = {
|
|
181
|
+
value: obs.valueString,
|
|
182
|
+
obsUuid: obs.id,
|
|
183
|
+
dataType: 'Text',
|
|
184
|
+
};
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
case 'Numeric': {
|
|
188
|
+
const decimalPlaces: number | undefined = config.data.find(
|
|
189
|
+
(ele: any) => ele.concept === obs.conceptUuid,
|
|
190
|
+
)?.decimalPlaces;
|
|
87
191
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
192
|
+
let value;
|
|
193
|
+
if (obs.valueQuantity?.value % 1 !== 0) {
|
|
194
|
+
value = obs.valueQuantity?.value.toFixed(decimalPlaces);
|
|
195
|
+
} else {
|
|
196
|
+
value = obs.valueQuantity?.value;
|
|
197
|
+
}
|
|
198
|
+
columnData.obs[obs.conceptUuid] = {
|
|
199
|
+
value: value,
|
|
200
|
+
obsUuid: obs.id,
|
|
201
|
+
dataType: 'Numeric',
|
|
202
|
+
};
|
|
203
|
+
break;
|
|
91
204
|
}
|
|
205
|
+
|
|
206
|
+
case 'Coded':
|
|
207
|
+
columnData.obs[obs.conceptUuid] = {
|
|
208
|
+
value: obs.valueCodeableConcept?.coding[0]?.code,
|
|
209
|
+
display: obs.valueCodeableConcept?.coding[0]?.display,
|
|
210
|
+
obsUuid: obs.id,
|
|
211
|
+
dataType: 'Coded',
|
|
212
|
+
};
|
|
213
|
+
break;
|
|
92
214
|
}
|
|
215
|
+
}
|
|
93
216
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
217
|
+
return columnData;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return [...existingColumns, ...temporaryEncounters];
|
|
221
|
+
}, [config.data, obssGroupedByEncounters, conceptByUuid, temporaryEncounters, editPrivilegePerEncounterReference]);
|
|
98
222
|
|
|
99
223
|
const { results, goTo, currentPage } = usePagination(tableColumns, config.maxColumns);
|
|
100
224
|
|
|
@@ -104,8 +228,22 @@ const ObsTableHorizontal: React.FC<ObsTableHorizontalProps> = ({ patientUuid })
|
|
|
104
228
|
<div className={styles.backgroundDataFetchingIndicator}>
|
|
105
229
|
<span>{isValidating ? <InlineLoading /> : null}</span>
|
|
106
230
|
</div>
|
|
231
|
+
{isTablet && config.editable && (
|
|
232
|
+
<div className={styles.editabilityNote}>{t('editabilityNote', 'Tap an observation to edit')}</div>
|
|
233
|
+
)}
|
|
107
234
|
</CardHeader>
|
|
108
|
-
<HorizontalTable
|
|
235
|
+
<HorizontalTable
|
|
236
|
+
tableRowLabels={tableRowLabels}
|
|
237
|
+
tableColumns={results}
|
|
238
|
+
// If encounter types are not loaded or can't be loaded, assume the user does
|
|
239
|
+
// not have the necessary privileges.
|
|
240
|
+
editable={config.editable && !isLoadingEncounterTypes && !errorEncounterTypes}
|
|
241
|
+
patientUuid={patientUuid}
|
|
242
|
+
mutate={mutate}
|
|
243
|
+
concepts={concepts}
|
|
244
|
+
onAddEncounter={handleAddEncounter}
|
|
245
|
+
onEncounterCreated={handleEncounterCreated}
|
|
246
|
+
/>
|
|
109
247
|
<PatientChartPagination
|
|
110
248
|
currentItems={results.length}
|
|
111
249
|
totalItems={tableColumns.length}
|
|
@@ -117,8 +255,34 @@ const ObsTableHorizontal: React.FC<ObsTableHorizontalProps> = ({ patientUuid })
|
|
|
117
255
|
);
|
|
118
256
|
};
|
|
119
257
|
|
|
120
|
-
|
|
258
|
+
interface HorizontalTableProps {
|
|
259
|
+
tableRowLabels: Array<{ key: string; header: string }>;
|
|
260
|
+
tableColumns: Array<ColumnData>;
|
|
261
|
+
editable: boolean;
|
|
262
|
+
patientUuid: string;
|
|
263
|
+
concepts: Array<Concept>;
|
|
264
|
+
mutate: () => Promise<any>;
|
|
265
|
+
onAddEncounter?: () => void;
|
|
266
|
+
onEncounterCreated?: (tempEncounterId: string, encounterUuid: string) => void;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const HorizontalTable: React.FC<HorizontalTableProps> = ({
|
|
270
|
+
tableRowLabels,
|
|
271
|
+
tableColumns,
|
|
272
|
+
editable,
|
|
273
|
+
patientUuid,
|
|
274
|
+
concepts,
|
|
275
|
+
mutate,
|
|
276
|
+
onAddEncounter,
|
|
277
|
+
onEncounterCreated,
|
|
278
|
+
}) => {
|
|
121
279
|
const { t } = useTranslation();
|
|
280
|
+
const patientChartConfig = useConfig({ externalModuleName: '@openmrs/esm-patient-chart-app' });
|
|
281
|
+
const encounterEditableDuration = patientChartConfig?.encounterEditableDuration ?? 0;
|
|
282
|
+
const encounterEditableDurationOverridePrivileges =
|
|
283
|
+
patientChartConfig?.encounterEditableDurationOverridePrivileges ?? [];
|
|
284
|
+
const session = useSession();
|
|
285
|
+
|
|
122
286
|
return (
|
|
123
287
|
<TableContainer>
|
|
124
288
|
<Table experimentalAutoAlign={true} size="sm" useZebraStyles>
|
|
@@ -132,21 +296,267 @@ const HorizontalTable = ({ tableRowLabels, tableColumns }: { tableRowLabels: any
|
|
|
132
296
|
<div className={styles.headerTime}>{formatTime(column.date)}</div>
|
|
133
297
|
</TableHeader>
|
|
134
298
|
))}
|
|
299
|
+
{editable && (
|
|
300
|
+
<TableHeader>
|
|
301
|
+
<IconButton
|
|
302
|
+
align="bottom-end"
|
|
303
|
+
kind="ghost"
|
|
304
|
+
size="sm"
|
|
305
|
+
label={t('addEncounter', 'Add encounter')}
|
|
306
|
+
onClick={onAddEncounter}
|
|
307
|
+
>
|
|
308
|
+
<AddIcon size={16} />
|
|
309
|
+
</IconButton>
|
|
310
|
+
</TableHeader>
|
|
311
|
+
)}
|
|
135
312
|
</TableRow>
|
|
136
313
|
</TableHead>
|
|
137
314
|
<TableBody>
|
|
138
|
-
{tableRowLabels.map((label) =>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
{
|
|
142
|
-
<TableCell
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
315
|
+
{tableRowLabels.map((label) => {
|
|
316
|
+
const dataType = concepts.find((c) => c.uuid === label.key)?.dataType;
|
|
317
|
+
return (
|
|
318
|
+
<TableRow key={`obs-hz-row-${label.key}`}>
|
|
319
|
+
<TableCell>{label.header}</TableCell>
|
|
320
|
+
{tableColumns.map((column) => {
|
|
321
|
+
const cellData = column.obs[label.key];
|
|
322
|
+
const encounterAgeInMinutes = (Date.now() - column.date.getTime()) / (1000 * 60);
|
|
323
|
+
const canEditEncounter =
|
|
324
|
+
userHasAccess(column.encounter.editPrivilege?.uuid, session?.user) &&
|
|
325
|
+
(!encounterEditableDuration ||
|
|
326
|
+
encounterEditableDuration === 0 ||
|
|
327
|
+
(encounterEditableDuration > 0 && encounterAgeInMinutes <= encounterEditableDuration) ||
|
|
328
|
+
encounterEditableDurationOverridePrivileges.some((privilege) =>
|
|
329
|
+
userHasAccess(privilege, session?.user),
|
|
330
|
+
));
|
|
331
|
+
|
|
332
|
+
const canEditCell =
|
|
333
|
+
editable &&
|
|
334
|
+
canEditEncounter &&
|
|
335
|
+
label.key !== 'encounter' &&
|
|
336
|
+
['Text', 'Numeric', 'Coded'].includes(dataType);
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<Cell
|
|
340
|
+
key={`obs-hz-value-${column.id}-${label.key}`}
|
|
341
|
+
cellData={cellData}
|
|
342
|
+
dataType={dataType}
|
|
343
|
+
editable={canEditCell}
|
|
344
|
+
patientUuid={patientUuid}
|
|
345
|
+
column={column}
|
|
346
|
+
label={label}
|
|
347
|
+
concepts={concepts}
|
|
348
|
+
mutate={mutate}
|
|
349
|
+
onEncounterCreated={onEncounterCreated}
|
|
350
|
+
/>
|
|
351
|
+
);
|
|
352
|
+
})}
|
|
353
|
+
{onAddEncounter && <TableCell />}
|
|
354
|
+
</TableRow>
|
|
355
|
+
);
|
|
356
|
+
})}
|
|
146
357
|
</TableBody>
|
|
147
358
|
</Table>
|
|
148
359
|
</TableContainer>
|
|
149
360
|
);
|
|
150
361
|
};
|
|
151
362
|
|
|
363
|
+
const Cell: React.FC<{
|
|
364
|
+
cellData: CellData;
|
|
365
|
+
editable: boolean;
|
|
366
|
+
dataType: string;
|
|
367
|
+
patientUuid: string;
|
|
368
|
+
column: ColumnData;
|
|
369
|
+
label: { key: string; header: string };
|
|
370
|
+
concepts: Array<Concept>;
|
|
371
|
+
mutate: () => Promise<any>;
|
|
372
|
+
onEncounterCreated?: (tempEncounterId: string, encounterUuid: string) => void;
|
|
373
|
+
}> = ({ cellData, dataType, editable, patientUuid, column, label, concepts, mutate, onEncounterCreated }) => {
|
|
374
|
+
const { t } = useTranslation();
|
|
375
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
376
|
+
const [editingValue, setEditingValue] = useState<string | number>('');
|
|
377
|
+
const isTablet = !isDesktop(useLayoutType());
|
|
378
|
+
const session = useSession();
|
|
379
|
+
const locationUuid = session?.sessionLocation?.uuid;
|
|
380
|
+
const { encounterTypeToCreateUuid } = useConfig<ConfigObjectHorizontal>();
|
|
381
|
+
|
|
382
|
+
const conceptKey = label.key;
|
|
383
|
+
const cellKey = `obs-hz-value-${column.id}-${label.key}`;
|
|
384
|
+
|
|
385
|
+
const handleEditClick = useCallback(() => {
|
|
386
|
+
setIsEditing(true);
|
|
387
|
+
setEditingValue(cellData?.value ?? '');
|
|
388
|
+
}, [cellData?.value]);
|
|
389
|
+
|
|
390
|
+
const handleCreateEncounter = useCallback(async () => {
|
|
391
|
+
const response = await createEncounter(patientUuid, encounterTypeToCreateUuid, locationUuid, [
|
|
392
|
+
{ concept: conceptKey, value: editingValue },
|
|
393
|
+
]);
|
|
394
|
+
const createdEncounterUuid = response?.data?.uuid;
|
|
395
|
+
if (createdEncounterUuid) {
|
|
396
|
+
// onEncounterCreated will call mutate() and remove the temporary encounter
|
|
397
|
+
onEncounterCreated(column.id, createdEncounterUuid);
|
|
398
|
+
} else {
|
|
399
|
+
throw new Error('Failed to create encounter');
|
|
400
|
+
}
|
|
401
|
+
}, [patientUuid, encounterTypeToCreateUuid, locationUuid, conceptKey, editingValue, onEncounterCreated, column.id]);
|
|
402
|
+
|
|
403
|
+
const handleSave = useCallback(
|
|
404
|
+
async (conceptKey: string) => {
|
|
405
|
+
if (!editingValue || editingValue === cellData?.value) {
|
|
406
|
+
setIsEditing(false);
|
|
407
|
+
setEditingValue('');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
if (cellData?.obsUuid) {
|
|
413
|
+
await updateObservation(cellData.obsUuid, editingValue);
|
|
414
|
+
await mutate();
|
|
415
|
+
} else if (column.isTemporary) {
|
|
416
|
+
await handleCreateEncounter();
|
|
417
|
+
} else if (column.encounterUuid) {
|
|
418
|
+
await createObservationInEncounter(column.encounterUuid, patientUuid, conceptKey, editingValue);
|
|
419
|
+
await mutate();
|
|
420
|
+
} else {
|
|
421
|
+
throw new Error('Cannot save observation: missing encounter information');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
showSnackbar({
|
|
425
|
+
title: t('observationSaved', 'Observation saved successfully'),
|
|
426
|
+
kind: 'success',
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
setIsEditing(false);
|
|
430
|
+
setEditingValue('');
|
|
431
|
+
} catch (error) {
|
|
432
|
+
await mutate();
|
|
433
|
+
showSnackbar({
|
|
434
|
+
title: t('errorSavingObservation', 'Error saving observation'),
|
|
435
|
+
kind: 'error',
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
[
|
|
440
|
+
editingValue,
|
|
441
|
+
patientUuid,
|
|
442
|
+
mutate,
|
|
443
|
+
t,
|
|
444
|
+
cellData?.obsUuid,
|
|
445
|
+
cellData?.value,
|
|
446
|
+
column.encounterUuid,
|
|
447
|
+
column.isTemporary,
|
|
448
|
+
handleCreateEncounter,
|
|
449
|
+
],
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const handleCancel = useCallback(() => {
|
|
453
|
+
setIsEditing(false);
|
|
454
|
+
setEditingValue('');
|
|
455
|
+
}, []);
|
|
456
|
+
|
|
457
|
+
const handleKeyDown = useCallback(
|
|
458
|
+
(e: React.KeyboardEvent) => {
|
|
459
|
+
if (e.key === 'Enter') {
|
|
460
|
+
e.preventDefault();
|
|
461
|
+
handleSave(conceptKey);
|
|
462
|
+
} else if (e.key === 'Escape') {
|
|
463
|
+
e.preventDefault();
|
|
464
|
+
handleCancel();
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
[handleSave, handleCancel, conceptKey],
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
if (isEditing) {
|
|
471
|
+
return (
|
|
472
|
+
<TableCell key={cellKey}>
|
|
473
|
+
<div className={styles.editContainer}>
|
|
474
|
+
{dataType === 'Numeric' ? (
|
|
475
|
+
<NumberInput
|
|
476
|
+
id={cellKey}
|
|
477
|
+
size="sm"
|
|
478
|
+
value={editingValue}
|
|
479
|
+
onChange={(e: any, data: any) => setEditingValue(data.value ?? '')}
|
|
480
|
+
onKeyDown={(e) => handleKeyDown(e)}
|
|
481
|
+
autoFocus
|
|
482
|
+
hideSteppers
|
|
483
|
+
allowEmpty
|
|
484
|
+
/>
|
|
485
|
+
) : dataType === 'Text' ? (
|
|
486
|
+
<TextInput
|
|
487
|
+
id={cellKey}
|
|
488
|
+
labelText=""
|
|
489
|
+
size="sm"
|
|
490
|
+
value={editingValue}
|
|
491
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEditingValue(e.target.value)}
|
|
492
|
+
onKeyDown={(e) => handleKeyDown(e)}
|
|
493
|
+
autoFocus
|
|
494
|
+
/>
|
|
495
|
+
) : (
|
|
496
|
+
<Select
|
|
497
|
+
id={cellKey}
|
|
498
|
+
labelText=""
|
|
499
|
+
size="sm"
|
|
500
|
+
value={editingValue}
|
|
501
|
+
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
502
|
+
setEditingValue(e.target.value);
|
|
503
|
+
}}
|
|
504
|
+
onKeyDown={(e) => handleKeyDown(e)}
|
|
505
|
+
autoFocus
|
|
506
|
+
>
|
|
507
|
+
<SelectItem text={t('noValue', 'No value')} value="" />
|
|
508
|
+
{concepts
|
|
509
|
+
.find((c) => c.uuid === label.key)
|
|
510
|
+
?.answers?.map((answer) => <SelectItem key={answer.uuid} text={answer.display} value={answer.uuid} />)}
|
|
511
|
+
</Select>
|
|
512
|
+
)}
|
|
513
|
+
<div className={styles.editButtons}>
|
|
514
|
+
<IconButton
|
|
515
|
+
kind="ghost"
|
|
516
|
+
size="sm"
|
|
517
|
+
label={getCoreTranslation('cancel')}
|
|
518
|
+
onClick={handleCancel}
|
|
519
|
+
className={styles.cancelButton}
|
|
520
|
+
>
|
|
521
|
+
<Close size={16} />
|
|
522
|
+
</IconButton>
|
|
523
|
+
<IconButton
|
|
524
|
+
kind="ghost"
|
|
525
|
+
size="sm"
|
|
526
|
+
label={getCoreTranslation('save')}
|
|
527
|
+
onClick={() => handleSave(conceptKey)}
|
|
528
|
+
className={styles.saveButton}
|
|
529
|
+
>
|
|
530
|
+
<Checkmark size={16} />
|
|
531
|
+
</IconButton>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
</TableCell>
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return (
|
|
539
|
+
<TableCell
|
|
540
|
+
key={cellKey}
|
|
541
|
+
onClick={() => isTablet && editable && handleEditClick()}
|
|
542
|
+
className={classNames(editable ? styles.editableCell : undefined)}
|
|
543
|
+
>
|
|
544
|
+
<div className={styles.cellContent}>
|
|
545
|
+
<div className={styles.cellValue}>{cellData?.display ?? cellData?.value ?? '--'}</div>
|
|
546
|
+
{editable && (
|
|
547
|
+
<IconButton
|
|
548
|
+
kind="ghost"
|
|
549
|
+
size="sm"
|
|
550
|
+
label={getCoreTranslation('edit')}
|
|
551
|
+
onClick={() => handleEditClick()}
|
|
552
|
+
className={styles.editButton}
|
|
553
|
+
>
|
|
554
|
+
<EditIcon size={16} />
|
|
555
|
+
</IconButton>
|
|
556
|
+
)}
|
|
557
|
+
</div>
|
|
558
|
+
</TableCell>
|
|
559
|
+
);
|
|
560
|
+
};
|
|
561
|
+
|
|
152
562
|
export default ObsTableHorizontal;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
|
|
3
|
+
export function updateObservation(observationUuid: string, value: string | number) {
|
|
4
|
+
return openmrsFetch(`${restBaseUrl}/obs/${observationUuid}`, {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
headers: {
|
|
7
|
+
'Content-Type': 'application/json',
|
|
8
|
+
},
|
|
9
|
+
body: JSON.stringify({ value }),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a new encounter
|
|
15
|
+
* @param patientUuid - The UUID of the patient
|
|
16
|
+
* @param encounterTypeUuid - The UUID of the encounter type
|
|
17
|
+
* @param locationUuid - The UUID of the location
|
|
18
|
+
* @param obs - Array of observations to include in the encounter
|
|
19
|
+
* @returns Promise with the created encounter
|
|
20
|
+
*/
|
|
21
|
+
export function createEncounter(
|
|
22
|
+
patientUuid: string,
|
|
23
|
+
encounterTypeUuid: string,
|
|
24
|
+
locationUuid: string,
|
|
25
|
+
obs: Array<{ concept: string; value: string | number }>,
|
|
26
|
+
) {
|
|
27
|
+
return openmrsFetch(`${restBaseUrl}/encounter`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
patient: patientUuid,
|
|
34
|
+
encounterType: encounterTypeUuid,
|
|
35
|
+
location: locationUuid,
|
|
36
|
+
encounterDatetime: new Date().toISOString(),
|
|
37
|
+
obs: obs,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a new observation in an existing encounter
|
|
44
|
+
* @returns Promise with the updated encounter containing the new observation
|
|
45
|
+
*/
|
|
46
|
+
export function createObservationInEncounter(
|
|
47
|
+
encounterUuid: string,
|
|
48
|
+
patientUuid: string,
|
|
49
|
+
conceptUuid: string,
|
|
50
|
+
value: string | number,
|
|
51
|
+
) {
|
|
52
|
+
return openmrsFetch(`${restBaseUrl}/encounter/${encounterUuid}`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
patient: patientUuid,
|
|
59
|
+
obs: [
|
|
60
|
+
{
|
|
61
|
+
concept: conceptUuid,
|
|
62
|
+
value: value,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
}
|