@kenyaemr/esm-imaging-orders-app 4.0.1-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +188 -0
- package/README.md +8 -0
- package/dist/123.js +2 -0
- package/dist/123.js.LICENSE.txt +40 -0
- package/dist/123.js.map +1 -0
- package/dist/144.js +2 -0
- package/dist/144.js.LICENSE.txt +19 -0
- package/dist/144.js.map +1 -0
- package/dist/225.js +1 -0
- package/dist/225.js.map +1 -0
- package/dist/300.js +1 -0
- package/dist/364.js +1 -0
- package/dist/364.js.map +1 -0
- package/dist/372.js +1 -0
- package/dist/372.js.map +1 -0
- package/dist/41.js +2 -0
- package/dist/41.js.LICENSE.txt +9 -0
- package/dist/41.js.map +1 -0
- package/dist/495.js +1 -0
- package/dist/495.js.map +1 -0
- package/dist/606.js +1 -0
- package/dist/606.js.map +1 -0
- package/dist/831.js +2 -0
- package/dist/831.js.LICENSE.txt +5 -0
- package/dist/831.js.map +1 -0
- package/dist/876.js +2 -0
- package/dist/876.js.LICENSE.txt +9 -0
- package/dist/876.js.map +1 -0
- package/dist/913.js +2 -0
- package/dist/913.js.LICENSE.txt +32 -0
- package/dist/913.js.map +1 -0
- package/dist/kenyaemr-esm-imaging-orders-app.js +1 -0
- package/dist/kenyaemr-esm-imaging-orders-app.js.buildmanifest.json +401 -0
- package/dist/kenyaemr-esm-imaging-orders-app.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +60 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +8 -0
- package/package.json +55 -0
- package/src/config-schema.ts +51 -0
- package/src/constants.ts +3 -0
- package/src/declarations.d.ts +6 -0
- package/src/form/imaging-orders/add-imaging-orders/add-imaging-order.scss +44 -0
- package/src/form/imaging-orders/add-imaging-orders/add-imaging-order.workspace.tsx +86 -0
- package/src/form/imaging-orders/add-imaging-orders/imaging-order-form.component.tsx +354 -0
- package/src/form/imaging-orders/add-imaging-orders/imaging-order-form.scss +79 -0
- package/src/form/imaging-orders/add-imaging-orders/imaging-order.ts +19 -0
- package/src/form/imaging-orders/add-imaging-orders/imaging-type-search.scss +115 -0
- package/src/form/imaging-orders/add-imaging-orders/imaging-type-search.tsx +235 -0
- package/src/form/imaging-orders/add-imaging-orders/useImagingTypes.ts +89 -0
- package/src/form/imaging-orders/api.ts +230 -0
- package/src/form/imaging-orders/imaging-order-basket-panel/imaging-icon.component.tsx +40 -0
- package/src/form/imaging-orders/imaging-order-basket-panel/imaging-order-basket-item-tile.component.tsx +96 -0
- package/src/form/imaging-orders/imaging-order-basket-panel/imaging-order-basket-item-tile.scss +72 -0
- package/src/form/imaging-orders/imaging-order-basket-panel/imaging-order-basket-panel.extension.tsx +191 -0
- package/src/form/imaging-orders/imaging-order-basket-panel/imaging-order-basket-panel.scss +74 -0
- package/src/form/imaging-orders/useOrderConfig.ts +48 -0
- package/src/form/imaging-report-form/imaging-report-form.component.tsx +161 -0
- package/src/form/imaging-report-form/imaging-report-form.scss +30 -0
- package/src/form/imaging-report-form/imaging.resource.ts +360 -0
- package/src/header/imagining-header.component.tsx +17 -0
- package/src/header/imagining-header.scss +5 -0
- package/src/hooks/useOrdersWorklist.ts +59 -0
- package/src/hooks/useSearchGroupedResults.ts +27 -0
- package/src/hooks/useSearchResults.ts +51 -0
- package/src/imaging-orders.component.tsx +14 -0
- package/src/imaging-tabs/approved/approved-orders.component.tsx +31 -0
- package/src/imaging-tabs/approved/approved-orders.scss +0 -0
- package/src/imaging-tabs/imaging-tabs.component.tsx +79 -0
- package/src/imaging-tabs/imaging-tabs.scss +5 -0
- package/src/imaging-tabs/orders-not-done/orders-not-done.component.tsx +42 -0
- package/src/imaging-tabs/referred-test/referred-ordered.component.tsx +26 -0
- package/src/imaging-tabs/referred-test/referred-ordered.scss +6 -0
- package/src/imaging-tabs/review-ordered/review-imaging-report-modal/review-imaging-report-dialog.component.tsx +138 -0
- package/src/imaging-tabs/review-ordered/review-imaging-report-modal/review-imaging-report-dialog.scss +5 -0
- package/src/imaging-tabs/review-ordered/review-ordered.component.tsx +28 -0
- package/src/imaging-tabs/review-ordered/review-ordered.scss +0 -0
- package/src/imaging-tabs/test-ordered/pick-imaging-order/add-to-worklist-dialog.component.tsx +94 -0
- package/src/imaging-tabs/test-ordered/pick-imaging-order/add-to-worklist-dialog.resource.ts +137 -0
- package/src/imaging-tabs/test-ordered/pick-imaging-order/add-to-worklist-dialog.scss +38 -0
- package/src/imaging-tabs/test-ordered/reject-order-dialog/radiology-reject-reason.component.tsx +40 -0
- package/src/imaging-tabs/test-ordered/reject-order-dialog/reject-order-dialog.component.tsx +95 -0
- package/src/imaging-tabs/test-ordered/reject-order-dialog/reject-order-dialog.scss +14 -0
- package/src/imaging-tabs/test-ordered/tests-ordered.component.tsx +35 -0
- package/src/imaging-tabs/test-ordered/tests-ordered.scss +13 -0
- package/src/imaging-tabs/test-ordered/transition-patient-new-queue/transition-latest-queue-entry-button.component.tsx +34 -0
- package/src/imaging-tabs/test-ordered/transition-patient-new-queue/transition-latest-queue-entry-button.scss +14 -0
- package/src/imaging-tabs/work-list/work-list.component.tsx +45 -0
- package/src/imaging-tabs/work-list/work-list.resource.ts +150 -0
- package/src/imaging-tabs/work-list/work-list.scss +207 -0
- package/src/index.ts +45 -0
- package/src/left-panel-link.tsx +42 -0
- package/src/root.component.tsx +19 -0
- package/src/routes.json +58 -0
- package/src/shared/imaging.resource.tsx +65 -0
- package/src/shared/ui/common/action-button/action-button.component.tsx +66 -0
- package/src/shared/ui/common/action-button/order-action-extension.component.tsx +21 -0
- package/src/shared/ui/common/grouped-imaging-types.ts +48 -0
- package/src/shared/ui/common/grouped-orders-table.component.tsx +154 -0
- package/src/shared/ui/common/grouped-orders-table.scss +13 -0
- package/src/shared/ui/common/list-order-details.component.tsx +72 -0
- package/src/shared/ui/common/list-order-details.scss +52 -0
- package/src/shared/ui/common/order-detail.component.tsx +14 -0
- package/src/shared/ui/common/order-detail.scss +14 -0
- package/src/types/index.ts +49 -0
- package/src/utils/functions.ts +238 -0
- package/translations/en.json +69 -0
- package/tsconfig.json +5 -0
- package/webpack.config.js +1 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import Root from './root.component';
|
|
2
|
+
import { moduleName } from './constants';
|
|
3
|
+
import { configSchema } from './config-schema';
|
|
4
|
+
|
|
5
|
+
import { defineConfigSchema, getSyncLifecycle } from '@openmrs/esm-framework';
|
|
6
|
+
import { createLeftPanelLink } from './left-panel-link';
|
|
7
|
+
import RejectImagingOrderModal from './imaging-tabs/test-ordered/reject-order-dialog/reject-order-dialog.component';
|
|
8
|
+
import ReviewImagingReportModal from './imaging-tabs/review-ordered/review-imaging-report-modal/review-imaging-report-dialog.component';
|
|
9
|
+
import ImagingReportForm from './form/imaging-report-form/imaging-report-form.component';
|
|
10
|
+
import AddImagingOrderWorkspace from './form/imaging-orders/add-imaging-orders/add-imaging-order.workspace';
|
|
11
|
+
import ImagingOrderBasketPanelExtension from './form/imaging-orders/imaging-order-basket-panel/imaging-order-basket-panel.extension';
|
|
12
|
+
import AddImagingToWorkListModal from './imaging-tabs/test-ordered/pick-imaging-order/add-to-worklist-dialog.component';
|
|
13
|
+
|
|
14
|
+
const options = {
|
|
15
|
+
featureName: 'esm-imaging-orders-app',
|
|
16
|
+
moduleName,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
|
|
20
|
+
|
|
21
|
+
export function startupApp() {
|
|
22
|
+
defineConfigSchema(moduleName, configSchema);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const root = getSyncLifecycle(Root, options);
|
|
26
|
+
|
|
27
|
+
export const imagingOrdersLink = getSyncLifecycle(
|
|
28
|
+
createLeftPanelLink({
|
|
29
|
+
name: 'imaging-orders',
|
|
30
|
+
title: 'Imaging Orders',
|
|
31
|
+
}),
|
|
32
|
+
options,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Modals
|
|
36
|
+
export const reviewImagingReportModal = getSyncLifecycle(ReviewImagingReportModal, options);
|
|
37
|
+
|
|
38
|
+
export const imagingOrderPanel = getSyncLifecycle(ImagingOrderBasketPanelExtension, options);
|
|
39
|
+
export const rejectImagingOrderModal = getSyncLifecycle(RejectImagingOrderModal, options);
|
|
40
|
+
|
|
41
|
+
// t('addImagingOrderWorkspaceTitle', 'Add Imaging order')
|
|
42
|
+
export const addImagingOrderWorkspace = getSyncLifecycle(AddImagingOrderWorkspace, options);
|
|
43
|
+
|
|
44
|
+
export const imagingReportForm = getSyncLifecycle(ImagingReportForm, options);
|
|
45
|
+
export const addImagingToWorkListModal = getSyncLifecycle(AddImagingToWorkListModal, options);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import last from 'lodash-es/last';
|
|
3
|
+
import { BrowserRouter, useLocation } from 'react-router-dom';
|
|
4
|
+
import { ConfigurableLink } from '@openmrs/esm-framework';
|
|
5
|
+
|
|
6
|
+
export interface LinkConfig {
|
|
7
|
+
name: string;
|
|
8
|
+
title: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const isUuid = (value: string) => {
|
|
12
|
+
const regex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/;
|
|
13
|
+
return regex.test(value);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const createLeftPanelLink = (config: LinkConfig) => () => {
|
|
17
|
+
return (
|
|
18
|
+
<BrowserRouter>
|
|
19
|
+
<LinkExtension config={config} />
|
|
20
|
+
</BrowserRouter>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function LinkExtension({ config }: { config: LinkConfig }) {
|
|
25
|
+
const { name, title } = config;
|
|
26
|
+
const location = useLocation();
|
|
27
|
+
const spaBasePath = window.getOpenmrsSpaBase() + 'home';
|
|
28
|
+
|
|
29
|
+
let urlSegment = useMemo(() => decodeURIComponent(last(location.pathname.split('/'))), [location.pathname]);
|
|
30
|
+
|
|
31
|
+
if (isUuid(urlSegment)) {
|
|
32
|
+
urlSegment = 'billing';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<ConfigurableLink
|
|
37
|
+
to={spaBasePath + '/' + name}
|
|
38
|
+
className={`cds--side-nav__link ${name === urlSegment && 'active-left-nav-link'}`}>
|
|
39
|
+
{title}
|
|
40
|
+
</ConfigurableLink>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
|
3
|
+
import ImagingOrders from './imaging-orders.component';
|
|
4
|
+
import { WorkspaceContainer } from '@openmrs/esm-framework';
|
|
5
|
+
|
|
6
|
+
const Root: React.FC = () => {
|
|
7
|
+
const baseName = window.getOpenmrsSpaBase() + 'home/imaging-orders';
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<BrowserRouter basename={baseName}>
|
|
11
|
+
<Routes>
|
|
12
|
+
<Route path="/" element={<ImagingOrders />} />
|
|
13
|
+
</Routes>
|
|
14
|
+
<WorkspaceContainer contextKey="imaging-orders" />
|
|
15
|
+
</BrowserRouter>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default Root;
|
package/src/routes.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.openmrs.org/routes.schema.json",
|
|
3
|
+
"backendDependencies": {
|
|
4
|
+
"fhir2": ">=1.2",
|
|
5
|
+
"webservices.rest": "^2.24.0"
|
|
6
|
+
},
|
|
7
|
+
"extensions": [
|
|
8
|
+
{
|
|
9
|
+
"component": "imagingOrdersLink",
|
|
10
|
+
"name": "imaging-orders-link",
|
|
11
|
+
"slot": "homepage-dashboard-slot",
|
|
12
|
+
"meta": {
|
|
13
|
+
"name": "imaging-orders",
|
|
14
|
+
"title": "imaging-orders",
|
|
15
|
+
"slot": "imaging-dashboard-slot"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"component": "root",
|
|
20
|
+
"name": "imaging-dashboard-root",
|
|
21
|
+
"slot": "imaging-dashboard-slot"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "imaging-order-panel",
|
|
25
|
+
"component": "imagingOrderPanel",
|
|
26
|
+
"slot": "order-basket-slot",
|
|
27
|
+
"order": 3
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"workspaces": [
|
|
31
|
+
{
|
|
32
|
+
"name": "add-imaging-order",
|
|
33
|
+
"type": "order",
|
|
34
|
+
"component": "addImagingOrderWorkspace",
|
|
35
|
+
"title": "Add Imaging order"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "imaging-report-form",
|
|
39
|
+
"component": "imagingReportForm",
|
|
40
|
+
"title": "Imaging Report Form",
|
|
41
|
+
"type": "form"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"modals": [
|
|
45
|
+
{
|
|
46
|
+
"name": "review-imaging-report-modal",
|
|
47
|
+
"component": "reviewImagingReportModal"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "reject-imaging-order-modal",
|
|
51
|
+
"component": "rejectImagingOrderModal"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "add-imaging-to-work-list-modal",
|
|
55
|
+
"component": "addImagingToWorkListModal"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import useSWR, { mutate } from 'swr';
|
|
2
|
+
import { openmrsFetch, restBaseUrl, useConfig } from '@openmrs/esm-framework';
|
|
3
|
+
|
|
4
|
+
import { Result } from '../imaging-tabs/work-list/work-list.resource';
|
|
5
|
+
import { useCallback, useMemo } from 'react';
|
|
6
|
+
import { ImagingConfig } from '../config-schema';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook to fetch and process imaging order statistics based on fulfiller status.
|
|
10
|
+
*
|
|
11
|
+
* @param fulfillerStatus - The status of the order to filter by.
|
|
12
|
+
* @returns An object containing the count of orders, loading state, error state, and a mutate function.
|
|
13
|
+
*/
|
|
14
|
+
export function useImagingOrderStats(fulfillerStatus: string) {
|
|
15
|
+
const {
|
|
16
|
+
orders: { radiologyOrderTypeUuid },
|
|
17
|
+
radiologyConceptClassUuid,
|
|
18
|
+
} = useConfig<ImagingConfig>();
|
|
19
|
+
|
|
20
|
+
const responseFormat =
|
|
21
|
+
'custom:(uuid,orderNumber,patient:ref,concept:(uuid,display,conceptClass),action,careSetting,orderer:ref,urgency,instructions,commentToFulfiller,display,fulfillerStatus,dateStopped)';
|
|
22
|
+
const apiUrl = useMemo(() => {
|
|
23
|
+
const orderTypeParam = `orderTypes=${radiologyOrderTypeUuid}&fulfillerStatus=${fulfillerStatus}&v=${responseFormat}`;
|
|
24
|
+
return `/ws/rest/v1/order?${orderTypeParam}`;
|
|
25
|
+
}, [radiologyOrderTypeUuid, fulfillerStatus, responseFormat]);
|
|
26
|
+
|
|
27
|
+
const mutateOrders = useCallback(() => {
|
|
28
|
+
return mutate(
|
|
29
|
+
(key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/order?orderType=${radiologyOrderTypeUuid}`),
|
|
30
|
+
);
|
|
31
|
+
}, [radiologyOrderTypeUuid]);
|
|
32
|
+
|
|
33
|
+
const { data, error, isLoading } = useSWR<{ data: { results: Array<Result> } }, Error>(apiUrl, openmrsFetch);
|
|
34
|
+
|
|
35
|
+
const radiologyOrders = useMemo(() => {
|
|
36
|
+
return data?.data?.results?.filter((order) => {
|
|
37
|
+
const baseConditions =
|
|
38
|
+
order.dateStopped === null && order.concept.conceptClass.uuid === radiologyConceptClassUuid;
|
|
39
|
+
|
|
40
|
+
switch (fulfillerStatus) {
|
|
41
|
+
case '':
|
|
42
|
+
return baseConditions && order.fulfillerStatus === null && order.action === 'NEW';
|
|
43
|
+
case 'IN_PROGRESS':
|
|
44
|
+
case 'DECLINED':
|
|
45
|
+
case 'COMPLETED':
|
|
46
|
+
case 'EXCEPTION':
|
|
47
|
+
return baseConditions && order.fulfillerStatus === fulfillerStatus && order.action !== 'DISCONTINUE';
|
|
48
|
+
default:
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}, [data, fulfillerStatus, radiologyConceptClassUuid]);
|
|
53
|
+
|
|
54
|
+
const count = useMemo(
|
|
55
|
+
() => (fulfillerStatus != null ? radiologyOrders?.length ?? 0 : 0),
|
|
56
|
+
[fulfillerStatus, radiologyOrders],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
count,
|
|
61
|
+
isLoading,
|
|
62
|
+
isError: error,
|
|
63
|
+
mutate: mutateOrders,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button } from '@carbon/react';
|
|
4
|
+
import { showModal, launchWorkspace } from '@openmrs/esm-framework';
|
|
5
|
+
import { Order } from '@openmrs/esm-patient-common-lib';
|
|
6
|
+
import OrderActionExtension from './order-action-extension.component';
|
|
7
|
+
import { Result } from '../../../../imaging-tabs/work-list/work-list.resource';
|
|
8
|
+
|
|
9
|
+
type ActionButtonProps = {
|
|
10
|
+
action: {
|
|
11
|
+
actionName: string;
|
|
12
|
+
order: number;
|
|
13
|
+
};
|
|
14
|
+
order: Result;
|
|
15
|
+
patientUuid: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ActionButton: React.FC<ActionButtonProps> = ({ action, order, patientUuid }) => {
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
|
|
21
|
+
const handleOpenImagingReportForm = () => {
|
|
22
|
+
launchWorkspace('imaging-report-form', {
|
|
23
|
+
patientUuid,
|
|
24
|
+
order,
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
switch (action.actionName) {
|
|
29
|
+
case 'add-imaging-to-work-list-modal':
|
|
30
|
+
return <OrderActionExtension order={order as unknown as Order} />;
|
|
31
|
+
|
|
32
|
+
case 'imaging-report-form':
|
|
33
|
+
return (
|
|
34
|
+
<Button kind="primary" onClick={handleOpenImagingReportForm}>
|
|
35
|
+
{t('imagingReportForm', 'Imaging Report Form')}
|
|
36
|
+
</Button>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
case 'review-imaging-report-dialog':
|
|
40
|
+
case 'reject-imaging-order-modal':
|
|
41
|
+
return (
|
|
42
|
+
<Button
|
|
43
|
+
kind={action.actionName === 'reject-imaging-order-modal' ? 'danger' : 'tertiary'}
|
|
44
|
+
onClick={() => {
|
|
45
|
+
const dispose = showModal(action.actionName, {
|
|
46
|
+
closeModal: () => dispose(),
|
|
47
|
+
order: order,
|
|
48
|
+
});
|
|
49
|
+
}}>
|
|
50
|
+
{t(
|
|
51
|
+
action.actionName.replace(/-/g, ''),
|
|
52
|
+
action.actionName
|
|
53
|
+
.split('-')
|
|
54
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
55
|
+
.join(' ')
|
|
56
|
+
.replace('Modal', ''),
|
|
57
|
+
)}
|
|
58
|
+
</Button>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
default:
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default ActionButton;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Order } from '@openmrs/esm-patient-common-lib';
|
|
3
|
+
import { ExtensionSlot, restBaseUrl } from '@openmrs/esm-framework';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
type OrderActionProps = {
|
|
7
|
+
order: Order;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const OrderActionExtension: React.FC<OrderActionProps> = ({ order }) => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const state = {
|
|
13
|
+
order: order,
|
|
14
|
+
modalName: 'add-imaging-to-work-list-modal',
|
|
15
|
+
actionText: t('pickImagingOrder', 'Pick Imaging Order'),
|
|
16
|
+
additionalProps: { mutateUrl: `${restBaseUrl}/order` },
|
|
17
|
+
};
|
|
18
|
+
return <ExtensionSlot name="imaging-orders-action" state={state} />;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default OrderActionExtension;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Result } from '../../../imaging-tabs/work-list/work-list.resource';
|
|
2
|
+
|
|
3
|
+
export type FulfillerStatus = '' | 'IN_PROGRESS' | 'DECLINED' | 'COMPLETED' | 'EXCEPTION';
|
|
4
|
+
|
|
5
|
+
export type WorkListProps = {
|
|
6
|
+
fulfillerStatus: FulfillerStatus;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export interface ResultsOrderProps {
|
|
10
|
+
order: Result;
|
|
11
|
+
patientUuid: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RejectOrderProps {
|
|
15
|
+
order: Result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface InstructionsProps {
|
|
19
|
+
order: Result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface GroupedOrders {
|
|
23
|
+
patientId: string;
|
|
24
|
+
orders: Array<Result>;
|
|
25
|
+
}
|
|
26
|
+
export interface GroupedOrdersTableProps {
|
|
27
|
+
orders: Array<Result>;
|
|
28
|
+
showStatus: boolean;
|
|
29
|
+
showStartButton: boolean;
|
|
30
|
+
showActions: boolean;
|
|
31
|
+
showOrderType: boolean;
|
|
32
|
+
actions: Array<OrderAction>;
|
|
33
|
+
title: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ListOrdersDetailsProps {
|
|
37
|
+
groupedOrders: GroupedOrders;
|
|
38
|
+
showStatus: boolean;
|
|
39
|
+
showStartButton: boolean;
|
|
40
|
+
showActions: boolean;
|
|
41
|
+
showOrderType: boolean;
|
|
42
|
+
actions: Array<OrderAction>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface OrderAction {
|
|
46
|
+
actionName: string;
|
|
47
|
+
order: 0 | number;
|
|
48
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import styles from './grouped-orders-table.scss';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { usePagination } from '@openmrs/esm-framework';
|
|
5
|
+
import { GroupedOrdersTableProps } from './grouped-imaging-types';
|
|
6
|
+
import {
|
|
7
|
+
Table,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableRow,
|
|
10
|
+
TableHeader,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableExpandRow,
|
|
13
|
+
TableExpandedRow,
|
|
14
|
+
TableExpandHeader,
|
|
15
|
+
TableCell,
|
|
16
|
+
DataTable,
|
|
17
|
+
TableContainer,
|
|
18
|
+
TableToolbarSearch,
|
|
19
|
+
TableToolbarContent,
|
|
20
|
+
TableToolbar,
|
|
21
|
+
} from '@carbon/react';
|
|
22
|
+
import ListOrderDetails from './list-order-details.component';
|
|
23
|
+
import { EmptyState } from '@openmrs/esm-patient-common-lib';
|
|
24
|
+
import { useSearchGroupedResults } from '../../../hooks/useSearchGroupedResults';
|
|
25
|
+
import TransitionLatestQueueEntryButton from '../../../imaging-tabs/test-ordered/transition-patient-new-queue/transition-latest-queue-entry-button.component';
|
|
26
|
+
|
|
27
|
+
const GroupedOrdersTable: React.FC<GroupedOrdersTableProps> = (props) => {
|
|
28
|
+
const workListEntries = props.orders;
|
|
29
|
+
const { t } = useTranslation();
|
|
30
|
+
const [currentPageSize] = useState<number>(10);
|
|
31
|
+
const [searchString, setSearchString] = useState<string>('');
|
|
32
|
+
|
|
33
|
+
function groupOrdersById(orders) {
|
|
34
|
+
if (orders && orders.length > 0) {
|
|
35
|
+
const groupedOrders = orders.reduce((acc, item) => {
|
|
36
|
+
if (!acc[item.patient.uuid]) {
|
|
37
|
+
acc[item.patient.uuid] = [];
|
|
38
|
+
}
|
|
39
|
+
acc[item.patient.uuid].push(item);
|
|
40
|
+
return acc;
|
|
41
|
+
}, {});
|
|
42
|
+
|
|
43
|
+
// Convert the result to an array of objects with patientId and orders
|
|
44
|
+
return Object.keys(groupedOrders).map((patientId) => ({
|
|
45
|
+
patientId: patientId,
|
|
46
|
+
orders: groupedOrders[patientId],
|
|
47
|
+
}));
|
|
48
|
+
} else {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const groupedOrdersByPatient = groupOrdersById(workListEntries);
|
|
53
|
+
const searchResults = useSearchGroupedResults(groupedOrdersByPatient, searchString);
|
|
54
|
+
const { goTo, results: paginatedResults, currentPage } = usePagination(searchResults, currentPageSize);
|
|
55
|
+
|
|
56
|
+
const rowData = useMemo(() => {
|
|
57
|
+
return paginatedResults.map((patient) => ({
|
|
58
|
+
id: patient.patientId,
|
|
59
|
+
patientName: patient.orders[0].patient?.display?.split('-')[1],
|
|
60
|
+
orders: patient.orders,
|
|
61
|
+
totalOrders: patient.orders?.length,
|
|
62
|
+
fulfillerStatus: patient.orders[0].fulfillerStatus,
|
|
63
|
+
action:
|
|
64
|
+
patient.orders[0].fulfillerStatus === 'COMPLETED' ? (
|
|
65
|
+
<TransitionLatestQueueEntryButton patientUuid={patient.patientId} />
|
|
66
|
+
) : null,
|
|
67
|
+
}));
|
|
68
|
+
}, [paginatedResults]);
|
|
69
|
+
|
|
70
|
+
const tableColumns = useMemo(() => {
|
|
71
|
+
const baseColumns = [
|
|
72
|
+
{ key: 'patientName', header: t('patientName', 'Patient Name') },
|
|
73
|
+
{ key: 'totalOrders', header: t('totalOrders', 'Total Orders') },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const showActionColumn = workListEntries.some((order) => order.fulfillerStatus === 'COMPLETED');
|
|
77
|
+
|
|
78
|
+
return showActionColumn ? [...baseColumns, { key: 'action', header: t('action', 'Action') }] : baseColumns;
|
|
79
|
+
}, [workListEntries, t]);
|
|
80
|
+
|
|
81
|
+
if (paginatedResults.length === 0) {
|
|
82
|
+
return <EmptyState headerTitle={props.title} displayText={t('noOrdersDescription', 'No orders')} />;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<DataTable size="md" useZebraStyle rows={rowData} headers={tableColumns}>
|
|
87
|
+
{({ rows, headers, getHeaderProps, getRowProps, getExpandedRowProps, getTableProps, getTableContainerProps }) => (
|
|
88
|
+
<TableContainer
|
|
89
|
+
className={styles.dataTable}
|
|
90
|
+
title={props.title}
|
|
91
|
+
description={t('groupedOrdersTableDescription', 'Orders grouped by patient, expand row to view all orders')}
|
|
92
|
+
{...getTableContainerProps()}>
|
|
93
|
+
<TableToolbar>
|
|
94
|
+
<TableToolbarContent>
|
|
95
|
+
<TableToolbarSearch
|
|
96
|
+
size="sm"
|
|
97
|
+
placeholder="Search by patient name"
|
|
98
|
+
persistent={true}
|
|
99
|
+
onChange={(event) => setSearchString(event.target.value)}
|
|
100
|
+
/>
|
|
101
|
+
</TableToolbarContent>
|
|
102
|
+
</TableToolbar>
|
|
103
|
+
<Table {...getTableProps()} aria-label="sample table">
|
|
104
|
+
<TableHead>
|
|
105
|
+
<TableRow>
|
|
106
|
+
<TableExpandHeader aria-label="expand row" />
|
|
107
|
+
{headers.map((header, i) => (
|
|
108
|
+
<TableHeader
|
|
109
|
+
key={i}
|
|
110
|
+
{...getHeaderProps({
|
|
111
|
+
header,
|
|
112
|
+
})}>
|
|
113
|
+
{header.header}
|
|
114
|
+
</TableHeader>
|
|
115
|
+
))}
|
|
116
|
+
</TableRow>
|
|
117
|
+
</TableHead>
|
|
118
|
+
<TableBody>
|
|
119
|
+
{rows.map((row) => (
|
|
120
|
+
<React.Fragment key={row.id}>
|
|
121
|
+
<TableExpandRow
|
|
122
|
+
{...getRowProps({
|
|
123
|
+
row,
|
|
124
|
+
})}>
|
|
125
|
+
{row.cells.map((cell) => (
|
|
126
|
+
<TableCell key={cell.id}>{cell.value}</TableCell>
|
|
127
|
+
))}
|
|
128
|
+
</TableExpandRow>
|
|
129
|
+
<TableExpandedRow
|
|
130
|
+
colSpan={headers.length + 1}
|
|
131
|
+
className="demo-expanded-td"
|
|
132
|
+
{...getExpandedRowProps({
|
|
133
|
+
row,
|
|
134
|
+
})}>
|
|
135
|
+
<ListOrderDetails
|
|
136
|
+
actions={props.actions}
|
|
137
|
+
groupedOrders={groupedOrdersByPatient.find((item) => item.patientId === row.id)}
|
|
138
|
+
showActions={props.showActions}
|
|
139
|
+
showOrderType={props.showOrderType}
|
|
140
|
+
showStartButton={props.showStartButton}
|
|
141
|
+
showStatus={props.showStatus}
|
|
142
|
+
/>
|
|
143
|
+
</TableExpandedRow>
|
|
144
|
+
</React.Fragment>
|
|
145
|
+
))}
|
|
146
|
+
</TableBody>
|
|
147
|
+
</Table>
|
|
148
|
+
</TableContainer>
|
|
149
|
+
)}
|
|
150
|
+
</DataTable>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export default GroupedOrdersTable;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import styles from './list-order-details.scss';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { formatDate, parseDate } from '@openmrs/esm-framework';
|
|
5
|
+
import { ListOrdersDetailsProps } from './grouped-imaging-types';
|
|
6
|
+
import { Tile } from '@carbon/react';
|
|
7
|
+
import { OrderDetail } from './order-detail.component';
|
|
8
|
+
import ActionButton from './action-button/action-button.component';
|
|
9
|
+
|
|
10
|
+
const ListOrderDetails: React.FC<ListOrdersDetailsProps> = (props) => {
|
|
11
|
+
const orders = props.groupedOrders?.orders;
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const orderRows = useMemo(() => {
|
|
14
|
+
return orders
|
|
15
|
+
?.filter((item) => item.action === 'NEW')
|
|
16
|
+
.map((entry) => ({
|
|
17
|
+
...entry,
|
|
18
|
+
id: entry.uuid,
|
|
19
|
+
orderNumber: entry.orderNumber,
|
|
20
|
+
procedure: entry.display,
|
|
21
|
+
status: entry.fulfillerStatus ? entry.fulfillerStatus : '--',
|
|
22
|
+
urgency: entry.urgency,
|
|
23
|
+
orderer: entry.orderer?.display,
|
|
24
|
+
instructions: entry.instructions ? entry.instructions : '--',
|
|
25
|
+
date: <span className={styles['single-line-display']}>{formatDate(parseDate(entry?.dateActivated))}</span>,
|
|
26
|
+
}));
|
|
27
|
+
}, [orders]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className={styles.ordersContainer}>
|
|
31
|
+
{orderRows.map((row, index) => (
|
|
32
|
+
<Tile className={styles.orderTile}>
|
|
33
|
+
{props.showActions && (
|
|
34
|
+
<div className={styles.actionBtns}>
|
|
35
|
+
{props.actions
|
|
36
|
+
.sort((a, b) => {
|
|
37
|
+
// Replace 'property' with the actual property you want to sort by
|
|
38
|
+
if (a.order < b.order) {
|
|
39
|
+
return -1;
|
|
40
|
+
}
|
|
41
|
+
if (a.order > b.order) {
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
})
|
|
46
|
+
.map((action) => (
|
|
47
|
+
<ActionButton
|
|
48
|
+
key={action.actionName}
|
|
49
|
+
action={action}
|
|
50
|
+
order={orders.find((order) => order.uuid === row.id)}
|
|
51
|
+
patientUuid={row.patient.uuid}
|
|
52
|
+
/>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
<div>
|
|
57
|
+
<OrderDetail label={t('date', 'DATE').toUpperCase()} value={row.date} />
|
|
58
|
+
<OrderDetail label={t('orderNumber', 'Order Number').toUpperCase()} value={row.orderNumber} />
|
|
59
|
+
<OrderDetail label={t('procedure', 'procedure').toUpperCase()} value={row.procedure} />
|
|
60
|
+
|
|
61
|
+
{props.showStatus && <OrderDetail label={t('status', 'Status').toUpperCase()} value={row.status} />}
|
|
62
|
+
<OrderDetail label={t('urgency', 'urgency').toUpperCase()} value={row.urgency} />
|
|
63
|
+
<OrderDetail label={t('orderer', 'orderer').toUpperCase()} value={row.orderer} />
|
|
64
|
+
<OrderDetail label={t('instructions', 'Instructions').toUpperCase()} value={row.instructions} />
|
|
65
|
+
</div>
|
|
66
|
+
</Tile>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default ListOrderDetails;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/colors';
|
|
3
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
4
|
+
|
|
5
|
+
.orderTile {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: row-reverse;
|
|
8
|
+
justify-content: space-between;
|
|
9
|
+
width: 100%;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.ordersContainer {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
max-width: 100%;
|
|
16
|
+
margin-bottom: 1rem;
|
|
17
|
+
|
|
18
|
+
&:global(.cds--tile) {
|
|
19
|
+
min-height: 3rem !important;
|
|
20
|
+
padding-left: 10px !important;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.ordersContainer > :global(.cds--tile) {
|
|
25
|
+
min-height: 3rem !important;
|
|
26
|
+
padding-left: 10px !important;
|
|
27
|
+
margin: auto;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.orderPropertyDisplay {
|
|
31
|
+
font-size: 15px !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.bodyLong01 {
|
|
35
|
+
font-size: 13px !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.displayValue {
|
|
39
|
+
color: #525252;
|
|
40
|
+
font-weight: bold;
|
|
41
|
+
width: layout.$spacing-05;
|
|
42
|
+
height: layout.$spacing-05;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.actionBtns {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: flex-end;
|
|
48
|
+
column-gap: layout.$spacing-01;
|
|
49
|
+
& > button {
|
|
50
|
+
max-height: layout.$spacing-07;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from './order-detail.scss';
|
|
3
|
+
|
|
4
|
+
export const OrderDetail: React.FC<{ label: string; value: string | any }> = ({ label, value }) => {
|
|
5
|
+
return (
|
|
6
|
+
<div>
|
|
7
|
+
<p className={styles.bodyLong01}>
|
|
8
|
+
<span className={styles.label01}>{label}</span>
|
|
9
|
+
{' : '}
|
|
10
|
+
<span className={styles.displayValue}>{value}</span>
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
};
|