@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.
Files changed (42) hide show
  1. package/.turbo/cache/a60408b5239ac3b0-meta.json +1 -0
  2. package/.turbo/cache/a60408b5239ac3b0.tar.zst +0 -0
  3. package/.turbo/turbo-build.log +3 -3
  4. package/dist/1120.js +1 -1
  5. package/dist/1120.js.map +1 -1
  6. package/dist/1788.js +1 -1
  7. package/dist/1788.js.map +1 -1
  8. package/dist/3656.js +1 -1
  9. package/dist/3656.js.map +1 -1
  10. package/dist/4069.js +1 -1
  11. package/dist/4069.js.map +1 -1
  12. package/dist/4300.js +1 -1
  13. package/dist/5085.js +1 -1
  14. package/dist/5085.js.map +1 -1
  15. package/dist/6134.js +1 -1
  16. package/dist/6134.js.map +1 -1
  17. package/dist/7423.js +1 -1
  18. package/dist/7423.js.map +1 -1
  19. package/dist/8554.js +1 -1
  20. package/dist/8554.js.map +1 -1
  21. package/dist/8667.js +1 -1
  22. package/dist/8667.js.map +1 -1
  23. package/dist/main.js +1 -1
  24. package/dist/main.js.map +1 -1
  25. package/dist/openmrs-esm-laboratory-app.js +1 -1
  26. package/dist/openmrs-esm-laboratory-app.js.buildmanifest.json +34 -34
  27. package/dist/routes.json +1 -1
  28. package/package.json +1 -1
  29. package/src/components/orders-table/list-order-details.component.tsx +31 -24
  30. package/src/components/orders-table/list-order-details.scss +0 -1
  31. package/src/components/orders-table/orders-data-table.component.tsx +143 -56
  32. package/src/components/orders-table/orders-data-table.test.tsx +215 -0
  33. package/src/config-schema.ts +20 -1
  34. package/src/lab-tabs/data-table-extensions/tests-ordered-table.extension.tsx +1 -1
  35. package/src/lab-tiles/all-lab-requests-tile.component.tsx +1 -1
  36. package/src/lab-tiles/completed-lab-requests-tile.component.tsx +1 -1
  37. package/src/lab-tiles/in-progress-lab-requests-tile.component.tsx +1 -1
  38. package/src/laboratory-resource.ts +24 -33
  39. package/src/types.ts +20 -17
  40. package/translations/en.json +2 -2
  41. package/.turbo/cache/26dd8861bd3eca6d-meta.json +0 -1
  42. package/.turbo/cache/26dd8861bd3eca6d.tar.zst +0 -0
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useMemo, useState } from '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 { ExtensionSlot, formatDate, parseDate, showModal, usePagination } from '@openmrs/esm-framework';
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 { FulfillerStatus, OrdersDataTableProps } from '../../types';
29
- import { useLabOrders, useSearchGroupedResults } from '../../laboratory-resource';
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.excludeCanceledAndDiscontinuedOrders,
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: Order[] = useMemo(() => {
106
+ const flattenedLabOrders: Array<FlattenedOrder> = useMemo(() => {
45
107
  return (
46
108
  labOrders?.map((order) => {
47
109
  return {
48
- ...order,
110
+ id: order.uuid,
111
+ patientUuid: order.patient.uuid,
112
+ orderNumber: order.orderNumber,
49
113
  dateActivated: formatDate(parseDate(order.dateActivated)),
50
- patientName: order.patient?.person.display,
51
- patientUuid: order.patient?.uuid,
52
- patientAge: order.patient?.person?.age,
53
- status: order.fulfillerStatus ?? '--',
54
- orderer: order.orderer,
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
- function groupOrdersById(orders) {
61
- if (orders && orders.length > 0) {
62
- const groupedOrders = orders.reduce((acc, item) => {
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 Object.keys(groupedOrders).map((patientId) => ({
71
- patientId: patientId,
72
- orders: groupedOrders[patientId],
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 groupedOrdersByPatient = groupOrdersById(flattenedLabOrders);
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
- const searchResults = useSearchGroupedResults(groupedOrdersByPatient, searchString);
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
- const baseColumns = [
96
- { id: 0, header: t('patient', 'Patient'), key: 'patientName' },
97
- { id: 1, header: t('age', 'Age'), key: 'patientAge' },
98
- { id: 2, header: t('gender', 'Gender'), key: 'patientGender' },
99
- { id: 3, header: t('totalOrders', 'Total Orders'), key: 'totalOrders' },
100
- ];
101
-
102
- const showActionColumn = flattenedLabOrders.some((order) => order.fulfillerStatus === 'COMPLETED');
103
-
104
- return showActionColumn ? [...baseColumns, { id: 4, header: t('action', 'Action'), key: 'action' }] : baseColumns;
105
- }, [t, flattenedLabOrders]);
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((order) => ({
131
- id: order.patientId,
132
- patientName: order.orders[0]?.patient.person.display,
133
- orders: order.orders,
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: order?.patientId }}
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(order?.orders)}
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(order?.orders)}
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.patientId === row.id)}
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
+ });
@@ -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
- fulfillerStatus="NEW"
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('NEW');
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, FulfillerStatus, GroupedOrders } from './types';
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(status: 'NEW' | FulfillerStatus = null, excludeCanceled = true) {
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 fulfillerStatus = useMemo(() => (status === 'NEW' ? null : status), [status]);
21
- const newOrdersOnly = status === 'NEW';
22
- const customRepresentation =
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 = fulfillerStatus ? url + `&fulfillerStatus=${fulfillerStatus}` : 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 || data?.data.results || [],
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',