@kenyaemr/esm-etims-app 5.4.5-pre.1109
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 +6 -0
- package/README.md +3 -0
- package/dist/1067.js +1 -0
- package/dist/1067.js.map +1 -0
- package/dist/1072.js +1 -0
- package/dist/1072.js.map +1 -0
- package/dist/1148.js +1 -0
- package/dist/1148.js.map +1 -0
- package/dist/1278.js +1 -0
- package/dist/1278.js.map +1 -0
- package/dist/1442.js +1 -0
- package/dist/1442.js.map +1 -0
- package/dist/15.js +1 -0
- package/dist/15.js.map +1 -0
- package/dist/1620.js +1 -0
- package/dist/1620.js.map +1 -0
- package/dist/1638.js +1 -0
- package/dist/1638.js.map +1 -0
- package/dist/1789.js +1 -0
- package/dist/1789.js.map +1 -0
- package/dist/1831.js +1 -0
- package/dist/1831.js.map +1 -0
- package/dist/1844.js +1 -0
- package/dist/1844.js.map +1 -0
- package/dist/1946.js +1 -0
- package/dist/1946.js.map +1 -0
- package/dist/2007.js +1 -0
- package/dist/2007.js.map +1 -0
- package/dist/2115.js +1 -0
- package/dist/2115.js.map +1 -0
- package/dist/2131.js +1 -0
- package/dist/2131.js.map +1 -0
- package/dist/2152.js +1 -0
- package/dist/2152.js.map +1 -0
- package/dist/2202.js +1 -0
- package/dist/2202.js.map +1 -0
- package/dist/2394.js +1 -0
- package/dist/2394.js.map +1 -0
- package/dist/2534.js +1 -0
- package/dist/2534.js.map +1 -0
- package/dist/2537.js +1 -0
- package/dist/2537.js.map +1 -0
- package/dist/2556.js +1 -0
- package/dist/2556.js.map +1 -0
- package/dist/2621.js +1 -0
- package/dist/2621.js.map +1 -0
- package/dist/2648.js +1 -0
- package/dist/2648.js.map +1 -0
- package/dist/2795.js +1 -0
- package/dist/2795.js.map +1 -0
- package/dist/2842.js +1 -0
- package/dist/2842.js.map +1 -0
- package/dist/2916.js +1 -0
- package/dist/2916.js.map +1 -0
- package/dist/2924.js +1 -0
- package/dist/2924.js.map +1 -0
- package/dist/2929.js +1 -0
- package/dist/2929.js.map +1 -0
- package/dist/2947.js +1 -0
- package/dist/2947.js.map +1 -0
- package/dist/2971.js +1 -0
- package/dist/2971.js.map +1 -0
- package/dist/3284.js +1 -0
- package/dist/3284.js.map +1 -0
- package/dist/3285.js +1 -0
- package/dist/3285.js.map +1 -0
- package/dist/3335.js +1 -0
- package/dist/3335.js.map +1 -0
- package/dist/335.js +1 -0
- package/dist/335.js.map +1 -0
- package/dist/3378.js +1 -0
- package/dist/3378.js.map +1 -0
- package/dist/338.js +1 -0
- package/dist/338.js.map +1 -0
- package/dist/3504.js +1 -0
- package/dist/3504.js.map +1 -0
- package/dist/3544.js +1 -0
- package/dist/3544.js.map +1 -0
- package/dist/356.js +1 -0
- package/dist/356.js.map +1 -0
- package/dist/3569.js +1 -0
- package/dist/3569.js.map +1 -0
- package/dist/3723.js +1 -0
- package/dist/3723.js.map +1 -0
- package/dist/3745.js +1 -0
- package/dist/3745.js.map +1 -0
- package/dist/3813.js +1 -0
- package/dist/3813.js.map +1 -0
- package/dist/393.js +6 -0
- package/dist/393.js.map +1 -0
- package/dist/3949.js +1 -0
- package/dist/3949.js.map +1 -0
- package/dist/3953.js +1 -0
- package/dist/3953.js.map +1 -0
- package/dist/3989.js +1 -0
- package/dist/3989.js.map +1 -0
- package/dist/4178.js +1 -0
- package/dist/4178.js.map +1 -0
- package/dist/4303.js +1 -0
- package/dist/4303.js.map +1 -0
- package/dist/4308.js +1 -0
- package/dist/4308.js.map +1 -0
- package/dist/4487.js +11 -0
- package/dist/4487.js.map +1 -0
- package/dist/4588.js +1 -0
- package/dist/4588.js.map +1 -0
- package/dist/4637.js +1 -0
- package/dist/4637.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/4817.js +1 -0
- package/dist/4817.js.map +1 -0
- package/dist/4818.js +1 -0
- package/dist/4818.js.map +1 -0
- package/dist/500.js +1 -0
- package/dist/500.js.map +1 -0
- package/dist/5086.js +1 -0
- package/dist/5086.js.map +1 -0
- package/dist/5117.js +1 -0
- package/dist/5117.js.map +1 -0
- package/dist/5159.js +1 -0
- package/dist/5159.js.map +1 -0
- package/dist/5285.js +1 -0
- package/dist/5285.js.map +1 -0
- package/dist/5361.js +1 -0
- package/dist/5361.js.map +1 -0
- package/dist/5366.js +1 -0
- package/dist/5366.js.map +1 -0
- package/dist/5484.js +1 -0
- package/dist/5484.js.map +1 -0
- package/dist/5598.js +1 -0
- package/dist/5598.js.map +1 -0
- package/dist/5676.js +1 -0
- package/dist/5676.js.map +1 -0
- package/dist/5694.js +1 -0
- package/dist/5694.js.map +1 -0
- package/dist/5731.js +5 -0
- package/dist/5731.js.map +1 -0
- package/dist/5743.js +1 -0
- package/dist/5743.js.map +1 -0
- package/dist/5825.js +1 -0
- package/dist/5825.js.map +1 -0
- package/dist/5932.js +1 -0
- package/dist/5932.js.map +1 -0
- package/dist/5941.js +1 -0
- package/dist/5941.js.map +1 -0
- package/dist/5967.js +1 -0
- package/dist/5967.js.map +1 -0
- package/dist/6104.js +1 -0
- package/dist/6104.js.map +1 -0
- package/dist/624.js +1 -0
- package/dist/624.js.map +1 -0
- package/dist/6259.js +1 -0
- package/dist/6259.js.map +1 -0
- package/dist/6273.js +1 -0
- package/dist/6273.js.map +1 -0
- package/dist/6337.js +1 -0
- package/dist/6337.js.map +1 -0
- package/dist/6427.js +1 -0
- package/dist/6427.js.map +1 -0
- package/dist/647.js +1 -0
- package/dist/647.js.map +1 -0
- package/dist/6478.js +7 -0
- package/dist/6478.js.map +1 -0
- package/dist/6508.js +1 -0
- package/dist/6508.js.map +1 -0
- package/dist/6567.js +1 -0
- package/dist/6567.js.map +1 -0
- package/dist/6634.js +1 -0
- package/dist/6634.js.map +1 -0
- package/dist/6868.js +1 -0
- package/dist/6868.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/7022.js +1 -0
- package/dist/7022.js.map +1 -0
- package/dist/7101.js +1 -0
- package/dist/7101.js.map +1 -0
- package/dist/7209.js +1 -0
- package/dist/7209.js.map +1 -0
- package/dist/7293.js +1 -0
- package/dist/7293.js.map +1 -0
- package/dist/7360.js +1 -0
- package/dist/7360.js.map +1 -0
- package/dist/7453.js +1 -0
- package/dist/7453.js.map +1 -0
- package/dist/7478.js +1 -0
- package/dist/7478.js.map +1 -0
- package/dist/7782.js +1 -0
- package/dist/7782.js.map +1 -0
- package/dist/7808.js +1 -0
- package/dist/7808.js.map +1 -0
- package/dist/7876.js +1 -0
- package/dist/7876.js.map +1 -0
- package/dist/8028.js +1 -0
- package/dist/8028.js.map +1 -0
- package/dist/8196.js +1 -0
- package/dist/8196.js.map +1 -0
- package/dist/820.js +1 -0
- package/dist/820.js.map +1 -0
- package/dist/8240.js +1 -0
- package/dist/8240.js.map +1 -0
- package/dist/8458.js +1 -0
- package/dist/8458.js.map +1 -0
- package/dist/8472.js +1 -0
- package/dist/8472.js.map +1 -0
- package/dist/8602.js +1 -0
- package/dist/8602.js.map +1 -0
- package/dist/8607.js +1 -0
- package/dist/8607.js.map +1 -0
- package/dist/8640.js +1 -0
- package/dist/8640.js.map +1 -0
- package/dist/8762.js +1 -0
- package/dist/8762.js.map +1 -0
- package/dist/9060.js +1 -0
- package/dist/9060.js.map +1 -0
- package/dist/9061.js +1 -0
- package/dist/9061.js.map +1 -0
- package/dist/9087.js +1 -0
- package/dist/9087.js.map +1 -0
- package/dist/9168.js +1 -0
- package/dist/9168.js.map +1 -0
- package/dist/9213.js +1 -0
- package/dist/9213.js.map +1 -0
- package/dist/9286.js +1 -0
- package/dist/9286.js.map +1 -0
- package/dist/9425.js +1 -0
- package/dist/9520.js +1 -0
- package/dist/9520.js.map +1 -0
- package/dist/9547.js +1 -0
- package/dist/9547.js.map +1 -0
- package/dist/9612.js +1 -0
- package/dist/9612.js.map +1 -0
- package/dist/9632.js +1 -0
- package/dist/9632.js.map +1 -0
- package/dist/9771.js +1 -0
- package/dist/9771.js.map +1 -0
- package/dist/9774.js +1 -0
- package/dist/9774.js.map +1 -0
- package/dist/9806.js +1 -0
- package/dist/9806.js.map +1 -0
- package/dist/9885.js +1 -0
- package/dist/9885.js.map +1 -0
- package/dist/992.js +1 -0
- package/dist/992.js.map +1 -0
- package/dist/kenyaemr-esm-etims-app.js +3 -0
- package/dist/kenyaemr-esm-etims-app.js.buildmanifest.json +3354 -0
- package/dist/kenyaemr-esm-etims-app.js.map +1 -0
- package/dist/main.js +3 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/package.json +48 -0
- package/rspack.config.js +1 -0
- package/src/components/header/header.component.tsx +22 -0
- package/src/components/header/header.scss +6 -0
- package/src/components/side-menu/left-panel.component.tsx +22 -0
- package/src/components/side-menu/left-panel.scss +34 -0
- package/src/components/user-menu-link/etims-nav-link.scss +29 -0
- package/src/components/user-menu-link/user-menu-link.component.tsx +25 -0
- package/src/config-schema.ts +1 -0
- package/src/credit-note/etims-credit-note-dashboard.component.tsx +23 -0
- package/src/credit-note/etims-credit-note-dashboard.scss +8 -0
- package/src/credit-note/etims-credit-note-table.component.tsx +284 -0
- package/src/credit-note/etims-credit-note-table.scss +16 -0
- package/src/credit-note/etims-credit-note.resource.ts +96 -0
- package/src/credit-note/etims-credit-note.types.ts +56 -0
- package/src/credit-note/etims-credit-note.workspace.scss +76 -0
- package/src/credit-note/etims-credit-note.workspace.tsx +267 -0
- package/src/declarations.d.ts +2 -0
- package/src/global-properties/etims-global-properties-dashboard.component.tsx +141 -0
- package/src/global-properties/etims-global-properties-dashboard.scss +62 -0
- package/src/global-properties/etims-global-properties-form.workspace.scss +74 -0
- package/src/global-properties/etims-global-properties-form.workspace.tsx +244 -0
- package/src/global-properties/etims-global-properties.resource.ts +60 -0
- package/src/index.ts +100 -0
- package/src/item-mapping/etims-item-mapping-dashboard.component.tsx +29 -0
- package/src/item-mapping/etims-item-mapping-dashboard.scss +42 -0
- package/src/item-mapping/etims-item-mapping-form.fields.tsx +136 -0
- package/src/item-mapping/etims-item-mapping-form.schema.ts +35 -0
- package/src/item-mapping/etims-item-mapping-form.workspace.scss +54 -0
- package/src/item-mapping/etims-item-mapping-form.workspace.tsx +555 -0
- package/src/item-mapping/etims-item-mapping-table.component.tsx +398 -0
- package/src/item-mapping/etims-item-mapping-table.scss +20 -0
- package/src/item-mapping/etims-item-mapping.resource.ts +318 -0
- package/src/item-mapping/etims-item-mapping.types.ts +92 -0
- package/src/item-mapping/register-item-mapping.modal.tsx +113 -0
- package/src/item-mapping/void-item-mapping.modal.tsx +80 -0
- package/src/left-panel-link.component.tsx +39 -0
- package/src/left-panel-link.scss +12 -0
- package/src/reference-data/etims-catalog-table.component.tsx +178 -0
- package/src/reference-data/etims-catalog-table.scss +16 -0
- package/src/reference-data/etims-code-table.component.tsx +87 -0
- package/src/reference-data/etims-item-class-table.component.tsx +86 -0
- package/src/reference-data/etims-reference-data-dashboard.component.tsx +38 -0
- package/src/reference-data/etims-reference-data-dashboard.scss +11 -0
- package/src/reference-data/etims-reference-data.resource.ts +40 -0
- package/src/reference-data/etims-reference-data.types.ts +38 -0
- package/src/reference-data/etims-sync-meta.modal.tsx +62 -0
- package/src/root.component.tsx +38 -0
- package/src/root.scss +11 -0
- package/src/routes.json +117 -0
- package/src/status/etims-status-dashboard.component.tsx +212 -0
- package/src/status/etims-status-dashboard.scss +116 -0
- package/src/status/etims-status.resource.ts +20 -0
- package/src/status/etims-status.types.ts +30 -0
- package/src/transaction/confirm-transaction-action.modal.tsx +113 -0
- package/src/transaction/etims-transaction-dashboard.component.tsx +23 -0
- package/src/transaction/etims-transaction-dashboard.scss +8 -0
- package/src/transaction/etims-transaction-details.workspace.scss +76 -0
- package/src/transaction/etims-transaction-details.workspace.tsx +201 -0
- package/src/transaction/etims-transaction-table.component.tsx +450 -0
- package/src/transaction/etims-transaction-table.scss +20 -0
- package/src/transaction/etims-transaction.resource.ts +180 -0
- package/src/transaction/etims-transaction.types.ts +78 -0
- package/src/transaction/override-reconciliation.modal.scss +7 -0
- package/src/transaction/override-reconciliation.modal.tsx +203 -0
- package/translations/am.json +129 -0
- package/translations/en.json +129 -0
- package/translations/fr.json +130 -0
- package/translations/sw.json +129 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import useSWR from 'swr';
|
|
4
|
+
import type { EtimsCode, EtimsItemClass } from '../reference-data/etims-reference-data.types';
|
|
5
|
+
import type {
|
|
6
|
+
BillableServiceOption,
|
|
7
|
+
CodeOption,
|
|
8
|
+
EtimsItemMapping,
|
|
9
|
+
EtimsItemMappingPayload,
|
|
10
|
+
EtimsRegistrationResult,
|
|
11
|
+
ItemClassOption,
|
|
12
|
+
ServicePrice,
|
|
13
|
+
StockItemOption,
|
|
14
|
+
} from './etims-item-mapping.types';
|
|
15
|
+
|
|
16
|
+
export const etimsItemMappingUrl = `${restBaseUrl}/cashier/etimsItemMapping`;
|
|
17
|
+
export const etimsItemRegistrationUrl = `${restBaseUrl}/cashier/etimsItemRegistration`;
|
|
18
|
+
|
|
19
|
+
const TAX_TYPE_CATEGORY = '04';
|
|
20
|
+
const QUANTITY_UNIT_CATEGORY = '10';
|
|
21
|
+
const PACKAGING_UNIT_CATEGORY = '17';
|
|
22
|
+
|
|
23
|
+
const ITEM_TYPE_FALLBACKS: CodeOption[] = [
|
|
24
|
+
{ code: '1', name: 'Raw Material' },
|
|
25
|
+
{ code: '2', name: 'Finished Product' },
|
|
26
|
+
{ code: '3', name: 'Service without stock' },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const matchesCategory = (code: EtimsCode, needles: string[]) => {
|
|
30
|
+
const category = `${code.codeCategory ?? ''} ${code.codeCategoryName ?? ''}`.toLowerCase();
|
|
31
|
+
return needles.some((needle) => category.includes(needle));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const extractErrorMessage = async (response: FetchResponse, fallback: string) => {
|
|
35
|
+
const payload =
|
|
36
|
+
(response.data as { error?: string | { message?: string; detail?: string } } | undefined) ??
|
|
37
|
+
(await response.json().catch(() => ({})));
|
|
38
|
+
if (typeof payload?.error === 'string' && payload.error.trim()) {
|
|
39
|
+
return payload.error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (payload?.error && typeof payload.error === 'object') {
|
|
43
|
+
return payload.error.message ?? payload.error.detail ?? fallback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return fallback;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const requireOk = async <T>(response: FetchResponse<T>, fallback: string) => {
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(await extractErrorMessage(response, fallback));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return response.data;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function useEtimsItemMappings() {
|
|
58
|
+
const url = `${etimsItemMappingUrl}?v=default&limit=100`;
|
|
59
|
+
const { data, error, isLoading, isValidating, mutate } = useSWR<FetchResponse<{ results: Array<EtimsItemMapping> }>>(
|
|
60
|
+
url,
|
|
61
|
+
openmrsFetch,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
mappings: data?.data?.results ?? [],
|
|
66
|
+
error,
|
|
67
|
+
isLoading,
|
|
68
|
+
isValidating,
|
|
69
|
+
mutate,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function useEtimsCodeOptions() {
|
|
74
|
+
const params = new URLSearchParams({
|
|
75
|
+
v: 'default',
|
|
76
|
+
active: 'true',
|
|
77
|
+
catalog: 'CODE_LIST',
|
|
78
|
+
limit: '100',
|
|
79
|
+
});
|
|
80
|
+
const {
|
|
81
|
+
data,
|
|
82
|
+
error: catalogError,
|
|
83
|
+
isLoading: isCatalogLoading,
|
|
84
|
+
} = useSWR<FetchResponse<{ results: Array<EtimsCode> }>>(
|
|
85
|
+
`${restBaseUrl}/cashier/etimsCode?${params.toString()}`,
|
|
86
|
+
openmrsFetch,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const {
|
|
90
|
+
data: taxTypeData,
|
|
91
|
+
error: taxTypeError,
|
|
92
|
+
isLoading: areTaxTypesLoading,
|
|
93
|
+
} = useSWR<FetchResponse<{ results: Array<EtimsCode> }>>(
|
|
94
|
+
`${restBaseUrl}/cashier/etimsCode?codeCategory=${TAX_TYPE_CATEGORY}`,
|
|
95
|
+
openmrsFetch,
|
|
96
|
+
);
|
|
97
|
+
const {
|
|
98
|
+
data: quantityUnitData,
|
|
99
|
+
error: quantityUnitError,
|
|
100
|
+
isLoading: areQuantityUnitsLoading,
|
|
101
|
+
} = useSWR<FetchResponse<{ results: Array<EtimsCode> }>>(
|
|
102
|
+
`${restBaseUrl}/cashier/etimsCode?codeCategory=${QUANTITY_UNIT_CATEGORY}`,
|
|
103
|
+
openmrsFetch,
|
|
104
|
+
);
|
|
105
|
+
const {
|
|
106
|
+
data: packagingUnitData,
|
|
107
|
+
error: packagingUnitError,
|
|
108
|
+
isLoading: arePackagingUnitsLoading,
|
|
109
|
+
} = useSWR<FetchResponse<{ results: Array<EtimsCode> }>>(
|
|
110
|
+
`${restBaseUrl}/cashier/etimsCode?codeCategory=${PACKAGING_UNIT_CATEGORY}`,
|
|
111
|
+
openmrsFetch,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const codes = (data?.data?.results ?? []).filter((code): code is EtimsCode & CodeOption => Boolean(code.code));
|
|
115
|
+
const taxTypes = (taxTypeData?.data?.results ?? []).filter((code): code is EtimsCode & CodeOption =>
|
|
116
|
+
Boolean(code.code),
|
|
117
|
+
);
|
|
118
|
+
const quantityUnits = (quantityUnitData?.data?.results ?? []).filter((code): code is EtimsCode & CodeOption =>
|
|
119
|
+
Boolean(code.code),
|
|
120
|
+
);
|
|
121
|
+
const packagingUnits = (packagingUnitData?.data?.results ?? []).filter((code): code is EtimsCode & CodeOption =>
|
|
122
|
+
Boolean(code.code),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
taxTypes,
|
|
127
|
+
itemTypes: (() => {
|
|
128
|
+
const catalogTypes = codes.filter((code) => matchesCategory(code, ['item type', 'product type']));
|
|
129
|
+
return catalogTypes.length > 0 ? catalogTypes : ITEM_TYPE_FALLBACKS;
|
|
130
|
+
})(),
|
|
131
|
+
quantityUnits,
|
|
132
|
+
packagingUnits,
|
|
133
|
+
countries: codes.filter((code) => matchesCategory(code, ['country'])),
|
|
134
|
+
error: catalogError ?? taxTypeError ?? quantityUnitError ?? packagingUnitError,
|
|
135
|
+
isLoading: isCatalogLoading || areTaxTypesLoading || areQuantityUnitsLoading || arePackagingUnitsLoading,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function useEtimsItemClassOptions(searchTerm?: string) {
|
|
140
|
+
const params = new URLSearchParams({ v: 'default', active: 'true', limit: '50' });
|
|
141
|
+
const trimmed = searchTerm?.trim();
|
|
142
|
+
|
|
143
|
+
if (trimmed) {
|
|
144
|
+
params.set('q', trimmed);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { data, error, isLoading } = useSWR<FetchResponse<{ results: Array<EtimsItemClass> }>>(
|
|
148
|
+
`${restBaseUrl}/cashier/etimsItemClass?${params.toString()}`,
|
|
149
|
+
openmrsFetch,
|
|
150
|
+
{ keepPreviousData: true },
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
itemClasses: (data?.data?.results ?? []).filter((item): item is EtimsItemClass & ItemClassOption =>
|
|
155
|
+
Boolean(item.itemClassCode),
|
|
156
|
+
),
|
|
157
|
+
error,
|
|
158
|
+
isLoading,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function useBillableServiceOptions(searchTerm?: string) {
|
|
163
|
+
const trimmed = searchTerm?.trim() ?? '';
|
|
164
|
+
const params = new URLSearchParams({
|
|
165
|
+
v: 'custom:(uuid,id,name,shortName,serviceStatus,servicePrices:(uuid,name,price,paymentMode:(uuid,name)),stockItem:(uuid,display))',
|
|
166
|
+
status: 'ENABLED',
|
|
167
|
+
limit: '15',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (trimmed) {
|
|
171
|
+
params.set('q', trimmed);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const { data, error, isLoading } = useSWR<FetchResponse<{ results: Array<BillableServiceOption> }>>(
|
|
175
|
+
trimmed ? `${restBaseUrl}/cashier/billableService?${params.toString()}` : null,
|
|
176
|
+
openmrsFetch,
|
|
177
|
+
{ keepPreviousData: true },
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
billableServices: data?.data?.results ?? [],
|
|
182
|
+
error,
|
|
183
|
+
isLoading,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function useStockItemOptions(searchTerm?: string) {
|
|
188
|
+
const trimmed = searchTerm?.trim() ?? '';
|
|
189
|
+
const params = new URLSearchParams({
|
|
190
|
+
v: 'default',
|
|
191
|
+
limit: '15',
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (trimmed) {
|
|
195
|
+
params.set('q', trimmed);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { data, error, isLoading } = useSWR<FetchResponse<{ results: Array<StockItemOption> }>>(
|
|
199
|
+
trimmed ? `${restBaseUrl}/stockmanagement/stockitem?${params.toString()}` : null,
|
|
200
|
+
openmrsFetch,
|
|
201
|
+
{ keepPreviousData: true },
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
stockItems: data?.data?.results ?? [],
|
|
206
|
+
error,
|
|
207
|
+
isLoading,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function saveEtimsItemMapping(payload: EtimsItemMappingPayload, uuid?: string) {
|
|
212
|
+
const url = uuid ? `${etimsItemMappingUrl}/${uuid}` : etimsItemMappingUrl;
|
|
213
|
+
const response = await openmrsFetch<EtimsItemMapping>(url, {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers: { 'Content-Type': 'application/json' },
|
|
216
|
+
body: JSON.stringify(payload),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return requireOk(response, 'Failed to save eTIMS item mapping');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function voidEtimsItemMapping(uuid: string, reason: string) {
|
|
223
|
+
const params = new URLSearchParams({ reason });
|
|
224
|
+
const response = await openmrsFetch(`${etimsItemMappingUrl}/${uuid}?${params.toString()}`, {
|
|
225
|
+
method: 'DELETE',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return requireOk(response, 'Failed to void eTIMS item mapping');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function registerEtimsItemMapping(uuid: string) {
|
|
232
|
+
const response = await openmrsFetch<EtimsRegistrationResult>(`${etimsItemMappingUrl}/${uuid}/registration`, {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: { 'Content-Type': 'application/json' },
|
|
235
|
+
body: JSON.stringify({ allocateItemCode: true }),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return requireOk(response, 'Failed to register eTIMS item with KRA');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function registerEtimsItemMappings(mappingUuids: string[]) {
|
|
242
|
+
const response = await openmrsFetch<EtimsRegistrationResult>(etimsItemRegistrationUrl, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: { 'Content-Type': 'application/json' },
|
|
245
|
+
body: JSON.stringify({ mappingUuids }),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return requireOk(response, 'Failed to register selected eTIMS items with KRA');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function registerAllEtimsItemMappings() {
|
|
252
|
+
const response = await openmrsFetch<EtimsRegistrationResult>(etimsItemRegistrationUrl, {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: { 'Content-Type': 'application/json' },
|
|
255
|
+
body: JSON.stringify({ all: true }),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return requireOk(response, 'Failed to register eTIMS items with KRA');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export const formatCodeOption = (item?: CodeOption | null) => {
|
|
262
|
+
if (!item) {
|
|
263
|
+
return '';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return item.name ? `${item.code} — ${item.name}` : item.code;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const formatItemClassOption = (item?: ItemClassOption | null) => {
|
|
270
|
+
if (!item) {
|
|
271
|
+
return '';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return item.name ? `${item.itemClassCode} — ${item.name}` : item.itemClassCode;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export const resolveCodeOption = (items: CodeOption[], value?: string | null): CodeOption | null => {
|
|
278
|
+
if (!value) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return items.find((item) => item.code === value) ?? { code: value, name: value };
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const resolveItemClassOption = (items: ItemClassOption[], value?: string | null): ItemClassOption | null => {
|
|
286
|
+
if (!value) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return items.find((item) => item.itemClassCode === value) ?? { itemClassCode: value, name: value };
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export const getServicePriceAmount = (item?: ServicePrice | null): number | null => {
|
|
294
|
+
const price = item?.price;
|
|
295
|
+
if (price === null || price === undefined || price === '') {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const numericPrice = Number(price);
|
|
300
|
+
return Number.isFinite(numericPrice) ? numericPrice : null;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export const formatServicePrice = (item?: ServicePrice | null): string => {
|
|
304
|
+
if (!item) {
|
|
305
|
+
return '';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const paymentMode = typeof item.paymentMode === 'string' ? item.paymentMode : item.paymentMode?.name;
|
|
309
|
+
const label = item.name ?? paymentMode ?? '';
|
|
310
|
+
const amount = getServicePriceAmount(item);
|
|
311
|
+
const amountText = amount === null ? '' : amount.toLocaleString();
|
|
312
|
+
|
|
313
|
+
if (label && amountText) {
|
|
314
|
+
return `${label} — ${amountText}`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return label || amountText;
|
|
318
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export type EtimsRegistrationStatus = 'PENDING' | 'REGISTERED' | 'FAILED';
|
|
2
|
+
|
|
3
|
+
export type EtimsItemSourceType = 'billableService' | 'stockItem';
|
|
4
|
+
|
|
5
|
+
export interface EtimsItemMapping {
|
|
6
|
+
uuid: string;
|
|
7
|
+
display?: string;
|
|
8
|
+
billableServiceId?: number | null;
|
|
9
|
+
stockItemId?: number | null;
|
|
10
|
+
businessPin?: string | null;
|
|
11
|
+
branchId?: string | null;
|
|
12
|
+
itemCode?: string | null;
|
|
13
|
+
itemName?: string | null;
|
|
14
|
+
itemClassCode?: string | null;
|
|
15
|
+
taxType?: string | null;
|
|
16
|
+
registrationStatus?: EtimsRegistrationStatus | string | null;
|
|
17
|
+
countryCode?: string | null;
|
|
18
|
+
itemType?: string | null;
|
|
19
|
+
quantityUnit?: string | null;
|
|
20
|
+
packagingUnit?: string | null;
|
|
21
|
+
defaultSellingPrice?: number | null;
|
|
22
|
+
registrationResultCode?: string | null;
|
|
23
|
+
registrationResultMessage?: string | null;
|
|
24
|
+
registeredAt?: string | null;
|
|
25
|
+
lastVerifiedAt?: string | null;
|
|
26
|
+
voided?: boolean;
|
|
27
|
+
dateCreated?: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EtimsItemMappingPayload {
|
|
31
|
+
billableServiceId?: number | null;
|
|
32
|
+
stockItemId?: number | null;
|
|
33
|
+
businessPin?: string | null;
|
|
34
|
+
branchId?: string | null;
|
|
35
|
+
itemName: string;
|
|
36
|
+
itemClassCode: string;
|
|
37
|
+
countryCode: string;
|
|
38
|
+
itemType: string;
|
|
39
|
+
quantityUnit: string;
|
|
40
|
+
packagingUnit: string;
|
|
41
|
+
taxType: string;
|
|
42
|
+
defaultSellingPrice?: number | null | string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface EtimsRegistrationResult {
|
|
46
|
+
registered: number;
|
|
47
|
+
skipped: number;
|
|
48
|
+
failed: number;
|
|
49
|
+
message?: string | null;
|
|
50
|
+
total: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ServicePrice {
|
|
54
|
+
uuid?: string;
|
|
55
|
+
name?: string;
|
|
56
|
+
price?: number | string | null;
|
|
57
|
+
paymentMode?: { uuid?: string; name?: string } | string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface BillableServiceOption {
|
|
61
|
+
uuid: string;
|
|
62
|
+
id?: number;
|
|
63
|
+
name?: string;
|
|
64
|
+
display?: string;
|
|
65
|
+
serviceStatus?: string;
|
|
66
|
+
servicePrices?: Array<ServicePrice>;
|
|
67
|
+
stockItem?: { uuid?: string; display?: string } | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface StockItemOption {
|
|
71
|
+
uuid: string;
|
|
72
|
+
id?: number;
|
|
73
|
+
commonName?: string;
|
|
74
|
+
display?: string;
|
|
75
|
+
purchasePrice?: number | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CodeOption {
|
|
79
|
+
uuid?: string;
|
|
80
|
+
code: string;
|
|
81
|
+
name?: string | null;
|
|
82
|
+
display?: string;
|
|
83
|
+
codeCategory?: string | null;
|
|
84
|
+
codeCategoryName?: string | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ItemClassOption {
|
|
88
|
+
uuid?: string;
|
|
89
|
+
itemClassCode: string;
|
|
90
|
+
name?: string | null;
|
|
91
|
+
display?: string;
|
|
92
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button, InlineLoading } from '@carbon/react';
|
|
3
|
+
import { showSnackbar } from '@openmrs/esm-framework';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import {
|
|
6
|
+
registerAllEtimsItemMappings,
|
|
7
|
+
registerEtimsItemMapping,
|
|
8
|
+
registerEtimsItemMappings,
|
|
9
|
+
} from './etims-item-mapping.resource';
|
|
10
|
+
import type { TFunction } from 'i18next';
|
|
11
|
+
import type { EtimsRegistrationResult } from './etims-item-mapping.types';
|
|
12
|
+
|
|
13
|
+
type RegisterScope = 'one' | 'selected' | 'all';
|
|
14
|
+
|
|
15
|
+
type RegisterItemMappingModalProps = {
|
|
16
|
+
close: () => void;
|
|
17
|
+
scope: RegisterScope;
|
|
18
|
+
uuid?: string;
|
|
19
|
+
mappingUuids?: string[];
|
|
20
|
+
itemName?: string;
|
|
21
|
+
onRegistered: () => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const describeResult = (result: EtimsRegistrationResult, t: TFunction) =>
|
|
25
|
+
t('registrationResultSummary', '{{registered}} registered, {{skipped}} skipped, {{failed}} failed', {
|
|
26
|
+
registered: result.registered,
|
|
27
|
+
skipped: result.skipped,
|
|
28
|
+
failed: result.failed,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const RegisterItemMappingModal: React.FC<RegisterItemMappingModalProps> = ({
|
|
32
|
+
close,
|
|
33
|
+
scope,
|
|
34
|
+
uuid,
|
|
35
|
+
mappingUuids = [],
|
|
36
|
+
itemName,
|
|
37
|
+
onRegistered,
|
|
38
|
+
}) => {
|
|
39
|
+
const { t } = useTranslation();
|
|
40
|
+
const [isRegistering, setIsRegistering] = useState(false);
|
|
41
|
+
|
|
42
|
+
const heading =
|
|
43
|
+
scope === 'all'
|
|
44
|
+
? t('syncAllToKra', 'Sync all to KRA')
|
|
45
|
+
: scope === 'selected'
|
|
46
|
+
? t('syncSelectedToKra', 'Sync selected to KRA')
|
|
47
|
+
: t('syncToKra', 'Sync to KRA');
|
|
48
|
+
|
|
49
|
+
const message =
|
|
50
|
+
scope === 'all'
|
|
51
|
+
? t(
|
|
52
|
+
'syncAllToKraConfirmation',
|
|
53
|
+
'Submit every mapped item that still needs registration or has changed since the last successful KRA sync. Already registered unchanged items are skipped.',
|
|
54
|
+
)
|
|
55
|
+
: scope === 'selected'
|
|
56
|
+
? t(
|
|
57
|
+
'syncSelectedToKraConfirmation',
|
|
58
|
+
'Submit {{count}} selected mapping(s) to KRA. Unchanged registered items are skipped.',
|
|
59
|
+
{ count: mappingUuids.length },
|
|
60
|
+
)
|
|
61
|
+
: t('syncOneToKraConfirmation', 'Submit "{{itemName}}" to KRA for item registration.', {
|
|
62
|
+
itemName: itemName ?? t('thisMapping', 'this mapping'),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const handleRegister = async () => {
|
|
66
|
+
setIsRegistering(true);
|
|
67
|
+
try {
|
|
68
|
+
const result =
|
|
69
|
+
scope === 'all'
|
|
70
|
+
? await registerAllEtimsItemMappings()
|
|
71
|
+
: scope === 'selected'
|
|
72
|
+
? await registerEtimsItemMappings(mappingUuids)
|
|
73
|
+
: await registerEtimsItemMapping(uuid as string);
|
|
74
|
+
|
|
75
|
+
showSnackbar({
|
|
76
|
+
kind: result.failed > 0 ? 'warning' : 'success',
|
|
77
|
+
title: t('kraSyncComplete', 'KRA sync complete'),
|
|
78
|
+
subtitle: result.message || describeResult(result, t),
|
|
79
|
+
});
|
|
80
|
+
onRegistered();
|
|
81
|
+
close();
|
|
82
|
+
} catch (error: any) {
|
|
83
|
+
showSnackbar({
|
|
84
|
+
kind: 'error',
|
|
85
|
+
title: t('kraSyncError', 'Unable to sync items to KRA'),
|
|
86
|
+
subtitle: error?.message,
|
|
87
|
+
});
|
|
88
|
+
} finally {
|
|
89
|
+
setIsRegistering(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<>
|
|
95
|
+
<div className="cds--modal-header">
|
|
96
|
+
<h3 className="cds--modal-header__heading">{heading}</h3>
|
|
97
|
+
</div>
|
|
98
|
+
<div className="cds--modal-content">
|
|
99
|
+
<p>{message}</p>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="cds--modal-footer">
|
|
102
|
+
<Button kind="secondary" onClick={close} disabled={isRegistering}>
|
|
103
|
+
{t('cancel', 'Cancel')}
|
|
104
|
+
</Button>
|
|
105
|
+
<Button kind="primary" onClick={handleRegister} disabled={isRegistering}>
|
|
106
|
+
{isRegistering ? <InlineLoading description={t('syncing', 'Syncing...')} /> : heading}
|
|
107
|
+
</Button>
|
|
108
|
+
</div>
|
|
109
|
+
</>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export default RegisterItemMappingModal;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button, InlineLoading, TextInput } from '@carbon/react';
|
|
3
|
+
import { showSnackbar } from '@openmrs/esm-framework';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { voidEtimsItemMapping } from './etims-item-mapping.resource';
|
|
6
|
+
|
|
7
|
+
type VoidItemMappingModalProps = {
|
|
8
|
+
close: () => void;
|
|
9
|
+
uuid: string;
|
|
10
|
+
itemName: string;
|
|
11
|
+
onVoided: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const VoidItemMappingModal: React.FC<VoidItemMappingModalProps> = ({ close, uuid, itemName, onVoided }) => {
|
|
15
|
+
const { t } = useTranslation();
|
|
16
|
+
const [reason, setReason] = useState('');
|
|
17
|
+
const [isVoiding, setIsVoiding] = useState(false);
|
|
18
|
+
|
|
19
|
+
const handleVoid = async () => {
|
|
20
|
+
const trimmedReason = reason.trim();
|
|
21
|
+
if (!trimmedReason) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setIsVoiding(true);
|
|
26
|
+
try {
|
|
27
|
+
await voidEtimsItemMapping(uuid, trimmedReason);
|
|
28
|
+
showSnackbar({
|
|
29
|
+
kind: 'success',
|
|
30
|
+
title: t('itemMappingVoided', 'Item mapping voided'),
|
|
31
|
+
subtitle: t('itemMappingVoidedSubtitle', '{{itemName}} was voided.', { itemName }),
|
|
32
|
+
});
|
|
33
|
+
onVoided();
|
|
34
|
+
close();
|
|
35
|
+
} catch (error: any) {
|
|
36
|
+
showSnackbar({
|
|
37
|
+
kind: 'error',
|
|
38
|
+
title: t('itemMappingVoidError', 'Unable to void item mapping'),
|
|
39
|
+
subtitle: error?.message,
|
|
40
|
+
});
|
|
41
|
+
} finally {
|
|
42
|
+
setIsVoiding(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
<div className="cds--modal-header">
|
|
49
|
+
<h3 className="cds--modal-header__heading">{t('voidItemMapping', 'Void item mapping')}</h3>
|
|
50
|
+
</div>
|
|
51
|
+
<div className="cds--modal-content">
|
|
52
|
+
<p>
|
|
53
|
+
{t(
|
|
54
|
+
'voidItemMappingConfirmation',
|
|
55
|
+
'Void the mapping for "{{itemName}}"? This mapping will no longer be used for KRA registration.',
|
|
56
|
+
{
|
|
57
|
+
itemName,
|
|
58
|
+
},
|
|
59
|
+
)}
|
|
60
|
+
</p>
|
|
61
|
+
<TextInput
|
|
62
|
+
id="void-reason"
|
|
63
|
+
labelText={t('voidReason', 'Reason')}
|
|
64
|
+
value={reason}
|
|
65
|
+
onChange={(event) => setReason(event.target.value)}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="cds--modal-footer">
|
|
69
|
+
<Button kind="secondary" onClick={close} disabled={isVoiding}>
|
|
70
|
+
{t('cancel', 'Cancel')}
|
|
71
|
+
</Button>
|
|
72
|
+
<Button kind="danger" onClick={handleVoid} disabled={isVoiding || !reason.trim()}>
|
|
73
|
+
{isVoiding ? <InlineLoading description={t('voiding', 'Voiding...')} /> : t('void', 'Void')}
|
|
74
|
+
</Button>
|
|
75
|
+
</div>
|
|
76
|
+
</>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default VoidItemMappingModal;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React, { type ComponentType } from 'react';
|
|
2
|
+
import { ConfigurableLink, MaybeIcon, type IconId } from '@openmrs/esm-framework';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { BrowserRouter, useLocation } from 'react-router-dom';
|
|
5
|
+
import styles from './left-panel-link.scss';
|
|
6
|
+
|
|
7
|
+
interface LinkConfig {
|
|
8
|
+
title: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
icon?: IconId;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function LinkExtension({ config }: Readonly<{ config: LinkConfig }>) {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const location = useLocation();
|
|
16
|
+
const { name, title, icon } = config;
|
|
17
|
+
const etimsBasePath = `${window.getOpenmrsSpaBase()}etims`;
|
|
18
|
+
const targetPath = name ? `${etimsBasePath}/${name}` : etimsBasePath;
|
|
19
|
+
const pathAfterEtims = location.pathname.replace(etimsBasePath, '').replace(/^\//, '');
|
|
20
|
+
const isActive = name ? pathAfterEtims === name || pathAfterEtims.startsWith(`${name}/`) : pathAfterEtims === '';
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<ConfigurableLink to={targetPath} className={`cds--side-nav__link${isActive ? ' active-left-nav-link' : ''}`}>
|
|
24
|
+
<span className={styles.menu}>
|
|
25
|
+
<MaybeIcon icon={icon} className={styles.icon} size={16} />
|
|
26
|
+
<span>{t(title)}</span>
|
|
27
|
+
</span>
|
|
28
|
+
</ConfigurableLink>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const createLeftPanelLink = (config: LinkConfig): ComponentType<unknown> =>
|
|
33
|
+
function EtimsLeftPanelLink() {
|
|
34
|
+
return (
|
|
35
|
+
<BrowserRouter>
|
|
36
|
+
<LinkExtension config={config} />
|
|
37
|
+
</BrowserRouter>
|
|
38
|
+
);
|
|
39
|
+
};
|