@openmrs/esm-generic-patient-widgets-app 11.3.0 → 11.3.1-patch.9310
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 +33 -26
- package/dist/1119.js +1 -1
- package/dist/1197.js +1 -1
- package/dist/2146.js +1 -1
- package/dist/2499.js +2 -0
- package/dist/2499.js.map +1 -0
- package/dist/251.js +2 -0
- package/dist/251.js.map +1 -0
- package/dist/2690.js +1 -1
- package/dist/3099.js +1 -1
- package/dist/3204.js +2 -0
- package/dist/{9582.js.LICENSE.txt → 3204.js.LICENSE.txt} +1 -1
- package/dist/3204.js.map +1 -0
- package/dist/3584.js +1 -1
- package/dist/4055.js +1 -1
- package/dist/4132.js +1 -1
- package/dist/4300.js +1 -1
- package/dist/4335.js +1 -1
- package/dist/4341.js +1 -0
- package/dist/4341.js.map +1 -0
- package/dist/4618.js +1 -1
- package/dist/4652.js +1 -1
- package/dist/4944.js +1 -1
- package/dist/5173.js +1 -1
- package/dist/5241.js +1 -1
- package/dist/5442.js +1 -1
- package/dist/5661.js +1 -1
- package/dist/5670.js +1 -0
- package/dist/5670.js.map +1 -0
- package/dist/6022.js +1 -1
- package/dist/6336.js +1 -0
- package/dist/6336.js.map +1 -0
- package/dist/6468.js +1 -1
- package/dist/6679.js +1 -1
- package/dist/6840.js +1 -1
- package/dist/6859.js +1 -1
- package/dist/7097.js +1 -1
- package/dist/7159.js +1 -1
- package/dist/723.js +1 -1
- package/dist/7617.js +1 -1
- package/dist/795.js +1 -1
- package/dist/8163.js +1 -1
- package/dist/8349.js +1 -1
- package/dist/8618.js +1 -1
- package/dist/890.js +1 -1
- package/dist/9214.js +1 -1
- package/dist/9351.js +1 -0
- package/dist/9351.js.map +1 -0
- package/dist/9538.js +1 -1
- package/dist/9569.js +1 -1
- package/dist/986.js +1 -1
- package/dist/9879.js +1 -1
- package/dist/9895.js +1 -1
- package/dist/9900.js +1 -1
- package/dist/9913.js +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.LICENSE.txt +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 +241 -292
- package/dist/openmrs-esm-generic-patient-widgets-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +6 -5
- package/src/config-schema-obs-switchable.ts +9 -2
- package/src/obs-graph/obs-graph.component.tsx +175 -83
- package/src/obs-graph/obs-graph.scss +21 -22
- package/src/obs-switchable/obs-switchable.component.tsx +27 -27
- package/src/obs-switchable/obs-switchable.scss +1 -1
- package/src/obs-switchable/obs-switchable.test.tsx +291 -0
- package/src/obs-table/obs-table.component.tsx +6 -4
- package/src/obs-table-horizontal/obs-table-horizontal.component.tsx +7 -4
- package/src/resources/useConcepts.ts +41 -0
- package/src/resources/useObs.ts +32 -5
- package/translations/am.json +2 -4
- package/translations/ar.json +2 -4
- package/translations/ar_SY.json +2 -4
- package/translations/bn.json +2 -4
- package/translations/de.json +2 -4
- package/translations/en.json +2 -4
- package/translations/en_US.json +2 -4
- package/translations/es.json +2 -4
- package/translations/es_MX.json +2 -4
- package/translations/fr.json +2 -4
- package/translations/he.json +2 -4
- package/translations/hi.json +2 -4
- package/translations/hi_IN.json +2 -4
- package/translations/id.json +2 -4
- package/translations/it.json +2 -4
- package/translations/ka.json +2 -4
- package/translations/km.json +2 -4
- package/translations/ku.json +2 -4
- package/translations/ky.json +2 -4
- package/translations/lg.json +2 -4
- package/translations/ne.json +2 -4
- package/translations/pl.json +2 -4
- package/translations/pt.json +2 -4
- package/translations/pt_BR.json +2 -4
- package/translations/qu.json +2 -4
- package/translations/ro_RO.json +2 -4
- package/translations/ru_RU.json +2 -4
- package/translations/si.json +2 -4
- package/translations/sw.json +2 -4
- package/translations/sw_KE.json +2 -4
- package/translations/tr.json +2 -4
- package/translations/tr_TR.json +2 -4
- package/translations/uk.json +2 -4
- package/translations/uz.json +2 -4
- package/translations/uz@Latn.json +2 -4
- package/translations/uz_UZ.json +2 -4
- package/translations/vi.json +2 -4
- package/translations/zh.json +2 -4
- package/translations/zh_CN.json +2 -4
- package/dist/30.js +0 -2
- package/dist/30.js.map +0 -1
- package/dist/4051.js +0 -1
- package/dist/4051.js.map +0 -1
- package/dist/5048.js +0 -1
- package/dist/5048.js.map +0 -1
- package/dist/521.js +0 -2
- package/dist/521.js.map +0 -1
- package/dist/5639.js +0 -1
- package/dist/5639.js.map +0 -1
- package/dist/5986.js +0 -1
- package/dist/5986.js.map +0 -1
- package/dist/6432.js +0 -1
- package/dist/6432.js.map +0 -1
- package/dist/717.js +0 -1
- package/dist/717.js.map +0 -1
- package/dist/9582.js +0 -2
- package/dist/9582.js.map +0 -1
- /package/dist/{521.js.LICENSE.txt → 2499.js.LICENSE.txt} +0 -0
- /package/dist/{30.js.LICENSE.txt → 251.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { LineChart } from '@carbon/charts-react';
|
|
5
|
+
import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
|
|
6
|
+
import ObsSwitchable from './obs-switchable.component';
|
|
7
|
+
import { useObs, type ObsResult } from '../resources/useObs';
|
|
8
|
+
import { configSchemaSwitchable } from '../config-schema-obs-switchable';
|
|
9
|
+
|
|
10
|
+
jest.mock('../resources/useObs', () => ({
|
|
11
|
+
useObs: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const mockLineChart = jest.mocked(LineChart);
|
|
15
|
+
|
|
16
|
+
const mockUseConfig = jest.mocked(useConfig);
|
|
17
|
+
|
|
18
|
+
// Make sure this respects the sort order of useObs
|
|
19
|
+
const mockObsData = [
|
|
20
|
+
{
|
|
21
|
+
code: { text: 'Height' },
|
|
22
|
+
conceptUuid: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
23
|
+
dataType: 'Number',
|
|
24
|
+
effectiveDateTime: '2021-02-01T00:00:00Z',
|
|
25
|
+
valueQuantity: { value: 182 },
|
|
26
|
+
encounter: { reference: 'Encounter/234' },
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
code: { text: 'Weight' },
|
|
30
|
+
conceptUuid: '2154AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
31
|
+
dataType: 'Number',
|
|
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: 'Number',
|
|
40
|
+
effectiveDateTime: '2021-01-01T00:00:00Z',
|
|
41
|
+
valueQuantity: { value: 180 },
|
|
42
|
+
encounter: { reference: 'Encounter/123' },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
code: { text: 'Weight' },
|
|
46
|
+
conceptUuid: '2154AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
47
|
+
dataType: 'Number',
|
|
48
|
+
effectiveDateTime: '2021-01-01T00:00:00Z',
|
|
49
|
+
valueQuantity: { value: 70 },
|
|
50
|
+
encounter: { reference: 'Encounter/123' },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
code: { text: 'Chief Complaint' },
|
|
54
|
+
conceptUuid: '164162AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
55
|
+
dataType: 'Text',
|
|
56
|
+
effectiveDateTime: '2021-01-01T00:00:00Z',
|
|
57
|
+
valueString: 'Too strong',
|
|
58
|
+
encounter: { reference: 'Encounter/123' },
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
code: { text: 'Power Level' },
|
|
62
|
+
conceptUuid: '164163AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
63
|
+
dataType: 'Number',
|
|
64
|
+
effectiveDateTime: '2021-01-01T00:00:00Z',
|
|
65
|
+
valueQuantity: { value: 9001 },
|
|
66
|
+
encounter: { reference: 'Encounter/123' },
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const mockConceptData = [
|
|
71
|
+
{ uuid: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Height' },
|
|
72
|
+
{ uuid: '2154AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Weight' },
|
|
73
|
+
{ uuid: '164162AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Chief Complaint' },
|
|
74
|
+
{ uuid: '164163AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', display: 'Power Level' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const mockUseObs = jest.mocked(useObs);
|
|
78
|
+
|
|
79
|
+
describe('ObsSwitchable', () => {
|
|
80
|
+
it('should render all obs in table and numeric obs in graph', async () => {
|
|
81
|
+
mockUseObs.mockReturnValue({
|
|
82
|
+
data: { observations: mockObsData as Array<ObsResult>, concepts: mockConceptData },
|
|
83
|
+
error: null,
|
|
84
|
+
isLoading: false,
|
|
85
|
+
isValidating: false,
|
|
86
|
+
});
|
|
87
|
+
mockUseConfig.mockReturnValue({
|
|
88
|
+
...(getDefaultsFromConfigSchema(configSchemaSwitchable) as Object),
|
|
89
|
+
title: 'My Stats',
|
|
90
|
+
data: [
|
|
91
|
+
{
|
|
92
|
+
concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
93
|
+
label: 'Tallitude',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
concept: '2154AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
97
|
+
},
|
|
98
|
+
{ concept: '164162AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
|
|
99
|
+
{ concept: '164163AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
render(<ObsSwitchable patientUuid="123" />);
|
|
104
|
+
|
|
105
|
+
// Check table
|
|
106
|
+
expect(screen.getAllByRole('row')).toHaveLength(3);
|
|
107
|
+
const headerRow = screen.getAllByRole('row')[0];
|
|
108
|
+
expect(headerRow).toHaveTextContent('Tallitude');
|
|
109
|
+
expect(headerRow).toHaveTextContent('Weight');
|
|
110
|
+
expect(headerRow).toHaveTextContent('Chief Complaint');
|
|
111
|
+
expect(headerRow).toHaveTextContent('Power Level');
|
|
112
|
+
const firstRow = screen.getAllByRole('row')[1];
|
|
113
|
+
expect(firstRow).toHaveTextContent('Jan');
|
|
114
|
+
expect(firstRow).toHaveTextContent('180');
|
|
115
|
+
expect(firstRow).toHaveTextContent('70');
|
|
116
|
+
expect(firstRow).toHaveTextContent('Too strong');
|
|
117
|
+
expect(firstRow).toHaveTextContent('9001');
|
|
118
|
+
const secondRow = screen.getAllByRole('row')[2];
|
|
119
|
+
expect(secondRow).toHaveTextContent('Feb');
|
|
120
|
+
expect(secondRow).toHaveTextContent('182');
|
|
121
|
+
expect(secondRow).toHaveTextContent('72');
|
|
122
|
+
expect(secondRow).toHaveTextContent('--');
|
|
123
|
+
expect(secondRow).toHaveTextContent('--');
|
|
124
|
+
|
|
125
|
+
const user = userEvent.setup();
|
|
126
|
+
const chartViewButton = screen.getByLabelText('Chart view');
|
|
127
|
+
expect(chartViewButton).toBeInTheDocument();
|
|
128
|
+
await user.click(chartViewButton);
|
|
129
|
+
|
|
130
|
+
const tabs = screen.getByLabelText('Obs tabs');
|
|
131
|
+
expect(tabs).toHaveTextContent('Tallitude');
|
|
132
|
+
expect(tabs).toHaveTextContent('Weight');
|
|
133
|
+
expect(tabs).not.toHaveTextContent('Chief Complaint');
|
|
134
|
+
expect(tabs).toHaveTextContent('Power Level');
|
|
135
|
+
expect(tabs).not.toHaveTextContent('Mystery Concept');
|
|
136
|
+
|
|
137
|
+
expect(mockLineChart).toHaveBeenNthCalledWith(
|
|
138
|
+
1,
|
|
139
|
+
expect.objectContaining({
|
|
140
|
+
data: [
|
|
141
|
+
{ group: 'Tallitude', key: new Date('2021-02-01T00:00:00.000Z'), value: 182 },
|
|
142
|
+
{ group: 'Tallitude', key: new Date('2021-01-01T00:00:00.000Z'), value: 180 },
|
|
143
|
+
],
|
|
144
|
+
options: expect.any(Object),
|
|
145
|
+
}),
|
|
146
|
+
{},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(mockLineChart).toHaveBeenNthCalledWith(
|
|
150
|
+
2,
|
|
151
|
+
expect.objectContaining({
|
|
152
|
+
data: [
|
|
153
|
+
{ group: 'Weight', key: new Date('2021-02-01T00:00:00.000Z'), value: 72 },
|
|
154
|
+
{ group: 'Weight', key: new Date('2021-01-01T00:00:00.000Z'), value: 70 },
|
|
155
|
+
],
|
|
156
|
+
options: expect.any(Object),
|
|
157
|
+
}),
|
|
158
|
+
{},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(mockLineChart).not.toHaveBeenCalledWith(
|
|
162
|
+
expect.objectContaining({
|
|
163
|
+
data: [expect.objectContaining({ group: 'Chief Complaint' })],
|
|
164
|
+
options: expect.any(Object),
|
|
165
|
+
}),
|
|
166
|
+
{},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(mockLineChart).toHaveBeenNthCalledWith(
|
|
170
|
+
3,
|
|
171
|
+
expect.objectContaining({
|
|
172
|
+
data: [{ group: 'Power Level', key: new Date('2021-01-01T00:00:00.000Z'), value: 9001 }],
|
|
173
|
+
options: expect.any(Object),
|
|
174
|
+
}),
|
|
175
|
+
{},
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should support showing graph tab by default', async () => {
|
|
180
|
+
mockUseObs.mockReturnValue({
|
|
181
|
+
data: { observations: mockObsData as Array<ObsResult>, concepts: mockConceptData },
|
|
182
|
+
error: null,
|
|
183
|
+
isLoading: false,
|
|
184
|
+
isValidating: false,
|
|
185
|
+
});
|
|
186
|
+
mockUseConfig.mockReturnValue({
|
|
187
|
+
...(getDefaultsFromConfigSchema(configSchemaSwitchable) as Object),
|
|
188
|
+
title: 'My Stats',
|
|
189
|
+
data: [{ concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }, { concept: '2154AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }],
|
|
190
|
+
showGraphByDefault: true,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
render(<ObsSwitchable patientUuid="123" />);
|
|
194
|
+
|
|
195
|
+
const tabs = screen.getByLabelText('Obs tabs');
|
|
196
|
+
expect(tabs).toHaveTextContent('Height');
|
|
197
|
+
expect(tabs).toHaveTextContent('Weight');
|
|
198
|
+
|
|
199
|
+
expect(mockLineChart).toHaveBeenNthCalledWith(
|
|
200
|
+
1,
|
|
201
|
+
expect.objectContaining({
|
|
202
|
+
data: [
|
|
203
|
+
{ group: 'Height', key: new Date('2021-02-01T00:00:00.000Z'), value: 182 },
|
|
204
|
+
{ group: 'Height', key: new Date('2021-01-01T00:00:00.000Z'), value: 180 },
|
|
205
|
+
],
|
|
206
|
+
options: expect.any(Object),
|
|
207
|
+
}),
|
|
208
|
+
{},
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should support grouping into multiline graphs', async () => {
|
|
213
|
+
mockUseObs.mockReturnValue({
|
|
214
|
+
data: { observations: mockObsData as Array<ObsResult>, concepts: mockConceptData },
|
|
215
|
+
error: null,
|
|
216
|
+
isLoading: false,
|
|
217
|
+
isValidating: false,
|
|
218
|
+
});
|
|
219
|
+
mockUseConfig.mockReturnValue({
|
|
220
|
+
...(getDefaultsFromConfigSchema(configSchemaSwitchable) as Object),
|
|
221
|
+
title: 'My Stats',
|
|
222
|
+
showGraphByDefault: true,
|
|
223
|
+
data: [
|
|
224
|
+
{ concept: '164163AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', graphGroup: 'Power Level' },
|
|
225
|
+
{ concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', graphGroup: 'Biometrics' },
|
|
226
|
+
{ concept: '2154AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', graphGroup: 'Biometrics' },
|
|
227
|
+
],
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
render(<ObsSwitchable patientUuid="123" />);
|
|
231
|
+
|
|
232
|
+
const tabs = screen.getByLabelText('Obs tabs');
|
|
233
|
+
expect(tabs).toHaveTextContent('Power Level');
|
|
234
|
+
expect(tabs).toHaveTextContent('Biometrics');
|
|
235
|
+
|
|
236
|
+
expect(mockLineChart).toHaveBeenNthCalledWith(
|
|
237
|
+
1,
|
|
238
|
+
expect.objectContaining({
|
|
239
|
+
data: [{ group: 'Power Level', key: new Date('2021-01-01T00:00:00.000Z'), value: 9001 }],
|
|
240
|
+
options: expect.any(Object),
|
|
241
|
+
}),
|
|
242
|
+
{},
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect(mockLineChart).toHaveBeenNthCalledWith(
|
|
246
|
+
2,
|
|
247
|
+
expect.objectContaining({
|
|
248
|
+
data: [
|
|
249
|
+
{ group: 'Height', key: new Date('2021-02-01T00:00:00.000Z'), value: 182 },
|
|
250
|
+
{ group: 'Height', key: new Date('2021-01-01T00:00:00.000Z'), value: 180 },
|
|
251
|
+
{ group: 'Weight', key: new Date('2021-02-01T00:00:00.000Z'), value: 72 },
|
|
252
|
+
{ group: 'Weight', key: new Date('2021-01-01T00:00:00.000Z'), value: 70 },
|
|
253
|
+
],
|
|
254
|
+
options: expect.any(Object),
|
|
255
|
+
}),
|
|
256
|
+
{},
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should hide the graph tab selection if there is only one graph', async () => {
|
|
261
|
+
mockUseObs.mockReturnValue({
|
|
262
|
+
data: {
|
|
263
|
+
observations: mockObsData.filter(
|
|
264
|
+
(o) => o.conceptUuid === '164163AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
265
|
+
) as Array<ObsResult>,
|
|
266
|
+
concepts: mockConceptData,
|
|
267
|
+
},
|
|
268
|
+
error: null,
|
|
269
|
+
isLoading: false,
|
|
270
|
+
isValidating: false,
|
|
271
|
+
});
|
|
272
|
+
mockUseConfig.mockReturnValue({
|
|
273
|
+
...(getDefaultsFromConfigSchema(configSchemaSwitchable) as Object),
|
|
274
|
+
title: 'My Stats',
|
|
275
|
+
showGraphByDefault: true,
|
|
276
|
+
data: [{ concept: '164163AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }],
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
render(<ObsSwitchable patientUuid="123" />);
|
|
280
|
+
|
|
281
|
+
expect(screen.queryByLabelText('Obs tabs')).not.toBeInTheDocument();
|
|
282
|
+
|
|
283
|
+
expect(mockLineChart).toHaveBeenCalledWith(
|
|
284
|
+
expect.objectContaining({
|
|
285
|
+
data: [{ group: 'Power Level', key: new Date('2021-01-01T00:00:00.000Z'), value: 9001 }],
|
|
286
|
+
options: expect.any(Object),
|
|
287
|
+
}),
|
|
288
|
+
{},
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -23,17 +23,19 @@ interface ObsTableProps {
|
|
|
23
23
|
const ObsTable: React.FC<ObsTableProps> = ({ patientUuid }) => {
|
|
24
24
|
const { t } = useTranslation();
|
|
25
25
|
const config = useConfig<ConfigObjectSwitchable>();
|
|
26
|
-
const {
|
|
27
|
-
|
|
26
|
+
const {
|
|
27
|
+
data: { observations, concepts },
|
|
28
|
+
} = useObs(patientUuid);
|
|
29
|
+
const uniqueEncounterReferences = [...new Set(observations.map((o) => o.encounter.reference))].sort();
|
|
28
30
|
const obssGroupedByEncounters = uniqueEncounterReferences.map((reference) =>
|
|
29
|
-
|
|
31
|
+
observations.filter((o) => o.encounter.reference === reference),
|
|
30
32
|
);
|
|
31
33
|
|
|
32
34
|
const tableHeaders = [
|
|
33
35
|
{ key: 'date', header: t('dateAndTime', 'Date and time'), isSortable: true },
|
|
34
36
|
...config.data.map(({ concept, label }) => ({
|
|
35
37
|
key: concept,
|
|
36
|
-
header: label,
|
|
38
|
+
header: label || concepts.find((c) => c.uuid == concept)?.display,
|
|
37
39
|
})),
|
|
38
40
|
];
|
|
39
41
|
|
|
@@ -23,10 +23,13 @@ interface ObsTableHorizontalProps {
|
|
|
23
23
|
const ObsTableHorizontal: React.FC<ObsTableHorizontalProps> = ({ patientUuid }) => {
|
|
24
24
|
const { t } = useTranslation();
|
|
25
25
|
const config = useConfig<ConfigObjectHorizontal>();
|
|
26
|
-
const {
|
|
27
|
-
|
|
26
|
+
const {
|
|
27
|
+
data: { observations, concepts },
|
|
28
|
+
isValidating,
|
|
29
|
+
} = useObs(patientUuid);
|
|
30
|
+
const uniqueEncounterReferences = [...new Set(observations.map((o) => o.encounter.reference))].sort();
|
|
28
31
|
let obssGroupedByEncounters = uniqueEncounterReferences.map((reference) =>
|
|
29
|
-
|
|
32
|
+
observations.filter((o) => o.encounter.reference === reference),
|
|
30
33
|
);
|
|
31
34
|
|
|
32
35
|
if (config.oldestFirst) {
|
|
@@ -41,7 +44,7 @@ const ObsTableHorizontal: React.FC<ObsTableHorizontalProps> = ({ patientUuid })
|
|
|
41
44
|
|
|
42
45
|
let tableRowLabels = config.data.map(({ concept, label }) => ({
|
|
43
46
|
key: concept,
|
|
44
|
-
header: t(label, label) ||
|
|
47
|
+
header: t(label, label) || concepts.find((c) => c.uuid === concept)?.display,
|
|
45
48
|
}));
|
|
46
49
|
|
|
47
50
|
if (config.showEncounterType) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { openmrsFetch, type FetchResponse, restBaseUrl, showSnackbar } from '@openmrs/esm-framework';
|
|
2
|
+
import chunk from 'lodash/chunk';
|
|
3
|
+
import useSWRImmutable from 'swr/immutable';
|
|
4
|
+
|
|
5
|
+
export interface ConceptReferenceResponse {
|
|
6
|
+
[key: string]: {
|
|
7
|
+
uuid: string;
|
|
8
|
+
display: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useConcepts(conceptUuids: Array<string>) {
|
|
13
|
+
const { data, error, isLoading } = useSWRImmutable<Array<FetchResponse<ConceptReferenceResponse>>, Error>(
|
|
14
|
+
conceptUuids && conceptUuids.length > 0 ? getConceptReferenceUrls(conceptUuids) : null,
|
|
15
|
+
(key: Array<string>) => Promise.all(key.map((url) => openmrsFetch<ConceptReferenceResponse>(url))),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const ob: ConceptReferenceResponse = data?.reduce((acc, response) => ({ ...acc, ...response.data }), {});
|
|
19
|
+
const concepts = ob
|
|
20
|
+
? Object.values(ob).map((value) => ({
|
|
21
|
+
uuid: value.uuid,
|
|
22
|
+
display: value.display,
|
|
23
|
+
}))
|
|
24
|
+
: [];
|
|
25
|
+
|
|
26
|
+
if (error) {
|
|
27
|
+
showSnackbar({
|
|
28
|
+
title: error.name,
|
|
29
|
+
subtitle: error.message,
|
|
30
|
+
kind: 'error',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { concepts, isLoading };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getConceptReferenceUrls(conceptUuids: Array<string>) {
|
|
38
|
+
return chunk(conceptUuids, 10).map(
|
|
39
|
+
(partition) => `${restBaseUrl}/conceptreferences?references=${partition.join(',')}&v=custom:(uuid,display)`,
|
|
40
|
+
);
|
|
41
|
+
}
|
package/src/resources/useObs.ts
CHANGED
|
@@ -2,17 +2,21 @@ import useSWR from 'swr';
|
|
|
2
2
|
import { openmrsFetch, fhirBaseUrl, useConfig } from '@openmrs/esm-framework';
|
|
3
3
|
import { type ConfigObjectSwitchable } from '../config-schema-obs-switchable';
|
|
4
4
|
import { type ConfigObjectHorizontal } from '../config-schema-obs-horizontal';
|
|
5
|
+
import { useConcepts } from './useConcepts';
|
|
5
6
|
|
|
6
7
|
type CommonConfig = ConfigObjectSwitchable | ConfigObjectHorizontal;
|
|
7
8
|
|
|
8
9
|
export interface UseObsResult {
|
|
9
|
-
data:
|
|
10
|
+
data: {
|
|
11
|
+
observations: Array<ObsResult>;
|
|
12
|
+
concepts: Array<{ uuid: string; display: string }>;
|
|
13
|
+
};
|
|
10
14
|
error: Error;
|
|
11
15
|
isLoading: boolean;
|
|
12
16
|
isValidating: boolean;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
type ObsResult = fhir.Observation & {
|
|
19
|
+
export type ObsResult = fhir.Observation & {
|
|
16
20
|
conceptUuid: string;
|
|
17
21
|
dataType?: string;
|
|
18
22
|
valueDateTime?: string;
|
|
@@ -27,10 +31,16 @@ type ObsResult = fhir.Observation & {
|
|
|
27
31
|
|
|
28
32
|
export const pageSize = 100;
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Fetches the observations for the concepts in the config for this widget.
|
|
36
|
+
* For any concept that has neither label nor obs, the concept is fetched to
|
|
37
|
+
* get the label.
|
|
38
|
+
*/
|
|
39
|
+
export function useObs(patientUuid: string): UseObsResult {
|
|
31
40
|
const { encounterTypes, data, showEncounterType } = useConfig<CommonConfig>();
|
|
32
41
|
const urlEncounterTypes: string = encounterTypes.length ? `&encounter.type=${encounterTypes.toString()}` : '';
|
|
33
42
|
|
|
43
|
+
// TODO: Make sorting respect oldestFirst/graphOldestFirst
|
|
34
44
|
let url = `${fhirBaseUrl}/Observation?subject:Patient=${patientUuid}&code=${data
|
|
35
45
|
.map((d) => d.concept)
|
|
36
46
|
.join(',')}&_summary=data&_sort=-date&_count=${pageSize}${urlEncounterTypes}`;
|
|
@@ -41,11 +51,28 @@ export function useObs(patientUuid: string, includeEncounters: boolean = false):
|
|
|
41
51
|
|
|
42
52
|
const { data: result, error, isLoading, isValidating } = useSWR<{ data: fhir.Bundle }, Error>(url, openmrsFetch);
|
|
43
53
|
|
|
54
|
+
const obsForConcept = Object.fromEntries(
|
|
55
|
+
data.map((d) => [
|
|
56
|
+
d.concept,
|
|
57
|
+
result?.data?.entry?.find((e) => (e.resource as fhir.Observation).code.coding.find((c) => d.concept === c.code)),
|
|
58
|
+
]),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const conceptsNeedingLabel = data.filter((d) => result && !d.label && !obsForConcept[d.concept]);
|
|
62
|
+
|
|
63
|
+
const { concepts: conceptsForLabels } = useConcepts(conceptsNeedingLabel.map((d) => d.concept));
|
|
64
|
+
const concepts = data.map((d) => ({
|
|
65
|
+
uuid: d.concept,
|
|
66
|
+
display:
|
|
67
|
+
d.label ||
|
|
68
|
+
obsForConcept[d.concept]?.resource?.code?.text ||
|
|
69
|
+
conceptsForLabels.find((c) => c.uuid === d.concept)?.display,
|
|
70
|
+
}));
|
|
71
|
+
|
|
44
72
|
const encounters = showEncounterType ? getEncountersByResources(result?.data?.entry) : [];
|
|
45
73
|
const observations = filterAndMapObservations(result?.data?.entry, encounters);
|
|
46
|
-
|
|
47
74
|
return {
|
|
48
|
-
data: observations,
|
|
75
|
+
data: { observations, concepts },
|
|
49
76
|
error: error,
|
|
50
77
|
isLoading,
|
|
51
78
|
isValidating,
|
package/translations/am.json
CHANGED
package/translations/ar.json
CHANGED
package/translations/ar_SY.json
CHANGED
package/translations/bn.json
CHANGED
package/translations/de.json
CHANGED
package/translations/en.json
CHANGED
package/translations/en_US.json
CHANGED
package/translations/es.json
CHANGED
package/translations/es_MX.json
CHANGED
package/translations/fr.json
CHANGED
package/translations/he.json
CHANGED
package/translations/hi.json
CHANGED
package/translations/hi_IN.json
CHANGED
package/translations/id.json
CHANGED
package/translations/it.json
CHANGED
package/translations/ka.json
CHANGED
package/translations/km.json
CHANGED
package/translations/ku.json
CHANGED