@ampath/esm-laboratory-app 1.3.0-next.2
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/.editorconfig +12 -0
- package/.eslintignore +2 -0
- package/.eslintrc +68 -0
- package/.husky/pre-commit +7 -0
- package/.husky/pre-push +6 -0
- package/.prettierignore +12 -0
- package/.prettierrc +8 -0
- package/.tx/config +11 -0
- package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
- package/README.md +54 -0
- package/__mocks__/react-i18next.js +50 -0
- package/assets/screenshots/labs_enter_results.png +0 -0
- package/assets/screenshots/labs_general_dashboard.png +0 -0
- package/dist/1119.js +1 -0
- package/dist/1197.js +1 -0
- package/dist/1222.js +1 -0
- package/dist/1222.js.map +1 -0
- package/dist/1243.js +1 -0
- package/dist/1243.js.map +1 -0
- package/dist/2146.js +1 -0
- package/dist/2690.js +1 -0
- package/dist/3099.js +1 -0
- package/dist/3106.js +1 -0
- package/dist/3106.js.map +1 -0
- package/dist/312.js +1 -0
- package/dist/312.js.map +1 -0
- package/dist/3352.js +1 -0
- package/dist/3352.js.map +1 -0
- package/dist/3535.js +1 -0
- package/dist/3535.js.map +1 -0
- package/dist/3584.js +1 -0
- package/dist/4044.js +1 -0
- package/dist/4044.js.map +1 -0
- package/dist/4055.js +1 -0
- package/dist/4132.js +1 -0
- package/dist/4300.js +1 -0
- package/dist/4335.js +1 -0
- package/dist/439.js +1 -0
- package/dist/4535.js +1 -0
- package/dist/4535.js.map +1 -0
- package/dist/4618.js +1 -0
- package/dist/4652.js +1 -0
- package/dist/4748.js +2 -0
- package/dist/4748.js.LICENSE.txt +9 -0
- package/dist/4748.js.map +1 -0
- package/dist/4920.js +1 -0
- package/dist/4920.js.map +1 -0
- package/dist/4944.js +1 -0
- package/dist/5048.js +2 -0
- package/dist/5048.js.LICENSE.txt +29 -0
- package/dist/5048.js.map +1 -0
- package/dist/5088.js +1 -0
- package/dist/5088.js.map +1 -0
- package/dist/5173.js +1 -0
- package/dist/5241.js +1 -0
- package/dist/53.js +1 -0
- package/dist/53.js.map +1 -0
- package/dist/5339.js +1 -0
- package/dist/5339.js.map +1 -0
- package/dist/5348.js +1 -0
- package/dist/5348.js.map +1 -0
- package/dist/5380.js +1 -0
- package/dist/5380.js.map +1 -0
- package/dist/5442.js +1 -0
- package/dist/5661.js +1 -0
- package/dist/5780.js +2 -0
- package/dist/5780.js.LICENSE.txt +9 -0
- package/dist/5780.js.map +1 -0
- package/dist/6022.js +1 -0
- package/dist/6468.js +1 -0
- package/dist/6589.js +1 -0
- package/dist/6679.js +1 -0
- package/dist/6753.js +1 -0
- package/dist/6753.js.map +1 -0
- package/dist/6777.js +2 -0
- package/dist/6777.js.LICENSE.txt +19 -0
- package/dist/6777.js.map +1 -0
- package/dist/679.js +2 -0
- package/dist/679.js.LICENSE.txt +9 -0
- package/dist/679.js.map +1 -0
- package/dist/6840.js +1 -0
- package/dist/6859.js +1 -0
- package/dist/7097.js +1 -0
- package/dist/7129.js +1 -0
- package/dist/7129.js.map +1 -0
- package/dist/7159.js +1 -0
- package/dist/723.js +1 -0
- package/dist/7617.js +1 -0
- package/dist/791.js +1 -0
- package/dist/791.js.map +1 -0
- package/dist/795.js +1 -0
- package/dist/8163.js +1 -0
- package/dist/8349.js +1 -0
- package/dist/8371.js +1 -0
- package/dist/841.js +1 -0
- package/dist/841.js.map +1 -0
- package/dist/8618.js +1 -0
- package/dist/8627.js +2 -0
- package/dist/8627.js.LICENSE.txt +25 -0
- package/dist/8627.js.map +1 -0
- package/dist/8898.js +2 -0
- package/dist/8898.js.LICENSE.txt +32 -0
- package/dist/8898.js.map +1 -0
- package/dist/890.js +1 -0
- package/dist/9214.js +1 -0
- package/dist/9321.js +1 -0
- package/dist/9321.js.map +1 -0
- package/dist/9452.js +1 -0
- package/dist/9452.js.map +1 -0
- package/dist/9538.js +1 -0
- package/dist/9569.js +1 -0
- package/dist/9695.js +1 -0
- package/dist/9695.js.map +1 -0
- package/dist/986.js +1 -0
- package/dist/9879.js +1 -0
- package/dist/9895.js +1 -0
- package/dist/9900.js +1 -0
- package/dist/9910.js +1 -0
- package/dist/9910.js.map +1 -0
- package/dist/9913.js +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +45 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-laboratory-app.js +1 -0
- package/dist/openmrs-esm-laboratory-app.js.buildmanifest.json +1744 -0
- package/dist/openmrs-esm-laboratory-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/e2e/README.md +117 -0
- package/e2e/commands/encounter-operations.ts +63 -0
- package/e2e/commands/index.ts +5 -0
- package/e2e/commands/patient-operations.ts +109 -0
- package/e2e/commands/provider-operations.ts +9 -0
- package/e2e/commands/test-order-operations.ts +46 -0
- package/e2e/commands/types/index.ts +157 -0
- package/e2e/commands/visit-operations.ts +38 -0
- package/e2e/core/global-setup.ts +32 -0
- package/e2e/core/index.ts +1 -0
- package/e2e/core/test.ts +31 -0
- package/e2e/fixtures/api.ts +27 -0
- package/e2e/fixtures/fhirApi.ts +28 -0
- package/e2e/fixtures/index.ts +2 -0
- package/e2e/pages/index.ts +1 -0
- package/e2e/pages/laboratory-page.ts +28 -0
- package/e2e/specs/add-lab-results.spec.ts +111 -0
- package/e2e/specs/reject-lab-request.spec.ts +88 -0
- package/e2e/specs/test-orders.spec.ts +69 -0
- package/e2e/support/github/Dockerfile +34 -0
- package/e2e/support/github/docker-compose.yml +24 -0
- package/e2e/support/github/run-e2e-docker-env.sh +58 -0
- package/example.env +7 -0
- package/jest.config.js +35 -0
- package/package.json +105 -0
- package/playwright.config.ts +42 -0
- package/src/components/create-dashboard-link.component.tsx +37 -0
- package/src/components/loader/loader.component.tsx +11 -0
- package/src/components/loader/loader.scss +9 -0
- package/src/components/orders-table/list-order-details.component.tsx +143 -0
- package/src/components/orders-table/list-order-details.scss +136 -0
- package/src/components/orders-table/order-detail.scss +18 -0
- package/src/components/orders-table/orders-data-table.component.tsx +349 -0
- package/src/components/orders-table/orders-data-table.scss +129 -0
- package/src/components/orders-table/orders-data-table.test.tsx +214 -0
- package/src/components/orders-table/orders-date-range-picker.component.tsx +32 -0
- package/src/components/orders-table/orders-date-range-picker.scss +7 -0
- package/src/components/summary-tile/lab-summary-tile.component.tsx +31 -0
- package/src/components/summary-tile/lab-summary-tile.scss +64 -0
- package/src/config-schema.ts +39 -0
- package/src/constants.ts +2 -0
- package/src/declarations.d.ts +2 -0
- package/src/index.ts +123 -0
- package/src/lab-tabs/actions/actions.scss +26 -0
- package/src/lab-tabs/actions/add-lab-request-results-action.component.tsx +46 -0
- package/src/lab-tabs/actions/amend-lab-results-action.component.tsx +40 -0
- package/src/lab-tabs/actions/approve-lab-results-action.component.tsx +36 -0
- package/src/lab-tabs/actions/pickup-lab-request-action.component.tsx +36 -0
- package/src/lab-tabs/actions/reject-lab-request-action.component.tsx +36 -0
- package/src/lab-tabs/data-table-extensions/completed-lab-requests-table.extension.tsx +8 -0
- package/src/lab-tabs/data-table-extensions/declined-lab-requests-table-extension.tsx +8 -0
- package/src/lab-tabs/data-table-extensions/in-progress-lab-requests-table.extension.tsx +8 -0
- package/src/lab-tabs/data-table-extensions/pending-review-lab-request-table.extension.tsx +8 -0
- package/src/lab-tabs/data-table-extensions/tests-ordered-table.extension.tsx +8 -0
- package/src/lab-tabs/laboratory-tabs.component.tsx +81 -0
- package/src/lab-tabs/laboratory-tabs.scss +38 -0
- package/src/lab-tabs/modals/approval-lab-results-modal.component.tsx +70 -0
- package/src/lab-tabs/modals/pickup-lab-request-modal.component.tsx +67 -0
- package/src/lab-tabs/modals/pickup-lab-request-modal.test.tsx +127 -0
- package/src/lab-tabs/modals/reject-lab-request-modal.component.tsx +86 -0
- package/src/lab-tabs/modals/reject-lab-request-modal.scss +13 -0
- package/src/lab-tabs/modals/reject-lab-request-modal.test.tsx +152 -0
- package/src/lab-tiles/all-lab-requests-tile.component.tsx +19 -0
- package/src/lab-tiles/completed-lab-requests-tile.component.tsx +19 -0
- package/src/lab-tiles/in-progress-lab-requests-tile.component.tsx +19 -0
- package/src/lab-tiles/laboratory-summary-tiles.component.tsx +52 -0
- package/src/lab-tiles/laboratory-summary-tiles.scss +16 -0
- package/src/lab-tiles/pending-review-lab-results-tile.component.tsx +22 -0
- package/src/laboratory-dashboard.component.tsx +30 -0
- package/src/laboratory-dashboard.scss +5 -0
- package/src/laboratory.resource.ts +108 -0
- package/src/root.component.tsx +15 -0
- package/src/routes.json +204 -0
- package/src/types.ts +39 -0
- package/tools/i18next-parser.config.js +93 -0
- package/tools/index.ts +1 -0
- package/tools/setup-tests.ts +8 -0
- package/tools/test-utils.ts +44 -0
- package/tools/update-openmrs-deps.mjs +42 -0
- package/translations/am.json +79 -0
- package/translations/ar.json +79 -0
- package/translations/ar_SY.json +79 -0
- package/translations/bn.json +79 -0
- package/translations/cs.json +79 -0
- package/translations/de.json +79 -0
- package/translations/en.json +79 -0
- package/translations/en_US.json +79 -0
- package/translations/es.json +79 -0
- package/translations/es_MX.json +79 -0
- package/translations/fr.json +79 -0
- package/translations/he.json +79 -0
- package/translations/hi.json +79 -0
- package/translations/hi_IN.json +79 -0
- package/translations/id.json +79 -0
- package/translations/it.json +79 -0
- package/translations/ka.json +79 -0
- package/translations/km.json +79 -0
- package/translations/ku.json +79 -0
- package/translations/ky.json +79 -0
- package/translations/lg.json +79 -0
- package/translations/ne.json +79 -0
- package/translations/pl.json +79 -0
- package/translations/pt.json +79 -0
- package/translations/pt_BR.json +79 -0
- package/translations/qu.json +79 -0
- package/translations/ro_RO.json +79 -0
- package/translations/ru_RU.json +79 -0
- package/translations/si.json +79 -0
- package/translations/sq.json +79 -0
- package/translations/sw.json +79 -0
- package/translations/sw_KE.json +79 -0
- package/translations/tr.json +79 -0
- package/translations/tr_TR.json +79 -0
- package/translations/uk.json +79 -0
- package/translations/uz.json +79 -0
- package/translations/uz@Latn.json +79 -0
- package/translations/uz_UZ.json +79 -0
- package/translations/vi.json +79 -0
- package/translations/zh.json +79 -0
- package/translations/zh_CN.json +79 -0
- package/translations/zh_TW.json +79 -0
- package/tsconfig.json +28 -0
- package/turbo.json +15 -0
- package/webpack.config.js +25 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
4
|
+
|
|
5
|
+
.orderTabs {
|
|
6
|
+
grid-column: 'span 2';
|
|
7
|
+
padding: 0 layout.$spacing-05;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.tabsContainer {
|
|
11
|
+
background-color: $ui-02;
|
|
12
|
+
|
|
13
|
+
:global(.cds--tabs__nav-item--selected) {
|
|
14
|
+
box-shadow: inset 0 2px 0 0 var(--brand-03) !important;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:global(.cds--tab--list) button {
|
|
18
|
+
max-width: 12rem !important;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.hiddenTabsContent,
|
|
23
|
+
.tabs .hiddenTabsContent {
|
|
24
|
+
display: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.tab {
|
|
28
|
+
min-width: 8.875rem;
|
|
29
|
+
|
|
30
|
+
&:active,
|
|
31
|
+
&:focus {
|
|
32
|
+
outline: 2px solid var(--brand-03) !important;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&[aria-selected='true'] {
|
|
36
|
+
box-shadow: inset 0 2px 0 0 var(--brand-03) !important;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
|
|
3
|
+
import { ExtensionSlot, showNotification, showSnackbar, useAbortController, type Order } from '@openmrs/esm-framework';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { setFulfillerStatus, useInvalidateLabOrders } from '../../laboratory.resource';
|
|
6
|
+
|
|
7
|
+
interface ApproveLabResultsModal {
|
|
8
|
+
closeModal: () => void;
|
|
9
|
+
order: Order;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ApproveLabResultsModal: React.FC<ApproveLabResultsModal> = ({ order, closeModal }) => {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
15
|
+
const abortController = useAbortController();
|
|
16
|
+
const invalidateLabOrders = useInvalidateLabOrders();
|
|
17
|
+
|
|
18
|
+
const handleApproval = () => {
|
|
19
|
+
setIsSubmitting(true);
|
|
20
|
+
setFulfillerStatus(order.uuid, 'COMPLETED', abortController).then(
|
|
21
|
+
() => {
|
|
22
|
+
invalidateLabOrders();
|
|
23
|
+
setIsSubmitting(false);
|
|
24
|
+
closeModal();
|
|
25
|
+
showSnackbar({
|
|
26
|
+
isLowContrast: true,
|
|
27
|
+
title: t('resultsApproved', 'Results Approved'),
|
|
28
|
+
kind: 'success',
|
|
29
|
+
subtitle: t('labResultsApprovedSuccessfully', 'Lab results have been successfully approved and finalized'),
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
(error) => {
|
|
33
|
+
setIsSubmitting(false);
|
|
34
|
+
showNotification({
|
|
35
|
+
title: t('errorApprovingResults', 'Error approving results'),
|
|
36
|
+
kind: 'error',
|
|
37
|
+
critical: true,
|
|
38
|
+
description: error?.message,
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
<ModalHeader closeModal={closeModal} title={t('approveLabResults', 'Approve Lab Results')} />
|
|
47
|
+
<ModalBody>
|
|
48
|
+
<p>
|
|
49
|
+
{t(
|
|
50
|
+
'approveResultsConfirmationText',
|
|
51
|
+
'You are about to approve and finalize these lab results. Once approved, the results will be marked as complete and made available to clinicians. Are you sure you want to proceed?',
|
|
52
|
+
)}
|
|
53
|
+
</p>
|
|
54
|
+
<>
|
|
55
|
+
<ExtensionSlot state={{ order: order }} name="completed-lab-order-results-slot" />
|
|
56
|
+
</>
|
|
57
|
+
</ModalBody>
|
|
58
|
+
<ModalFooter>
|
|
59
|
+
<Button kind="secondary" onClick={closeModal}>
|
|
60
|
+
{t('cancel', 'Cancel')}
|
|
61
|
+
</Button>
|
|
62
|
+
<Button type="submit" onClick={handleApproval} disabled={isSubmitting}>
|
|
63
|
+
{t('approveResults', 'Approve Results')}
|
|
64
|
+
</Button>
|
|
65
|
+
</ModalFooter>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default ApproveLabResultsModal;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
|
|
4
|
+
import { showNotification, showSnackbar, useAbortController, type Order } from '@openmrs/esm-framework';
|
|
5
|
+
import { setFulfillerStatus, useInvalidateLabOrders } from '../../laboratory.resource';
|
|
6
|
+
|
|
7
|
+
interface PickupLabRequestModal {
|
|
8
|
+
closeModal: () => void;
|
|
9
|
+
order: Order;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PickupLabRequestModal: React.FC<PickupLabRequestModal> = ({ order, closeModal }) => {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
15
|
+
const abortController = useAbortController();
|
|
16
|
+
const invalidateLabOrders = useInvalidateLabOrders();
|
|
17
|
+
|
|
18
|
+
const handlePickup = () => {
|
|
19
|
+
setIsSubmitting(true);
|
|
20
|
+
setFulfillerStatus(order.uuid, 'IN_PROGRESS', abortController).then(
|
|
21
|
+
() => {
|
|
22
|
+
invalidateLabOrders();
|
|
23
|
+
setIsSubmitting(false);
|
|
24
|
+
closeModal();
|
|
25
|
+
showSnackbar({
|
|
26
|
+
isLowContrast: true,
|
|
27
|
+
title: t('pickedAnOrder', 'Picked an order'),
|
|
28
|
+
kind: 'success',
|
|
29
|
+
subtitle: t('orderPickedSuccessfully', 'You have successfully picked an order'),
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
(error) => {
|
|
33
|
+
setIsSubmitting(false);
|
|
34
|
+
showNotification({
|
|
35
|
+
title: t('errorPickingOrder', 'Error picking order'),
|
|
36
|
+
kind: 'error',
|
|
37
|
+
critical: true,
|
|
38
|
+
description: error?.message,
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
<ModalHeader closeModal={closeModal} title={t('pickRequest', 'Pick lab request')} />
|
|
47
|
+
<ModalBody>
|
|
48
|
+
<p>
|
|
49
|
+
{t(
|
|
50
|
+
'pickRequestConfirmationText',
|
|
51
|
+
'Continuing will update the request status to "In Progress" and advance it to the next stage. Are you sure you want to proceed?',
|
|
52
|
+
)}
|
|
53
|
+
</p>
|
|
54
|
+
</ModalBody>
|
|
55
|
+
<ModalFooter>
|
|
56
|
+
<Button kind="secondary" onClick={closeModal}>
|
|
57
|
+
{t('discard', 'Discard')}
|
|
58
|
+
</Button>
|
|
59
|
+
<Button type="submit" onClick={handlePickup} disabled={isSubmitting}>
|
|
60
|
+
{t('pickupLabRequest', 'Pick up lab request')}
|
|
61
|
+
</Button>
|
|
62
|
+
</ModalFooter>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default PickupLabRequestModal;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { showSnackbar, showNotification, type Order } from '@openmrs/esm-framework';
|
|
5
|
+
import { setFulfillerStatus, useInvalidateLabOrders } from '../../laboratory.resource';
|
|
6
|
+
import PickupLabRequestModal from './pickup-lab-request-modal.component';
|
|
7
|
+
|
|
8
|
+
jest.mock('../../laboratory.resource', () => ({
|
|
9
|
+
setFulfillerStatus: jest.fn(),
|
|
10
|
+
useInvalidateLabOrders: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const mockSetFulfillerStatus = jest.mocked(setFulfillerStatus);
|
|
14
|
+
const mockUseInvalidateLabOrders = jest.mocked(useInvalidateLabOrders);
|
|
15
|
+
const mockShowSnackbar = jest.mocked(showSnackbar);
|
|
16
|
+
const mockShowNotification = jest.mocked(showNotification);
|
|
17
|
+
|
|
18
|
+
const mockOrder: Partial<Order> = {
|
|
19
|
+
uuid: 'order-uuid-1',
|
|
20
|
+
orderNumber: 'ORD-001',
|
|
21
|
+
patient: {
|
|
22
|
+
uuid: 'patient-uuid-1',
|
|
23
|
+
display: 'Test Patient',
|
|
24
|
+
} as Order['patient'],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe('PickupLabRequestModal', () => {
|
|
28
|
+
const mockCloseModal = jest.fn();
|
|
29
|
+
const mockInvalidateLabOrders = jest.fn();
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
mockUseInvalidateLabOrders.mockReturnValue(mockInvalidateLabOrders);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should render the modal with confirmation text', () => {
|
|
36
|
+
render(<PickupLabRequestModal order={mockOrder as Order} closeModal={mockCloseModal} />);
|
|
37
|
+
|
|
38
|
+
expect(screen.getByText('Pick lab request')).toBeInTheDocument();
|
|
39
|
+
expect(screen.getByText(/Continuing will update the request status to "In Progress"/)).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByRole('button', { name: 'Discard' })).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByRole('button', { name: 'Pick up lab request' })).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should close modal when discard button is clicked', async () => {
|
|
45
|
+
render(<PickupLabRequestModal order={mockOrder as Order} closeModal={mockCloseModal} />);
|
|
46
|
+
|
|
47
|
+
const user = userEvent.setup();
|
|
48
|
+
await user.click(screen.getByRole('button', { name: 'Discard' }));
|
|
49
|
+
|
|
50
|
+
expect(mockCloseModal).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should call setFulfillerStatus and show success snackbar when picking up order succeeds', async () => {
|
|
54
|
+
mockSetFulfillerStatus.mockResolvedValue({} as any);
|
|
55
|
+
|
|
56
|
+
render(<PickupLabRequestModal order={mockOrder as Order} closeModal={mockCloseModal} />);
|
|
57
|
+
|
|
58
|
+
const user = userEvent.setup();
|
|
59
|
+
await user.click(screen.getByRole('button', { name: 'Pick up lab request' }));
|
|
60
|
+
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(mockSetFulfillerStatus).toHaveBeenCalledWith(
|
|
63
|
+
'order-uuid-1',
|
|
64
|
+
'IN_PROGRESS',
|
|
65
|
+
expect.objectContaining({ signal: expect.any(Object) }),
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await waitFor(() => {
|
|
70
|
+
expect(mockInvalidateLabOrders).toHaveBeenCalled();
|
|
71
|
+
expect(mockCloseModal).toHaveBeenCalled();
|
|
72
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith({
|
|
73
|
+
isLowContrast: true,
|
|
74
|
+
title: 'Picked an order',
|
|
75
|
+
kind: 'success',
|
|
76
|
+
subtitle: 'You have successfully picked an order',
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should show error notification when picking up order fails', async () => {
|
|
82
|
+
const error = new Error('Network error');
|
|
83
|
+
mockSetFulfillerStatus.mockRejectedValue(error);
|
|
84
|
+
|
|
85
|
+
render(<PickupLabRequestModal order={mockOrder as Order} closeModal={mockCloseModal} />);
|
|
86
|
+
|
|
87
|
+
const user = userEvent.setup();
|
|
88
|
+
await user.click(screen.getByRole('button', { name: 'Pick up lab request' }));
|
|
89
|
+
|
|
90
|
+
await waitFor(() => {
|
|
91
|
+
expect(mockShowNotification).toHaveBeenCalledWith({
|
|
92
|
+
title: 'Error picking order',
|
|
93
|
+
kind: 'error',
|
|
94
|
+
critical: true,
|
|
95
|
+
description: 'Network error',
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(mockCloseModal).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should disable submit button while submitting', async () => {
|
|
103
|
+
let resolvePromise: (value: unknown) => void;
|
|
104
|
+
mockSetFulfillerStatus.mockImplementation(
|
|
105
|
+
() =>
|
|
106
|
+
new Promise((resolve) => {
|
|
107
|
+
resolvePromise = resolve;
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
render(<PickupLabRequestModal order={mockOrder as Order} closeModal={mockCloseModal} />);
|
|
112
|
+
|
|
113
|
+
const user = userEvent.setup();
|
|
114
|
+
const submitButton = screen.getByRole('button', { name: 'Pick up lab request' });
|
|
115
|
+
expect(submitButton).not.toBeDisabled();
|
|
116
|
+
|
|
117
|
+
await user.click(submitButton);
|
|
118
|
+
|
|
119
|
+
expect(submitButton).toBeDisabled();
|
|
120
|
+
|
|
121
|
+
resolvePromise!({});
|
|
122
|
+
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
expect(mockCloseModal).toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button, Form, ModalBody, ModalFooter, ModalHeader, TextArea, Layer } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { type Order, showNotification, showSnackbar, useAbortController } from '@openmrs/esm-framework';
|
|
5
|
+
import { rejectLabOrder, useInvalidateLabOrders } from '../../laboratory.resource';
|
|
6
|
+
import styles from './reject-lab-request-modal.scss';
|
|
7
|
+
|
|
8
|
+
interface RejectLabRequestModalProps {
|
|
9
|
+
order: Order;
|
|
10
|
+
closeModal: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RejectLabRequestModal: React.FC<RejectLabRequestModalProps> = ({ order, closeModal }) => {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const [fulfillerComment, setFulfillerComment] = useState('');
|
|
16
|
+
const abortController = useAbortController();
|
|
17
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
18
|
+
const invalidateLabOrders = useInvalidateLabOrders();
|
|
19
|
+
|
|
20
|
+
const handleRejectOrder = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
21
|
+
event.preventDefault();
|
|
22
|
+
setIsSubmitting(true);
|
|
23
|
+
rejectLabOrder(order.uuid, fulfillerComment, abortController).then(
|
|
24
|
+
() => {
|
|
25
|
+
invalidateLabOrders();
|
|
26
|
+
setIsSubmitting(false);
|
|
27
|
+
closeModal();
|
|
28
|
+
showSnackbar({
|
|
29
|
+
isLowContrast: true,
|
|
30
|
+
title: t('rejectLabRequestTitle', 'Lab request rejected'),
|
|
31
|
+
kind: 'success',
|
|
32
|
+
subtitle: t(
|
|
33
|
+
'rejectLabRequestSuccessMessage',
|
|
34
|
+
'Lab request with order number "{{orderNumber}}" rejected successfully',
|
|
35
|
+
{ orderNumber: order.orderNumber },
|
|
36
|
+
),
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
(err) => {
|
|
40
|
+
setIsSubmitting(false);
|
|
41
|
+
showNotification({
|
|
42
|
+
title: t('errorRejectingRequest', 'Error rejecting lab request'),
|
|
43
|
+
kind: 'error',
|
|
44
|
+
critical: true,
|
|
45
|
+
description: err?.message,
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Form onSubmit={handleRejectOrder}>
|
|
53
|
+
<ModalHeader
|
|
54
|
+
closeModal={closeModal}
|
|
55
|
+
title={`${t('rejectLabRequest', 'Reject lab request')} [${order.orderNumber}]`}
|
|
56
|
+
/>
|
|
57
|
+
<ModalBody>
|
|
58
|
+
<div className={styles.modalBody}>
|
|
59
|
+
<Layer>
|
|
60
|
+
<p className={styles.section}>{`${t('testType', 'Test type')}: ${order.concept?.display}`}</p>
|
|
61
|
+
</Layer>
|
|
62
|
+
<br />
|
|
63
|
+
<Layer>
|
|
64
|
+
<TextArea
|
|
65
|
+
labelText={t('fulfillerComment', 'Fulfiller comment')}
|
|
66
|
+
id="commentField"
|
|
67
|
+
maxCount={500}
|
|
68
|
+
enableCounter
|
|
69
|
+
onChange={(e) => setFulfillerComment(e.target.value)}
|
|
70
|
+
/>
|
|
71
|
+
</Layer>
|
|
72
|
+
</div>
|
|
73
|
+
</ModalBody>
|
|
74
|
+
<ModalFooter>
|
|
75
|
+
<Button kind="secondary" onClick={closeModal}>
|
|
76
|
+
{t('cancel', 'Cancel')}
|
|
77
|
+
</Button>
|
|
78
|
+
<Button kind="danger" type="submit" disabled={isSubmitting}>
|
|
79
|
+
{t('reject', 'Reject')}
|
|
80
|
+
</Button>
|
|
81
|
+
</ModalFooter>
|
|
82
|
+
</Form>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default RejectLabRequestModal;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { showSnackbar, showNotification, type Order } from '@openmrs/esm-framework';
|
|
5
|
+
import { rejectLabOrder, useInvalidateLabOrders } from '../../laboratory.resource';
|
|
6
|
+
import RejectLabRequestModal from './reject-lab-request-modal.component';
|
|
7
|
+
|
|
8
|
+
jest.mock('../../laboratory.resource', () => ({
|
|
9
|
+
rejectLabOrder: jest.fn(),
|
|
10
|
+
useInvalidateLabOrders: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const mockRejectLabOrder = jest.mocked(rejectLabOrder);
|
|
14
|
+
const mockUseInvalidateLabOrders = jest.mocked(useInvalidateLabOrders);
|
|
15
|
+
const mockShowSnackbar = jest.mocked(showSnackbar);
|
|
16
|
+
const mockShowNotification = jest.mocked(showNotification);
|
|
17
|
+
|
|
18
|
+
const mockOrder: Partial<Order> = {
|
|
19
|
+
uuid: 'order-uuid-1',
|
|
20
|
+
orderNumber: 'ORD-001',
|
|
21
|
+
patient: {
|
|
22
|
+
uuid: 'patient-uuid-1',
|
|
23
|
+
display: 'Test Patient',
|
|
24
|
+
} as Order['patient'],
|
|
25
|
+
concept: {
|
|
26
|
+
uuid: 'concept-uuid-1',
|
|
27
|
+
display: 'Blood Test',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('RejectLabRequestModal', () => {
|
|
32
|
+
const mockCloseModal = jest.fn();
|
|
33
|
+
const mockInvalidateLabOrders = jest.fn();
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
mockUseInvalidateLabOrders.mockReturnValue(mockInvalidateLabOrders);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should render the modal with order details and comment field', () => {
|
|
40
|
+
render(<RejectLabRequestModal order={mockOrder as Order} closeModal={mockCloseModal} />);
|
|
41
|
+
|
|
42
|
+
expect(screen.getByText(/Reject lab request.*\[ORD-001\]/)).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByText('Test type: Blood Test')).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByLabelText('Fulfiller comment')).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
|
46
|
+
expect(screen.getByRole('button', { name: /Reject/ })).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should close modal when cancel button is clicked', async () => {
|
|
50
|
+
render(<RejectLabRequestModal order={mockOrder as Order} closeModal={mockCloseModal} />);
|
|
51
|
+
|
|
52
|
+
const user = userEvent.setup();
|
|
53
|
+
await user.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
54
|
+
|
|
55
|
+
expect(mockCloseModal).toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should call rejectLabOrder with comment and show success snackbar when rejection succeeds', async () => {
|
|
59
|
+
mockRejectLabOrder.mockResolvedValue({} as any);
|
|
60
|
+
|
|
61
|
+
render(<RejectLabRequestModal order={mockOrder as Order} closeModal={mockCloseModal} />);
|
|
62
|
+
|
|
63
|
+
const user = userEvent.setup();
|
|
64
|
+
const commentInput = screen.getByLabelText('Fulfiller comment');
|
|
65
|
+
await user.type(commentInput, 'Sample contaminated');
|
|
66
|
+
await user.click(screen.getByRole('button', { name: /Reject/ }));
|
|
67
|
+
|
|
68
|
+
await waitFor(() => {
|
|
69
|
+
expect(mockRejectLabOrder).toHaveBeenCalledWith(
|
|
70
|
+
'order-uuid-1',
|
|
71
|
+
'Sample contaminated',
|
|
72
|
+
expect.objectContaining({ signal: expect.any(Object) }),
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
expect(mockInvalidateLabOrders).toHaveBeenCalled();
|
|
78
|
+
expect(mockCloseModal).toHaveBeenCalled();
|
|
79
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
80
|
+
expect.objectContaining({
|
|
81
|
+
isLowContrast: true,
|
|
82
|
+
title: 'Lab request rejected',
|
|
83
|
+
kind: 'success',
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should show error notification when rejection fails', async () => {
|
|
90
|
+
const error = new Error('Server error');
|
|
91
|
+
mockRejectLabOrder.mockRejectedValue(error);
|
|
92
|
+
|
|
93
|
+
render(<RejectLabRequestModal order={mockOrder as Order} closeModal={mockCloseModal} />);
|
|
94
|
+
|
|
95
|
+
const user = userEvent.setup();
|
|
96
|
+
await user.click(screen.getByRole('button', { name: /Reject/ }));
|
|
97
|
+
|
|
98
|
+
await waitFor(() => {
|
|
99
|
+
expect(mockShowNotification).toHaveBeenCalledWith({
|
|
100
|
+
title: 'Error rejecting lab request',
|
|
101
|
+
kind: 'error',
|
|
102
|
+
critical: true,
|
|
103
|
+
description: 'Server error',
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(mockCloseModal).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should disable submit button while submitting', async () => {
|
|
111
|
+
let resolvePromise: (value: unknown) => void;
|
|
112
|
+
mockRejectLabOrder.mockImplementation(
|
|
113
|
+
() =>
|
|
114
|
+
new Promise((resolve) => {
|
|
115
|
+
resolvePromise = resolve;
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
render(<RejectLabRequestModal order={mockOrder as Order} closeModal={mockCloseModal} />);
|
|
120
|
+
|
|
121
|
+
const user = userEvent.setup();
|
|
122
|
+
const submitButton = screen.getByRole('button', { name: /Reject/ });
|
|
123
|
+
expect(submitButton).not.toBeDisabled();
|
|
124
|
+
|
|
125
|
+
await user.click(submitButton);
|
|
126
|
+
|
|
127
|
+
expect(submitButton).toBeDisabled();
|
|
128
|
+
|
|
129
|
+
resolvePromise!({});
|
|
130
|
+
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(mockCloseModal).toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should allow submitting with empty comment', async () => {
|
|
137
|
+
mockRejectLabOrder.mockResolvedValue({} as any);
|
|
138
|
+
|
|
139
|
+
render(<RejectLabRequestModal order={mockOrder as Order} closeModal={mockCloseModal} />);
|
|
140
|
+
|
|
141
|
+
const user = userEvent.setup();
|
|
142
|
+
await user.click(screen.getByRole('button', { name: /Reject/ }));
|
|
143
|
+
|
|
144
|
+
await waitFor(() => {
|
|
145
|
+
expect(mockRejectLabOrder).toHaveBeenCalledWith(
|
|
146
|
+
'order-uuid-1',
|
|
147
|
+
'',
|
|
148
|
+
expect.objectContaining({ signal: expect.any(Object) }),
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useLabOrders } from '../laboratory.resource';
|
|
4
|
+
import LabSummaryTile from '../components/summary-tile/lab-summary-tile.component';
|
|
5
|
+
|
|
6
|
+
const AllLabRequestsTile = () => {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const { labOrders } = useLabOrders({ newOrdersOnly: true });
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<LabSummaryTile
|
|
12
|
+
label={t('orders', 'Orders')}
|
|
13
|
+
value={labOrders?.length}
|
|
14
|
+
headerLabel={t('testsOrdered', 'Tests ordered')}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default AllLabRequestsTile;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useLabOrders } from '../laboratory.resource';
|
|
4
|
+
import LabSummaryTile from '../components/summary-tile/lab-summary-tile.component';
|
|
5
|
+
|
|
6
|
+
const CompletedLabRequestsTile = () => {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const { labOrders } = useLabOrders({ status: 'COMPLETED', excludeCanceled: false });
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<LabSummaryTile
|
|
12
|
+
label={t('completed', 'Completed')}
|
|
13
|
+
value={labOrders?.length}
|
|
14
|
+
headerLabel={t('results', 'Results')}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default CompletedLabRequestsTile;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useLabOrders } from '../laboratory.resource';
|
|
4
|
+
import LabSummaryTile from '../components/summary-tile/lab-summary-tile.component';
|
|
5
|
+
|
|
6
|
+
const InProgressLabRequestsTile = () => {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const { labOrders } = useLabOrders({ status: 'IN_PROGRESS' });
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<LabSummaryTile
|
|
12
|
+
label={t('inProgress', 'In progress')}
|
|
13
|
+
value={labOrders?.length}
|
|
14
|
+
headerLabel={t('worklist', 'Worklist')}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default InProgressLabRequestsTile;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { type AssignedExtension, useAssignedExtensions, Extension, useConfig } from '@openmrs/esm-framework';
|
|
4
|
+
// ComponentContext is not part of the public API but is needed here to render extensions
|
|
5
|
+
// in a custom layout (CSS grid tiles) without the wrapper div that ExtensionSlot adds.
|
|
6
|
+
// eslint-disable-next-line no-restricted-imports
|
|
7
|
+
import { ComponentContext } from '@openmrs/esm-framework/src/internal';
|
|
8
|
+
import styles from './laboratory-summary-tiles.scss';
|
|
9
|
+
import { type Config } from '../config-schema';
|
|
10
|
+
|
|
11
|
+
const LaboratorySummaryTiles: React.FC = () => {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const { enableReviewingLabResultsBeforeApproval } = useConfig<Config>();
|
|
14
|
+
const labTileSlot = 'lab-tiles-slot';
|
|
15
|
+
const tilesExtensions = useAssignedExtensions(labTileSlot) as AssignedExtension[];
|
|
16
|
+
|
|
17
|
+
const filteredExtensions = tilesExtensions.filter((extension) => {
|
|
18
|
+
const meta = extension.meta ?? {};
|
|
19
|
+
if (Object.keys(meta).length === 0) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (extension.name === 'pending-review-list-tile-component') {
|
|
23
|
+
return enableReviewingLabResultsBeforeApproval === true;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className={styles.cardContainer}>
|
|
30
|
+
{filteredExtensions.map((extension, index) => {
|
|
31
|
+
return (
|
|
32
|
+
<ComponentContext.Provider
|
|
33
|
+
key={extension.id}
|
|
34
|
+
value={{
|
|
35
|
+
moduleName: extension.moduleName,
|
|
36
|
+
featureName: 'laboratory',
|
|
37
|
+
extension: {
|
|
38
|
+
extensionId: extension.id,
|
|
39
|
+
extensionSlotName: labTileSlot,
|
|
40
|
+
extensionSlotModuleName: extension.moduleName,
|
|
41
|
+
},
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<Extension />
|
|
45
|
+
</ComponentContext.Provider>
|
|
46
|
+
);
|
|
47
|
+
})}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default LaboratorySummaryTiles;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
4
|
+
|
|
5
|
+
.cardContainer {
|
|
6
|
+
background-color: $ui-02;
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-wrap: wrap;
|
|
9
|
+
padding: layout.$spacing-03;
|
|
10
|
+
gap: 0 layout.$spacing-01;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.cardContainer > div {
|
|
14
|
+
flex: 1 1 300px;
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
}
|