@openmrs/esm-patient-tests-app 11.3.1-pre.9435 → 11.3.1-pre.9439
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 +3 -3
- package/dist/1477.js +1 -1
- package/dist/1477.js.map +1 -1
- package/dist/1935.js +1 -1
- package/dist/1935.js.map +1 -1
- package/dist/3509.js +1 -1
- package/dist/3509.js.map +1 -1
- package/dist/4300.js +1 -1
- package/dist/6301.js +1 -1
- package/dist/6301.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-patient-tests-app.js +1 -1
- package/dist/openmrs-esm-patient-tests-app.js.buildmanifest.json +15 -15
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/routes.json +1 -1
- package/src/test-orders/add-test-order/add-test-order.test.tsx +1 -1
- package/src/test-orders/add-test-order/add-test-order.workspace.tsx +1 -1
- package/src/test-orders/add-test-order/test-order-form.component.tsx +1 -1
- package/src/test-orders/add-test-order/test-type-search.component.tsx +1 -1
- package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.test.tsx +2 -2
- package/src/test-results/filter/filter-context.test.tsx +556 -0
- package/src/test-results/filter/filter-context.tsx +1 -1
- package/src/test-results/filter/filter-reducer.test.ts +540 -0
- package/src/test-results/filter/filter-reducer.ts +1 -1
- package/src/test-results/filter/filter-set.test.tsx +694 -0
- package/src/test-results/grouped-timeline/grid.component.tsx +4 -2
- package/src/test-results/grouped-timeline/grouped-timeline.component.tsx +20 -22
- package/src/test-results/grouped-timeline/grouped-timeline.test.tsx +1 -1
- package/src/test-results/grouped-timeline/useObstreeData.test.ts +471 -0
- package/src/test-results/individual-results-table-tablet/usePanelData.tsx +40 -26
- package/src/test-results/loadPatientTestData/helpers.ts +29 -12
- package/src/test-results/loadPatientTestData/usePatientResultsData.ts +18 -7
- package/src/test-results/overview/external-overview.extension.tsx +1 -2
- package/src/test-results/print-modal/print-modal.extension.tsx +1 -1
- package/src/test-results/results-viewer/results-viewer.extension.tsx +7 -3
- package/src/test-results/tree-view/tree-view.component.tsx +16 -3
- package/src/test-results/tree-view/tree-view.test.tsx +117 -1
- package/src/test-results/trendline/trendline.component.tsx +88 -52
- package/src/test-results/ui-elements/reset-filters-empty-state/filter-empty-data-illustration.tsx +2 -2
- package/translations/en.json +1 -1
|
@@ -39,7 +39,7 @@ export function addUserDataToCache(patientUuid: string, data: PatientData, indic
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
async function getLatestObsUuid(patientUuid: string): Promise<string> {
|
|
42
|
+
async function getLatestObsUuid(patientUuid: string): Promise<string | undefined> {
|
|
43
43
|
const request = fhirObservationRequests({
|
|
44
44
|
patient: patientUuid,
|
|
45
45
|
category: 'laboratory',
|
|
@@ -61,12 +61,13 @@ async function getLatestObsUuid(patientUuid: string): Promise<string> {
|
|
|
61
61
|
* @param { string } indicator UUID of the newest observation
|
|
62
62
|
*/
|
|
63
63
|
export function getUserDataFromCache(patientUuid: string): [PatientData | undefined, Promise<boolean>] {
|
|
64
|
-
const
|
|
64
|
+
const cacheEntry = patientResultsDataCache[patientUuid];
|
|
65
|
+
const [data, , indicator] = cacheEntry || [];
|
|
65
66
|
|
|
66
67
|
return [
|
|
67
68
|
data,
|
|
68
|
-
!!data
|
|
69
|
-
? getLatestObsUuid(patientUuid).then((obsUuid) => obsUuid !==
|
|
69
|
+
!!data && indicator
|
|
70
|
+
? getLatestObsUuid(patientUuid).then((obsUuid) => obsUuid !== indicator)
|
|
70
71
|
: Promise.resolve(true),
|
|
71
72
|
];
|
|
72
73
|
}
|
|
@@ -109,31 +110,47 @@ export const loadObsEntries = async (patientUuid: string): Promise<Array<ObsReco
|
|
|
109
110
|
|
|
110
111
|
let responses = await Promise.all(retrieveFromIterator(requests, CHUNK_PREFETCH_COUNT));
|
|
111
112
|
|
|
112
|
-
const total = responses[0]
|
|
113
|
+
const total = responses[0]?.total ?? 0;
|
|
113
114
|
|
|
114
115
|
if (total > CHUNK_PREFETCH_COUNT * PAGE_SIZE) {
|
|
115
116
|
const missingRequestsCount = Math.ceil(total / PAGE_SIZE) - CHUNK_PREFETCH_COUNT;
|
|
116
117
|
responses = [...responses, ...(await Promise.all(retrieveFromIterator(requests, missingRequestsCount)))];
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
return responses.slice(0, Math.ceil(total / PAGE_SIZE)).flatMap((res) => res
|
|
120
|
+
return responses.slice(0, Math.ceil(total / PAGE_SIZE)).flatMap((res) => res?.entry?.map((e) => e.resource) ?? []);
|
|
120
121
|
};
|
|
121
122
|
|
|
122
|
-
export const getEntryConceptClassUuid = (entry) =>
|
|
123
|
+
export const getEntryConceptClassUuid = (entry: ObsRecord | FHIRObservationResource): string =>
|
|
124
|
+
entry?.code?.coding?.[0]?.code ?? '';
|
|
123
125
|
|
|
124
126
|
const conceptCache: Record<ConceptUuid, Promise<ConceptRecord>> = {};
|
|
125
127
|
/**
|
|
126
128
|
* fetch all concepts for all given observation entries
|
|
127
129
|
*/
|
|
128
130
|
export function loadPresentConcepts(entries: Array<ObsRecord>): Promise<Array<ConceptRecord>> {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
const conceptUuids = [...new Set(entries.map(getEntryConceptClassUuid).filter(Boolean))];
|
|
132
|
+
|
|
133
|
+
return Promise.allSettled(
|
|
134
|
+
conceptUuids.map(
|
|
131
135
|
(conceptUuid) =>
|
|
132
136
|
conceptCache[conceptUuid] ||
|
|
133
|
-
(conceptCache[conceptUuid] = fetch(`${window.openmrsBase}${restBaseUrl}/concept/${conceptUuid}?v=full`)
|
|
134
|
-
(res) =>
|
|
135
|
-
|
|
137
|
+
(conceptCache[conceptUuid] = fetch(`${window.openmrsBase}${restBaseUrl}/concept/${conceptUuid}?v=full`)
|
|
138
|
+
.then((res) => {
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
throw new Error(`Failed to fetch concept ${conceptUuid}: ${res.statusText}`);
|
|
141
|
+
}
|
|
142
|
+
return res.json();
|
|
143
|
+
})
|
|
144
|
+
.catch((error) => {
|
|
145
|
+
// Remove failed promise from cache so it can be retried
|
|
146
|
+
delete conceptCache[conceptUuid];
|
|
147
|
+
throw error;
|
|
148
|
+
})),
|
|
136
149
|
),
|
|
150
|
+
).then((results) =>
|
|
151
|
+
results
|
|
152
|
+
.filter((result): result is PromiseFulfilledResult<ConceptRecord> => result.status === 'fulfilled')
|
|
153
|
+
.map((result) => result.value),
|
|
137
154
|
);
|
|
138
155
|
}
|
|
139
156
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useEffect, useState, useRef } from 'react';
|
|
2
2
|
import { type PatientData } from '@openmrs/esm-patient-common-lib';
|
|
3
3
|
import loadPatientData from './loadPatientData';
|
|
4
4
|
|
|
@@ -14,18 +14,29 @@ const usePatientResultsData = (patientUuid: string): LoadingState => {
|
|
|
14
14
|
loaded: false,
|
|
15
15
|
error: undefined,
|
|
16
16
|
});
|
|
17
|
+
const isMountedRef = useRef(true);
|
|
17
18
|
|
|
18
19
|
useEffect(() => {
|
|
19
|
-
|
|
20
|
+
isMountedRef.current = true;
|
|
20
21
|
if (patientUuid) {
|
|
21
22
|
const [data, reloadedDataPromise] = loadPatientData(patientUuid);
|
|
22
|
-
if (!!data)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
if (!!data && isMountedRef.current) {
|
|
24
|
+
setState({ sortedObs: data, loaded: true, error: undefined });
|
|
25
|
+
}
|
|
26
|
+
reloadedDataPromise
|
|
27
|
+
.then((reloadedData) => {
|
|
28
|
+
if (reloadedData !== data && isMountedRef.current) {
|
|
29
|
+
setState({ sortedObs: reloadedData, loaded: true, error: undefined });
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.catch((error) => {
|
|
33
|
+
if (isMountedRef.current) {
|
|
34
|
+
setState({ sortedObs: {}, loaded: true, error });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
26
37
|
}
|
|
27
38
|
return () => {
|
|
28
|
-
|
|
39
|
+
isMountedRef.current = false;
|
|
29
40
|
};
|
|
30
41
|
}, [patientUuid]);
|
|
31
42
|
|
|
@@ -37,7 +37,6 @@ function getFilteredOverviewData(sortedObs: PatientData, filter) {
|
|
|
37
37
|
|
|
38
38
|
function useFilteredOverviewData(patientUuid: string, filter: (filterProps: PanelFilterProps) => boolean = () => true) {
|
|
39
39
|
const { sortedObs, loaded, error } = usePatientResultsData(patientUuid);
|
|
40
|
-
|
|
41
40
|
const overviewData = useMemo(() => getFilteredOverviewData(sortedObs, filter), [filter, sortedObs]);
|
|
42
41
|
|
|
43
42
|
return { overviewData, loaded, error };
|
|
@@ -46,8 +45,8 @@ function useFilteredOverviewData(patientUuid: string, filter: (filterProps: Pane
|
|
|
46
45
|
const ExternalOverview: React.FC<ExternalOverviewProps> = ({ patientUuid, filter }) => {
|
|
47
46
|
const { t } = useTranslation();
|
|
48
47
|
const { overviewData, loaded } = useFilteredOverviewData(patientUuid, filter);
|
|
49
|
-
|
|
50
48
|
const cardTitle = t('recentResults', 'Recent Results');
|
|
49
|
+
|
|
51
50
|
const handleSeeAll = useCallback(() => {
|
|
52
51
|
navigate({ to: `\${openmrsSpaBase}/patient/${patientUuid}/chart/Results` });
|
|
53
52
|
}, [patientUuid]);
|
|
@@ -87,7 +87,7 @@ function PrintModal({
|
|
|
87
87
|
|
|
88
88
|
const identifiers =
|
|
89
89
|
patient?.identifier?.filter(
|
|
90
|
-
(identifier) => !excludePatientIdentifierCodeTypes?.uuids.includes(identifier.type
|
|
90
|
+
(identifier) => !excludePatientIdentifierCodeTypes?.uuids.includes(identifier.type?.coding?.[0]?.code),
|
|
91
91
|
) ?? [];
|
|
92
92
|
|
|
93
93
|
return {
|
|
@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import type { TFunction } from 'i18next';
|
|
5
|
-
import { ContentSwitcher, Switch, Button } from '@carbon/react';
|
|
5
|
+
import { ContentSwitcher, Switch, Button, DataTableSkeleton } from '@carbon/react';
|
|
6
6
|
import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib';
|
|
7
7
|
import { RenewIcon, useConfig, useLayoutType } from '@openmrs/esm-framework';
|
|
8
8
|
import { type ConfigObject } from '../../config-schema';
|
|
@@ -36,9 +36,13 @@ const RoutedResultsViewer: React.FC<ResultsViewerProps> = ({ basePath, patientUu
|
|
|
36
36
|
return <ErrorState error={error} headerTitle={t('dataLoadError', 'Data Load Error')} />;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
if (isLoading) {
|
|
40
|
+
return <DataTableSkeleton role="progressbar" />;
|
|
41
|
+
}
|
|
42
|
+
|
|
39
43
|
if (roots?.length) {
|
|
40
44
|
return (
|
|
41
|
-
<FilterProvider roots={
|
|
45
|
+
<FilterProvider roots={roots as Roots} isLoading={isLoading}>
|
|
42
46
|
<ResultsViewer patientUuid={patientUuid} basePath={basePath} />
|
|
43
47
|
</FilterProvider>
|
|
44
48
|
);
|
|
@@ -47,7 +51,7 @@ const RoutedResultsViewer: React.FC<ResultsViewerProps> = ({ basePath, patientUu
|
|
|
47
51
|
return (
|
|
48
52
|
<EmptyState
|
|
49
53
|
headerTitle={t('testResults_title', 'Test Results')}
|
|
50
|
-
displayText={t('testResultsData', '
|
|
54
|
+
displayText={t('testResultsData', 'test results data')}
|
|
51
55
|
/>
|
|
52
56
|
);
|
|
53
57
|
};
|
|
@@ -99,11 +99,16 @@ const TreeView: React.FC<TreeViewProps> = ({ patientUuid, expanded, view }) => {
|
|
|
99
99
|
return <ErrorState error={error} headerTitle={t('dataLoadError', 'Data Load Error')} />;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// Don't show empty state while loading - wait for data to finish loading
|
|
103
|
+
if (isLoading) {
|
|
104
|
+
return <DataTableSkeleton role="progressbar" />;
|
|
105
|
+
}
|
|
106
|
+
|
|
102
107
|
if (!roots || roots.length === 0) {
|
|
103
108
|
return (
|
|
104
109
|
<EmptyState
|
|
105
110
|
headerTitle={t('testResults_title', 'Test Results')}
|
|
106
|
-
displayText={t('testResultsData', '
|
|
111
|
+
displayText={t('testResultsData', 'test results data')}
|
|
107
112
|
/>
|
|
108
113
|
);
|
|
109
114
|
}
|
|
@@ -165,13 +170,21 @@ const TreeView: React.FC<TreeViewProps> = ({ patientUuid, expanded, view }) => {
|
|
|
165
170
|
{isLoading ? (
|
|
166
171
|
<DataTableSkeleton />
|
|
167
172
|
) : view === 'individual-test' ? (
|
|
168
|
-
|
|
173
|
+
tableData && tableData.length > 0 ? (
|
|
174
|
+
<div className={styles.panelViewTimeline}>
|
|
175
|
+
<GroupedPanelsTables
|
|
176
|
+
patientUuid={patientUuid}
|
|
177
|
+
className={styles.groupPanelsTables}
|
|
178
|
+
loadingPanelData={isLoading}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
) : (
|
|
169
182
|
<GroupedPanelsTables
|
|
170
183
|
patientUuid={patientUuid}
|
|
171
184
|
className={styles.groupPanelsTables}
|
|
172
185
|
loadingPanelData={isLoading}
|
|
173
186
|
/>
|
|
174
|
-
|
|
187
|
+
)
|
|
175
188
|
) : view === 'over-time' ? (
|
|
176
189
|
<GroupedTimeline patientUuid={patientUuid} />
|
|
177
190
|
) : null}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
3
4
|
import { getDefaultsFromConfigSchema, useConfig, useLayoutType } from '@openmrs/esm-framework';
|
|
4
5
|
import { mockPatient } from 'tools';
|
|
5
6
|
import { mockResults } from '__mocks__';
|
|
6
7
|
import { type ConfigObject, configSchema } from '../../config-schema';
|
|
7
8
|
import { useGetManyObstreeData } from '../grouped-timeline';
|
|
8
|
-
import TreeView from './tree-view.component';
|
|
9
9
|
import { FilterProvider, type Roots } from '../filter/filter-context';
|
|
10
10
|
import { type ObsTreeNode } from '../grouped-timeline/useObstreeData';
|
|
11
|
+
import TreeView from './tree-view.component';
|
|
11
12
|
|
|
12
13
|
const mockUseConfig = jest.mocked(useConfig<ConfigObject>);
|
|
13
14
|
const mockUseLayoutType = jest.mocked(useLayoutType);
|
|
@@ -106,4 +107,119 @@ describe('TreeView', () => {
|
|
|
106
107
|
expect(screen.getAllByText('Haemoglobin').length).toBeGreaterThan(0);
|
|
107
108
|
expect(screen.getAllByText('Hematocrit').length).toBeGreaterThan(0);
|
|
108
109
|
});
|
|
110
|
+
|
|
111
|
+
describe('Reset button - Tablet overlay', () => {
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
mockUseLayoutType.mockReturnValue('tablet');
|
|
114
|
+
mockUseGetManyObstreeData.mockReturnValue({
|
|
115
|
+
roots: mockResults as unknown as Array<ObsTreeNode>,
|
|
116
|
+
isLoading: false,
|
|
117
|
+
error: null,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should show reset tree button in tablet overlay when tree is opened', async () => {
|
|
122
|
+
const user = userEvent.setup();
|
|
123
|
+
|
|
124
|
+
renderTreeViewWithMockContext();
|
|
125
|
+
|
|
126
|
+
// Open the tree overlay by clicking the tree button
|
|
127
|
+
const treeButton = screen.getByRole('button', { name: /show tree/i });
|
|
128
|
+
await user.click(treeButton);
|
|
129
|
+
|
|
130
|
+
// Reset tree button should be visible in the overlay
|
|
131
|
+
const resetButton = screen.getByRole('button', { name: /reset tree/i });
|
|
132
|
+
expect(resetButton).toBeInTheDocument();
|
|
133
|
+
expect(resetButton).toBeEnabled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should show both reset and view buttons in overlay', async () => {
|
|
137
|
+
const user = userEvent.setup();
|
|
138
|
+
|
|
139
|
+
renderTreeViewWithMockContext();
|
|
140
|
+
|
|
141
|
+
// Open the tree overlay
|
|
142
|
+
const treeButton = screen.getByRole('button', { name: /show tree/i });
|
|
143
|
+
await user.click(treeButton);
|
|
144
|
+
|
|
145
|
+
// Both buttons should be visible and enabled
|
|
146
|
+
const resetButton = screen.getByRole('button', { name: /reset tree/i });
|
|
147
|
+
const viewButton = screen.getByRole('button', { name: /view.*results/i });
|
|
148
|
+
|
|
149
|
+
expect(resetButton).toBeInTheDocument();
|
|
150
|
+
expect(resetButton).toBeEnabled();
|
|
151
|
+
expect(viewButton).toBeInTheDocument();
|
|
152
|
+
expect(viewButton).toBeEnabled();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should reset filters when reset tree button is clicked', async () => {
|
|
156
|
+
const user = userEvent.setup();
|
|
157
|
+
|
|
158
|
+
renderTreeViewWithMockContext();
|
|
159
|
+
|
|
160
|
+
// Open the tree overlay
|
|
161
|
+
const treeButton = screen.getByRole('button', { name: /show tree/i });
|
|
162
|
+
await user.click(treeButton);
|
|
163
|
+
|
|
164
|
+
// Find an enabled checkbox (one with hasData: true in mock data)
|
|
165
|
+
// Mock data has "Platelets" with data
|
|
166
|
+
const plateletsCheckboxes = screen.getAllByRole('checkbox', { name: /platelets/i });
|
|
167
|
+
// Find the first enabled one
|
|
168
|
+
const plateletsCheckbox = plateletsCheckboxes.find((cb) => !cb.hasAttribute('disabled'));
|
|
169
|
+
|
|
170
|
+
expect(plateletsCheckbox).toBeDefined();
|
|
171
|
+
await user.click(plateletsCheckbox);
|
|
172
|
+
expect(plateletsCheckbox).toBeChecked();
|
|
173
|
+
|
|
174
|
+
// Click reset tree button
|
|
175
|
+
const resetButton = screen.getByRole('button', { name: /reset tree/i });
|
|
176
|
+
await user.click(resetButton);
|
|
177
|
+
|
|
178
|
+
// Checkbox should be unchecked
|
|
179
|
+
expect(plateletsCheckbox).not.toBeChecked();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should show filtered results count in view button', async () => {
|
|
183
|
+
const user = userEvent.setup();
|
|
184
|
+
|
|
185
|
+
renderTreeViewWithMockContext();
|
|
186
|
+
|
|
187
|
+
// Open the tree overlay
|
|
188
|
+
const treeButton = screen.getByRole('button', { name: /show tree/i });
|
|
189
|
+
await user.click(treeButton);
|
|
190
|
+
|
|
191
|
+
// Initially should show total count
|
|
192
|
+
const viewButton = screen.getByRole('button', { name: /view.*results/i });
|
|
193
|
+
expect(viewButton).toBeInTheDocument();
|
|
194
|
+
|
|
195
|
+
// Check a filter - find an enabled checkbox
|
|
196
|
+
const plateletsCheckboxes = screen.getAllByRole('checkbox', { name: /platelets/i });
|
|
197
|
+
const plateletsCheckbox = plateletsCheckboxes.find((cb) => !cb.hasAttribute('disabled'));
|
|
198
|
+
await user.click(plateletsCheckbox);
|
|
199
|
+
|
|
200
|
+
// View button should update with filtered count
|
|
201
|
+
expect(screen.getByRole('button', { name: /view.*results/i })).toBeInTheDocument();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should close overlay when view results button is clicked', async () => {
|
|
205
|
+
const user = userEvent.setup();
|
|
206
|
+
|
|
207
|
+
renderTreeViewWithMockContext();
|
|
208
|
+
|
|
209
|
+
// Open the tree overlay
|
|
210
|
+
const treeButton = screen.getByRole('button', { name: /show tree/i });
|
|
211
|
+
await user.click(treeButton);
|
|
212
|
+
|
|
213
|
+
// Reset tree button should be visible in the overlay
|
|
214
|
+
const resetButton = screen.getByRole('button', { name: /reset tree/i });
|
|
215
|
+
expect(resetButton).toBeInTheDocument();
|
|
216
|
+
|
|
217
|
+
// Click view results button
|
|
218
|
+
const viewButton = screen.getByRole('button', { name: /view.*results/i });
|
|
219
|
+
await user.click(viewButton);
|
|
220
|
+
|
|
221
|
+
// Overlay should be closed (reset button no longer visible)
|
|
222
|
+
expect(screen.queryByRole('button', { name: /reset tree/i })).not.toBeInTheDocument();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
109
225
|
});
|
|
@@ -4,23 +4,43 @@ import { Button, InlineLoading, SkeletonText } from '@carbon/react';
|
|
|
4
4
|
import { LineChart, ScaleTypes, TickRotations } from '@carbon/charts-react';
|
|
5
5
|
import { ArrowLeftIcon, ConfigurableLink, formatDate } from '@openmrs/esm-framework';
|
|
6
6
|
import { EmptyState, type OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib';
|
|
7
|
-
import { useObstreeData } from './trendline-resource';
|
|
8
7
|
import { testResultsBasePath } from '../helpers';
|
|
8
|
+
import { useObstreeData } from './trendline-resource';
|
|
9
9
|
import CommonDataTable from '../overview/common-datatable.component';
|
|
10
10
|
import RangeSelector from './range-selector.component';
|
|
11
11
|
import styles from './trendline.scss';
|
|
12
12
|
|
|
13
13
|
interface TrendlineProps {
|
|
14
|
-
patientUuid: string;
|
|
15
|
-
conceptUuid: string;
|
|
16
14
|
basePath: string;
|
|
15
|
+
conceptUuid: string;
|
|
16
|
+
patientUuid: string;
|
|
17
17
|
hideTrendlineHeader?: boolean;
|
|
18
18
|
showBackToTimelineButton?: boolean;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
interface TrendLineBackgroundProps {
|
|
22
|
+
children?: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const TrendLineBackground: React.FC<TrendLineBackgroundProps> = ({ ...props }) => (
|
|
26
|
+
<div {...props} className={styles.background} />
|
|
27
|
+
);
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
interface TrendlineHeaderProps {
|
|
30
|
+
isValidating: boolean;
|
|
31
|
+
patientUuid: string;
|
|
32
|
+
referenceRange: string;
|
|
33
|
+
showBackToTimelineButton: boolean;
|
|
34
|
+
title: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const TrendlineHeader: React.FC<TrendlineHeaderProps> = ({
|
|
38
|
+
patientUuid,
|
|
39
|
+
title,
|
|
40
|
+
referenceRange,
|
|
41
|
+
isValidating,
|
|
42
|
+
showBackToTimelineButton,
|
|
43
|
+
}) => {
|
|
24
44
|
const { t } = useTranslation();
|
|
25
45
|
return (
|
|
26
46
|
<div className={styles.header}>
|
|
@@ -28,9 +48,9 @@ const TrendlineHeader = ({ patientUuid, title, referenceRange, isValidating, sho
|
|
|
28
48
|
{showBackToTimelineButton && (
|
|
29
49
|
<ConfigurableLink to={testResultsBasePath(`/patient/${patientUuid}/chart`)}>
|
|
30
50
|
<Button
|
|
51
|
+
iconDescription={t('returnToTimeline', 'Return to timeline')}
|
|
31
52
|
kind="ghost"
|
|
32
53
|
renderIcon={(props: ComponentProps<typeof ArrowLeftIcon>) => <ArrowLeftIcon size={24} {...props} />}
|
|
33
|
-
iconDescription={t('returnToTimeline', 'Return to timeline')}
|
|
34
54
|
>
|
|
35
55
|
<span>{t('backToTimeline', 'Back to timeline')}</span>
|
|
36
56
|
</Button>
|
|
@@ -52,8 +72,8 @@ const Trendline: React.FC<TrendlineProps> = ({
|
|
|
52
72
|
hideTrendlineHeader = false,
|
|
53
73
|
showBackToTimelineButton = false,
|
|
54
74
|
}) => {
|
|
55
|
-
const { trendlineData, isLoading, isValidating } = useObstreeData(patientUuid, conceptUuid);
|
|
56
75
|
const { t } = useTranslation();
|
|
76
|
+
const { trendlineData, isLoading, isValidating } = useObstreeData(patientUuid, conceptUuid);
|
|
57
77
|
const { obs, display: chartTitle, hiNormal, lowNormal, units: leftAxisTitle, range: referenceRange } = trendlineData;
|
|
58
78
|
const bottomAxisTitle = t('date', 'Date');
|
|
59
79
|
const [range, setRange] = useState<[Date, Date]>();
|
|
@@ -83,52 +103,52 @@ const Trendline: React.FC<TrendlineProps> = ({
|
|
|
83
103
|
}
|
|
84
104
|
}, [obs]);
|
|
85
105
|
|
|
86
|
-
const data
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
const { data, tableData } = useMemo(() => {
|
|
107
|
+
const chartData: Array<{
|
|
108
|
+
date: Date;
|
|
109
|
+
value: number;
|
|
110
|
+
group: string;
|
|
111
|
+
min?: number;
|
|
112
|
+
max?: number;
|
|
113
|
+
}> = [];
|
|
93
114
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
};
|
|
103
|
-
}> = [];
|
|
115
|
+
const table: Array<{
|
|
116
|
+
id: string;
|
|
117
|
+
dateTime: string;
|
|
118
|
+
value: {
|
|
119
|
+
value: number;
|
|
120
|
+
interpretation: OBSERVATION_INTERPRETATION;
|
|
121
|
+
};
|
|
122
|
+
}> = [];
|
|
104
123
|
|
|
105
|
-
|
|
124
|
+
obs.forEach((observation, idx) => {
|
|
125
|
+
const normalRange =
|
|
126
|
+
hiNormal && lowNormal
|
|
127
|
+
? {
|
|
128
|
+
max: hiNormal,
|
|
129
|
+
min: lowNormal,
|
|
130
|
+
}
|
|
131
|
+
: {};
|
|
106
132
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
: {};
|
|
133
|
+
chartData.push({
|
|
134
|
+
date: new Date(Date.parse(observation.obsDatetime)),
|
|
135
|
+
value: parseFloat(observation.value),
|
|
136
|
+
group: chartTitle,
|
|
137
|
+
...normalRange,
|
|
138
|
+
});
|
|
115
139
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
140
|
+
table.push({
|
|
141
|
+
id: `${idx}`,
|
|
142
|
+
dateTime: observation.obsDatetime,
|
|
143
|
+
value: {
|
|
144
|
+
value: parseFloat(observation.value),
|
|
145
|
+
interpretation: observation.interpretation,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
121
148
|
});
|
|
122
149
|
|
|
123
|
-
tableData
|
|
124
|
-
|
|
125
|
-
dateTime: obs.obsDatetime,
|
|
126
|
-
value: {
|
|
127
|
-
value: parseFloat(obs.value),
|
|
128
|
-
interpretation: obs.interpretation,
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
});
|
|
150
|
+
return { data: chartData, tableData: table };
|
|
151
|
+
}, [obs, chartTitle, hiNormal, lowNormal]);
|
|
132
152
|
|
|
133
153
|
const chartOptions = useMemo(
|
|
134
154
|
() => ({
|
|
@@ -143,7 +163,6 @@ const Trendline: React.FC<TrendlineProps> = ({
|
|
|
143
163
|
scaleType: ScaleTypes.TIME,
|
|
144
164
|
ticks: {
|
|
145
165
|
rotation: TickRotations.ALWAYS,
|
|
146
|
-
// formatter: x => x.toLocaleDateString("en-US", TableDateFormatOption)
|
|
147
166
|
},
|
|
148
167
|
domain: range,
|
|
149
168
|
},
|
|
@@ -239,11 +258,11 @@ const Trendline: React.FC<TrendlineProps> = ({
|
|
|
239
258
|
<div className={styles.container}>
|
|
240
259
|
{!hideTrendlineHeader && (
|
|
241
260
|
<TrendlineHeader
|
|
242
|
-
showBackToTimelineButton={showBackToTimelineButton}
|
|
243
261
|
isValidating={isValidating}
|
|
244
262
|
patientUuid={patientUuid}
|
|
245
|
-
title={dataset}
|
|
246
263
|
referenceRange={referenceRange}
|
|
264
|
+
showBackToTimelineButton={showBackToTimelineButton}
|
|
265
|
+
title={chartTitle}
|
|
247
266
|
/>
|
|
248
267
|
)}
|
|
249
268
|
<TrendLineBackground>
|
|
@@ -269,8 +288,25 @@ const Trendline: React.FC<TrendlineProps> = ({
|
|
|
269
288
|
);
|
|
270
289
|
};
|
|
271
290
|
|
|
272
|
-
|
|
273
|
-
|
|
291
|
+
interface DrawTableProps {
|
|
292
|
+
tableData: Array<{
|
|
293
|
+
id: string;
|
|
294
|
+
dateTime: string;
|
|
295
|
+
value: {
|
|
296
|
+
value: number;
|
|
297
|
+
interpretation: OBSERVATION_INTERPRETATION;
|
|
298
|
+
};
|
|
299
|
+
}>;
|
|
300
|
+
tableHeaderData: Array<{
|
|
301
|
+
header: string;
|
|
302
|
+
key: string;
|
|
303
|
+
}>;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const DrawTable = React.memo<DrawTableProps>(({ tableData, tableHeaderData }) => {
|
|
307
|
+
return <CommonDataTable data={tableData as any} tableHeaders={tableHeaderData} />;
|
|
274
308
|
});
|
|
275
309
|
|
|
310
|
+
DrawTable.displayName = 'DrawTable';
|
|
311
|
+
|
|
276
312
|
export default Trendline;
|
package/src/test-results/ui-elements/reset-filters-empty-state/filter-empty-data-illustration.tsx
CHANGED
|
@@ -14,8 +14,8 @@ const FilterEmptyDataIllustration: React.FC<FilterEmptyDataIllustrationProps> =
|
|
|
14
14
|
<path
|
|
15
15
|
d="M57 57H1V1h56v56zM1 29h56M1 15h56M1 43h56M29 1v56M15 1v56M43 1v56"
|
|
16
16
|
stroke="#9ACBCA"
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
strokeWidth="1.44"
|
|
18
|
+
strokeLinejoin="round"
|
|
19
19
|
/>
|
|
20
20
|
</g>
|
|
21
21
|
</svg>
|
package/translations/en.json
CHANGED
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"testName": "Test name",
|
|
101
101
|
"testResults": "test results",
|
|
102
102
|
"testResults_title": "Test Results",
|
|
103
|
-
"testResultsData": "
|
|
103
|
+
"testResultsData": "test results data",
|
|
104
104
|
"tests": "Tests",
|
|
105
105
|
"testType": "Test type",
|
|
106
106
|
"testTypeRequired": "Test type is required",
|