Skip to content

Commit 2325da4

Browse files
authored
Merge branch 'main' into O3-4970
2 parents f3518a8 + 1022ae2 commit 2325da4

File tree

8 files changed

+254
-158
lines changed

8 files changed

+254
-158
lines changed

packages/esm-generic-patient-widgets-app/src/obs-graph/obs-graph.component.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ interface ObsGraphProps {
2020
const ObsGraph: React.FC<ObsGraphProps> = ({ patientUuid }) => {
2121
const config = useConfig<ConfigObjectSwitchable>();
2222
const { t } = useTranslation();
23-
const { data: observations } = useObs(patientUuid);
23+
const {
24+
data: { observations, concepts },
25+
} = useObs(patientUuid);
2426

2527
const obsForConcepts = useMemo(() => {
2628
return Object.fromEntries(
@@ -30,7 +32,7 @@ const ObsGraph: React.FC<ObsGraphProps> = ({ patientUuid }) => {
3032
);
3133
}, [config.data, observations]);
3234

33-
const conceptForObs = useMemo(() => {
35+
const configForObs = useMemo(() => {
3436
return Object.fromEntries(
3537
observations.map((o) => [o.conceptUuid, config.data.find((c) => c.concept == o.conceptUuid)]),
3638
);
@@ -46,7 +48,7 @@ const ObsGraph: React.FC<ObsGraphProps> = ({ patientUuid }) => {
4648
.reduce((acc, curr) => {
4749
if (!curr.graphGroup) {
4850
acc.push({
49-
groupLabel: curr.label || obsForConcepts[curr.concept][0]?.code.text,
51+
groupLabel: curr.label || concepts.find((c) => c.uuid == curr.concept)?.display,
5052
concepts: [curr],
5153
});
5254
} else if (acc.find((a) => a.groupLabel == curr.graphGroup)) {
@@ -59,7 +61,7 @@ const ObsGraph: React.FC<ObsGraphProps> = ({ patientUuid }) => {
5961
}
6062
return acc;
6163
}, [] as ConceptGroupDescriptor[]),
62-
[config.data, obsForConcepts],
64+
[config.data, obsForConcepts, concepts],
6365
);
6466

6567
const [selectedMenuItem, setSelectedMenuItem] = useState<ConceptGroupDescriptor>(groupedConfigData[0]);
@@ -70,8 +72,8 @@ const ObsGraph: React.FC<ObsGraphProps> = ({ patientUuid }) => {
7072
.map((c) => obsForConcepts[c.concept])
7173
.flat()
7274
.map((obs) => ({
73-
group: conceptForObs[obs.conceptUuid].label
74-
? t(conceptForObs[obs.conceptUuid].label, conceptForObs[obs.conceptUuid].label)
75+
group: configForObs[obs.conceptUuid].label
76+
? t(configForObs[obs.conceptUuid].label, configForObs[obs.conceptUuid].label)
7577
: obs.code.text,
7678
key: new Date(obs.effectiveDateTime),
7779
value: obs.valueQuantity.value,
@@ -83,7 +85,7 @@ const ObsGraph: React.FC<ObsGraphProps> = ({ patientUuid }) => {
8385

8486
return chartRecords;
8587
},
86-
[obsForConcepts, conceptForObs, config.graphOldestFirst, t],
88+
[obsForConcepts, configForObs, config.graphOldestFirst, t],
8789
);
8890

8991
const chartColors = Object.fromEntries(selectedMenuItem.concepts.map((d) => [d.label, d.color]));

packages/esm-generic-patient-widgets-app/src/obs-switchable/obs-switchable.component.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,21 @@ const ObsSwitchable: React.FC<ObsSwitchableProps> = ({ patientUuid }) => {
2020
const [chartView, setChartView] = React.useState<boolean>(config.showGraphByDefault);
2121
const isTablet = !isDesktop(useLayoutType());
2222

23-
const { data: obss, error, isLoading, isValidating } = useObs(patientUuid);
23+
const {
24+
data: { observations },
25+
error,
26+
isLoading,
27+
isValidating,
28+
} = useObs(patientUuid);
2429

25-
const hasNumberType = obss.find((obs) => obs.dataType === 'Number');
30+
const hasNumberType = observations.find((obs) => obs.dataType === 'Number');
2631

2732
return (
2833
<>
2934
{(() => {
3035
if (isLoading) return <DataTableSkeleton role="progressbar" />;
3136
if (error) return <ErrorState error={error} headerTitle={config.title} />;
32-
if (obss?.length) {
37+
if (observations?.length) {
3338
return (
3439
<div className={styles.widgetContainer}>
3540
<CardHeader title={t(config.title)}>

packages/esm-generic-patient-widgets-app/src/obs-switchable/obs-switchable.test.tsx

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,8 @@ const mockLineChart = jest.mocked(LineChart);
1515

1616
const mockUseConfig = jest.mocked(useConfig);
1717

18+
// Make sure this respects the sort order of useObs
1819
const mockObsData = [
19-
{
20-
code: { text: 'Height' },
21-
conceptUuid: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
22-
dataType: 'Number',
23-
effectiveDateTime: '2021-01-01T00:00:00Z',
24-
valueQuantity: { value: 180 },
25-
encounter: { reference: 'Encounter/123' },
26-
},
2720
{
2821
code: { text: 'Height' },
2922
conceptUuid: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
@@ -36,17 +29,25 @@ const mockObsData = [
3629
code: { text: 'Weight' },
3730
conceptUuid: '2154AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
3831
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',
3940
effectiveDateTime: '2021-01-01T00:00:00Z',
40-
valueQuantity: { value: 70 },
41+
valueQuantity: { value: 180 },
4142
encounter: { reference: 'Encounter/123' },
4243
},
4344
{
4445
code: { text: 'Weight' },
4546
conceptUuid: '2154AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
4647
dataType: 'Number',
47-
effectiveDateTime: '2021-02-01T00:00:00Z',
48-
valueQuantity: { value: 72 },
49-
encounter: { reference: 'Encounter/234' },
48+
effectiveDateTime: '2021-01-01T00:00:00Z',
49+
valueQuantity: { value: 70 },
50+
encounter: { reference: 'Encounter/123' },
5051
},
5152
{
5253
code: { text: 'Chief Complaint' },
@@ -66,12 +67,19 @@ const mockObsData = [
6667
},
6768
];
6869

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+
6977
const mockUseObs = jest.mocked(useObs);
7078

7179
describe('ObsSwitchable', () => {
7280
it('should render all obs in table and numeric obs in graph', async () => {
7381
mockUseObs.mockReturnValue({
74-
data: mockObsData as Array<ObsResult>,
82+
data: { observations: mockObsData as Array<ObsResult>, concepts: mockConceptData },
7583
error: null,
7684
isLoading: false,
7785
isValidating: false,
@@ -102,11 +110,13 @@ describe('ObsSwitchable', () => {
102110
expect(headerRow).toHaveTextContent('Chief Complaint');
103111
expect(headerRow).toHaveTextContent('Power Level');
104112
const firstRow = screen.getAllByRole('row')[1];
113+
expect(firstRow).toHaveTextContent('Jan');
105114
expect(firstRow).toHaveTextContent('180');
106115
expect(firstRow).toHaveTextContent('70');
107116
expect(firstRow).toHaveTextContent('Too strong');
108117
expect(firstRow).toHaveTextContent('9001');
109118
const secondRow = screen.getAllByRole('row')[2];
119+
expect(secondRow).toHaveTextContent('Feb');
110120
expect(secondRow).toHaveTextContent('182');
111121
expect(secondRow).toHaveTextContent('72');
112122
expect(secondRow).toHaveTextContent('--');
@@ -122,13 +132,14 @@ describe('ObsSwitchable', () => {
122132
expect(tabs).toHaveTextContent('Weight');
123133
expect(tabs).not.toHaveTextContent('Chief Complaint');
124134
expect(tabs).toHaveTextContent('Power Level');
135+
expect(tabs).not.toHaveTextContent('Mystery Concept');
125136

126137
expect(mockLineChart).toHaveBeenNthCalledWith(
127138
1,
128139
expect.objectContaining({
129140
data: [
130-
{ group: 'Tallitude', key: new Date('2021-01-01T00:00:00.000Z'), value: 180 },
131141
{ 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 },
132143
],
133144
options: expect.any(Object),
134145
}),
@@ -139,8 +150,8 @@ describe('ObsSwitchable', () => {
139150
2,
140151
expect.objectContaining({
141152
data: [
142-
{ group: 'Weight', key: new Date('2021-01-01T00:00:00.000Z'), value: 70 },
143153
{ 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 },
144155
],
145156
options: expect.any(Object),
146157
}),
@@ -167,7 +178,7 @@ describe('ObsSwitchable', () => {
167178

168179
it('should support showing graph tab by default', async () => {
169180
mockUseObs.mockReturnValue({
170-
data: mockObsData as Array<ObsResult>,
181+
data: { observations: mockObsData as Array<ObsResult>, concepts: mockConceptData },
171182
error: null,
172183
isLoading: false,
173184
isValidating: false,
@@ -189,8 +200,8 @@ describe('ObsSwitchable', () => {
189200
1,
190201
expect.objectContaining({
191202
data: [
192-
{ group: 'Height', key: new Date('2021-01-01T00:00:00.000Z'), value: 180 },
193203
{ 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 },
194205
],
195206
options: expect.any(Object),
196207
}),
@@ -200,7 +211,7 @@ describe('ObsSwitchable', () => {
200211

201212
it('should support grouping into multiline graphs', async () => {
202213
mockUseObs.mockReturnValue({
203-
data: mockObsData as Array<ObsResult>,
214+
data: { observations: mockObsData as Array<ObsResult>, concepts: mockConceptData },
204215
error: null,
205216
isLoading: false,
206217
isValidating: false,
@@ -235,10 +246,10 @@ describe('ObsSwitchable', () => {
235246
2,
236247
expect.objectContaining({
237248
data: [
238-
{ group: 'Height', key: new Date('2021-01-01T00:00:00.000Z'), value: 180 },
239249
{ group: 'Height', key: new Date('2021-02-01T00:00:00.000Z'), value: 182 },
240-
{ group: 'Weight', key: new Date('2021-01-01T00:00:00.000Z'), value: 70 },
250+
{ group: 'Height', key: new Date('2021-01-01T00:00:00.000Z'), value: 180 },
241251
{ 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 },
242253
],
243254
options: expect.any(Object),
244255
}),
@@ -248,7 +259,12 @@ describe('ObsSwitchable', () => {
248259

249260
it('should hide the graph tab selection if there is only one graph', async () => {
250261
mockUseObs.mockReturnValue({
251-
data: mockObsData.filter((o) => o.conceptUuid === '164163AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') as Array<ObsResult>,
262+
data: {
263+
observations: mockObsData.filter(
264+
(o) => o.conceptUuid === '164163AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
265+
) as Array<ObsResult>,
266+
concepts: mockConceptData,
267+
},
252268
error: null,
253269
isLoading: false,
254270
isValidating: false,

packages/esm-generic-patient-widgets-app/src/obs-table-horizontal/obs-table-horizontal.component.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ interface ObsTableHorizontalProps {
2323
const ObsTableHorizontal: React.FC<ObsTableHorizontalProps> = ({ patientUuid }) => {
2424
const { t } = useTranslation();
2525
const config = useConfig<ConfigObjectHorizontal>();
26-
const { data: obss, isValidating } = useObs(patientUuid, config.showEncounterType);
27-
const uniqueEncounterReferences = [...new Set(obss.map((o) => o.encounter.reference))].sort();
26+
const {
27+
data: { observations, concepts },
28+
isValidating,
29+
} = useObs(patientUuid);
30+
const uniqueEncounterReferences = [...new Set(observations.map((o) => o.encounter.reference))].sort();
2831
let obssGroupedByEncounters = uniqueEncounterReferences.map((reference) =>
29-
obss.filter((o) => o.encounter.reference === reference),
32+
observations.filter((o) => o.encounter.reference === reference),
3033
);
3134

3235
if (config.oldestFirst) {
@@ -41,7 +44,7 @@ const ObsTableHorizontal: React.FC<ObsTableHorizontalProps> = ({ patientUuid })
4144

4245
let tableRowLabels = config.data.map(({ concept, label }) => ({
4346
key: concept,
44-
header: t(label, label) || obss.find((o) => o.conceptUuid === concept)?.code?.text,
47+
header: t(label, label) || concepts.find((c) => c.uuid === concept)?.display,
4548
}));
4649

4750
if (config.showEncounterType) {

packages/esm-generic-patient-widgets-app/src/obs-table/obs-table.component.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,19 @@ interface ObsTableProps {
2323
const ObsTable: React.FC<ObsTableProps> = ({ patientUuid }) => {
2424
const { t } = useTranslation();
2525
const config = useConfig<ConfigObjectSwitchable>();
26-
const { data: obss } = useObs(patientUuid, config.showEncounterType);
27-
const uniqueEncounterReferences = [...new Set(obss.map((o) => o.encounter.reference))].sort();
26+
const {
27+
data: { observations, concepts },
28+
} = useObs(patientUuid);
29+
const uniqueEncounterReferences = [...new Set(observations.map((o) => o.encounter.reference))].sort();
2830
const obssGroupedByEncounters = uniqueEncounterReferences.map((reference) =>
29-
obss.filter((o) => o.encounter.reference === reference),
31+
observations.filter((o) => o.encounter.reference === reference),
3032
);
3133

3234
const tableHeaders = [
3335
{ key: 'date', header: t('dateAndTime', 'Date and time'), isSortable: true },
3436
...config.data.map(({ concept, label }) => ({
3537
key: concept,
36-
header: label || obss.find((o) => o.conceptUuid == concept)?.code.text,
38+
header: label || concepts.find((c) => c.uuid == concept)?.display,
3739
})),
3840
];
3941

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)