@openmrs/esm-laboratory-app 1.2.1-pre.739 → 1.2.1-pre.745
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/cache/a60408b5239ac3b0-meta.json +1 -0
- package/.turbo/cache/a60408b5239ac3b0.tar.zst +0 -0
- package/.turbo/turbo-build.log +3 -3
- package/dist/1120.js +1 -1
- package/dist/1120.js.map +1 -1
- package/dist/1788.js +1 -1
- package/dist/1788.js.map +1 -1
- package/dist/3656.js +1 -1
- package/dist/3656.js.map +1 -1
- package/dist/4069.js +1 -1
- package/dist/4069.js.map +1 -1
- package/dist/4300.js +1 -1
- package/dist/5085.js +1 -1
- package/dist/5085.js.map +1 -1
- package/dist/6134.js +1 -1
- package/dist/6134.js.map +1 -1
- package/dist/7423.js +1 -1
- package/dist/7423.js.map +1 -1
- package/dist/8554.js +1 -1
- package/dist/8554.js.map +1 -1
- package/dist/8667.js +1 -1
- package/dist/8667.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-laboratory-app.js +1 -1
- package/dist/openmrs-esm-laboratory-app.js.buildmanifest.json +34 -34
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/components/orders-table/list-order-details.component.tsx +31 -24
- package/src/components/orders-table/list-order-details.scss +0 -1
- package/src/components/orders-table/orders-data-table.component.tsx +143 -56
- package/src/components/orders-table/orders-data-table.test.tsx +215 -0
- package/src/config-schema.ts +20 -1
- package/src/lab-tabs/data-table-extensions/tests-ordered-table.extension.tsx +1 -1
- package/src/lab-tiles/all-lab-requests-tile.component.tsx +1 -1
- package/src/lab-tiles/completed-lab-requests-tile.component.tsx +1 -1
- package/src/lab-tiles/in-progress-lab-requests-tile.component.tsx +1 -1
- package/src/laboratory-resource.ts +24 -33
- package/src/types.ts +20 -17
- package/translations/en.json +2 -2
- package/.turbo/cache/26dd8861bd3eca6d-meta.json +0 -1
- package/.turbo/cache/26dd8861bd3eca6d.tar.zst +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
DataTable,
|
|
4
4
|
DataTableSkeleton,
|
|
@@ -22,67 +22,152 @@ import {
|
|
|
22
22
|
TableToolbarSearch,
|
|
23
23
|
Tile,
|
|
24
24
|
} from '@carbon/react';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
ExtensionSlot,
|
|
27
|
+
formatDate,
|
|
28
|
+
parseDate,
|
|
29
|
+
type Patient,
|
|
30
|
+
showModal,
|
|
31
|
+
useConfig,
|
|
32
|
+
usePagination,
|
|
33
|
+
} from '@openmrs/esm-framework';
|
|
26
34
|
import { useTranslation } from 'react-i18next';
|
|
27
|
-
import { type Order } from '@openmrs/esm-patient-common-lib';
|
|
28
|
-
import type
|
|
29
|
-
import { useLabOrders
|
|
35
|
+
import { type Order, type FulfillerStatus } from '@openmrs/esm-patient-common-lib';
|
|
36
|
+
import { type FlattenedOrder, type OrderAction } from '../../types';
|
|
37
|
+
import { useLabOrders } from '../../laboratory-resource';
|
|
30
38
|
import { OrdersDateRangePicker } from './orders-date-range-picker.component';
|
|
31
39
|
import ListOrderDetails from './list-order-details.component';
|
|
32
40
|
import styles from './orders-data-table.scss';
|
|
41
|
+
import { type Config } from '../../config-schema';
|
|
42
|
+
|
|
43
|
+
const labTableColumnSpec = {
|
|
44
|
+
name: {
|
|
45
|
+
// t('patient', 'Patient')
|
|
46
|
+
headerLabelKey: 'patient',
|
|
47
|
+
headerLabelDefault: 'Patient',
|
|
48
|
+
key: 'patientName',
|
|
49
|
+
},
|
|
50
|
+
age: {
|
|
51
|
+
// t('age', 'Age')
|
|
52
|
+
headerLabelKey: 'age',
|
|
53
|
+
headerLabelDefault: 'Age',
|
|
54
|
+
key: 'patientAge',
|
|
55
|
+
},
|
|
56
|
+
sex: {
|
|
57
|
+
// t('sex', 'Sex')
|
|
58
|
+
headerLabelKey: 'sex',
|
|
59
|
+
headerLabelDefault: 'Sex',
|
|
60
|
+
key: 'patientSex',
|
|
61
|
+
},
|
|
62
|
+
totalOrders: {
|
|
63
|
+
// t('totalOrders', 'Total Orders')
|
|
64
|
+
headerLabelKey: 'totalOrders',
|
|
65
|
+
headerLabelDefault: 'Total Orders',
|
|
66
|
+
key: 'totalOrders',
|
|
67
|
+
},
|
|
68
|
+
action: {
|
|
69
|
+
// t('action', 'Action')
|
|
70
|
+
headerLabelKey: 'action',
|
|
71
|
+
headerLabelDefault: 'Action',
|
|
72
|
+
key: 'action',
|
|
73
|
+
},
|
|
74
|
+
patientId: {
|
|
75
|
+
// t('patientId', 'Patient ID')
|
|
76
|
+
headerLabelKey: 'patientId',
|
|
77
|
+
headerLabelDefault: 'Patient ID',
|
|
78
|
+
key: 'patientId',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export interface OrdersDataTableProps {
|
|
83
|
+
/* Whether the data table should include a status filter dropdown */
|
|
84
|
+
useFilter?: boolean;
|
|
85
|
+
actionsSlotName?: string;
|
|
86
|
+
excludeColumns?: Array<string>;
|
|
87
|
+
fulfillerStatus?: FulfillerStatus;
|
|
88
|
+
newOrdersOnly?: boolean;
|
|
89
|
+
excludeCanceledAndDiscontinuedOrders?: boolean;
|
|
90
|
+
actions?: Array<OrderAction>;
|
|
91
|
+
}
|
|
33
92
|
|
|
34
93
|
const OrdersDataTable: React.FC<OrdersDataTableProps> = (props) => {
|
|
35
94
|
const { t } = useTranslation();
|
|
36
95
|
const [filter, setFilter] = useState<FulfillerStatus>(null);
|
|
37
96
|
const [searchString, setSearchString] = useState('');
|
|
97
|
+
const { labTableColumns, patientIdIdentifierTypeUuid } = useConfig<Config>();
|
|
38
98
|
|
|
39
|
-
const { labOrders, isLoading } = useLabOrders(
|
|
40
|
-
props.useFilter ? filter : props.fulfillerStatus,
|
|
41
|
-
props.
|
|
42
|
-
|
|
99
|
+
const { labOrders, isLoading } = useLabOrders({
|
|
100
|
+
status: props.useFilter ? filter : props.fulfillerStatus,
|
|
101
|
+
newOrdersOnly: props.newOrdersOnly,
|
|
102
|
+
excludeCanceled: props.excludeCanceledAndDiscontinuedOrders,
|
|
103
|
+
includePatientId: labTableColumns.includes('patientId'),
|
|
104
|
+
});
|
|
43
105
|
|
|
44
|
-
const flattenedLabOrders:
|
|
106
|
+
const flattenedLabOrders: Array<FlattenedOrder> = useMemo(() => {
|
|
45
107
|
return (
|
|
46
108
|
labOrders?.map((order) => {
|
|
47
109
|
return {
|
|
48
|
-
|
|
110
|
+
id: order.uuid,
|
|
111
|
+
patientUuid: order.patient.uuid,
|
|
112
|
+
orderNumber: order.orderNumber,
|
|
49
113
|
dateActivated: formatDate(parseDate(order.dateActivated)),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
114
|
+
fulfillerStatus: order.fulfillerStatus,
|
|
115
|
+
urgency: order.urgency,
|
|
116
|
+
orderer: order.orderer?.display,
|
|
117
|
+
instructions: order.instructions,
|
|
118
|
+
fulfillerComment: order.fulfillerComment,
|
|
119
|
+
display: order.display,
|
|
55
120
|
};
|
|
56
121
|
}) ?? []
|
|
57
122
|
);
|
|
58
123
|
}, [labOrders]);
|
|
59
124
|
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
const
|
|
63
|
-
if (!acc[item.patientUuid]) {
|
|
64
|
-
acc[item.patientUuid] = [];
|
|
65
|
-
}
|
|
66
|
-
acc[item.patientUuid].push(item);
|
|
67
|
-
return acc;
|
|
68
|
-
}, {});
|
|
125
|
+
const groupedOrdersByPatient = useMemo(() => {
|
|
126
|
+
if (labOrders && labOrders.length > 0) {
|
|
127
|
+
const patientUuids = [...new Set(labOrders.map((order) => order.patient.uuid))];
|
|
69
128
|
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
129
|
+
return patientUuids.map((patientUuid) => {
|
|
130
|
+
const labOrdersForPatient = labOrders.filter((order) => order.patient.uuid === patientUuid);
|
|
131
|
+
const patient: Patient = labOrdersForPatient[0]?.patient;
|
|
132
|
+
const flattenedLabOrdersForPatient = flattenedLabOrders.filter((order) => order.patientUuid === patientUuid);
|
|
133
|
+
return {
|
|
134
|
+
patientId: patient.identifiers?.find(
|
|
135
|
+
(identifier) =>
|
|
136
|
+
identifier.preferred &&
|
|
137
|
+
!identifier.voided &&
|
|
138
|
+
identifier.identifierType.uuid === patientIdIdentifierTypeUuid,
|
|
139
|
+
)?.identifier,
|
|
140
|
+
patientUuid: patientUuid,
|
|
141
|
+
patientName: patient.person.display,
|
|
142
|
+
patientAge: patient.person.age,
|
|
143
|
+
patientSex: patient.person.gender,
|
|
144
|
+
totalOrders: flattenedLabOrdersForPatient.length,
|
|
145
|
+
orders: flattenedLabOrdersForPatient,
|
|
146
|
+
originalOrders: labOrdersForPatient,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
74
149
|
} else {
|
|
75
150
|
return [];
|
|
76
151
|
}
|
|
77
|
-
}
|
|
152
|
+
}, [flattenedLabOrders, labOrders, patientIdIdentifierTypeUuid]);
|
|
78
153
|
|
|
79
|
-
const
|
|
154
|
+
const searchResults = useMemo(() => {
|
|
155
|
+
if (searchString && searchString.trim() !== '') {
|
|
156
|
+
// Normalize the search string to lowercase
|
|
157
|
+
const lowerSearchString = searchString.toLowerCase();
|
|
158
|
+
return groupedOrdersByPatient.filter(
|
|
159
|
+
(orderGroup) =>
|
|
160
|
+
(labTableColumns.includes('name') && orderGroup.patientName.toLowerCase().includes(lowerSearchString)) ||
|
|
161
|
+
(labTableColumns.includes('patientId') && orderGroup.patientId.toLowerCase().includes(lowerSearchString)) ||
|
|
162
|
+
orderGroup.orders.some((order) => order.orderNumber.toLowerCase().includes(lowerSearchString)),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
80
165
|
|
|
81
|
-
|
|
166
|
+
return groupedOrdersByPatient;
|
|
167
|
+
}, [searchString, groupedOrdersByPatient, labTableColumns]);
|
|
82
168
|
|
|
83
169
|
const orderStatuses = [
|
|
84
170
|
{ value: null, display: t('all', 'All') },
|
|
85
|
-
{ value: 'NEW', display: t('newStatus', 'NEW') },
|
|
86
171
|
{ value: 'RECEIVED', display: t('receivedStatus', 'RECEIVED') },
|
|
87
172
|
{ value: 'IN_PROGRESS', display: t('inProgressStatus', 'IN_PROGRESS') },
|
|
88
173
|
{ value: 'COMPLETED', display: t('completedStatus', 'COMPLETED') },
|
|
@@ -92,17 +177,23 @@ const OrdersDataTable: React.FC<OrdersDataTableProps> = (props) => {
|
|
|
92
177
|
];
|
|
93
178
|
|
|
94
179
|
const columns = useMemo(() => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
180
|
+
return labTableColumns
|
|
181
|
+
.map((column) => {
|
|
182
|
+
const spec = labTableColumnSpec[column];
|
|
183
|
+
if (!spec) {
|
|
184
|
+
throw new Error(`Lab table has been configured with an invalid column: ${column}`);
|
|
185
|
+
}
|
|
186
|
+
if (spec.key === 'action') {
|
|
187
|
+
const showActionColumn = flattenedLabOrders.some((order) => order.fulfillerStatus === 'COMPLETED');
|
|
188
|
+
if (!showActionColumn) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { header: t(spec.headerLabelKey, spec.headerLabelDefault), key: spec.key };
|
|
193
|
+
})
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.map((column, index) => ({ ...column, id: index }));
|
|
196
|
+
}, [t, flattenedLabOrders, labTableColumns]);
|
|
106
197
|
|
|
107
198
|
const pageSizes = [10, 20, 30, 40, 50];
|
|
108
199
|
const [currentPageSize, setPageSize] = useState(10);
|
|
@@ -127,32 +218,28 @@ const OrdersDataTable: React.FC<OrdersDataTableProps> = (props) => {
|
|
|
127
218
|
};
|
|
128
219
|
|
|
129
220
|
const tableRows = useMemo(() => {
|
|
130
|
-
return paginatedLabOrders.map((
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
totalOrders: order.orders?.length,
|
|
135
|
-
patientAge: order.orders[0]?.patient?.person?.age,
|
|
136
|
-
patientGender: order.orders[0]?.patient?.person?.gender || '',
|
|
137
|
-
action: order.orders.some((o) => o.fulfillerStatus === 'COMPLETED') ? (
|
|
221
|
+
return paginatedLabOrders.map((groupedOrder) => ({
|
|
222
|
+
...groupedOrder,
|
|
223
|
+
id: groupedOrder.patientUuid,
|
|
224
|
+
action: groupedOrder.orders.some((o) => o.fulfillerStatus === 'COMPLETED') ? (
|
|
138
225
|
<div className={styles.actionCell}>
|
|
139
226
|
<OverflowMenu aria-label="Actions" flipped iconDescription="Actions">
|
|
140
227
|
<ExtensionSlot
|
|
141
228
|
className={styles.transitionOverflowMenuItemSlot}
|
|
142
229
|
name="transition-overflow-menu-item-slot"
|
|
143
|
-
state={{ patientUuid:
|
|
230
|
+
state={{ patientUuid: groupedOrder.patientUuid }}
|
|
144
231
|
// Without tabIndex={0} here, the overflow menu incorrectly sets initial focus to the second item instead of the first.
|
|
145
232
|
tabIndex={0}
|
|
146
233
|
/>
|
|
147
234
|
<OverflowMenuItem
|
|
148
235
|
className={styles.menuitem}
|
|
149
236
|
itemText={t('editResults', 'Edit results')}
|
|
150
|
-
onClick={() => handleLaunchModal(
|
|
237
|
+
onClick={() => handleLaunchModal(groupedOrder.originalOrders)}
|
|
151
238
|
/>
|
|
152
239
|
<OverflowMenuItem
|
|
153
240
|
className={styles.menuitem}
|
|
154
241
|
itemText={t('printTestResults', 'Print test results')}
|
|
155
|
-
onClick={() => handlePrintModal(
|
|
242
|
+
onClick={() => handlePrintModal(groupedOrder.originalOrders)}
|
|
156
243
|
/>
|
|
157
244
|
</OverflowMenu>
|
|
158
245
|
</div>
|
|
@@ -218,7 +305,7 @@ const OrdersDataTable: React.FC<OrdersDataTableProps> = (props) => {
|
|
|
218
305
|
<TableExpandedRow colSpan={headers.length + 2}>
|
|
219
306
|
<ListOrderDetails
|
|
220
307
|
actions={props.actions}
|
|
221
|
-
groupedOrders={groupedOrdersByPatient.find((item) => item.
|
|
308
|
+
groupedOrders={groupedOrdersByPatient.find((item) => item.patientUuid === row.id)}
|
|
222
309
|
/>
|
|
223
310
|
</TableExpandedRow>
|
|
224
311
|
) : (
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, within } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import OrdersDataTable from './orders-data-table.component';
|
|
5
|
+
import { useConfig, getDefaultsFromConfigSchema, type Patient, Person } from '@openmrs/esm-framework';
|
|
6
|
+
import { configSchema, type Config } from '../../config-schema';
|
|
7
|
+
import { useLabOrders, type UseLabOrdersParams } from '../../laboratory-resource';
|
|
8
|
+
import { type Order } from '@openmrs/esm-patient-common-lib';
|
|
9
|
+
|
|
10
|
+
jest.mock('../../laboratory-resource', () => ({
|
|
11
|
+
useLabOrders: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const mockUseConfig = jest.mocked(useConfig<Config>);
|
|
15
|
+
const mockUseLabOrders = jest.mocked(useLabOrders);
|
|
16
|
+
|
|
17
|
+
function mockUseLabOrdersImplementation(props: Partial<UseLabOrdersParams>) {
|
|
18
|
+
const mockPatient1: Partial<Patient> = {
|
|
19
|
+
uuid: 'patient-uuid-1',
|
|
20
|
+
display: 'PAT-001 - Pete Seeger',
|
|
21
|
+
identifiers: props.includePatientId
|
|
22
|
+
? [
|
|
23
|
+
{
|
|
24
|
+
uuid: 'identifier-uuid-1',
|
|
25
|
+
identifier: 'PAT-001',
|
|
26
|
+
preferred: true,
|
|
27
|
+
voided: false,
|
|
28
|
+
identifierType: {
|
|
29
|
+
uuid: 'identifier-type-uuid-1',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
uuid: 'identifier-uuid-2',
|
|
34
|
+
identifier: 'BAD-ID-NOT-PREFERRED',
|
|
35
|
+
preferred: false,
|
|
36
|
+
voided: false,
|
|
37
|
+
identifierType: {
|
|
38
|
+
uuid: 'identifier-type-uuid-1',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
]
|
|
42
|
+
: undefined,
|
|
43
|
+
person: {
|
|
44
|
+
uuid: 'person-uuid-1',
|
|
45
|
+
display: 'Pete Seeger',
|
|
46
|
+
age: 70,
|
|
47
|
+
gender: 'M',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const mockPatient2: Partial<Patient> = {
|
|
51
|
+
uuid: 'patient-uuid-2',
|
|
52
|
+
display: 'PAT-002 - Bob Dylan',
|
|
53
|
+
identifiers: props.includePatientId
|
|
54
|
+
? [
|
|
55
|
+
{
|
|
56
|
+
uuid: 'identifier-uuid-3',
|
|
57
|
+
identifier: 'BAD-ID-WRONG-TYPE',
|
|
58
|
+
preferred: true,
|
|
59
|
+
voided: false,
|
|
60
|
+
identifierType: {
|
|
61
|
+
uuid: '05a29f94-c0ed-11e2-94be-8c13b969e334',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
uuid: 'identifier-uuid-4',
|
|
66
|
+
identifier: 'PAT-002',
|
|
67
|
+
preferred: true,
|
|
68
|
+
voided: false,
|
|
69
|
+
identifierType: {
|
|
70
|
+
uuid: 'identifier-type-uuid-1',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
: undefined,
|
|
75
|
+
person: {
|
|
76
|
+
uuid: 'person-uuid-2',
|
|
77
|
+
display: 'Bob Dylan',
|
|
78
|
+
age: 60,
|
|
79
|
+
gender: 'M',
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const mockOrderer = {
|
|
84
|
+
uuid: 'orderer-uuid-1',
|
|
85
|
+
display: 'Dr. John Doe',
|
|
86
|
+
person: {
|
|
87
|
+
display: 'Dr. John Doe',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const labOrders = [
|
|
92
|
+
{
|
|
93
|
+
uuid: 'order-uuid-1',
|
|
94
|
+
orderNumber: 'ORD-001',
|
|
95
|
+
patient: mockPatient1 as Patient,
|
|
96
|
+
dateActivated: '2021-01-01',
|
|
97
|
+
fulfillerStatus: 'RECEIVED',
|
|
98
|
+
urgency: 'ROUTINE',
|
|
99
|
+
orderer: mockOrderer,
|
|
100
|
+
instructions: 'Inspect banjo & check tuning',
|
|
101
|
+
fulfillerComment: null,
|
|
102
|
+
display: 'Banjo Inspection',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
uuid: 'order-uuid-2',
|
|
106
|
+
orderNumber: 'ORD-002',
|
|
107
|
+
patient: mockPatient1 as Patient,
|
|
108
|
+
dateActivated: '2021-01-01',
|
|
109
|
+
fulfillerStatus: 'RECEIVED',
|
|
110
|
+
urgency: 'ROUTINE',
|
|
111
|
+
orderer: mockOrderer,
|
|
112
|
+
instructions: 'Give it a strum',
|
|
113
|
+
fulfillerComment: null,
|
|
114
|
+
display: 'Guitar Inspection',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
uuid: 'order-uuid-3',
|
|
118
|
+
orderNumber: 'ORD-003',
|
|
119
|
+
patient: mockPatient2 as Patient,
|
|
120
|
+
dateActivated: '2021-01-01',
|
|
121
|
+
fulfillerStatus: 'RECEIVED',
|
|
122
|
+
urgency: 'EMERGENCY',
|
|
123
|
+
orderer: mockOrderer,
|
|
124
|
+
instructions: 'Make some noise',
|
|
125
|
+
fulfillerComment: null,
|
|
126
|
+
display: 'Sound Check',
|
|
127
|
+
},
|
|
128
|
+
]
|
|
129
|
+
.filter((order) => !props.status || order.fulfillerStatus === props.status)
|
|
130
|
+
.filter((order) => !props.excludeCanceled || order.fulfillerStatus !== 'CANCELLED') as Array<Order>;
|
|
131
|
+
return {
|
|
132
|
+
labOrders,
|
|
133
|
+
isLoading: false,
|
|
134
|
+
isError: false,
|
|
135
|
+
mutate: jest.fn(),
|
|
136
|
+
isValidating: false,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
describe('OrdersDataTable', () => {
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
mockUseLabOrders.mockImplementation(mockUseLabOrdersImplementation);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should render one row per patient and show lab details', async () => {
|
|
146
|
+
mockUseConfig.mockReturnValue({
|
|
147
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
render(<OrdersDataTable />);
|
|
151
|
+
const table = screen.getByRole('table');
|
|
152
|
+
expect(table).toBeInTheDocument();
|
|
153
|
+
const rows = screen.getAllByRole('row');
|
|
154
|
+
expect(rows).toHaveLength(5);
|
|
155
|
+
const dataRows = rows.slice(1).filter((row) => !row.classList.contains('hiddenRow'));
|
|
156
|
+
expect(dataRows).toHaveLength(2);
|
|
157
|
+
const headerRow = rows[0];
|
|
158
|
+
expect(headerRow).toHaveTextContent('Patient');
|
|
159
|
+
expect(headerRow).toHaveTextContent('Age');
|
|
160
|
+
expect(headerRow).toHaveTextContent('Sex');
|
|
161
|
+
expect(headerRow).toHaveTextContent('Total Orders');
|
|
162
|
+
const row1 = dataRows[0];
|
|
163
|
+
expect(row1).toHaveTextContent('Pete Seeger');
|
|
164
|
+
expect(row1).toHaveTextContent('70');
|
|
165
|
+
expect(row1).toHaveTextContent('M');
|
|
166
|
+
expect(row1).toHaveTextContent('2');
|
|
167
|
+
const row2 = dataRows[1];
|
|
168
|
+
expect(row2).toHaveTextContent('Bob Dylan');
|
|
169
|
+
expect(row2).toHaveTextContent('60');
|
|
170
|
+
expect(row2).toHaveTextContent('M');
|
|
171
|
+
expect(row2).toHaveTextContent('1');
|
|
172
|
+
|
|
173
|
+
const user = userEvent.setup();
|
|
174
|
+
await user.click(within(row1).getByLabelText('Expand current row'));
|
|
175
|
+
|
|
176
|
+
const orderDetailsTables = within(table).getAllByRole('table');
|
|
177
|
+
expect(orderDetailsTables).toHaveLength(2);
|
|
178
|
+
const orderDetailsTable1 = orderDetailsTables[0];
|
|
179
|
+
const orderDetailsTable2 = orderDetailsTables[1];
|
|
180
|
+
expect(orderDetailsTable1).toHaveTextContent('Banjo Inspection');
|
|
181
|
+
expect(orderDetailsTable1).toHaveTextContent('Inspect banjo & check tuning');
|
|
182
|
+
expect(orderDetailsTable1).toHaveTextContent('Dr. John Doe');
|
|
183
|
+
expect(orderDetailsTable1).toHaveTextContent('01-Jan-2021');
|
|
184
|
+
expect(orderDetailsTable1).toHaveTextContent('Received');
|
|
185
|
+
expect(orderDetailsTable1).toHaveTextContent('Routine');
|
|
186
|
+
expect(orderDetailsTable2).toHaveTextContent('Guitar Inspection');
|
|
187
|
+
expect(orderDetailsTable2).toHaveTextContent('Give it a strum');
|
|
188
|
+
expect(orderDetailsTable2).toHaveTextContent('Dr. John Doe');
|
|
189
|
+
expect(orderDetailsTable2).toHaveTextContent('01-Jan-2021');
|
|
190
|
+
expect(orderDetailsTable2).toHaveTextContent('Received');
|
|
191
|
+
expect(orderDetailsTable2).toHaveTextContent('Routine');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should show patient identifier if it is configured', () => {
|
|
195
|
+
mockUseConfig.mockReturnValue({
|
|
196
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
197
|
+
labTableColumns: ['patientId', 'age', 'totalOrders'],
|
|
198
|
+
patientIdIdentifierTypeUuid: 'identifier-type-uuid-1',
|
|
199
|
+
});
|
|
200
|
+
render(<OrdersDataTable />);
|
|
201
|
+
const rows = screen.getAllByRole('row');
|
|
202
|
+
expect(rows).toHaveLength(5);
|
|
203
|
+
const dataRows = rows.slice(1).filter((row) => !row.classList.contains('hiddenRow'));
|
|
204
|
+
expect(dataRows).toHaveLength(2);
|
|
205
|
+
const row1 = dataRows[0];
|
|
206
|
+
expect(row1).toHaveTextContent('PAT-001');
|
|
207
|
+
expect(row1).toHaveTextContent('70');
|
|
208
|
+
expect(row1).toHaveTextContent('2');
|
|
209
|
+
const row2 = dataRows[1];
|
|
210
|
+
expect(row2).toHaveTextContent('PAT-002');
|
|
211
|
+
expect(row2).toHaveTextContent('60');
|
|
212
|
+
expect(row2).toHaveTextContent('1');
|
|
213
|
+
expect(screen.queryByText(/BAD-ID/)).not.toBeInTheDocument();
|
|
214
|
+
});
|
|
215
|
+
});
|
package/src/config-schema.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { Type } from '@openmrs/esm-framework';
|
|
1
|
+
import { Type, validators } from '@openmrs/esm-framework';
|
|
2
|
+
|
|
3
|
+
const allowedLabTableColumns = ['name', 'age', 'sex', 'totalOrders', 'action', 'patientId'] as const;
|
|
4
|
+
type LabTableColumnName = (typeof allowedLabTableColumns)[number];
|
|
2
5
|
|
|
3
6
|
export const configSchema = {
|
|
4
7
|
laboratoryOrderTypeUuid: {
|
|
@@ -24,6 +27,20 @@ export const configSchema = {
|
|
|
24
27
|
},
|
|
25
28
|
_description: 'The patient chart dashboard to navigate to from the lab app.',
|
|
26
29
|
},
|
|
30
|
+
labTableColumns: {
|
|
31
|
+
_type: Type.Array,
|
|
32
|
+
_default: ['name', 'age', 'sex', 'totalOrders', 'action'] as Array<LabTableColumnName>,
|
|
33
|
+
_description: 'The columns to display in the lab table. Allowed values: ' + allowedLabTableColumns.join(', '),
|
|
34
|
+
_elements: {
|
|
35
|
+
_type: Type.String,
|
|
36
|
+
_validators: [validators.oneOf(allowedLabTableColumns)],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
patientIdIdentifierTypeUuid: {
|
|
40
|
+
_type: Type.UUID,
|
|
41
|
+
_default: '05a29f94-c0ed-11e2-94be-8c13b969e334',
|
|
42
|
+
_description: 'Needed if the "id" column of "labTableColumns" is used. Is the OpenMRS ID by default.',
|
|
43
|
+
},
|
|
27
44
|
};
|
|
28
45
|
|
|
29
46
|
export type Config = {
|
|
@@ -33,4 +50,6 @@ export type Config = {
|
|
|
33
50
|
redirectToResultsViewer: string;
|
|
34
51
|
redirectToOrders: string;
|
|
35
52
|
};
|
|
53
|
+
labTableColumns: Array<LabTableColumnName>;
|
|
54
|
+
patientIdIdentifierTypeUuid: string;
|
|
36
55
|
};
|
|
@@ -6,7 +6,7 @@ const TestsOrderedTable: React.FC = () => {
|
|
|
6
6
|
<OrdersDataTable
|
|
7
7
|
excludeColumns={[]}
|
|
8
8
|
actionsSlotName="tests-ordered-actions-slot"
|
|
9
|
-
|
|
9
|
+
newOrdersOnly={true}
|
|
10
10
|
actions={[
|
|
11
11
|
{ actionName: 'pickupLabRequest', order: 0 },
|
|
12
12
|
{ actionName: 'rejectLabRequest', order: 1 },
|
|
@@ -6,7 +6,7 @@ import LabSummaryTile from '../components/summary-tile/lab-summary-tile.componen
|
|
|
6
6
|
const AllLabRequestsTile = () => {
|
|
7
7
|
const { t } = useTranslation();
|
|
8
8
|
|
|
9
|
-
const { labOrders } = useLabOrders(
|
|
9
|
+
const { labOrders } = useLabOrders({ newOrdersOnly: true });
|
|
10
10
|
|
|
11
11
|
return (
|
|
12
12
|
<LabSummaryTile
|
|
@@ -5,7 +5,7 @@ import LabSummaryTile from '../components/summary-tile/lab-summary-tile.componen
|
|
|
5
5
|
|
|
6
6
|
const CompletedLabRequestsTile = () => {
|
|
7
7
|
const { t } = useTranslation();
|
|
8
|
-
const { labOrders } = useLabOrders('COMPLETED', false);
|
|
8
|
+
const { labOrders } = useLabOrders({ status: 'COMPLETED', excludeCanceled: false });
|
|
9
9
|
|
|
10
10
|
return (
|
|
11
11
|
<LabSummaryTile
|
|
@@ -5,7 +5,7 @@ import LabSummaryTile from '../components/summary-tile/lab-summary-tile.componen
|
|
|
5
5
|
|
|
6
6
|
const InProgressLabRequestsTile = () => {
|
|
7
7
|
const { t } = useTranslation();
|
|
8
|
-
const { labOrders } = useLabOrders('IN_PROGRESS');
|
|
8
|
+
const { labOrders } = useLabOrders({ status: 'IN_PROGRESS' });
|
|
9
9
|
|
|
10
10
|
return (
|
|
11
11
|
<LabSummaryTile
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
2
1
|
import dayjs from 'dayjs';
|
|
3
2
|
import useSWR from 'swr';
|
|
4
3
|
import { openmrsFetch, restBaseUrl, useAppContext, useConfig } from '@openmrs/esm-framework';
|
|
5
|
-
import { type Order } from '@openmrs/esm-patient-common-lib';
|
|
6
|
-
import type { DateFilterContext
|
|
4
|
+
import { type Order, type FulfillerStatus } from '@openmrs/esm-patient-common-lib';
|
|
5
|
+
import type { DateFilterContext } from './types';
|
|
6
|
+
|
|
7
|
+
const useLabOrdersDefaultParams: UseLabOrdersParams = {
|
|
8
|
+
status: null,
|
|
9
|
+
newOrdersOnly: false,
|
|
10
|
+
excludeCanceled: true,
|
|
11
|
+
includePatientId: false,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseLabOrdersParams {
|
|
15
|
+
status: FulfillerStatus;
|
|
16
|
+
newOrdersOnly: boolean;
|
|
17
|
+
excludeCanceled: boolean;
|
|
18
|
+
includePatientId: boolean;
|
|
19
|
+
}
|
|
7
20
|
|
|
8
21
|
/**
|
|
9
22
|
* Custom hook for retrieving laboratory orders based on the specified status.
|
|
@@ -11,18 +24,18 @@ import type { DateFilterContext, FulfillerStatus, GroupedOrders } from './types'
|
|
|
11
24
|
* @param status - The status of the orders to retrieve
|
|
12
25
|
* @param excludeCanceled - Whether to exclude canceled, discontinued and expired orders
|
|
13
26
|
*/
|
|
14
|
-
export function useLabOrders(
|
|
27
|
+
export function useLabOrders(params: Partial<UseLabOrdersParams> = useLabOrdersDefaultParams) {
|
|
28
|
+
const { status, newOrdersOnly, excludeCanceled, includePatientId } = { ...useLabOrdersDefaultParams, ...params };
|
|
15
29
|
const { dateRange } = useAppContext<DateFilterContext>('laboratory-date-filter') ?? {
|
|
16
30
|
dateRange: [dayjs().startOf('day').toDate(), new Date()],
|
|
17
31
|
};
|
|
18
32
|
|
|
19
33
|
const { laboratoryOrderTypeUuid } = useConfig();
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'custom:(uuid,orderNumber,patient:(uuid,display,person:(uuid,display,age,gender)),concept:(uuid,display),action,careSetting:(uuid,display,description,careSettingType,display),previousOrder,dateActivated,scheduledDate,dateStopped,autoExpireDate,encounter:(uuid,display),orderer:(uuid,display),orderReason,orderReasonNonCoded,orderType:(uuid,display,name,description,conceptClasses,parent),urgency,instructions,commentToFulfiller,display,fulfillerStatus,fulfillerComment,specimenSource,laterality,clinicalHistory,frequency,numberOfRepeats)';
|
|
34
|
+
const customRepresentation = `custom:(uuid,orderNumber,patient:(uuid,display,person:(uuid,display,age,gender)${
|
|
35
|
+
includePatientId ? ',identifiers' : ''
|
|
36
|
+
}),concept:(uuid,display),action,careSetting:(uuid,display,description,careSettingType,display),previousOrder,dateActivated,scheduledDate,dateStopped,autoExpireDate,encounter:(uuid,display),orderer:(uuid,display),orderReason,orderReasonNonCoded,orderType:(uuid,display,name,description,conceptClasses,parent),urgency,instructions,commentToFulfiller,display,fulfillerStatus,fulfillerComment,specimenSource,laterality,clinicalHistory,frequency,numberOfRepeats)`;
|
|
24
37
|
let url = `${restBaseUrl}/order?orderTypes=${laboratoryOrderTypeUuid}&v=${customRepresentation}`;
|
|
25
|
-
url =
|
|
38
|
+
url = status ? url + `&fulfillerStatus=${status}` : url;
|
|
26
39
|
url = excludeCanceled ? `${url}&excludeCanceledAndExpired=true&excludeDiscontinueOrders=true` : url;
|
|
27
40
|
// The usage of SWR's mutator seems to only suffice for cases where we don't apply a status filter
|
|
28
41
|
url = dateRange
|
|
@@ -36,11 +49,9 @@ export function useLabOrders(status: 'NEW' | FulfillerStatus = null, excludeCanc
|
|
|
36
49
|
}>(`${url}`, openmrsFetch);
|
|
37
50
|
|
|
38
51
|
const filteredOrders =
|
|
39
|
-
data?.data &&
|
|
40
|
-
newOrdersOnly &&
|
|
41
|
-
data.data.results.filter((order) => order?.action === 'NEW' && order?.fulfillerStatus === null);
|
|
52
|
+
data?.data?.results?.filter((order) => !newOrdersOnly || (order?.action === 'NEW' && order?.fulfillerStatus === null));
|
|
42
53
|
return {
|
|
43
|
-
labOrders: filteredOrders
|
|
54
|
+
labOrders: filteredOrders ?? [],
|
|
44
55
|
isLoading,
|
|
45
56
|
isError: error,
|
|
46
57
|
mutate,
|
|
@@ -48,26 +59,6 @@ export function useLabOrders(status: 'NEW' | FulfillerStatus = null, excludeCanc
|
|
|
48
59
|
};
|
|
49
60
|
}
|
|
50
61
|
|
|
51
|
-
export function useSearchGroupedResults(data: Array<GroupedOrders>, searchString: string) {
|
|
52
|
-
const searchResults = useMemo(() => {
|
|
53
|
-
if (searchString && searchString.trim() !== '') {
|
|
54
|
-
// Normalize the search string to lowercase
|
|
55
|
-
const lowerSearchString = searchString.toLowerCase();
|
|
56
|
-
return data.filter((orderGroup) =>
|
|
57
|
-
orderGroup.orders.some(
|
|
58
|
-
(order) =>
|
|
59
|
-
order.orderNumber.toLowerCase().includes(lowerSearchString) ||
|
|
60
|
-
order.patient.display.toLowerCase().includes(lowerSearchString),
|
|
61
|
-
),
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return data;
|
|
66
|
-
}, [searchString, data]);
|
|
67
|
-
|
|
68
|
-
return searchResults;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
62
|
export function setFulfillerStatus(orderId: string, status: FulfillerStatus, abortController: AbortController) {
|
|
72
63
|
return openmrsFetch(`${restBaseUrl}/order/${orderId}/fulfillerdetails/`, {
|
|
73
64
|
method: 'POST',
|