@openmrs/esm-generic-patient-widgets-app 11.3.1-patch.9064 → 11.3.1-patch.9508

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 (133) hide show
  1. package/.turbo/turbo-build.log +23 -23
  2. package/dist/1119.js +1 -1
  3. package/dist/1197.js +1 -1
  4. package/dist/1936.js +1 -0
  5. package/dist/1936.js.map +1 -0
  6. package/dist/2146.js +1 -1
  7. package/dist/2606.js +2 -0
  8. package/dist/2606.js.map +1 -0
  9. package/dist/2690.js +1 -1
  10. package/dist/3099.js +1 -1
  11. package/dist/3204.js +2 -0
  12. package/dist/3204.js.map +1 -0
  13. package/dist/3584.js +1 -1
  14. package/dist/4055.js +1 -1
  15. package/dist/4132.js +1 -1
  16. package/dist/4300.js +1 -1
  17. package/dist/4335.js +1 -1
  18. package/dist/439.js +1 -0
  19. package/dist/4618.js +1 -1
  20. package/dist/4652.js +1 -1
  21. package/dist/4944.js +1 -1
  22. package/dist/5173.js +1 -1
  23. package/dist/5241.js +1 -1
  24. package/dist/5442.js +1 -1
  25. package/dist/5661.js +1 -1
  26. package/dist/5670.js +1 -1
  27. package/dist/5670.js.map +1 -1
  28. package/dist/6022.js +1 -1
  29. package/dist/6336.js +1 -0
  30. package/dist/6336.js.map +1 -0
  31. package/dist/6468.js +1 -1
  32. package/dist/6589.js +1 -0
  33. package/dist/6679.js +1 -1
  34. package/dist/6840.js +1 -1
  35. package/dist/6859.js +1 -1
  36. package/dist/7097.js +1 -1
  37. package/dist/7159.js +1 -1
  38. package/dist/723.js +1 -1
  39. package/dist/7545.js +2 -0
  40. package/dist/7545.js.map +1 -0
  41. package/dist/7617.js +1 -1
  42. package/dist/795.js +1 -1
  43. package/dist/8163.js +1 -1
  44. package/dist/8349.js +1 -1
  45. package/dist/8371.js +1 -0
  46. package/dist/8618.js +1 -1
  47. package/dist/8803.js +1 -1
  48. package/dist/8803.js.map +1 -1
  49. package/dist/890.js +1 -1
  50. package/dist/9214.js +1 -1
  51. package/dist/9538.js +1 -1
  52. package/dist/9569.js +1 -1
  53. package/dist/986.js +1 -1
  54. package/dist/9879.js +1 -1
  55. package/dist/9895.js +1 -1
  56. package/dist/9900.js +1 -1
  57. package/dist/9913.js +1 -1
  58. package/dist/main.js +1 -1
  59. package/dist/main.js.map +1 -1
  60. package/dist/openmrs-esm-generic-patient-widgets-app.js +1 -1
  61. package/dist/openmrs-esm-generic-patient-widgets-app.js.buildmanifest.json +285 -219
  62. package/dist/openmrs-esm-generic-patient-widgets-app.js.map +1 -1
  63. package/dist/routes.json +1 -1
  64. package/package.json +4 -4
  65. package/src/config-schema-obs-horizontal.ts +12 -0
  66. package/src/config-schema-obs-switchable.ts +7 -1
  67. package/src/obs-graph/obs-graph.component.tsx +85 -36
  68. package/src/obs-graph/obs-graph.scss +19 -11
  69. package/src/obs-switchable/obs-switchable.component.tsx +12 -11
  70. package/src/obs-switchable/obs-switchable.test.tsx +145 -42
  71. package/src/obs-table/obs-table.component.tsx +104 -20
  72. package/src/obs-table-horizontal/obs-table-horizontal.component.tsx +470 -57
  73. package/src/obs-table-horizontal/obs-table-horizontal.resource.ts +67 -0
  74. package/src/obs-table-horizontal/obs-table-horizontal.scss +47 -0
  75. package/src/obs-table-horizontal/obs-table-horizontal.test.tsx +923 -0
  76. package/src/resources/useConcepts.ts +51 -0
  77. package/src/resources/useEncounterTypes.ts +34 -0
  78. package/src/resources/useObs.ts +40 -31
  79. package/translations/am.json +7 -1
  80. package/translations/ar.json +7 -1
  81. package/translations/ar_SY.json +7 -1
  82. package/translations/bn.json +7 -1
  83. package/translations/cs.json +10 -0
  84. package/translations/de.json +7 -1
  85. package/translations/en.json +7 -1
  86. package/translations/en_US.json +7 -1
  87. package/translations/es.json +7 -1
  88. package/translations/es_MX.json +7 -1
  89. package/translations/fr.json +7 -1
  90. package/translations/he.json +7 -1
  91. package/translations/hi.json +7 -1
  92. package/translations/hi_IN.json +7 -1
  93. package/translations/id.json +7 -1
  94. package/translations/it.json +7 -1
  95. package/translations/ka.json +7 -1
  96. package/translations/km.json +7 -1
  97. package/translations/ku.json +7 -1
  98. package/translations/ky.json +7 -1
  99. package/translations/lg.json +7 -1
  100. package/translations/ne.json +7 -1
  101. package/translations/pl.json +7 -1
  102. package/translations/pt.json +7 -1
  103. package/translations/pt_BR.json +7 -1
  104. package/translations/qu.json +7 -1
  105. package/translations/ro_RO.json +7 -1
  106. package/translations/ru_RU.json +7 -1
  107. package/translations/si.json +7 -1
  108. package/translations/sq.json +10 -0
  109. package/translations/sw.json +7 -1
  110. package/translations/sw_KE.json +7 -1
  111. package/translations/tr.json +7 -1
  112. package/translations/tr_TR.json +7 -1
  113. package/translations/uk.json +7 -1
  114. package/translations/uz.json +7 -1
  115. package/translations/uz@Latn.json +7 -1
  116. package/translations/uz_UZ.json +7 -1
  117. package/translations/vi.json +7 -1
  118. package/translations/zh.json +7 -1
  119. package/translations/zh_CN.json +7 -1
  120. package/translations/zh_TW.json +10 -0
  121. package/dist/1559.js +0 -2
  122. package/dist/1559.js.map +0 -1
  123. package/dist/251.js +0 -2
  124. package/dist/251.js.map +0 -1
  125. package/dist/5639.js +0 -1
  126. package/dist/5639.js.map +0 -1
  127. package/dist/5986.js +0 -1
  128. package/dist/5986.js.map +0 -1
  129. package/dist/6781.js +0 -2
  130. package/dist/6781.js.map +0 -1
  131. /package/dist/{251.js.LICENSE.txt → 2606.js.LICENSE.txt} +0 -0
  132. /package/dist/{6781.js.LICENSE.txt → 3204.js.LICENSE.txt} +0 -0
  133. /package/dist/{1559.js.LICENSE.txt → 7545.js.LICENSE.txt} +0 -0
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { render, screen, waitFor } from '@testing-library/react';
2
+ import { prettyDOM, render, screen } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
4
  import { LineChart } from '@carbon/charts-react';
5
5
  import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
@@ -15,19 +15,12 @@ const mockLineChart = jest.mocked(LineChart);
15
15
 
16
16
  const mockUseConfig = jest.mocked(useConfig);
17
17
 
18
+ // Make sure this respects the sort order of useObs
18
19
  const mockObsData = [
19
20
  {
20
21
  code: { text: 'Height' },
21
22
  conceptUuid: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
22
- dataType: 'Number',
23
- effectiveDateTime: '2021-01-01T00:00:00Z',
24
- valueQuantity: { value: 180 },
25
- encounter: { reference: 'Encounter/123' },
26
- },
27
- {
28
- code: { text: 'Height' },
29
- conceptUuid: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
30
- dataType: 'Number',
23
+ dataType: 'Numeric',
31
24
  effectiveDateTime: '2021-02-01T00:00:00Z',
32
25
  valueQuantity: { value: 182 },
33
26
  encounter: { reference: 'Encounter/234' },
@@ -35,18 +28,26 @@ const mockObsData = [
35
28
  {
36
29
  code: { text: 'Weight' },
37
30
  conceptUuid: '2154AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
38
- dataType: 'Number',
31
+ dataType: 'Numeric',
32
+ effectiveDateTime: '2021-02-01T00:00:00Z',
33
+ valueQuantity: { value: 72 },
34
+ encounter: { reference: 'Encounter/234' },
35
+ },
36
+ {
37
+ code: { text: 'Height' },
38
+ conceptUuid: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
39
+ dataType: 'Numeric',
39
40
  effectiveDateTime: '2021-01-01T00:00:00Z',
40
- valueQuantity: { value: 70 },
41
+ valueQuantity: { value: 180 },
41
42
  encounter: { reference: 'Encounter/123' },
42
43
  },
43
44
  {
44
45
  code: { text: 'Weight' },
45
46
  conceptUuid: '2154AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
46
- dataType: 'Number',
47
- effectiveDateTime: '2021-02-01T00:00:00Z',
48
- valueQuantity: { value: 72 },
49
- encounter: { reference: 'Encounter/234' },
47
+ dataType: 'Numeric',
48
+ effectiveDateTime: '2021-01-01T00:00:00Z',
49
+ valueQuantity: { value: 70 },
50
+ encounter: { reference: 'Encounter/123' },
50
51
  },
51
52
  {
52
53
  code: { text: 'Chief Complaint' },
@@ -59,22 +60,35 @@ const mockObsData = [
59
60
  {
60
61
  code: { text: 'Power Level' },
61
62
  conceptUuid: '164163AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
62
- dataType: 'Number',
63
+ dataType: 'Numeric',
63
64
  effectiveDateTime: '2021-01-01T00:00:00Z',
64
65
  valueQuantity: { value: 9001 },
65
66
  encounter: { reference: 'Encounter/123' },
66
67
  },
67
68
  ];
68
69
 
70
+ const mockConceptData = [
71
+ { uuid: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Height', dataType: 'Numeric' },
72
+ { uuid: '2154AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Weight', dataType: 'Numeric' },
73
+ { uuid: '164162AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Chief Complaint', dataType: 'Text' },
74
+ { uuid: '164163AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Power Level', dataType: 'Numeric' },
75
+ ];
76
+
77
+ const mockEncounters = [
78
+ { reference: 'Encounter/123', display: 'Outpatient Visit', encounterTypeUuid: 'encounter-type-uuid-1' },
79
+ { reference: 'Encounter/234', display: 'Outpatient Visit', encounterTypeUuid: 'encounter-type-uuid-1' },
80
+ ];
81
+
69
82
  const mockUseObs = jest.mocked(useObs);
70
83
 
71
84
  describe('ObsSwitchable', () => {
72
85
  it('should render all obs in table and numeric obs in graph', async () => {
73
86
  mockUseObs.mockReturnValue({
74
- data: mockObsData as Array<ObsResult>,
87
+ data: { observations: mockObsData as Array<ObsResult>, concepts: mockConceptData, encounters: mockEncounters },
75
88
  error: null,
76
89
  isLoading: false,
77
90
  isValidating: false,
91
+ mutate: jest.fn(),
78
92
  });
79
93
  mockUseConfig.mockReturnValue({
80
94
  ...(getDefaultsFromConfigSchema(configSchemaSwitchable) as Object),
@@ -102,15 +116,17 @@ describe('ObsSwitchable', () => {
102
116
  expect(headerRow).toHaveTextContent('Chief Complaint');
103
117
  expect(headerRow).toHaveTextContent('Power Level');
104
118
  const firstRow = screen.getAllByRole('row')[1];
105
- expect(firstRow).toHaveTextContent('180');
106
- expect(firstRow).toHaveTextContent('70');
107
- expect(firstRow).toHaveTextContent('Too strong');
108
- expect(firstRow).toHaveTextContent('9001');
119
+ expect(firstRow).toHaveTextContent('Feb');
120
+ expect(firstRow).toHaveTextContent('182');
121
+ expect(firstRow).toHaveTextContent('72');
122
+ expect(firstRow).toHaveTextContent('--');
123
+ expect(firstRow).toHaveTextContent('--');
109
124
  const secondRow = screen.getAllByRole('row')[2];
110
- expect(secondRow).toHaveTextContent('182');
111
- expect(secondRow).toHaveTextContent('72');
112
- expect(secondRow).toHaveTextContent('--');
113
- expect(secondRow).toHaveTextContent('--');
125
+ expect(secondRow).toHaveTextContent('Jan');
126
+ expect(secondRow).toHaveTextContent('180');
127
+ expect(secondRow).toHaveTextContent('70');
128
+ expect(secondRow).toHaveTextContent('Too strong');
129
+ expect(secondRow).toHaveTextContent('9001');
114
130
 
115
131
  const user = userEvent.setup();
116
132
  const chartViewButton = screen.getByLabelText('Chart view');
@@ -122,13 +138,14 @@ describe('ObsSwitchable', () => {
122
138
  expect(tabs).toHaveTextContent('Weight');
123
139
  expect(tabs).not.toHaveTextContent('Chief Complaint');
124
140
  expect(tabs).toHaveTextContent('Power Level');
141
+ expect(tabs).not.toHaveTextContent('Mystery Concept');
125
142
 
126
143
  expect(mockLineChart).toHaveBeenNthCalledWith(
127
144
  1,
128
145
  expect.objectContaining({
129
146
  data: [
130
- { group: 'Tallitude', key: '01-Jan-2021', value: 180 },
131
- { group: 'Tallitude', key: '01-Feb-2021', value: 182 },
147
+ { group: 'Tallitude', key: new Date('2021-02-01T00:00:00.000Z'), value: 182 },
148
+ { group: 'Tallitude', key: new Date('2021-01-01T00:00:00.000Z'), value: 180 },
132
149
  ],
133
150
  options: expect.any(Object),
134
151
  }),
@@ -139,8 +156,8 @@ describe('ObsSwitchable', () => {
139
156
  2,
140
157
  expect.objectContaining({
141
158
  data: [
142
- { group: 'Weight', key: '01-Jan-2021', value: 70 },
143
- { group: 'Weight', key: '01-Feb-2021', value: 72 },
159
+ { group: 'Weight', key: new Date('2021-02-01T00:00:00.000Z'), value: 72 },
160
+ { group: 'Weight', key: new Date('2021-01-01T00:00:00.000Z'), value: 70 },
144
161
  ],
145
162
  options: expect.any(Object),
146
163
  }),
@@ -158,19 +175,97 @@ describe('ObsSwitchable', () => {
158
175
  expect(mockLineChart).toHaveBeenNthCalledWith(
159
176
  3,
160
177
  expect.objectContaining({
161
- data: [{ group: 'Power Level', key: '01-Jan-2021', value: 9001 }],
178
+ data: [{ group: 'Power Level', key: new Date('2021-01-01T00:00:00.000Z'), value: 9001 }],
162
179
  options: expect.any(Object),
163
180
  }),
164
181
  {},
165
182
  );
166
183
  });
167
184
 
185
+ it('should sort by date and by obs correctly', async () => {
186
+ mockUseObs.mockReturnValue({
187
+ data: { observations: mockObsData as Array<ObsResult>, concepts: mockConceptData, encounters: mockEncounters },
188
+ error: null,
189
+ isLoading: false,
190
+ isValidating: false,
191
+ mutate: jest.fn(),
192
+ });
193
+ mockUseConfig.mockReturnValue({
194
+ ...(getDefaultsFromConfigSchema(configSchemaSwitchable) as Object),
195
+ title: 'My Stats',
196
+ data: [
197
+ {
198
+ concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
199
+ label: 'Tallitude',
200
+ },
201
+ {
202
+ concept: '2154AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
203
+ },
204
+ { concept: '164162AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
205
+ { concept: '164163AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
206
+ ],
207
+ });
208
+
209
+ render(<ObsSwitchable patientUuid="123" />);
210
+
211
+ const user = userEvent.setup();
212
+
213
+ const firstRowInitial = screen.getAllByRole('row')[1];
214
+ expect(firstRowInitial).toHaveTextContent('01 — Feb — 2021');
215
+ const secondRowInitial = screen.getAllByRole('row')[2];
216
+ expect(secondRowInitial).toHaveTextContent('01 — Jan — 2021');
217
+
218
+ const dateHeader = screen.getByText('Date and time');
219
+ await user.click(dateHeader);
220
+
221
+ const firstRow = screen.getAllByRole('row')[1];
222
+ expect(firstRow).toHaveTextContent('01 — Feb — 2021');
223
+ const secondRow = screen.getAllByRole('row')[2];
224
+ expect(secondRow).toHaveTextContent('01 — Jan — 2021');
225
+
226
+ await user.click(dateHeader);
227
+ const firstRow2 = screen.getAllByRole('row')[1];
228
+ expect(firstRow2).toHaveTextContent('01 — Jan — 2021');
229
+ const secondRow2 = screen.getAllByRole('row')[2];
230
+ expect(secondRow2).toHaveTextContent('01 — Feb — 2021');
231
+ });
232
+
233
+ it('supports table sorting oldest to newest', async () => {
234
+ mockUseObs.mockReturnValue({
235
+ data: { observations: mockObsData as Array<ObsResult>, concepts: mockConceptData, encounters: mockEncounters },
236
+ error: null,
237
+ isLoading: false,
238
+ isValidating: false,
239
+ mutate: jest.fn(),
240
+ });
241
+
242
+ mockUseConfig.mockReturnValue({
243
+ ...(getDefaultsFromConfigSchema(configSchemaSwitchable) as Object),
244
+ title: 'My Stats',
245
+ data: [{ concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }, { concept: '2154AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }],
246
+ tableSortOldestFirst: true,
247
+ });
248
+
249
+ render(<ObsSwitchable patientUuid="123" />);
250
+
251
+ const user = userEvent.setup();
252
+
253
+ const dateHeader = screen.getByText('Date and time');
254
+ await user.click(dateHeader);
255
+
256
+ const firstRow = screen.getAllByRole('row')[1];
257
+ expect(firstRow).toHaveTextContent('01 — Jan — 2021');
258
+ const secondRow = screen.getAllByRole('row')[2];
259
+ expect(secondRow).toHaveTextContent('01 — Feb — 2021');
260
+ });
261
+
168
262
  it('should support showing graph tab by default', async () => {
169
263
  mockUseObs.mockReturnValue({
170
- data: mockObsData as Array<ObsResult>,
264
+ data: { observations: mockObsData as Array<ObsResult>, concepts: mockConceptData, encounters: mockEncounters },
171
265
  error: null,
172
266
  isLoading: false,
173
267
  isValidating: false,
268
+ mutate: jest.fn(),
174
269
  });
175
270
  mockUseConfig.mockReturnValue({
176
271
  ...(getDefaultsFromConfigSchema(configSchemaSwitchable) as Object),
@@ -189,8 +284,8 @@ describe('ObsSwitchable', () => {
189
284
  1,
190
285
  expect.objectContaining({
191
286
  data: [
192
- { group: 'Height', key: '01-Jan-2021', value: 180 },
193
- { group: 'Height', key: '01-Feb-2021', value: 182 },
287
+ { group: 'Height', key: new Date('2021-02-01T00:00:00.000Z'), value: 182 },
288
+ { group: 'Height', key: new Date('2021-01-01T00:00:00.000Z'), value: 180 },
194
289
  ],
195
290
  options: expect.any(Object),
196
291
  }),
@@ -200,10 +295,11 @@ describe('ObsSwitchable', () => {
200
295
 
201
296
  it('should support grouping into multiline graphs', async () => {
202
297
  mockUseObs.mockReturnValue({
203
- data: mockObsData as Array<ObsResult>,
298
+ data: { observations: mockObsData as Array<ObsResult>, concepts: mockConceptData, encounters: mockEncounters },
204
299
  error: null,
205
300
  isLoading: false,
206
301
  isValidating: false,
302
+ mutate: jest.fn(),
207
303
  });
208
304
  mockUseConfig.mockReturnValue({
209
305
  ...(getDefaultsFromConfigSchema(configSchemaSwitchable) as Object),
@@ -225,7 +321,7 @@ describe('ObsSwitchable', () => {
225
321
  expect(mockLineChart).toHaveBeenNthCalledWith(
226
322
  1,
227
323
  expect.objectContaining({
228
- data: [{ group: 'Power Level', key: '01-Jan-2021', value: 9001 }],
324
+ data: [{ group: 'Power Level', key: new Date('2021-01-01T00:00:00.000Z'), value: 9001 }],
229
325
  options: expect.any(Object),
230
326
  }),
231
327
  {},
@@ -235,10 +331,10 @@ describe('ObsSwitchable', () => {
235
331
  2,
236
332
  expect.objectContaining({
237
333
  data: [
238
- { group: 'Height', key: '01-Jan-2021', value: 180 },
239
- { group: 'Height', key: '01-Feb-2021', value: 182 },
240
- { group: 'Weight', key: '01-Jan-2021', value: 70 },
241
- { group: 'Weight', key: '01-Feb-2021', value: 72 },
334
+ { group: 'Height', key: new Date('2021-02-01T00:00:00.000Z'), value: 182 },
335
+ { group: 'Height', key: new Date('2021-01-01T00:00:00.000Z'), value: 180 },
336
+ { group: 'Weight', key: new Date('2021-02-01T00:00:00.000Z'), value: 72 },
337
+ { group: 'Weight', key: new Date('2021-01-01T00:00:00.000Z'), value: 70 },
242
338
  ],
243
339
  options: expect.any(Object),
244
340
  }),
@@ -248,10 +344,17 @@ describe('ObsSwitchable', () => {
248
344
 
249
345
  it('should hide the graph tab selection if there is only one graph', async () => {
250
346
  mockUseObs.mockReturnValue({
251
- data: mockObsData.filter((o) => o.conceptUuid === '164163AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') as Array<ObsResult>,
347
+ data: {
348
+ observations: mockObsData.filter(
349
+ (o) => o.conceptUuid === '164163AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
350
+ ) as Array<ObsResult>,
351
+ concepts: mockConceptData,
352
+ encounters: mockEncounters,
353
+ },
252
354
  error: null,
253
355
  isLoading: false,
254
356
  isValidating: false,
357
+ mutate: jest.fn(),
255
358
  });
256
359
  mockUseConfig.mockReturnValue({
257
360
  ...(getDefaultsFromConfigSchema(configSchemaSwitchable) as Object),
@@ -266,7 +369,7 @@ describe('ObsSwitchable', () => {
266
369
 
267
370
  expect(mockLineChart).toHaveBeenCalledWith(
268
371
  expect.objectContaining({
269
- data: [{ group: 'Power Level', key: '01-Jan-2021', value: 9001 }],
372
+ data: [{ group: 'Power Level', key: new Date('2021-01-01T00:00:00.000Z'), value: 9001 }],
270
373
  options: expect.any(Object),
271
374
  }),
272
375
  {},
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useMemo, useState, useCallback } from 'react';
2
2
  import {
3
3
  DataTable,
4
4
  Table,
@@ -8,6 +8,7 @@ import {
8
8
  TableHead,
9
9
  TableHeader,
10
10
  TableRow,
11
+ type DataTableSortState,
11
12
  } from '@carbon/react';
12
13
  import { usePagination, useConfig, formatDatetime, formatDate, formatTime } from '@openmrs/esm-framework';
13
14
  import { PatientChartPagination } from '@openmrs/esm-patient-common-lib';
@@ -20,33 +21,77 @@ interface ObsTableProps {
20
21
  patientUuid: string;
21
22
  }
22
23
 
24
+ interface Row {
25
+ id: string;
26
+ date: string;
27
+ rawDate: string;
28
+ encounter: string;
29
+ [conceptUuid: string]: string | number;
30
+ }
31
+
32
+ interface Header {
33
+ key: string;
34
+ header: string;
35
+ sortFunc: (rowA: Row, rowB: Row) => number;
36
+ }
37
+
23
38
  const ObsTable: React.FC<ObsTableProps> = ({ patientUuid }) => {
24
39
  const { t } = useTranslation();
25
40
  const config = useConfig<ConfigObjectSwitchable>();
26
- const { data: obss } = useObs(patientUuid, config.showEncounterType);
27
- const uniqueEncounterReferences = [...new Set(obss.map((o) => o.encounter.reference))].sort();
41
+ const {
42
+ data: { observations, concepts },
43
+ } = useObs(patientUuid);
44
+
45
+ const uniqueEncounterReferences = [...new Set(observations.map((o) => o.encounter.reference))].sort();
28
46
  const obssGroupedByEncounters = uniqueEncounterReferences.map((reference) =>
29
- obss.filter((o) => o.encounter.reference === reference),
47
+ observations.filter((o) => o.encounter.reference === reference),
30
48
  );
31
49
 
32
- const tableHeaders = [
33
- { key: 'date', header: t('dateAndTime', 'Date and time'), isSortable: true },
34
- ...config.data.map(({ concept, label }) => ({
35
- key: concept,
36
- header: label || obss.find((o) => o.conceptUuid == concept)?.code.text,
37
- })),
38
- ];
39
-
40
- if (config.showEncounterType) {
41
- tableHeaders.splice(1, 0, { key: 'encounter', header: t('encounterType', 'Encounter type'), isSortable: true });
42
- }
50
+ const tableHeaders: Array<Header> = useMemo(() => {
51
+ const headers: Array<Header> = [
52
+ {
53
+ key: 'date',
54
+ header: t('dateAndTime', 'Date and time'),
55
+ sortFunc: (rowA: Row, rowB: Row) => new Date(rowB.rawDate).getTime() - new Date(rowA.rawDate).getTime(),
56
+ },
57
+ ];
58
+ if (config.showEncounterType) {
59
+ headers.splice(1, 0, {
60
+ key: 'encounter',
61
+ header: t('encounterType', 'Encounter type'),
62
+ sortFunc: (rowA: Row, rowB: Row) => rowA.encounter.localeCompare(rowB.encounter) as 1 | -1,
63
+ });
64
+ }
65
+ headers.push(
66
+ ...config.data.map(({ concept, label }) => ({
67
+ key: concept,
68
+ header: label || concepts.find((c) => c.uuid == concept)?.display,
69
+ sortFunc: (rowA: Row, rowB: Row) => {
70
+ const a = rowA[concept];
71
+ const b = rowB[concept];
72
+ if (a === b) {
73
+ return 0;
74
+ }
75
+ if (a == null) {
76
+ return 1;
77
+ }
78
+ if (b == null) {
79
+ return -1;
80
+ }
81
+ return a < b ? 1 : -1;
82
+ },
83
+ })),
84
+ );
85
+ return headers;
86
+ }, [t, config.data, config.showEncounterType, concepts]);
43
87
 
44
- const tableRows = React.useMemo(
88
+ const tableRows: Array<Row> = React.useMemo(
45
89
  () =>
46
90
  obssGroupedByEncounters?.map((obss, index) => {
47
91
  const rowData = {
48
92
  id: `${index}`,
49
93
  date: formatDatetime(new Date(obss[0].effectiveDateTime), { mode: 'wide' }),
94
+ rawDate: obss[0].effectiveDateTime,
50
95
  encounter: obss[0].encounter.name,
51
96
  };
52
97
 
@@ -56,7 +101,7 @@ const ObsTable: React.FC<ObsTableProps> = ({ patientUuid }) => {
56
101
  rowData[obs.conceptUuid] = obs.valueString;
57
102
  break;
58
103
 
59
- case 'Number': {
104
+ case 'Numeric': {
60
105
  const decimalPlaces: number | undefined = config.data.find(
61
106
  (ele: any) => ele.concept === obs.conceptUuid,
62
107
  )?.decimalPlaces;
@@ -98,11 +143,50 @@ const ObsTable: React.FC<ObsTableProps> = ({ patientUuid }) => {
98
143
  [config.data, config?.dateFormat, obssGroupedByEncounters],
99
144
  );
100
145
 
101
- const { results, goTo, currentPage } = usePagination(tableRows, config.table.pageSize);
146
+ const [sortParams, setSortParams] = useState<{ key: string; sortDirection: 'ASC' | 'DESC' | 'NONE' }>({
147
+ key: 'date',
148
+ sortDirection: config.tableSortOldestFirst ? 'ASC' : 'DESC',
149
+ });
150
+
151
+ const handleSorting = useCallback(
152
+ (cellA: any, cellB: any, { key, sortDirection }: { key: string; sortDirection: DataTableSortState }) => {
153
+ // Use setTimeout to avoid setState during render
154
+ setTimeout(() => {
155
+ if (sortDirection === 'NONE') {
156
+ setSortParams({ key: '', sortDirection });
157
+ } else {
158
+ setSortParams({ key, sortDirection });
159
+ }
160
+ }, 0);
161
+ return 0;
162
+ },
163
+ [],
164
+ );
165
+
166
+ const sortedData: Array<any> = useMemo(() => {
167
+ if (sortParams.sortDirection === 'NONE') {
168
+ return tableRows;
169
+ }
170
+
171
+ const header = tableHeaders.find((header) => header.key === sortParams.key);
172
+
173
+ if (!header) {
174
+ return tableRows;
175
+ }
176
+
177
+ const sortedRows = tableRows.slice().sort((rowA, rowB) => {
178
+ const sortingNum = header.sortFunc(rowA, rowB);
179
+ return sortParams.sortDirection === 'DESC' ? sortingNum : -sortingNum;
180
+ });
181
+
182
+ return sortedRows;
183
+ }, [tableHeaders, tableRows, sortParams]);
184
+
185
+ const { results, goTo, currentPage } = usePagination(sortedData, config.table.pageSize);
102
186
 
103
187
  return (
104
188
  <div>
105
- <DataTable rows={results} headers={tableHeaders} isSortable size="sm" useZebraStyles>
189
+ <DataTable rows={results} headers={tableHeaders} isSortable sortRow={handleSorting} size="sm" useZebraStyles>
106
190
  {({ rows, headers, getHeaderProps, getTableProps }) => (
107
191
  <TableContainer>
108
192
  <Table {...getTableProps()} className={styles.customRow}>
@@ -135,7 +219,7 @@ const ObsTable: React.FC<ObsTableProps> = ({ patientUuid }) => {
135
219
  </DataTable>
136
220
  <PatientChartPagination
137
221
  pageNumber={currentPage}
138
- totalItems={tableRows.length}
222
+ totalItems={sortedData.length}
139
223
  currentItems={results.length}
140
224
  pageSize={config.table.pageSize}
141
225
  onPageNumberChange={({ page }) => goTo(page)}