@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.
Files changed (110) hide show
  1. package/.turbo/turbo-build.log +188 -0
  2. package/README.md +8 -0
  3. package/dist/123.js +2 -0
  4. package/dist/123.js.LICENSE.txt +40 -0
  5. package/dist/123.js.map +1 -0
  6. package/dist/144.js +2 -0
  7. package/dist/144.js.LICENSE.txt +19 -0
  8. package/dist/144.js.map +1 -0
  9. package/dist/225.js +1 -0
  10. package/dist/225.js.map +1 -0
  11. package/dist/300.js +1 -0
  12. package/dist/364.js +1 -0
  13. package/dist/364.js.map +1 -0
  14. package/dist/372.js +1 -0
  15. package/dist/372.js.map +1 -0
  16. package/dist/41.js +2 -0
  17. package/dist/41.js.LICENSE.txt +9 -0
  18. package/dist/41.js.map +1 -0
  19. package/dist/495.js +1 -0
  20. package/dist/495.js.map +1 -0
  21. package/dist/606.js +1 -0
  22. package/dist/606.js.map +1 -0
  23. package/dist/831.js +2 -0
  24. package/dist/831.js.LICENSE.txt +5 -0
  25. package/dist/831.js.map +1 -0
  26. package/dist/876.js +2 -0
  27. package/dist/876.js.LICENSE.txt +9 -0
  28. package/dist/876.js.map +1 -0
  29. package/dist/913.js +2 -0
  30. package/dist/913.js.LICENSE.txt +32 -0
  31. package/dist/913.js.map +1 -0
  32. package/dist/kenyaemr-esm-imaging-orders-app.js +1 -0
  33. package/dist/kenyaemr-esm-imaging-orders-app.js.buildmanifest.json +401 -0
  34. package/dist/kenyaemr-esm-imaging-orders-app.js.map +1 -0
  35. package/dist/main.js +2 -0
  36. package/dist/main.js.LICENSE.txt +60 -0
  37. package/dist/main.js.map +1 -0
  38. package/dist/routes.json +1 -0
  39. package/jest.config.js +8 -0
  40. package/package.json +55 -0
  41. package/src/config-schema.ts +51 -0
  42. package/src/constants.ts +3 -0
  43. package/src/declarations.d.ts +6 -0
  44. package/src/form/imaging-orders/add-imaging-orders/add-imaging-order.scss +44 -0
  45. package/src/form/imaging-orders/add-imaging-orders/add-imaging-order.workspace.tsx +86 -0
  46. package/src/form/imaging-orders/add-imaging-orders/imaging-order-form.component.tsx +354 -0
  47. package/src/form/imaging-orders/add-imaging-orders/imaging-order-form.scss +79 -0
  48. package/src/form/imaging-orders/add-imaging-orders/imaging-order.ts +19 -0
  49. package/src/form/imaging-orders/add-imaging-orders/imaging-type-search.scss +115 -0
  50. package/src/form/imaging-orders/add-imaging-orders/imaging-type-search.tsx +235 -0
  51. package/src/form/imaging-orders/add-imaging-orders/useImagingTypes.ts +89 -0
  52. package/src/form/imaging-orders/api.ts +230 -0
  53. package/src/form/imaging-orders/imaging-order-basket-panel/imaging-icon.component.tsx +40 -0
  54. package/src/form/imaging-orders/imaging-order-basket-panel/imaging-order-basket-item-tile.component.tsx +96 -0
  55. package/src/form/imaging-orders/imaging-order-basket-panel/imaging-order-basket-item-tile.scss +72 -0
  56. package/src/form/imaging-orders/imaging-order-basket-panel/imaging-order-basket-panel.extension.tsx +191 -0
  57. package/src/form/imaging-orders/imaging-order-basket-panel/imaging-order-basket-panel.scss +74 -0
  58. package/src/form/imaging-orders/useOrderConfig.ts +48 -0
  59. package/src/form/imaging-report-form/imaging-report-form.component.tsx +161 -0
  60. package/src/form/imaging-report-form/imaging-report-form.scss +30 -0
  61. package/src/form/imaging-report-form/imaging.resource.ts +360 -0
  62. package/src/header/imagining-header.component.tsx +17 -0
  63. package/src/header/imagining-header.scss +5 -0
  64. package/src/hooks/useOrdersWorklist.ts +59 -0
  65. package/src/hooks/useSearchGroupedResults.ts +27 -0
  66. package/src/hooks/useSearchResults.ts +51 -0
  67. package/src/imaging-orders.component.tsx +14 -0
  68. package/src/imaging-tabs/approved/approved-orders.component.tsx +31 -0
  69. package/src/imaging-tabs/approved/approved-orders.scss +0 -0
  70. package/src/imaging-tabs/imaging-tabs.component.tsx +79 -0
  71. package/src/imaging-tabs/imaging-tabs.scss +5 -0
  72. package/src/imaging-tabs/orders-not-done/orders-not-done.component.tsx +42 -0
  73. package/src/imaging-tabs/referred-test/referred-ordered.component.tsx +26 -0
  74. package/src/imaging-tabs/referred-test/referred-ordered.scss +6 -0
  75. package/src/imaging-tabs/review-ordered/review-imaging-report-modal/review-imaging-report-dialog.component.tsx +138 -0
  76. package/src/imaging-tabs/review-ordered/review-imaging-report-modal/review-imaging-report-dialog.scss +5 -0
  77. package/src/imaging-tabs/review-ordered/review-ordered.component.tsx +28 -0
  78. package/src/imaging-tabs/review-ordered/review-ordered.scss +0 -0
  79. package/src/imaging-tabs/test-ordered/pick-imaging-order/add-to-worklist-dialog.component.tsx +94 -0
  80. package/src/imaging-tabs/test-ordered/pick-imaging-order/add-to-worklist-dialog.resource.ts +137 -0
  81. package/src/imaging-tabs/test-ordered/pick-imaging-order/add-to-worklist-dialog.scss +38 -0
  82. package/src/imaging-tabs/test-ordered/reject-order-dialog/radiology-reject-reason.component.tsx +40 -0
  83. package/src/imaging-tabs/test-ordered/reject-order-dialog/reject-order-dialog.component.tsx +95 -0
  84. package/src/imaging-tabs/test-ordered/reject-order-dialog/reject-order-dialog.scss +14 -0
  85. package/src/imaging-tabs/test-ordered/tests-ordered.component.tsx +35 -0
  86. package/src/imaging-tabs/test-ordered/tests-ordered.scss +13 -0
  87. package/src/imaging-tabs/test-ordered/transition-patient-new-queue/transition-latest-queue-entry-button.component.tsx +34 -0
  88. package/src/imaging-tabs/test-ordered/transition-patient-new-queue/transition-latest-queue-entry-button.scss +14 -0
  89. package/src/imaging-tabs/work-list/work-list.component.tsx +45 -0
  90. package/src/imaging-tabs/work-list/work-list.resource.ts +150 -0
  91. package/src/imaging-tabs/work-list/work-list.scss +207 -0
  92. package/src/index.ts +45 -0
  93. package/src/left-panel-link.tsx +42 -0
  94. package/src/root.component.tsx +19 -0
  95. package/src/routes.json +58 -0
  96. package/src/shared/imaging.resource.tsx +65 -0
  97. package/src/shared/ui/common/action-button/action-button.component.tsx +66 -0
  98. package/src/shared/ui/common/action-button/order-action-extension.component.tsx +21 -0
  99. package/src/shared/ui/common/grouped-imaging-types.ts +48 -0
  100. package/src/shared/ui/common/grouped-orders-table.component.tsx +154 -0
  101. package/src/shared/ui/common/grouped-orders-table.scss +13 -0
  102. package/src/shared/ui/common/list-order-details.component.tsx +72 -0
  103. package/src/shared/ui/common/list-order-details.scss +52 -0
  104. package/src/shared/ui/common/order-detail.component.tsx +14 -0
  105. package/src/shared/ui/common/order-detail.scss +14 -0
  106. package/src/types/index.ts +49 -0
  107. package/src/utils/functions.ts +238 -0
  108. package/translations/en.json +69 -0
  109. package/tsconfig.json +5 -0
  110. package/webpack.config.js +1 -0
@@ -0,0 +1,230 @@
1
+ import useSWR from 'swr';
2
+ import { type FetchResponse, openmrsFetch, restBaseUrl, showSnackbar } from '@openmrs/esm-framework';
3
+ import type { OrderPost } from '@openmrs/esm-patient-common-lib';
4
+ import useSWRImmutable from 'swr/immutable';
5
+ import { type ImagingOrderBasketItem } from '../../types';
6
+
7
+ // TODO: This should be dynamic through configs
8
+ export const careSettingUuid = '6f0c9a92-6f24-11e3-af88-005056821db0';
9
+
10
+ export function useOrderReasons(conceptUuids: Array<string>) {
11
+ const shouldFetch = conceptUuids && conceptUuids.length > 0;
12
+ const url = shouldFetch ? getConceptReferenceUrls(conceptUuids) : null;
13
+ const { data, error, isLoading } = useSWRImmutable<FetchResponse<ConceptResponse>, Error>(
14
+ shouldFetch ? `${restBaseUrl}/${url[0]}` : null,
15
+ openmrsFetch,
16
+ );
17
+
18
+ const ob = data?.data;
19
+ const orderReasons = ob
20
+ ? Object.entries(ob).map(([, value]) => ({
21
+ uuid: value.uuid,
22
+ display: value.display,
23
+ }))
24
+ : [];
25
+
26
+ if (error) {
27
+ showSnackbar({
28
+ title: error.name,
29
+ subtitle: error.message,
30
+ kind: 'error',
31
+ });
32
+ }
33
+
34
+ return { orderReasons: orderReasons, isLoading };
35
+ }
36
+
37
+ export interface ImagingOrderPost extends OrderPost {
38
+ scheduledDate?: Date | string;
39
+ commentToFulfiller?: string;
40
+ laterality?: string;
41
+ bodySite?: string;
42
+ }
43
+
44
+ export function prepImagingOrderPostData(
45
+ order: ImagingOrderBasketItem,
46
+ patientUuid: string,
47
+ encounterUuid: string,
48
+ ): ImagingOrderPost {
49
+ let payload = {};
50
+ if (order.action === 'NEW' || order.action === 'RENEW') {
51
+ payload = {
52
+ action: 'NEW',
53
+ type: 'procedureorder',
54
+ patient: patientUuid,
55
+ careSetting: careSettingUuid,
56
+ orderer: order.orderer,
57
+ encounter: encounterUuid,
58
+ concept: order.testType.conceptUuid,
59
+ instructions: order.instructions,
60
+ orderReason: order.orderReason,
61
+ commentToFulfiller: order.commentsToFulfiller,
62
+ laterality: order.laterality,
63
+ bodySite: order.bodySite,
64
+ urgency: order.urgency,
65
+ };
66
+ if (order.urgency === 'ON_SCHEDULED_DATE') {
67
+ payload['scheduledDate'] = order.scheduleDate;
68
+ }
69
+ return payload;
70
+ } else if (order.action === 'REVISE') {
71
+ payload = {
72
+ action: 'REVISE',
73
+ type: 'procedureorder',
74
+ patient: patientUuid,
75
+ careSetting: order.careSetting,
76
+ orderer: order.orderer,
77
+ encounter: encounterUuid,
78
+ concept: order.testType.conceptUuid,
79
+ instructions: order.instructions,
80
+ orderReason: order.orderReason,
81
+ commentToFulfiller: order.commentsToFulfiller,
82
+ laterality: order.laterality,
83
+ bodySite: order.bodySite,
84
+ };
85
+ if (order.urgency === 'ON_SCHEDULED_DATE') {
86
+ payload['scheduledDate'] = order.scheduleDate;
87
+ }
88
+ return payload;
89
+ } else if (order.action === 'DISCONTINUE') {
90
+ payload = {
91
+ action: 'DISCONTINUE',
92
+ type: 'procedureorder',
93
+ patient: patientUuid,
94
+ careSetting: order.careSetting,
95
+ orderer: order.orderer,
96
+ encounter: encounterUuid,
97
+ concept: order.testType.conceptUuid,
98
+ orderReason: order.orderReason,
99
+ commentToFulfiller: order.commentsToFulfiller,
100
+ laterality: order.laterality,
101
+ bodySite: order.bodySite,
102
+ };
103
+ if (order.urgency === 'ON_SCHEDULED_DATE') {
104
+ payload['scheduledDate'] = order.scheduleDate;
105
+ }
106
+ return payload;
107
+ } else {
108
+ throw new Error(`Unknown order action: ${order.action}.`);
109
+ }
110
+ }
111
+ const chunkSize = 10;
112
+ export function getConceptReferenceUrls(conceptUuids: Array<string>) {
113
+ const accumulator = [];
114
+ for (let i = 0; i < conceptUuids.length; i += chunkSize) {
115
+ accumulator.push(conceptUuids.slice(i, i + chunkSize));
116
+ }
117
+
118
+ return accumulator.map((partition) => `conceptreferences?references=${partition.join(',')}&v=custom:(uuid,display)`);
119
+ }
120
+
121
+ export type PostDataPrepLabOrderFunction = (
122
+ order: ImagingOrderBasketItem,
123
+ patientUuid: string,
124
+ encounterUuid: string,
125
+ ) => OrderPost;
126
+
127
+ export interface ConceptAnswers {
128
+ display: string;
129
+ uuid: string;
130
+ }
131
+ export interface ConceptResponse {
132
+ uuid: string;
133
+ display: string;
134
+ datatype: {
135
+ uuid: string;
136
+ display: string;
137
+ };
138
+ answers: Array<ConceptAnswers>;
139
+ setMembers: Array<ConceptAnswers>;
140
+ }
141
+
142
+ export interface OpenmrsObject {
143
+ uuid: string;
144
+ }
145
+
146
+ export type BaseOpenmrsObject = OpenmrsObject;
147
+
148
+ export interface SessionPriviledge {
149
+ uuid: string;
150
+ name: string;
151
+ }
152
+
153
+ export interface Person {
154
+ uuid: string;
155
+ display: string;
156
+ }
157
+
158
+ export interface Role {
159
+ role: string;
160
+ display: string;
161
+ }
162
+ export interface User {
163
+ uuid: string;
164
+ display: string;
165
+ givenName: string;
166
+ familyName: string;
167
+ firstName: string;
168
+ lastName: string;
169
+ person?: Person;
170
+ roles?: Role[];
171
+ privileges: SessionPriviledge[];
172
+ }
173
+ export interface Auditable extends OpenmrsObject {
174
+ creator: User;
175
+ dateCreated: Date;
176
+ changedBy: User;
177
+ dateChanged: Date;
178
+ }
179
+ export interface Retireable extends OpenmrsObject {
180
+ retired: boolean;
181
+ dateRetired: Date;
182
+ retiredBy: User;
183
+ retireReason: string;
184
+ }
185
+
186
+ export interface ConceptName extends BaseOpenmrsObject {
187
+ conceptNameId: number;
188
+ concept: Concept;
189
+ name: string;
190
+ localePreferred: boolean;
191
+ short: boolean;
192
+ preferred: boolean;
193
+ indexTerm: boolean;
194
+ synonym: boolean;
195
+ fullySpecifiedName: boolean;
196
+ }
197
+ export interface Concept extends BaseOpenmrsObject, Auditable, Retireable {
198
+ conceptId: number;
199
+ display: string;
200
+ set: boolean;
201
+ version: string;
202
+ names: ConceptName[];
203
+ name: ConceptName;
204
+ numeric: boolean;
205
+ complex: boolean;
206
+ shortNames: ConceptName[];
207
+ indexTerms: ConceptName[];
208
+ synonyms: ConceptName[];
209
+ setMembers: Concept[];
210
+ possibleValues: Concept[];
211
+ preferredName: ConceptName;
212
+ shortName: ConceptName;
213
+ fullySpecifiedName: ConceptName;
214
+ answers: Concept[];
215
+ }
216
+
217
+ export function useConceptById(id: string) {
218
+ const apiUrl = `ws/rest/v1/concept/${id}`;
219
+ const { data, error, isLoading } = useSWR<
220
+ {
221
+ data: Concept;
222
+ },
223
+ Error
224
+ >(apiUrl, openmrsFetch);
225
+ return {
226
+ items: data?.data || <Concept>{},
227
+ isLoading,
228
+ isError: error,
229
+ };
230
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+
3
+ interface ImagingProps {
4
+ isTablet: boolean;
5
+ }
6
+
7
+ export default function ImagingIcon({ isTablet }: ImagingProps) {
8
+ const size = isTablet ? 40 : 24;
9
+ return (
10
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
11
+ <g clip-path="url(#clip0_319_622)">
12
+ <mask id="mask0_319_622" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
13
+ <path d="M24 0H0V24H24V0Z" fill="white" />
14
+ </mask>
15
+ <g mask="url(#mask0_319_622)">
16
+ <path d="M24 0H0V24H24V0Z" fill="#B2FFC7" />
17
+ <g clip-path="url(#clip1_319_622)">
18
+ <path
19
+ d="M18.5625 6.3125H12.4375C12.3215 6.3125 12.2102 6.35859 12.1281 6.44064C12.0461 6.52269 12 6.63397 12 6.75V12.4375C12 12.5535 12.0461 12.6648 12.1281 12.7469C12.2102 12.8289 12.3215 12.875 12.4375 12.875H18.5625C18.6785 12.875 18.7898 12.8289 18.8718 12.7469C18.9539 12.6648 19 12.5535 19 12.4375V6.75C19 6.63397 18.9539 6.52269 18.8718 6.44064C18.7898 6.35859 18.6785 6.3125 18.5625 6.3125ZM17.25 8.9375H15.9375V10.25H17.25V11.125H15.9375V12H15.0625V11.125H13.75V10.25H15.0625V8.9375H13.75V8.0625H15.0625V7.1875H15.9375V8.0625H17.25V8.9375Z"
20
+ fill="#028E28"
21
+ />
22
+ <path
23
+ d="M11.5625 18.125H10.6875V16.375C10.6907 16.2018 10.6589 16.0297 10.594 15.869C10.5292 15.7083 10.4326 15.5624 10.3101 15.4399C10.1876 15.3173 10.0417 15.2208 9.88099 15.1559C9.72031 15.0911 9.54822 15.0593 9.37498 15.0625H7.62498C7.45175 15.0593 7.27965 15.0911 7.11898 15.1559C6.9583 15.2208 6.81235 15.3173 6.68983 15.4399C6.56732 15.5624 6.47076 15.7083 6.40592 15.869C6.34109 16.0297 6.30931 16.2018 6.31248 16.375V18.125H5.43748V16.375C5.43464 16.0869 5.48928 15.8012 5.59821 15.5345C5.70713 15.2678 5.86816 15.0256 6.07185 14.8219C6.27555 14.6182 6.51783 14.4572 6.78451 14.3482C7.0512 14.2393 7.33693 14.1847 7.62498 14.1875H9.37498C9.66304 14.1847 9.94877 14.2393 10.2155 14.3482C10.4821 14.4572 10.7244 14.6182 10.9281 14.8219C11.1318 15.0256 11.2928 15.2678 11.4018 15.5345C11.5107 15.8012 11.5653 16.0869 11.5625 16.375V18.125Z"
24
+ fill="#028E28"
25
+ />
26
+ <path
27
+ d="M8.49998 9.8125C8.84808 9.8125 9.18192 9.95078 9.42806 10.1969C9.6742 10.4431 9.81248 10.7769 9.81248 11.125C9.81248 11.4731 9.6742 11.8069 9.42806 12.0531C9.18192 12.2992 8.84808 12.4375 8.49998 12.4375C8.15189 12.4375 7.81805 12.2992 7.57191 12.0531C7.32576 11.8069 7.18748 11.4731 7.18748 11.125C7.18748 10.7769 7.32576 10.4431 7.57191 10.1969C7.81805 9.95078 8.15189 9.8125 8.49998 9.8125ZM8.49998 8.9375C7.91982 8.9375 7.36342 9.16797 6.95319 9.5782C6.54295 9.98844 6.31248 10.5448 6.31248 11.125C6.31248 11.7052 6.54295 12.2616 6.95319 12.6718C7.36342 13.082 7.91982 13.3125 8.49998 13.3125C9.08014 13.3125 9.63654 13.082 10.0468 12.6718C10.457 12.2616 10.6875 11.7052 10.6875 11.125C10.6875 10.5448 10.457 9.98844 10.0468 9.5782C9.63654 9.16797 9.08014 8.9375 8.49998 8.9375Z"
28
+ fill="#028E28"
29
+ />
30
+ </g>
31
+ </g>
32
+ </g>
33
+ <defs>
34
+ <clipPath id="clip0_319_622">
35
+ <rect width={size} height={size} fill="white" />
36
+ </clipPath>
37
+ </defs>
38
+ </svg>
39
+ );
40
+ }
@@ -0,0 +1,96 @@
1
+ import React, { useRef } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button, ClickableTile, Tile } from '@carbon/react';
5
+ import { TrashCan, Warning } from '@carbon/react/icons';
6
+ import { useLayoutType } from '@openmrs/esm-framework';
7
+ import styles from './imaging-order-basket-item-tile.scss';
8
+ import { type ImagingOrderBasketItem } from '../../../types';
9
+
10
+ export interface OrderBasketItemTileProps {
11
+ orderBasketItem: ImagingOrderBasketItem;
12
+ onItemClick: () => void;
13
+ onRemoveClick: () => void;
14
+ }
15
+
16
+ export function ImagingOrderBasketItemTile({ orderBasketItem, onItemClick, onRemoveClick }: OrderBasketItemTileProps) {
17
+ const { t } = useTranslation();
18
+ const isTablet = useLayoutType() === 'tablet';
19
+
20
+ // This here is really dirty, but required.
21
+ // If the ref's value is false, we won't react to the ClickableTile's handleClick function.
22
+ // Why is this necessary?
23
+ // The "Remove" button is nested inside the ClickableTile. If the button's clicked, the tile also raises the
24
+ // handleClick event later. Not sure if this is a bug, but this shouldn't be possible in our flows.
25
+ // Hence, we manually prevent the handleClick callback from being invoked as soon as the button is pressed once.
26
+ const shouldOnClickBeCalled = useRef(true);
27
+
28
+ const labTile = (
29
+ <div className={styles.orderBasketItemTile}>
30
+ <div className={styles.clipTextWithEllipsis}>
31
+ <OrderActionLabel orderBasketItem={orderBasketItem} />
32
+ <br />
33
+ <span className={styles.name}>{orderBasketItem.testType?.label}</span>
34
+ <span className={styles.label01}>
35
+ {!!orderBasketItem.orderError && (
36
+ <>
37
+ <br />
38
+ <span className={styles.orderErrorText}>
39
+ <Warning size={16} /> &nbsp; <span className={styles.label01}>{t('error', 'Error').toUpperCase()}</span>{' '}
40
+ &nbsp;
41
+ {orderBasketItem.orderError.responseBody?.error?.message ?? orderBasketItem.orderError.message}
42
+ </span>
43
+ </>
44
+ )}
45
+ </span>
46
+ </div>
47
+ <Button
48
+ className={styles.removeButton}
49
+ kind="ghost"
50
+ hasIconOnly={true}
51
+ renderIcon={(props) => <TrashCan size={16} {...props} />}
52
+ iconDescription={t('removeFromBasket', 'Remove from basket')}
53
+ onClick={() => {
54
+ shouldOnClickBeCalled.current = false;
55
+ onRemoveClick();
56
+ }}
57
+ tooltipPosition="left"
58
+ />
59
+ </div>
60
+ );
61
+
62
+ return orderBasketItem.action === 'DISCONTINUE' ? (
63
+ <Tile>{labTile}</Tile>
64
+ ) : (
65
+ <ClickableTile
66
+ role="listitem"
67
+ className={classNames({
68
+ [styles.clickableTileTablet]: isTablet,
69
+ [styles.clickableTileDesktop]: !isTablet,
70
+ })}
71
+ onClick={() => shouldOnClickBeCalled.current && onItemClick()}>
72
+ {labTile}
73
+ </ClickableTile>
74
+ );
75
+ }
76
+
77
+ function OrderActionLabel({ orderBasketItem }: { orderBasketItem: ImagingOrderBasketItem }) {
78
+ const { t } = useTranslation();
79
+
80
+ if (orderBasketItem.isOrderIncomplete) {
81
+ return <span className={styles.orderActionIncompleteLabel}>{t('orderActionIncomplete', 'Incomplete')}</span>;
82
+ }
83
+
84
+ switch (orderBasketItem.action) {
85
+ case 'NEW':
86
+ return <span className={styles.orderActionNewLabel}>{t('orderActionNew', 'New')}</span>;
87
+ case 'RENEW':
88
+ return <span className={styles.orderActionRenewLabel}>{t('orderActionRenew', 'Renew')}</span>;
89
+ case 'REVISE':
90
+ return <span className={styles.orderActionRevisedLabel}>{t('orderActionRevise', 'Modify')}</span>;
91
+ case 'DISCONTINUE':
92
+ return <span className={styles.orderActionDiscontinueLabel}>{t('orderActionDiscontinue', 'Discontinue')}</span>;
93
+ default:
94
+ return <></>;
95
+ }
96
+ }
@@ -0,0 +1,72 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @import '@openmrs/esm-styleguide/src/vars';
4
+
5
+ .clickableTileDesktop {
6
+ padding: 0.5rem 0.75rem;
7
+ border-bottom: 1px solid $ui-03;
8
+ }
9
+
10
+ .clickableTileTablet {
11
+ padding: 0.75rem 1rem;
12
+ background-color: $ui-02;
13
+ border-bottom: 1px solid $ui-03;
14
+ }
15
+
16
+ .orderBasketItemTile {
17
+ display: flex;
18
+ justify-content: space-between;
19
+ align-items: baseline;
20
+ }
21
+
22
+ .label {
23
+ display: inline-block;
24
+ @include type.type-style('label-01');
25
+ margin-bottom: 0.5rem;
26
+ }
27
+
28
+ .orderActionNewLabel {
29
+ @extend .label;
30
+ color: black;
31
+ background-color: #c4f4cc;
32
+ padding: 0 0.25rem;
33
+ }
34
+
35
+ .orderActionIncompleteLabel {
36
+ @extend .label;
37
+ color: white;
38
+ background-color: $danger;
39
+ padding: 0 0.25rem;
40
+ }
41
+
42
+ .orderActionRenewLabel {
43
+ @extend .label;
44
+ color: $support-02;
45
+ }
46
+
47
+ .orderActionRevisedLabel {
48
+ @extend .label;
49
+ color: #943d00;
50
+ }
51
+
52
+ .orderActionDiscontinueLabel {
53
+ @extend .label;
54
+ color: $danger;
55
+ }
56
+
57
+ .orderErrorText {
58
+ color: $danger;
59
+ display: flex;
60
+ align-items: center;
61
+ }
62
+
63
+ .name {
64
+ @include type.type-style('heading-compact-01');
65
+ color: black;
66
+ }
67
+
68
+ .removeButton {
69
+ svg {
70
+ fill: $danger !important;
71
+ }
72
+ }
@@ -0,0 +1,191 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button, Tile } from '@carbon/react';
5
+ import { Add, ChevronDown, ChevronUp } from '@carbon/react/icons';
6
+ import { useLayoutType, closeWorkspace } from '@openmrs/esm-framework';
7
+ import { launchPatientWorkspace, type OrderBasketItem, useOrderBasket } from '@openmrs/esm-patient-common-lib';
8
+ import { ImagingOrderBasketItemTile } from './imaging-order-basket-item-tile.component';
9
+ import { prepImagingOrderPostData } from '../api';
10
+ import ImagingIcon from './imaging-icon.component';
11
+ import styles from './imaging-order-basket-panel.scss';
12
+ import { type ImagingOrderBasketItem } from '../../../types';
13
+
14
+ /**
15
+ * Designs: https://app.zeplin.io/project/60d59321e8100b0324762e05/screen/648c44d9d4052c613e7f23da
16
+ */
17
+ export default function ImagingOrderBasketPanelExtension() {
18
+ const { t } = useTranslation();
19
+ const isTablet = useLayoutType() === 'tablet';
20
+ const { orders, setOrders } = useOrderBasket<ImagingOrderBasketItem>('imaging', prepImagingOrderPostData);
21
+ const [isExpanded, setIsExpanded] = useState(orders.length > 0);
22
+ const {
23
+ incompleteOrderBasketItems,
24
+ newOrderBasketItems,
25
+ renewedOrderBasketItems,
26
+ revisedOrderBasketItems,
27
+ discontinuedOrderBasketItems,
28
+ } = useMemo(() => {
29
+ const incompleteOrderBasketItems: Array<ImagingOrderBasketItem> = [];
30
+ const newOrderBasketItems: Array<ImagingOrderBasketItem> = [];
31
+ const renewedOrderBasketItems: Array<ImagingOrderBasketItem> = [];
32
+ const revisedOrderBasketItems: Array<ImagingOrderBasketItem> = [];
33
+ const discontinuedOrderBasketItems: Array<ImagingOrderBasketItem> = [];
34
+
35
+ orders.forEach((order) => {
36
+ if (order?.isOrderIncomplete) {
37
+ incompleteOrderBasketItems.push(order);
38
+ } else if (order.action === 'NEW') {
39
+ newOrderBasketItems.push(order);
40
+ } else if (order.action === 'RENEW') {
41
+ renewedOrderBasketItems.push(order);
42
+ } else if (order.action === 'REVISE') {
43
+ revisedOrderBasketItems.push(order);
44
+ } else if (order.action === 'DISCONTINUE') {
45
+ discontinuedOrderBasketItems.push(order);
46
+ }
47
+ });
48
+
49
+ return {
50
+ incompleteOrderBasketItems,
51
+ newOrderBasketItems,
52
+ renewedOrderBasketItems,
53
+ revisedOrderBasketItems,
54
+ discontinuedOrderBasketItems,
55
+ };
56
+ }, [orders]);
57
+
58
+ const launchImagingOrderForm = useCallback(() => {
59
+ closeWorkspace('order-basket', {
60
+ ignoreChanges: true,
61
+ onWorkspaceClose: () => launchPatientWorkspace('add-imaging-order'),
62
+ });
63
+ }, []);
64
+
65
+ const openImagingOrderFormForEditing = useCallback((order: OrderBasketItem) => {
66
+ closeWorkspace('order-basket', {
67
+ ignoreChanges: true,
68
+ onWorkspaceClose: () => launchPatientWorkspace('add-imaging-order', { order }),
69
+ });
70
+ }, []);
71
+
72
+ const removeLabOrder = useCallback(
73
+ (order: ImagingOrderBasketItem) => {
74
+ const newOrders = [...orders];
75
+ newOrders.splice(orders.indexOf(order), 1);
76
+ setOrders(newOrders);
77
+ },
78
+ [orders, setOrders],
79
+ );
80
+
81
+ useEffect(() => {
82
+ setIsExpanded(orders.length > 0);
83
+ }, [orders]);
84
+
85
+ return (
86
+ <Tile
87
+ className={classNames(isTablet ? styles.tabletTile : styles.desktopTile, {
88
+ [styles.collapsedTile]: !isExpanded,
89
+ })}>
90
+ <div className={styles.container}>
91
+ <div className={styles.iconAndLabel}>
92
+ <ImagingIcon isTablet={isTablet} />
93
+ <h4 className={styles.heading}>{`${t('imagingOrders', 'Imaging orders')} (${orders.length})`}</h4>
94
+ </div>
95
+ <div className={styles.buttonContainer}>
96
+ <Button
97
+ kind="ghost"
98
+ renderIcon={(props) => <Add size={16} {...props} />}
99
+ iconDescription="Add imaging order"
100
+ onClick={launchImagingOrderForm}
101
+ size={isTablet ? 'md' : 'sm'}>
102
+ {t('add', 'Add')}
103
+ </Button>
104
+ <Button
105
+ className={styles.chevron}
106
+ hasIconOnly
107
+ kind="ghost"
108
+ renderIcon={(props) =>
109
+ isExpanded ? <ChevronUp size={16} {...props} /> : <ChevronDown size={16} {...props} />
110
+ }
111
+ iconDescription="View"
112
+ disabled={orders.length === 0}
113
+ onClick={() => setIsExpanded(!isExpanded)}>
114
+ {t('add', 'Add')}
115
+ </Button>
116
+ </div>
117
+ </div>
118
+ {isExpanded && (
119
+ <>
120
+ {orders.length > 0 && (
121
+ <>
122
+ {incompleteOrderBasketItems.length > 0 && (
123
+ <>
124
+ {incompleteOrderBasketItems.map((order) => (
125
+ <ImagingOrderBasketItemTile
126
+ key={order.uuid}
127
+ orderBasketItem={order}
128
+ onItemClick={() => openImagingOrderFormForEditing(order)}
129
+ onRemoveClick={() => removeLabOrder(order)}
130
+ />
131
+ ))}
132
+ </>
133
+ )}
134
+ {newOrderBasketItems.length > 0 && (
135
+ <>
136
+ {newOrderBasketItems.map((order) => (
137
+ <ImagingOrderBasketItemTile
138
+ key={order.uuid}
139
+ orderBasketItem={order}
140
+ onItemClick={() => openImagingOrderFormForEditing(order)}
141
+ onRemoveClick={() => removeLabOrder(order)}
142
+ />
143
+ ))}
144
+ </>
145
+ )}
146
+
147
+ {renewedOrderBasketItems.length > 0 && (
148
+ <>
149
+ {renewedOrderBasketItems.map((order) => (
150
+ <ImagingOrderBasketItemTile
151
+ key={order.uuid}
152
+ orderBasketItem={order}
153
+ onItemClick={() => openImagingOrderFormForEditing(order)}
154
+ onRemoveClick={() => removeLabOrder(order)}
155
+ />
156
+ ))}
157
+ </>
158
+ )}
159
+
160
+ {revisedOrderBasketItems.length > 0 && (
161
+ <>
162
+ {revisedOrderBasketItems.map((order) => (
163
+ <ImagingOrderBasketItemTile
164
+ key={order.uuid}
165
+ orderBasketItem={order}
166
+ onItemClick={() => openImagingOrderFormForEditing(order)}
167
+ onRemoveClick={() => removeLabOrder(order)}
168
+ />
169
+ ))}
170
+ </>
171
+ )}
172
+
173
+ {discontinuedOrderBasketItems.length > 0 && (
174
+ <>
175
+ {discontinuedOrderBasketItems.map((order) => (
176
+ <ImagingOrderBasketItemTile
177
+ key={order.uuid}
178
+ orderBasketItem={order}
179
+ onItemClick={() => openImagingOrderFormForEditing(order)}
180
+ onRemoveClick={() => removeLabOrder(order)}
181
+ />
182
+ ))}
183
+ </>
184
+ )}
185
+ </>
186
+ )}
187
+ </>
188
+ )}
189
+ </Tile>
190
+ );
191
+ }
@@ -0,0 +1,74 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .desktopTile {
6
+ border-left: 4px solid #18cd69;
7
+ background-color: $ui-02;
8
+ border-top: 1px solid #8ecdaa;
9
+ border-right: none;
10
+ padding: 0;
11
+ }
12
+
13
+ .tabletTile {
14
+ @extend .desktopTile;
15
+ border-top: 1px solid #0ec862;
16
+ }
17
+
18
+ .collapsedTile {
19
+ border-bottom: none;
20
+ min-height: 0;
21
+
22
+ .orderBasketHeader {
23
+ margin-bottom: 0;
24
+ }
25
+ }
26
+
27
+ .tabletTile.collapsedTile {
28
+ border-bottom: 1px solid $grey-2;
29
+ }
30
+
31
+ .container {
32
+ background-color: $ui-02;
33
+ display: flex;
34
+ justify-content: space-between;
35
+ align-items: center;
36
+ text-align: left;
37
+ }
38
+
39
+ .desktopTile .container {
40
+ h4 {
41
+ @include type.type-style('heading-compact-02');
42
+ color: $text-02;
43
+ }
44
+ }
45
+
46
+ .tabletTile .container {
47
+ padding: spacing.$spacing-03;
48
+
49
+ h4 {
50
+ @include type.type-style('heading-03');
51
+ color: $ui-05;
52
+ }
53
+ }
54
+
55
+ .heading {
56
+ margin-left: spacing.$spacing-03;
57
+ }
58
+
59
+ .iconAndLabel {
60
+ display: flex;
61
+ align-items: center;
62
+ margin: spacing.$spacing-03;
63
+ }
64
+
65
+ .buttonContainer {
66
+ display: flex;
67
+ align-items: center;
68
+ }
69
+
70
+ .chevron {
71
+ svg {
72
+ fill: black;
73
+ }
74
+ }