@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.
Files changed (321) hide show
  1. package/.turbo/turbo-build.log +6 -0
  2. package/README.md +3 -0
  3. package/dist/1067.js +1 -0
  4. package/dist/1067.js.map +1 -0
  5. package/dist/1072.js +1 -0
  6. package/dist/1072.js.map +1 -0
  7. package/dist/1148.js +1 -0
  8. package/dist/1148.js.map +1 -0
  9. package/dist/1278.js +1 -0
  10. package/dist/1278.js.map +1 -0
  11. package/dist/1442.js +1 -0
  12. package/dist/1442.js.map +1 -0
  13. package/dist/15.js +1 -0
  14. package/dist/15.js.map +1 -0
  15. package/dist/1620.js +1 -0
  16. package/dist/1620.js.map +1 -0
  17. package/dist/1638.js +1 -0
  18. package/dist/1638.js.map +1 -0
  19. package/dist/1789.js +1 -0
  20. package/dist/1789.js.map +1 -0
  21. package/dist/1831.js +1 -0
  22. package/dist/1831.js.map +1 -0
  23. package/dist/1844.js +1 -0
  24. package/dist/1844.js.map +1 -0
  25. package/dist/1946.js +1 -0
  26. package/dist/1946.js.map +1 -0
  27. package/dist/2007.js +1 -0
  28. package/dist/2007.js.map +1 -0
  29. package/dist/2115.js +1 -0
  30. package/dist/2115.js.map +1 -0
  31. package/dist/2131.js +1 -0
  32. package/dist/2131.js.map +1 -0
  33. package/dist/2152.js +1 -0
  34. package/dist/2152.js.map +1 -0
  35. package/dist/2202.js +1 -0
  36. package/dist/2202.js.map +1 -0
  37. package/dist/2394.js +1 -0
  38. package/dist/2394.js.map +1 -0
  39. package/dist/2534.js +1 -0
  40. package/dist/2534.js.map +1 -0
  41. package/dist/2537.js +1 -0
  42. package/dist/2537.js.map +1 -0
  43. package/dist/2556.js +1 -0
  44. package/dist/2556.js.map +1 -0
  45. package/dist/2621.js +1 -0
  46. package/dist/2621.js.map +1 -0
  47. package/dist/2648.js +1 -0
  48. package/dist/2648.js.map +1 -0
  49. package/dist/2795.js +1 -0
  50. package/dist/2795.js.map +1 -0
  51. package/dist/2842.js +1 -0
  52. package/dist/2842.js.map +1 -0
  53. package/dist/2916.js +1 -0
  54. package/dist/2916.js.map +1 -0
  55. package/dist/2924.js +1 -0
  56. package/dist/2924.js.map +1 -0
  57. package/dist/2929.js +1 -0
  58. package/dist/2929.js.map +1 -0
  59. package/dist/2947.js +1 -0
  60. package/dist/2947.js.map +1 -0
  61. package/dist/2971.js +1 -0
  62. package/dist/2971.js.map +1 -0
  63. package/dist/3284.js +1 -0
  64. package/dist/3284.js.map +1 -0
  65. package/dist/3285.js +1 -0
  66. package/dist/3285.js.map +1 -0
  67. package/dist/3335.js +1 -0
  68. package/dist/3335.js.map +1 -0
  69. package/dist/335.js +1 -0
  70. package/dist/335.js.map +1 -0
  71. package/dist/3378.js +1 -0
  72. package/dist/3378.js.map +1 -0
  73. package/dist/338.js +1 -0
  74. package/dist/338.js.map +1 -0
  75. package/dist/3504.js +1 -0
  76. package/dist/3504.js.map +1 -0
  77. package/dist/3544.js +1 -0
  78. package/dist/3544.js.map +1 -0
  79. package/dist/356.js +1 -0
  80. package/dist/356.js.map +1 -0
  81. package/dist/3569.js +1 -0
  82. package/dist/3569.js.map +1 -0
  83. package/dist/3723.js +1 -0
  84. package/dist/3723.js.map +1 -0
  85. package/dist/3745.js +1 -0
  86. package/dist/3745.js.map +1 -0
  87. package/dist/3813.js +1 -0
  88. package/dist/3813.js.map +1 -0
  89. package/dist/393.js +6 -0
  90. package/dist/393.js.map +1 -0
  91. package/dist/3949.js +1 -0
  92. package/dist/3949.js.map +1 -0
  93. package/dist/3953.js +1 -0
  94. package/dist/3953.js.map +1 -0
  95. package/dist/3989.js +1 -0
  96. package/dist/3989.js.map +1 -0
  97. package/dist/4178.js +1 -0
  98. package/dist/4178.js.map +1 -0
  99. package/dist/4303.js +1 -0
  100. package/dist/4303.js.map +1 -0
  101. package/dist/4308.js +1 -0
  102. package/dist/4308.js.map +1 -0
  103. package/dist/4487.js +11 -0
  104. package/dist/4487.js.map +1 -0
  105. package/dist/4588.js +1 -0
  106. package/dist/4588.js.map +1 -0
  107. package/dist/4637.js +1 -0
  108. package/dist/4637.js.map +1 -0
  109. package/dist/466.js +1 -0
  110. package/dist/466.js.map +1 -0
  111. package/dist/4817.js +1 -0
  112. package/dist/4817.js.map +1 -0
  113. package/dist/4818.js +1 -0
  114. package/dist/4818.js.map +1 -0
  115. package/dist/500.js +1 -0
  116. package/dist/500.js.map +1 -0
  117. package/dist/5086.js +1 -0
  118. package/dist/5086.js.map +1 -0
  119. package/dist/5117.js +1 -0
  120. package/dist/5117.js.map +1 -0
  121. package/dist/5159.js +1 -0
  122. package/dist/5159.js.map +1 -0
  123. package/dist/5285.js +1 -0
  124. package/dist/5285.js.map +1 -0
  125. package/dist/5361.js +1 -0
  126. package/dist/5361.js.map +1 -0
  127. package/dist/5366.js +1 -0
  128. package/dist/5366.js.map +1 -0
  129. package/dist/5484.js +1 -0
  130. package/dist/5484.js.map +1 -0
  131. package/dist/5598.js +1 -0
  132. package/dist/5598.js.map +1 -0
  133. package/dist/5676.js +1 -0
  134. package/dist/5676.js.map +1 -0
  135. package/dist/5694.js +1 -0
  136. package/dist/5694.js.map +1 -0
  137. package/dist/5731.js +5 -0
  138. package/dist/5731.js.map +1 -0
  139. package/dist/5743.js +1 -0
  140. package/dist/5743.js.map +1 -0
  141. package/dist/5825.js +1 -0
  142. package/dist/5825.js.map +1 -0
  143. package/dist/5932.js +1 -0
  144. package/dist/5932.js.map +1 -0
  145. package/dist/5941.js +1 -0
  146. package/dist/5941.js.map +1 -0
  147. package/dist/5967.js +1 -0
  148. package/dist/5967.js.map +1 -0
  149. package/dist/6104.js +1 -0
  150. package/dist/6104.js.map +1 -0
  151. package/dist/624.js +1 -0
  152. package/dist/624.js.map +1 -0
  153. package/dist/6259.js +1 -0
  154. package/dist/6259.js.map +1 -0
  155. package/dist/6273.js +1 -0
  156. package/dist/6273.js.map +1 -0
  157. package/dist/6337.js +1 -0
  158. package/dist/6337.js.map +1 -0
  159. package/dist/6427.js +1 -0
  160. package/dist/6427.js.map +1 -0
  161. package/dist/647.js +1 -0
  162. package/dist/647.js.map +1 -0
  163. package/dist/6478.js +7 -0
  164. package/dist/6478.js.map +1 -0
  165. package/dist/6508.js +1 -0
  166. package/dist/6508.js.map +1 -0
  167. package/dist/6567.js +1 -0
  168. package/dist/6567.js.map +1 -0
  169. package/dist/6634.js +1 -0
  170. package/dist/6634.js.map +1 -0
  171. package/dist/6868.js +1 -0
  172. package/dist/6868.js.map +1 -0
  173. package/dist/689.js +1 -0
  174. package/dist/689.js.map +1 -0
  175. package/dist/7022.js +1 -0
  176. package/dist/7022.js.map +1 -0
  177. package/dist/7101.js +1 -0
  178. package/dist/7101.js.map +1 -0
  179. package/dist/7209.js +1 -0
  180. package/dist/7209.js.map +1 -0
  181. package/dist/7293.js +1 -0
  182. package/dist/7293.js.map +1 -0
  183. package/dist/7360.js +1 -0
  184. package/dist/7360.js.map +1 -0
  185. package/dist/7453.js +1 -0
  186. package/dist/7453.js.map +1 -0
  187. package/dist/7478.js +1 -0
  188. package/dist/7478.js.map +1 -0
  189. package/dist/7782.js +1 -0
  190. package/dist/7782.js.map +1 -0
  191. package/dist/7808.js +1 -0
  192. package/dist/7808.js.map +1 -0
  193. package/dist/7876.js +1 -0
  194. package/dist/7876.js.map +1 -0
  195. package/dist/8028.js +1 -0
  196. package/dist/8028.js.map +1 -0
  197. package/dist/8196.js +1 -0
  198. package/dist/8196.js.map +1 -0
  199. package/dist/820.js +1 -0
  200. package/dist/820.js.map +1 -0
  201. package/dist/8240.js +1 -0
  202. package/dist/8240.js.map +1 -0
  203. package/dist/8458.js +1 -0
  204. package/dist/8458.js.map +1 -0
  205. package/dist/8472.js +1 -0
  206. package/dist/8472.js.map +1 -0
  207. package/dist/8602.js +1 -0
  208. package/dist/8602.js.map +1 -0
  209. package/dist/8607.js +1 -0
  210. package/dist/8607.js.map +1 -0
  211. package/dist/8640.js +1 -0
  212. package/dist/8640.js.map +1 -0
  213. package/dist/8762.js +1 -0
  214. package/dist/8762.js.map +1 -0
  215. package/dist/9060.js +1 -0
  216. package/dist/9060.js.map +1 -0
  217. package/dist/9061.js +1 -0
  218. package/dist/9061.js.map +1 -0
  219. package/dist/9087.js +1 -0
  220. package/dist/9087.js.map +1 -0
  221. package/dist/9168.js +1 -0
  222. package/dist/9168.js.map +1 -0
  223. package/dist/9213.js +1 -0
  224. package/dist/9213.js.map +1 -0
  225. package/dist/9286.js +1 -0
  226. package/dist/9286.js.map +1 -0
  227. package/dist/9425.js +1 -0
  228. package/dist/9520.js +1 -0
  229. package/dist/9520.js.map +1 -0
  230. package/dist/9547.js +1 -0
  231. package/dist/9547.js.map +1 -0
  232. package/dist/9612.js +1 -0
  233. package/dist/9612.js.map +1 -0
  234. package/dist/9632.js +1 -0
  235. package/dist/9632.js.map +1 -0
  236. package/dist/9771.js +1 -0
  237. package/dist/9771.js.map +1 -0
  238. package/dist/9774.js +1 -0
  239. package/dist/9774.js.map +1 -0
  240. package/dist/9806.js +1 -0
  241. package/dist/9806.js.map +1 -0
  242. package/dist/9885.js +1 -0
  243. package/dist/9885.js.map +1 -0
  244. package/dist/992.js +1 -0
  245. package/dist/992.js.map +1 -0
  246. package/dist/kenyaemr-esm-etims-app.js +3 -0
  247. package/dist/kenyaemr-esm-etims-app.js.buildmanifest.json +3354 -0
  248. package/dist/kenyaemr-esm-etims-app.js.map +1 -0
  249. package/dist/main.js +3 -0
  250. package/dist/main.js.map +1 -0
  251. package/dist/routes.json +1 -0
  252. package/package.json +48 -0
  253. package/rspack.config.js +1 -0
  254. package/src/components/header/header.component.tsx +22 -0
  255. package/src/components/header/header.scss +6 -0
  256. package/src/components/side-menu/left-panel.component.tsx +22 -0
  257. package/src/components/side-menu/left-panel.scss +34 -0
  258. package/src/components/user-menu-link/etims-nav-link.scss +29 -0
  259. package/src/components/user-menu-link/user-menu-link.component.tsx +25 -0
  260. package/src/config-schema.ts +1 -0
  261. package/src/credit-note/etims-credit-note-dashboard.component.tsx +23 -0
  262. package/src/credit-note/etims-credit-note-dashboard.scss +8 -0
  263. package/src/credit-note/etims-credit-note-table.component.tsx +284 -0
  264. package/src/credit-note/etims-credit-note-table.scss +16 -0
  265. package/src/credit-note/etims-credit-note.resource.ts +96 -0
  266. package/src/credit-note/etims-credit-note.types.ts +56 -0
  267. package/src/credit-note/etims-credit-note.workspace.scss +76 -0
  268. package/src/credit-note/etims-credit-note.workspace.tsx +267 -0
  269. package/src/declarations.d.ts +2 -0
  270. package/src/global-properties/etims-global-properties-dashboard.component.tsx +141 -0
  271. package/src/global-properties/etims-global-properties-dashboard.scss +62 -0
  272. package/src/global-properties/etims-global-properties-form.workspace.scss +74 -0
  273. package/src/global-properties/etims-global-properties-form.workspace.tsx +244 -0
  274. package/src/global-properties/etims-global-properties.resource.ts +60 -0
  275. package/src/index.ts +100 -0
  276. package/src/item-mapping/etims-item-mapping-dashboard.component.tsx +29 -0
  277. package/src/item-mapping/etims-item-mapping-dashboard.scss +42 -0
  278. package/src/item-mapping/etims-item-mapping-form.fields.tsx +136 -0
  279. package/src/item-mapping/etims-item-mapping-form.schema.ts +35 -0
  280. package/src/item-mapping/etims-item-mapping-form.workspace.scss +54 -0
  281. package/src/item-mapping/etims-item-mapping-form.workspace.tsx +555 -0
  282. package/src/item-mapping/etims-item-mapping-table.component.tsx +398 -0
  283. package/src/item-mapping/etims-item-mapping-table.scss +20 -0
  284. package/src/item-mapping/etims-item-mapping.resource.ts +318 -0
  285. package/src/item-mapping/etims-item-mapping.types.ts +92 -0
  286. package/src/item-mapping/register-item-mapping.modal.tsx +113 -0
  287. package/src/item-mapping/void-item-mapping.modal.tsx +80 -0
  288. package/src/left-panel-link.component.tsx +39 -0
  289. package/src/left-panel-link.scss +12 -0
  290. package/src/reference-data/etims-catalog-table.component.tsx +178 -0
  291. package/src/reference-data/etims-catalog-table.scss +16 -0
  292. package/src/reference-data/etims-code-table.component.tsx +87 -0
  293. package/src/reference-data/etims-item-class-table.component.tsx +86 -0
  294. package/src/reference-data/etims-reference-data-dashboard.component.tsx +38 -0
  295. package/src/reference-data/etims-reference-data-dashboard.scss +11 -0
  296. package/src/reference-data/etims-reference-data.resource.ts +40 -0
  297. package/src/reference-data/etims-reference-data.types.ts +38 -0
  298. package/src/reference-data/etims-sync-meta.modal.tsx +62 -0
  299. package/src/root.component.tsx +38 -0
  300. package/src/root.scss +11 -0
  301. package/src/routes.json +117 -0
  302. package/src/status/etims-status-dashboard.component.tsx +212 -0
  303. package/src/status/etims-status-dashboard.scss +116 -0
  304. package/src/status/etims-status.resource.ts +20 -0
  305. package/src/status/etims-status.types.ts +30 -0
  306. package/src/transaction/confirm-transaction-action.modal.tsx +113 -0
  307. package/src/transaction/etims-transaction-dashboard.component.tsx +23 -0
  308. package/src/transaction/etims-transaction-dashboard.scss +8 -0
  309. package/src/transaction/etims-transaction-details.workspace.scss +76 -0
  310. package/src/transaction/etims-transaction-details.workspace.tsx +201 -0
  311. package/src/transaction/etims-transaction-table.component.tsx +450 -0
  312. package/src/transaction/etims-transaction-table.scss +20 -0
  313. package/src/transaction/etims-transaction.resource.ts +180 -0
  314. package/src/transaction/etims-transaction.types.ts +78 -0
  315. package/src/transaction/override-reconciliation.modal.scss +7 -0
  316. package/src/transaction/override-reconciliation.modal.tsx +203 -0
  317. package/translations/am.json +129 -0
  318. package/translations/en.json +129 -0
  319. package/translations/fr.json +130 -0
  320. package/translations/sw.json +129 -0
  321. package/tsconfig.json +5 -0
@@ -0,0 +1,178 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import {
3
+ DataTable,
4
+ Pagination,
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableContainer,
9
+ TableHead,
10
+ TableHeader,
11
+ TableRow,
12
+ TableToolbar,
13
+ TableToolbarContent,
14
+ TableToolbarSearch,
15
+ Tag,
16
+ Button,
17
+ type DataTableHeader,
18
+ } from '@carbon/react';
19
+ import { SyncSettings } from '@carbon/react/icons';
20
+ import { useTranslation } from 'react-i18next';
21
+ import { ErrorCard, useDebounce, usePaginationInfo } from '@openmrs/esm-framework';
22
+ import styles from './etims-catalog-table.scss';
23
+
24
+ const DEFAULT_PAGE_SIZE = 10;
25
+ const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
26
+
27
+ export type CatalogRow = {
28
+ id: string;
29
+ [key: string]: string;
30
+ };
31
+
32
+ type PaginationResult<T> = {
33
+ data?: T[];
34
+ error?: Error | null;
35
+ goTo: (page: number) => void;
36
+ currentPage: number;
37
+ totalCount?: number;
38
+ };
39
+
40
+ type EtimsCatalogTableProps<T> = {
41
+ title: string;
42
+ description: string;
43
+ searchPlaceholder: string;
44
+ errorTitle: string;
45
+ ariaLabel: string;
46
+ headers: Array<DataTableHeader>;
47
+ statusKeys?: string[];
48
+ fetchPage: (pageSize: number, searchTerm: string) => PaginationResult<T>;
49
+ mapRows: (items: T[]) => Array<CatalogRow>;
50
+ onSync: () => void;
51
+ };
52
+
53
+ const EtimsCatalogTable = <T,>({
54
+ title,
55
+ description,
56
+ searchPlaceholder,
57
+ errorTitle,
58
+ ariaLabel,
59
+ headers,
60
+ statusKeys = ['active'],
61
+ fetchPage,
62
+ mapRows,
63
+ onSync,
64
+ }: EtimsCatalogTableProps<T>) => {
65
+ const { t } = useTranslation();
66
+ const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
67
+ const [searchTerm, setSearchTerm] = useState('');
68
+ const debouncedSearchTerm = useDebounce(searchTerm, 300);
69
+
70
+ const { data: items = [], error, goTo, currentPage, totalCount } = fetchPage(pageSize, debouncedSearchTerm);
71
+
72
+ const { pageSizes } = usePaginationInfo(DEFAULT_PAGE_SIZE, totalCount ?? 0, currentPage, items.length);
73
+ const tableRows = useMemo(() => mapRows(items), [items, mapRows]);
74
+
75
+ const handleSearchChange = useCallback(
76
+ (event: React.ChangeEvent<HTMLInputElement> | '', value?: string) => {
77
+ const searchValue = typeof event === 'string' ? value ?? '' : event.target.value;
78
+ setSearchTerm(searchValue);
79
+ if (currentPage !== 1) {
80
+ goTo(1);
81
+ }
82
+ },
83
+ [currentPage, goTo],
84
+ );
85
+
86
+ const handlePaginationChange = useCallback(
87
+ ({ page, pageSize: newSize }: { page: number; pageSize: number }) => {
88
+ if (newSize !== pageSize) {
89
+ setPageSize(newSize);
90
+ }
91
+ goTo(page);
92
+ },
93
+ [pageSize, goTo],
94
+ );
95
+
96
+ if (error) {
97
+ return (
98
+ <div className={styles.dataTableContainer}>
99
+ <ErrorCard error={error} headerTitle={errorTitle} />
100
+ </div>
101
+ );
102
+ }
103
+
104
+ return (
105
+ <div className={styles.container}>
106
+ <DataTable size="sm" rows={tableRows} headers={headers} isSortable useZebraStyles>
107
+ {({
108
+ rows,
109
+ headers: tableHeaders,
110
+ getHeaderProps,
111
+ getRowProps,
112
+ getBatchActionProps,
113
+ getToolbarProps,
114
+ getTableProps,
115
+ getTableContainerProps,
116
+ }) => {
117
+ const batchActionProps = getBatchActionProps();
118
+
119
+ return (
120
+ <TableContainer title={title} description={description} {...getTableContainerProps()}>
121
+ <TableToolbar {...getToolbarProps()}>
122
+ <TableToolbarContent aria-hidden={batchActionProps.shouldShowBatchActions}>
123
+ <TableToolbarSearch persistent onChange={handleSearchChange} placeholder={searchPlaceholder} />
124
+ <Button onClick={onSync} kind="ghost" size="sm" renderIcon={SyncSettings}>
125
+ {t('sync', 'Sync')}
126
+ </Button>
127
+ </TableToolbarContent>
128
+ </TableToolbar>
129
+
130
+ <Table {...getTableProps()} aria-label={ariaLabel}>
131
+ <TableHead>
132
+ <TableRow>
133
+ {tableHeaders.map((header) => (
134
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
135
+ ))}
136
+ </TableRow>
137
+ </TableHead>
138
+ <TableBody>
139
+ {rows.map((row) => (
140
+ <TableRow {...getRowProps({ row })}>
141
+ {row.cells.map((cell) => {
142
+ if (statusKeys.includes(cell.info.header)) {
143
+ const isActive = cell.value === t('yes', 'Yes');
144
+ return (
145
+ <TableCell key={cell.id}>
146
+ <Tag type={isActive ? 'green' : 'gray'} size="sm">
147
+ {cell.value}
148
+ </Tag>
149
+ </TableCell>
150
+ );
151
+ }
152
+ return <TableCell key={cell.id}>{cell.value}</TableCell>;
153
+ })}
154
+ </TableRow>
155
+ ))}
156
+ </TableBody>
157
+ </Table>
158
+ </TableContainer>
159
+ );
160
+ }}
161
+ </DataTable>
162
+ <Pagination
163
+ itemsPerPageText={t('itemsPerPage', 'Items per page:')}
164
+ forwardText={t('nextPage', 'Next page')}
165
+ backwardText={t('previousPage', 'Previous page')}
166
+ itemRangeText={(min, max, total) => t('minMaxItems', '{{min}}-{{max}} of {{total}} items', { min, max, total })}
167
+ pageRangeText={(_current, total) => t('pageRangeText', 'of {{count}} pages', { count: total })}
168
+ page={currentPage}
169
+ pageSize={pageSize}
170
+ pageSizes={pageSizes?.length > 0 ? pageSizes : PAGE_SIZE_OPTIONS}
171
+ totalItems={totalCount ?? 0}
172
+ onChange={handlePaginationChange}
173
+ />
174
+ </div>
175
+ );
176
+ };
177
+
178
+ export default EtimsCatalogTable;
@@ -0,0 +1,16 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/colors';
3
+
4
+ .dataTableContainer {
5
+ height: 100%;
6
+ margin: layout.$layout-01;
7
+ }
8
+
9
+ .container {
10
+ margin: layout.$layout-01;
11
+ border: 1px solid colors.$gray-20;
12
+
13
+ :global(.cds--table-toolbar) {
14
+ position: relative !important;
15
+ }
16
+ }
@@ -0,0 +1,87 @@
1
+ import React, { useCallback, useMemo } from 'react';
2
+ import { type DataTableHeader } from '@carbon/react';
3
+ import { formatDatetime, parseDate, showModal } from '@openmrs/esm-framework';
4
+ import { useTranslation } from 'react-i18next';
5
+ import EtimsCatalogTable, { type CatalogRow } from './etims-catalog-table.component';
6
+ import { useEtimsCodes } from './etims-reference-data.resource';
7
+ import type { EtimsCode } from './etims-reference-data.types';
8
+
9
+ const displayValue = (value?: string | number | boolean | null) => {
10
+ if (value === null || value === undefined || value === '') {
11
+ return '--';
12
+ }
13
+
14
+ return String(value);
15
+ };
16
+
17
+ const formatNullableDatetime = (value?: string | null) => {
18
+ if (!value) {
19
+ return '--';
20
+ }
21
+
22
+ try {
23
+ return formatDatetime(parseDate(value), { mode: 'standard', noToday: true });
24
+ } catch {
25
+ return value;
26
+ }
27
+ };
28
+
29
+ const EtimsCodeTable: React.FC = () => {
30
+ const { t } = useTranslation();
31
+ const handleSync = () => {
32
+ const dispose = showModal('etims-kra-meta-data-sync-modal', {
33
+ onClose: () => dispose(),
34
+ category: 'CODE_LIST',
35
+ heading: t('syncCodeList', 'Sync code list'),
36
+ message: t(
37
+ 'syncCodeListMessage',
38
+ 'Synchronization of code list from KRA, this will update code list to latest version, Do you want to proceed?',
39
+ ),
40
+ });
41
+ };
42
+
43
+ const headers: Array<DataTableHeader> = useMemo(
44
+ () => [
45
+ { key: 'codeCategory', header: t('codeCategory', 'Category') },
46
+ { key: 'codeCategoryName', header: t('codeCategoryName', 'Category name') },
47
+ { key: 'code', header: t('code', 'Code') },
48
+ { key: 'name', header: t('name', 'Name') },
49
+ { key: 'active', header: t('active', 'Active') },
50
+ { key: 'dateCreated', header: t('dateCreated', 'Date created') },
51
+ ],
52
+ [t],
53
+ );
54
+
55
+ const mapRows = useCallback(
56
+ (codes: EtimsCode[]): Array<CatalogRow> =>
57
+ codes.map((code, index) => ({
58
+ id: code.uuid ?? `etims-code-${index}`,
59
+ codeCategory: displayValue(code.codeCategory),
60
+ codeCategoryName: displayValue(code.codeCategoryName),
61
+ code: displayValue(code.code),
62
+ name: displayValue(code.name),
63
+ active: code.active ? t('yes', 'Yes') : t('no', 'No'),
64
+ dateCreated: formatNullableDatetime(code.dateCreated),
65
+ })),
66
+ [t],
67
+ );
68
+
69
+ return (
70
+ <EtimsCatalogTable
71
+ title={t('etimsCodes', 'eTIMS codes')}
72
+ description={t(
73
+ 'etimsCodesDescription',
74
+ 'Synchronized KRA reference codes such as tax types, item types, and units. Search by name, code, or category.',
75
+ )}
76
+ searchPlaceholder={t('searchEtimsCodes', 'Search eTIMS codes')}
77
+ errorTitle={t('etimsCodesError', 'eTIMS codes')}
78
+ ariaLabel={t('etimsCodes', 'eTIMS codes')}
79
+ headers={headers}
80
+ fetchPage={useEtimsCodes}
81
+ mapRows={mapRows}
82
+ onSync={handleSync}
83
+ />
84
+ );
85
+ };
86
+
87
+ export default EtimsCodeTable;
@@ -0,0 +1,86 @@
1
+ import React, { useCallback, useMemo } from 'react';
2
+ import { type DataTableHeader } from '@carbon/react';
3
+ import { formatDatetime, parseDate, showModal } from '@openmrs/esm-framework';
4
+ import { useTranslation } from 'react-i18next';
5
+ import EtimsCatalogTable, { type CatalogRow } from './etims-catalog-table.component';
6
+ import { useEtimsItemClasses } from './etims-reference-data.resource';
7
+ import type { EtimsItemClass } from './etims-reference-data.types';
8
+
9
+ const displayValue = (value?: string | number | boolean | null) => {
10
+ if (value === null || value === undefined || value === '') {
11
+ return '--';
12
+ }
13
+
14
+ return String(value);
15
+ };
16
+
17
+ const formatNullableDatetime = (value?: string | null) => {
18
+ if (!value) {
19
+ return '--';
20
+ }
21
+
22
+ try {
23
+ return formatDatetime(parseDate(value), { mode: 'standard', noToday: true });
24
+ } catch {
25
+ return value;
26
+ }
27
+ };
28
+
29
+ const EtimsItemClassTable: React.FC = () => {
30
+ const { t } = useTranslation();
31
+
32
+ const handleSync = () => {
33
+ const dispose = showModal('etims-kra-meta-data-sync-modal', {
34
+ onClose: () => dispose(),
35
+ category: 'CLASS_LIST',
36
+ heading: t('syncCodeList', 'Sync class list'),
37
+ message: t(
38
+ 'syncClassListMessage',
39
+ 'Synchronization of class list from KRA, this will update class list to latest version, Do you want to proceed?',
40
+ ),
41
+ });
42
+ };
43
+
44
+ const headers: Array<DataTableHeader> = useMemo(
45
+ () => [
46
+ { key: 'itemClassCode', header: t('itemClassCode', 'Item class code') },
47
+ { key: 'name', header: t('name', 'Name') },
48
+ { key: 'description', header: t('description', 'Description') },
49
+ { key: 'active', header: t('active', 'Active') },
50
+ { key: 'dateCreated', header: t('dateCreated', 'Date created') },
51
+ ],
52
+ [t],
53
+ );
54
+
55
+ const mapRows = useCallback(
56
+ (itemClasses: EtimsItemClass[]): Array<CatalogRow> =>
57
+ itemClasses.map((itemClass, index) => ({
58
+ id: itemClass.uuid ?? `etims-item-class-${index}`,
59
+ itemClassCode: displayValue(itemClass.itemClassCode),
60
+ name: displayValue(itemClass.name),
61
+ description: displayValue(itemClass.description),
62
+ active: itemClass.active ? t('yes', 'Yes') : t('no', 'No'),
63
+ dateCreated: formatNullableDatetime(itemClass.dateCreated),
64
+ })),
65
+ [t],
66
+ );
67
+
68
+ return (
69
+ <EtimsCatalogTable
70
+ title={t('etimsItemClasses', 'eTIMS item classes')}
71
+ description={t(
72
+ 'etimsItemClassesDescription',
73
+ 'Synchronized KRA item classes used when mapping and registering cashier items. Search by name or item class code.',
74
+ )}
75
+ searchPlaceholder={t('searchEtimsItemClasses', 'Search eTIMS item classes')}
76
+ errorTitle={t('etimsItemClassesError', 'eTIMS item classes')}
77
+ ariaLabel={t('etimsItemClasses', 'eTIMS item classes')}
78
+ headers={headers}
79
+ fetchPage={useEtimsItemClasses}
80
+ mapRows={mapRows}
81
+ onSync={handleSync}
82
+ />
83
+ );
84
+ };
85
+
86
+ export default EtimsItemClassTable;
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@carbon/react';
3
+ import { UserHasAccess } from '@openmrs/esm-framework';
4
+ import { useTranslation } from 'react-i18next';
5
+ import Header from '../components/header/header.component';
6
+ import EtimsCodeTable from './etims-code-table.component';
7
+ import EtimsItemClassTable from './etims-item-class-table.component';
8
+ import styles from './etims-reference-data-dashboard.scss';
9
+
10
+ const EtimsReferenceDataDashboard: React.FC = () => {
11
+ const { t } = useTranslation();
12
+
13
+ return (
14
+ <div className={styles.dashboardContainer}>
15
+ <Header title={t('referenceData', 'Reference data')} />
16
+ <UserHasAccess privilege="Manage eTIMS Configuration">
17
+ <div className={styles.dashboard}>
18
+ <Tabs>
19
+ <TabList aria-label={t('referenceData', 'Reference data')} contained>
20
+ <Tab>{t('etimsCodes', 'eTIMS codes')}</Tab>
21
+ <Tab>{t('etimsItemClasses', 'eTIMS item classes')}</Tab>
22
+ </TabList>
23
+ <TabPanels>
24
+ <TabPanel className={styles.tabPanel}>
25
+ <EtimsCodeTable />
26
+ </TabPanel>
27
+ <TabPanel className={styles.tabPanel}>
28
+ <EtimsItemClassTable />
29
+ </TabPanel>
30
+ </TabPanels>
31
+ </Tabs>
32
+ </div>
33
+ </UserHasAccess>
34
+ </div>
35
+ );
36
+ };
37
+
38
+ export default EtimsReferenceDataDashboard;
@@ -0,0 +1,11 @@
1
+ @use '@carbon/layout';
2
+
3
+ .dashboard {
4
+ display: flex;
5
+ flex-direction: column;
6
+ padding: layout.$spacing-05;
7
+ }
8
+
9
+ .tabPanel {
10
+ padding: 0;
11
+ }
@@ -0,0 +1,40 @@
1
+ import { FetchResponse, openmrsFetch, restBaseUrl, useOpenmrsPagination } from '@openmrs/esm-framework';
2
+ import type { AllowedCategory, EtimsCode, EtimsItemClass, SyncKRAMetaDataResponse } from './etims-reference-data.types';
3
+
4
+ export const etimsCodeUrl = `${restBaseUrl}/cashier/etimsCode`;
5
+ export const etimsItemClassUrl = `${restBaseUrl}/cashier/etimsItemClass`;
6
+
7
+ const buildCatalogParams = (searchTerm?: string, extra?: Record<string, string>) => {
8
+ const params = new URLSearchParams({ v: 'default', active: 'true', ...extra });
9
+ const trimmed = searchTerm?.trim();
10
+
11
+ if (trimmed) {
12
+ params.set('q', trimmed);
13
+ }
14
+
15
+ return params;
16
+ };
17
+
18
+ export function useEtimsCodes(pageSize: number = 10, searchTerm?: string) {
19
+ const params = buildCatalogParams(searchTerm, { catalog: 'CODE_LIST' });
20
+ return useOpenmrsPagination<EtimsCode>(`${etimsCodeUrl}?${params.toString()}`, pageSize);
21
+ }
22
+
23
+ export function useEtimsItemClasses(pageSize: number = 10, searchTerm?: string) {
24
+ const params = buildCatalogParams(searchTerm);
25
+ return useOpenmrsPagination<EtimsItemClass>(`${etimsItemClassUrl}?${params.toString()}`, pageSize);
26
+ }
27
+
28
+ export function syncReferenceDataFromKRA(category: AllowedCategory) {
29
+ const url = `${restBaseUrl}/cashier/etimsReferenceSync`;
30
+ const syncBody = {
31
+ categories: [category],
32
+ };
33
+ return openmrsFetch<FetchResponse<SyncKRAMetaDataResponse>>(url, {
34
+ method: 'POST',
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ },
38
+ body: JSON.stringify(syncBody),
39
+ });
40
+ }
@@ -0,0 +1,38 @@
1
+ export interface EtimsCode {
2
+ uuid: string;
3
+ display?: string;
4
+ codeCategory?: string | null;
5
+ codeCategoryName?: string | null;
6
+ code?: string | null;
7
+ name?: string | null;
8
+ active?: boolean | null;
9
+ description?: string | null;
10
+ voided?: boolean;
11
+ dateCreated?: string | null;
12
+ }
13
+
14
+ export interface EtimsItemClass {
15
+ uuid: string;
16
+ display?: string;
17
+ itemClassCode?: string | null;
18
+ name?: string | null;
19
+ active?: boolean | null;
20
+ description?: string | null;
21
+ voided?: boolean;
22
+ dateCreated?: string | null;
23
+ }
24
+
25
+ export type AllowedCategory = 'CODE_LIST' | 'CLASS_LIST';
26
+
27
+ interface ResultDetails {
28
+ category: AllowedCategory;
29
+ success: boolean;
30
+ recordCount: number;
31
+ message: string | null;
32
+ at: number;
33
+ }
34
+
35
+ export interface SyncKRAMetaDataResponse {
36
+ total: number;
37
+ results: Partial<Record<AllowedCategory, ResultDetails>>;
38
+ }
@@ -0,0 +1,62 @@
1
+ import React, { useState } from 'react';
2
+ import { Button, InlineLoading } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { syncReferenceDataFromKRA } from './etims-reference-data.resource';
5
+ import { AllowedCategory } from './etims-reference-data.types';
6
+ import { showSnackbar } from '@openmrs/esm-framework';
7
+
8
+ type EtimsSyncMetaDataModalProps = {
9
+ heading: string;
10
+ message: string;
11
+ category: AllowedCategory;
12
+ onClose: () => void;
13
+ };
14
+
15
+ const EtimsSyncMetaDataModal: React.FC<EtimsSyncMetaDataModalProps> = ({ message, heading, category, onClose }) => {
16
+ const { t } = useTranslation();
17
+ const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
18
+
19
+ const handleSubmit = async () => {
20
+ setIsSubmitting(true);
21
+ try {
22
+ const response = await syncReferenceDataFromKRA(category);
23
+ if (response.ok) {
24
+ showSnackbar({
25
+ title: t('syncKRAMetaData', 'Successful synchronization'),
26
+ subtitle: t('syncKRAMetaDataSubTitle', 'Synchronization of {{category}} is successful', { category }),
27
+ kind: 'success',
28
+ timeoutInMs: 5000,
29
+ });
30
+ }
31
+ } catch (error) {
32
+ showSnackbar({
33
+ title: t('syncKRAMetaDataError', 'Synchronization of KRA failed'),
34
+ subtitle: t('syncKRAMetaDataSubTitleError', 'Failed to sync meta with error {errorCode}', {
35
+ errorCode: JSON.stringify(error),
36
+ }),
37
+ });
38
+ } finally {
39
+ onClose();
40
+ }
41
+ };
42
+ return (
43
+ <>
44
+ <div className="cds--modal-header">
45
+ <h3 className="cds--modal-header__heading">{heading}</h3>
46
+ </div>
47
+ <div className="cds--modal-content">
48
+ <p>{message}</p>
49
+ </div>
50
+ <div className="cds--modal-footer">
51
+ <Button kind="secondary" onClick={close}>
52
+ {t('cancel', 'Cancel')}
53
+ </Button>
54
+ <Button kind="primary" onClick={handleSubmit} disabled={isSubmitting}>
55
+ {isSubmitting ? <InlineLoading description={t('syncing', 'Syncing...')} /> : heading}
56
+ </Button>
57
+ </div>
58
+ </>
59
+ );
60
+ };
61
+
62
+ export default EtimsSyncMetaDataModal;
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { useLeftNav } from '@openmrs/esm-framework';
3
+ import { BrowserRouter, Route, Routes } from 'react-router-dom';
4
+ import LeftPanel from './components/side-menu/left-panel.component';
5
+ import EtimsCreditNoteDashboard from './credit-note/etims-credit-note-dashboard.component';
6
+ import EtimsGlobalPropertiesDashboard from './global-properties/etims-global-properties-dashboard.component';
7
+ import EtimsItemMappingDashboard from './item-mapping/etims-item-mapping-dashboard.component';
8
+ import EtimsReferenceDataDashboard from './reference-data/etims-reference-data-dashboard.component';
9
+ import EtimsStatusDashboard from './status/etims-status-dashboard.component';
10
+ import EtimsTransactionDashboard from './transaction/etims-transaction-dashboard.component';
11
+ import styles from './root.scss';
12
+
13
+ const Root: React.FC = () => {
14
+ const etimsBasename = `${window.getOpenmrsSpaBase()}etims`;
15
+
16
+ useLeftNav({
17
+ name: 'etims-left-panel-slot',
18
+ basePath: window.spaBase,
19
+ });
20
+
21
+ return (
22
+ <BrowserRouter basename={etimsBasename}>
23
+ <LeftPanel />
24
+ <main className={styles.container}>
25
+ <Routes>
26
+ <Route path="/" element={<EtimsStatusDashboard />} />
27
+ <Route path="/reference-data" element={<EtimsReferenceDataDashboard />} />
28
+ <Route path="/item-mappings" element={<EtimsItemMappingDashboard />} />
29
+ <Route path="/transactions" element={<EtimsTransactionDashboard />} />
30
+ <Route path="/credit-notes" element={<EtimsCreditNoteDashboard />} />
31
+ <Route path="/global-properties" element={<EtimsGlobalPropertiesDashboard />} />
32
+ </Routes>
33
+ </main>
34
+ </BrowserRouter>
35
+ );
36
+ };
37
+
38
+ export default Root;
package/src/root.scss ADDED
@@ -0,0 +1,11 @@
1
+ @use '@carbon/colors';
2
+
3
+ .container {
4
+ background-color: colors.$white-0;
5
+ height: calc(100vh - var(--omrs-topnav-height));
6
+ overflow-y: auto;
7
+ }
8
+
9
+ :global(.omrs-breakpoint-gt-tablet) .container {
10
+ margin-left: var(--omrs-sidenav-width);
11
+ }