@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,79 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+
5
+ .orderForm {
6
+ flex-grow: 1;
7
+ display: flex;
8
+ flex-direction: column;
9
+ justify-content: space-between;
10
+
11
+ :global(.cds--css-grid) {
12
+ max-width: unset;
13
+ }
14
+
15
+ :global(.cds--css-grid-column) {
16
+ margin: 0 layout.$spacing-03 !important;
17
+ }
18
+
19
+ :global(.cds--subgrid) {
20
+ margin: 0 calc(layout.$spacing-03 * -1) !important;
21
+ }
22
+
23
+ :global(.cds--number--nosteppers.cds--number input[type='number']) {
24
+ padding-right: layout.$spacing-05;
25
+ }
26
+
27
+ :global(.cds--number__control-btn) {
28
+ border: 0;
29
+ }
30
+ }
31
+
32
+ .form {
33
+ margin: layout.$spacing-05;
34
+ }
35
+
36
+ .gridRow {
37
+ padding: 0px;
38
+
39
+ :global(.cds--number) {
40
+ display: inline-block;
41
+ }
42
+ }
43
+
44
+ .field {
45
+ margin-bottom: layout.$spacing-05;
46
+ }
47
+
48
+ .buttonSet {
49
+ :global(.cds--btn) {
50
+ width: 50%;
51
+ max-width: unset;
52
+ }
53
+ }
54
+
55
+ .errorContainer {
56
+ margin: 1rem;
57
+ }
58
+
59
+ .errorNotification {
60
+ margin: 0.5rem 0;
61
+ }
62
+
63
+ /* Tablet */
64
+ :global(.omrs-breakpoint-lt-desktop) {
65
+ .orderForm {
66
+ height: calc(100vh - 6rem - var(--workspace-header-height));
67
+ background-color: #ededed;
68
+ }
69
+
70
+ .buttonSet {
71
+ padding: layout.$spacing-06 layout.$spacing-05;
72
+ background-color: colors.$gray-10;
73
+ }
74
+
75
+ .errorContainer {
76
+ margin: 1rem;
77
+ margin-bottom: calc(var(--bottom-nav-height) + 1rem);
78
+ }
79
+ }
@@ -0,0 +1,19 @@
1
+ import { type ImagingOrderBasketItem } from '../../../types';
2
+ import { type ImagingType } from './useImagingTypes';
3
+
4
+ // See the Urgency enum in https://github.com/openmrs/openmrs-core/blob/492dcd35b85d48730bd19da48f6db146cc882c22/api/src/main/java/org/openmrs/Order.java
5
+ export const priorityOptions = [
6
+ { value: 'ROUTINE', label: 'Routine' },
7
+ { value: 'STAT', label: 'Stat' },
8
+ { value: 'ON_SCHEDULED_DATE', label: 'Scheduled' },
9
+ ];
10
+
11
+ // TODO add priority option `{ value: "ON_SCHEDULED_DATE", label: "On scheduled date" }` once the form supports a date.
12
+ export function createEmptyLabOrder(testType: ImagingType, orderer: string): ImagingOrderBasketItem {
13
+ return {
14
+ action: 'NEW',
15
+ display: testType.label,
16
+ testType,
17
+ orderer,
18
+ };
19
+ }
@@ -0,0 +1,115 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ /** For TestTypeSearchResults */
6
+
7
+ .container {
8
+ margin: layout.$spacing-03 layout.$spacing-05;
9
+ }
10
+
11
+ .divider {
12
+ border: 0;
13
+ border-top: 1px solid $color-gray-30;
14
+ width: 66%;
15
+ }
16
+
17
+ .desktopDivider {
18
+ margin: layout.$spacing-07 auto layout.$spacing-05;
19
+ }
20
+
21
+ .tabletDivider {
22
+ margin: layout.$spacing-08 auto layout.$spacing-06;
23
+ }
24
+
25
+ .searchResultsCount {
26
+ @include type.type-style('body-compact-01');
27
+ color: $text-02;
28
+ }
29
+
30
+ .orderBasketSearchResultsHeader {
31
+ display: flex;
32
+ justify-content: space-between;
33
+ margin-bottom: layout.$spacing-03;
34
+ align-items: center;
35
+ }
36
+
37
+ /** For TestTypeSearchResultItem */
38
+
39
+ .searchResultTile {
40
+ padding: 0;
41
+ display: flex;
42
+ flex-direction: column;
43
+ justify-content: space-between;
44
+ border: 1px solid $grey-2;
45
+ &:not(:last-of-type) {
46
+ margin-bottom: layout.$spacing-03;
47
+ }
48
+ }
49
+
50
+ .tabletSearchResultTile {
51
+ margin-top: 5px;
52
+ background-color: $ui-02;
53
+ }
54
+
55
+ .searchResultTileContent {
56
+ padding: layout.$spacing-03 layout.$spacing-05;
57
+ }
58
+
59
+ .searchResultActions {
60
+ border-top: 1px solid $grey-2;
61
+ padding: 0 layout.$spacing-05;
62
+ display: flex;
63
+ justify-content: flex-end;
64
+ }
65
+
66
+ .searchResultSkeletonWrapper {
67
+ margin: layout.$spacing-03 1rem layout.$spacing-03;
68
+
69
+ :global(.cds--skeleton__text) {
70
+ margin: 0;
71
+ }
72
+
73
+ .searchResultCntSkeleton {
74
+ margin-right: layout.$spacing-07;
75
+ }
76
+
77
+ .skeletonTile {
78
+ display: flex;
79
+ align-items: center;
80
+ }
81
+ }
82
+
83
+ .emptyState {
84
+ margin: layout.$spacing-05;
85
+ display: flex;
86
+ justify-content: center;
87
+ align-items: center;
88
+ padding: layout.$spacing-07;
89
+ border: 1px solid $ui-03;
90
+ text-align: center;
91
+ background-color: $ui-01;
92
+ }
93
+
94
+ :global(.omrs-breakpoint-lt-small-desktop) .emptyState {
95
+ background-color: $ui-02;
96
+ }
97
+
98
+ .link {
99
+ color: $interactive-01;
100
+ text-decoration: underline;
101
+ cursor: pointer;
102
+ }
103
+
104
+ .resultsContainer {
105
+ overflow-y: scroll;
106
+ }
107
+
108
+ .resultsContainer::-webkit-scrollbar {
109
+ width: layout.$spacing-03;
110
+ }
111
+
112
+ .resultsContainer::-webkit-scrollbar-thumb {
113
+ background: $ui-04;
114
+ border-radius: layout.$spacing-02;
115
+ }
@@ -0,0 +1,235 @@
1
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button, ButtonSkeleton, Search, SkeletonText, Tile } from '@carbon/react';
5
+ import { ArrowRight, ShoppingCartArrowDown, ShoppingCartArrowUp } from '@carbon/react/icons';
6
+ import { useDebounce, useLayoutType, useSession, ResponsiveWrapper, closeWorkspace } from '@openmrs/esm-framework';
7
+ import { launchPatientWorkspace, useOrderBasket } from '@openmrs/esm-patient-common-lib';
8
+ import { prepImagingOrderPostData } from '../api';
9
+ import { type ImagingType, useImagingTypes } from './useImagingTypes';
10
+ import { createEmptyLabOrder } from './imaging-order';
11
+ import styles from './imaging-type-search.scss';
12
+ import { type ImagingOrderBasketItem } from '../../../types';
13
+
14
+ export interface TestTypeSearchProps {
15
+ openLabForm: (searchResult: ImagingOrderBasketItem) => void;
16
+ }
17
+
18
+ export function TestTypeSearch({ openLabForm }: TestTypeSearchProps) {
19
+ const { t } = useTranslation();
20
+ const [searchTerm, setSearchTerm] = useState('');
21
+ const debouncedSearchTerm = useDebounce(searchTerm);
22
+ const searchInputRef = useRef(null);
23
+
24
+ const focusAndClearSearchInput = () => {
25
+ setSearchTerm('');
26
+ searchInputRef.current?.focus();
27
+ };
28
+
29
+ const handleSearchTermChange = (event: React.ChangeEvent<HTMLInputElement>) => {
30
+ setSearchTerm(event.target.value ?? '');
31
+ };
32
+
33
+ return (
34
+ <>
35
+ <ResponsiveWrapper>
36
+ <Search
37
+ autoFocus
38
+ size="lg"
39
+ placeholder={t('searchFieldPlaceholder', 'Search for an imaging test')}
40
+ labelText={t('searchFieldPlaceholder', 'Search for an imaging test')}
41
+ onChange={handleSearchTermChange}
42
+ ref={searchInputRef}
43
+ value={searchTerm}
44
+ />
45
+ </ResponsiveWrapper>
46
+ <TestTypeSearchResults
47
+ searchTerm={debouncedSearchTerm}
48
+ openOrderForm={openLabForm}
49
+ focusAndClearSearchInput={focusAndClearSearchInput}
50
+ />
51
+ </>
52
+ );
53
+ }
54
+
55
+ interface TestTypeSearchResultsProps {
56
+ searchTerm: string;
57
+ openOrderForm: (searchResult: ImagingOrderBasketItem) => void;
58
+ focusAndClearSearchInput: () => void;
59
+ }
60
+
61
+ function TestTypeSearchResults({ searchTerm, openOrderForm, focusAndClearSearchInput }: TestTypeSearchResultsProps) {
62
+ const { t } = useTranslation();
63
+ const isTablet = useLayoutType() === 'tablet';
64
+ const { testTypes, isLoading, error } = useImagingTypes(searchTerm);
65
+
66
+ if (isLoading) {
67
+ return <TestTypeSearchSkeleton />;
68
+ }
69
+
70
+ if (error) {
71
+ return (
72
+ <Tile className={styles.emptyState}>
73
+ <div>
74
+ <h4 className={styles.productiveHeading01}>
75
+ {t('errorFetchingTestTypes', 'Error fetching results for "{{searchTerm}}"', {
76
+ searchTerm,
77
+ })}
78
+ </h4>
79
+ <p className={styles.bodyShort01}>
80
+ <span>{t('trySearchingAgain', 'Please try searching again')}</span>
81
+ </p>
82
+ </div>
83
+ </Tile>
84
+ );
85
+ }
86
+
87
+ return (
88
+ <>
89
+ {testTypes?.length ? (
90
+ <div className={styles.container}>
91
+ {searchTerm && (
92
+ <div className={styles.orderBasketSearchResultsHeader}>
93
+ <span className={styles.searchResultsCount}>
94
+ {t('searchResultsMatchesForTerm', '{{count}} results for "{{searchTerm}}"', {
95
+ count: testTypes?.length,
96
+ searchTerm,
97
+ })}
98
+ </span>
99
+ <Button kind="ghost" onClick={focusAndClearSearchInput} size={isTablet ? 'md' : 'sm'}>
100
+ {t('clearSearchResults', 'Clear Results')}
101
+ </Button>
102
+ </div>
103
+ )}
104
+ <div className={styles.resultsContainer}>
105
+ {testTypes.map((testType) => (
106
+ <TestTypeSearchResultItem key={testType.conceptUuid} testType={testType} openOrderForm={openOrderForm} />
107
+ ))}
108
+ </div>
109
+ </div>
110
+ ) : (
111
+ <Tile className={styles.emptyState}>
112
+ <div>
113
+ <h4 className={styles.productiveHeading01}>
114
+ {t('noResultsForTestTypeSearch', 'No results to display for "{{searchTerm}}"', {
115
+ searchTerm,
116
+ })}
117
+ </h4>
118
+ <p className={styles.bodyShort01}>
119
+ <span>{t('tryTo', 'Try to')}</span>{' '}
120
+ <span className={styles.link} role="link" tabIndex={0} onClick={focusAndClearSearchInput}>
121
+ {t('searchAgain', 'search again')}
122
+ </span>{' '}
123
+ <span>{t('usingADifferentTerm', 'using a different term')}</span>
124
+ </p>
125
+ </div>
126
+ </Tile>
127
+ )}
128
+ <hr className={classNames(styles.divider, isTablet ? styles.tabletDivider : styles.desktopDivider)} />
129
+ </>
130
+ );
131
+ }
132
+
133
+ interface TestTypeSearchResultItemProps {
134
+ testType: ImagingType;
135
+ openOrderForm: (searchResult: ImagingOrderBasketItem) => void;
136
+ }
137
+
138
+ const TestTypeSearchResultItem: React.FC<TestTypeSearchResultItemProps> = ({ testType, openOrderForm }) => {
139
+ const isTablet = useLayoutType() === 'tablet';
140
+ const session = useSession();
141
+ const { orders, setOrders } = useOrderBasket<ImagingOrderBasketItem>('imaging', prepImagingOrderPostData);
142
+ const testTypeAlreadyInBasket = useMemo(
143
+ () => orders?.some((order) => order.testType.conceptUuid === testType.conceptUuid),
144
+ [orders, testType],
145
+ );
146
+
147
+ const createLabOrder = useCallback(
148
+ (testType: ImagingType) => {
149
+ return createEmptyLabOrder(testType, session.currentProvider.uuid);
150
+ },
151
+ [session.currentProvider?.uuid],
152
+ );
153
+
154
+ const { t } = useTranslation();
155
+
156
+ const addToBasket = useCallback(() => {
157
+ const labOrder = createLabOrder(testType);
158
+ labOrder.isOrderIncomplete = true;
159
+ setOrders([...orders, labOrder]);
160
+ closeWorkspace('add-imaging-order', {
161
+ ignoreChanges: true,
162
+ onWorkspaceClose: () => launchPatientWorkspace('order-basket'),
163
+ });
164
+ }, [orders, setOrders, createLabOrder, testType]);
165
+
166
+ const removeFromBasket = useCallback(() => {
167
+ setOrders(orders.filter((order) => order.testType.conceptUuid !== testType.conceptUuid));
168
+ }, [orders, setOrders, testType.conceptUuid]);
169
+
170
+ return (
171
+ <Tile
172
+ className={classNames(styles.searchResultTile, {
173
+ [styles.tabletSearchResultTile]: isTablet,
174
+ })}
175
+ key={testType.conceptUuid}
176
+ role="listitem">
177
+ <div className={classNames(styles.searchResultTileContent, styles.text02)}>
178
+ <p>
179
+ <span className={styles.productiveHeading01}>{testType.label}</span>{' '}
180
+ </p>
181
+ </div>
182
+ <div className={styles.searchResultActions}>
183
+ {testTypeAlreadyInBasket ? (
184
+ <Button
185
+ kind="danger--ghost"
186
+ renderIcon={(props) => <ShoppingCartArrowUp size={16} {...props} />}
187
+ onClick={() => removeFromBasket()}>
188
+ {t('removeFromBasket', 'Remove from basket')}
189
+ </Button>
190
+ ) : (
191
+ <Button
192
+ kind="ghost"
193
+ renderIcon={(props) => <ShoppingCartArrowDown size={16} {...props} />}
194
+ onClick={() => addToBasket()}>
195
+ {t('directlyAddToBasket', 'Add to basket')}
196
+ </Button>
197
+ )}
198
+ <Button
199
+ kind="ghost"
200
+ renderIcon={(props) => <ArrowRight size={16} {...props} />}
201
+ onClick={() => openOrderForm(createLabOrder(testType))}>
202
+ {t('goToDrugOrderForm', 'Order form')}
203
+ </Button>
204
+ </div>
205
+ </Tile>
206
+ );
207
+ };
208
+
209
+ const TestTypeSearchSkeleton = () => {
210
+ const isTablet = useLayoutType() === 'tablet';
211
+ const tileClassName = `${isTablet ? `${styles.tabletSearchResultTile}` : `${styles.desktopSearchResultTile}`} ${
212
+ styles.skeletonTile
213
+ }`;
214
+ return (
215
+ <div className={styles.searchResultSkeletonWrapper}>
216
+ <div className={styles.orderBasketSearchResultsHeader}>
217
+ <SkeletonText className={styles.searchResultCntSkeleton} />
218
+ <ButtonSkeleton size={isTablet ? 'md' : 'sm'} />
219
+ </div>
220
+ <Tile className={tileClassName}>
221
+ <SkeletonText />
222
+ </Tile>
223
+ <Tile className={tileClassName}>
224
+ <SkeletonText />
225
+ </Tile>
226
+ <Tile className={tileClassName}>
227
+ <SkeletonText />
228
+ </Tile>
229
+ <Tile className={tileClassName}>
230
+ <SkeletonText />
231
+ </Tile>
232
+ <hr className={classNames(styles.divider, isTablet ? styles.tabletDivider : styles.desktopDivider)} />
233
+ </div>
234
+ );
235
+ };
@@ -0,0 +1,89 @@
1
+ import { useEffect, useMemo } from 'react';
2
+ import useSWRImmutable from 'swr/immutable';
3
+ import fuzzy from 'fuzzy';
4
+ import { type FetchResponse, openmrsFetch, useConfig, restBaseUrl, reportError } from '@openmrs/esm-framework';
5
+ import { type Concept } from '../../../types';
6
+ import { type ImagingConfig } from '../../../config-schema';
7
+
8
+ type ConceptResult = FetchResponse<Concept>;
9
+ type ConceptResults = FetchResponse<{ setMembers: Array<Concept> }>;
10
+
11
+ export interface ImagingType {
12
+ label: string;
13
+ conceptUuid: string;
14
+ }
15
+
16
+ export interface UseImagingType {
17
+ testTypes: Array<ImagingType>;
18
+ isLoading: boolean;
19
+ error: Error;
20
+ }
21
+
22
+ function openmrsFetchMultiple(urls: Array<string>) {
23
+ // SWR has an RFC for `useSWRList`:
24
+ // https://github.com/vercel/swr/discussions/1988
25
+ // If that ever is implemented we should switch to using that.
26
+ return Promise.all(urls.map((url) => openmrsFetch<{ results: Array<Concept> }>(url)));
27
+ }
28
+
29
+ function useImagingConceptsSWR(labOrderableConcepts?: Array<string>) {
30
+ const config = useConfig<ImagingConfig>();
31
+ const { data, isLoading, error } = useSWRImmutable(
32
+ () =>
33
+ labOrderableConcepts
34
+ ? labOrderableConcepts.map((c) => `${restBaseUrl}/concept/${c}`)
35
+ : `${restBaseUrl}/concept/${config.radiologyConceptSetUuid}?v=custom:setMembers`,
36
+ (labOrderableConcepts ? openmrsFetchMultiple : openmrsFetch) as any,
37
+ {
38
+ shouldRetryOnError(err) {
39
+ return err instanceof Response;
40
+ },
41
+ },
42
+ );
43
+
44
+ const results = useMemo(() => {
45
+ if (isLoading || error) {
46
+ return null;
47
+ }
48
+ return labOrderableConcepts
49
+ ? (data as Array<ConceptResult>)?.flatMap((d) => d.data.setMembers)
50
+ : (data as ConceptResults)?.data.setMembers ?? ([] as Concept[]);
51
+ }, [data, isLoading, error, labOrderableConcepts]);
52
+
53
+ return {
54
+ data: results,
55
+ isLoading,
56
+ error,
57
+ };
58
+ }
59
+
60
+ export function useImagingTypes(searchTerm = ''): UseImagingType {
61
+ const { labOrderableConcepts } = useConfig<ImagingConfig>().orders;
62
+
63
+ const { data, isLoading, error } = useImagingConceptsSWR(labOrderableConcepts.length ? labOrderableConcepts : null);
64
+
65
+ useEffect(() => {
66
+ if (error) {
67
+ reportError(error);
68
+ }
69
+ }, [error]);
70
+
71
+ const testConcepts = useMemo(() => {
72
+ return data?.map((concept) => ({
73
+ label: concept.display,
74
+ conceptUuid: concept.uuid,
75
+ }));
76
+ }, [data]);
77
+
78
+ const filteredTestTypes = useMemo(() => {
79
+ return searchTerm && !isLoading && !error
80
+ ? fuzzy.filter(searchTerm, testConcepts, { extract: (c) => c.label }).map((result) => result.original)
81
+ : testConcepts;
82
+ }, [testConcepts, searchTerm, isLoading, error]);
83
+
84
+ return {
85
+ testTypes: filteredTestTypes,
86
+ isLoading: isLoading,
87
+ error: error,
88
+ };
89
+ }