@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.
Files changed (38) hide show
  1. package/.turbo/turbo-build.log +16 -16
  2. package/dist/1936.js +1 -0
  3. package/dist/1936.js.map +1 -0
  4. package/dist/2606.js +2 -0
  5. package/dist/2606.js.map +1 -0
  6. package/dist/4300.js +1 -1
  7. package/dist/5670.js +1 -1
  8. package/dist/7545.js +2 -0
  9. package/dist/7545.js.map +1 -0
  10. package/dist/8803.js +1 -1
  11. package/dist/main.js +1 -1
  12. package/dist/main.js.map +1 -1
  13. package/dist/openmrs-esm-generic-patient-widgets-app.js +1 -1
  14. package/dist/openmrs-esm-generic-patient-widgets-app.js.buildmanifest.json +91 -91
  15. package/dist/openmrs-esm-generic-patient-widgets-app.js.map +1 -1
  16. package/dist/routes.json +1 -1
  17. package/package.json +2 -2
  18. package/src/config-schema-obs-horizontal.ts +12 -0
  19. package/src/obs-graph/obs-graph.component.tsx +12 -12
  20. package/src/obs-switchable/obs-switchable.component.tsx +5 -9
  21. package/src/obs-switchable/obs-switchable.test.tsx +22 -12
  22. package/src/obs-table/obs-table.component.tsx +2 -1
  23. package/src/obs-table-horizontal/obs-table-horizontal.component.tsx +466 -56
  24. package/src/obs-table-horizontal/obs-table-horizontal.resource.ts +67 -0
  25. package/src/obs-table-horizontal/obs-table-horizontal.scss +47 -0
  26. package/src/obs-table-horizontal/obs-table-horizontal.test.tsx +912 -0
  27. package/src/resources/useConcepts.ts +14 -4
  28. package/src/resources/useEncounterTypes.ts +34 -0
  29. package/src/resources/useObs.ts +29 -47
  30. package/translations/en.json +6 -1
  31. package/dist/251.js +0 -2
  32. package/dist/251.js.map +0 -1
  33. package/dist/8743.js +0 -2
  34. package/dist/8743.js.map +0 -1
  35. package/dist/9351.js +0 -1
  36. package/dist/9351.js.map +0 -1
  37. /package/dist/{251.js.LICENSE.txt → 2606.js.LICENSE.txt} +0 -0
  38. /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
- const uniqueEncounterReferences = [...new Set(observations.map((o) => o.encounter.reference))].sort();
31
- let obssGroupedByEncounters = uniqueEncounterReferences.map((reference) =>
32
- observations.filter((o) => o.encounter.reference === reference),
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 tableColumns = React.useMemo(
55
- () =>
56
- obssGroupedByEncounters?.map((obss, index) => {
57
- const rowData = {
58
- id: `${index}`,
59
- date: new Date(obss[0].effectiveDateTime),
60
- encounter: { value: obss[0].encounter.name },
61
- };
62
-
63
- for (const obs of obss) {
64
- switch (obs.dataType) {
65
- case 'Text':
66
- rowData[obs.conceptUuid] = {
67
- value: obs.valueString,
68
- };
69
- break;
70
-
71
- case 'Number': {
72
- const decimalPlaces: number | undefined = config.data.find(
73
- (ele: any) => ele.concept === obs.conceptUuid,
74
- )?.decimalPlaces;
75
-
76
- let value;
77
- if (obs.valueQuantity?.value % 1 !== 0) {
78
- value = obs.valueQuantity?.value.toFixed(decimalPlaces);
79
- } else {
80
- value = obs.valueQuantity?.value;
81
- }
82
- rowData[obs.conceptUuid] = {
83
- value: value,
84
- };
85
- break;
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
- case 'Coded':
89
- rowData[obs.conceptUuid] = { value: obs.valueCodeableConcept?.coding[0]?.display };
90
- break;
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
- return rowData;
95
- }),
96
- [config.data, obssGroupedByEncounters],
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 tableRowLabels={tableRowLabels} tableColumns={results} />
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
- const HorizontalTable = ({ tableRowLabels, tableColumns }: { tableRowLabels: any; tableColumns: any }) => {
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
- <TableRow key={`obs-hz-row-${label.key}`}>
140
- <TableCell>{label.header}</TableCell>
141
- {tableColumns.map((column) => (
142
- <TableCell key={`obs-hz-value-${column.id}-${label.key}`}>{column[label.key]?.value ?? '--'}</TableCell>
143
- ))}
144
- </TableRow>
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
+ }