@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,450 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import {
3
+ DataTable,
4
+ DataTableSkeleton,
5
+ Dropdown,
6
+ OverflowMenu,
7
+ OverflowMenuItem,
8
+ Pagination,
9
+ Table,
10
+ TableBody,
11
+ TableCell,
12
+ TableContainer,
13
+ TableHead,
14
+ TableHeader,
15
+ TableRow,
16
+ TableToolbar,
17
+ TableToolbarContent,
18
+ TableToolbarSearch,
19
+ Tag,
20
+ type DataTableHeader,
21
+ } from '@carbon/react';
22
+ import {
23
+ ErrorCard,
24
+ formatDatetime,
25
+ launchWorkspace2,
26
+ parseDate,
27
+ showModal,
28
+ showSnackbar,
29
+ useDebounce,
30
+ usePaginationInfo,
31
+ UserHasAccess,
32
+ } from '@openmrs/esm-framework';
33
+ import { useTranslation } from 'react-i18next';
34
+ import {
35
+ downloadEtimsTaxInvoice,
36
+ isConfirmed,
37
+ isReactivatable,
38
+ isReconcilable,
39
+ isRetryable,
40
+ useEtimsTransactions,
41
+ } from './etims-transaction.resource';
42
+ import type { EtimsTransaction, EtimsTransactionStatus } from './etims-transaction.types';
43
+ import styles from './etims-transaction-table.scss';
44
+
45
+ const DEFAULT_PAGE_SIZE = 10;
46
+ const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
47
+
48
+ type StatusFilter = 'ALL' | EtimsTransactionStatus;
49
+
50
+ type TransactionRow = {
51
+ id: string;
52
+ billId: string;
53
+ transactionType: string;
54
+ status: string;
55
+ traderInvoiceNumber: string;
56
+ kraInvoiceNumber: string;
57
+ resultMessage: string;
58
+ attemptCount: string;
59
+ dateCreated: string;
60
+ confirmedAt: string;
61
+ };
62
+
63
+ const displayValue = (value?: string | number | boolean | null) => {
64
+ if (value === null || value === undefined || value === '') {
65
+ return '--';
66
+ }
67
+
68
+ return String(value);
69
+ };
70
+
71
+ const formatNullableDatetime = (value?: string | null) => {
72
+ if (!value) {
73
+ return '--';
74
+ }
75
+
76
+ try {
77
+ return formatDatetime(parseDate(value), { mode: 'standard', noToday: true });
78
+ } catch {
79
+ return value;
80
+ }
81
+ };
82
+
83
+ const statusTagType = (status?: string | null): 'green' | 'red' | 'blue' | 'cyan' | 'magenta' | 'purple' | 'gray' => {
84
+ if (status === 'CONFIRMED') {
85
+ return 'green';
86
+ }
87
+ if (status === 'PERMANENTLY_FAILED' || status === 'ACTION_REQUIRED') {
88
+ return 'red';
89
+ }
90
+ if (status === 'RECONCILIATION_REQUIRED') {
91
+ return 'magenta';
92
+ }
93
+ if (status === 'RETRY') {
94
+ return 'purple';
95
+ }
96
+ if (status === 'SUBMITTED' || status === 'PROCESSING') {
97
+ return 'cyan';
98
+ }
99
+ if (status === 'PENDING') {
100
+ return 'blue';
101
+ }
102
+ return 'gray';
103
+ };
104
+
105
+ const transactionLabel = (transaction: EtimsTransaction) =>
106
+ transaction.traderInvoiceNumber ?? transaction.kraInvoiceNumber ?? transaction.uuid;
107
+
108
+ const EtimsTransactionTable: React.FC = () => {
109
+ const { t } = useTranslation();
110
+ const [page, setPage] = useState(1);
111
+ const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
112
+ const [searchTerm, setSearchTerm] = useState('');
113
+ const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');
114
+ const [isDownloading, setIsDownloading] = useState<string | null>(null);
115
+ const debouncedSearchTerm = useDebounce(searchTerm, 300);
116
+ const billIdFilter = /^\d+$/.test(debouncedSearchTerm.trim()) ? debouncedSearchTerm.trim() : undefined;
117
+ const { transactions, error, isLoading, mutate } = useEtimsTransactions({
118
+ status: billIdFilter ? 'ALL' : statusFilter,
119
+ billId: billIdFilter,
120
+ });
121
+
122
+ const statusItems = useMemo(
123
+ () => [
124
+ { id: 'ALL', label: t('allStatuses', 'All statuses') },
125
+ { id: 'PENDING', label: t('pending', 'Pending') },
126
+ { id: 'PROCESSING', label: t('processing', 'Processing') },
127
+ { id: 'SUBMITTED', label: t('submitted', 'Submitted') },
128
+ { id: 'CONFIRMED', label: t('confirmed', 'Confirmed') },
129
+ { id: 'RETRY', label: t('retry', 'Retry') },
130
+ { id: 'RECONCILIATION_REQUIRED', label: t('reconciliationRequired', 'Reconciliation required') },
131
+ { id: 'ACTION_REQUIRED', label: t('actionRequired', 'Action required') },
132
+ { id: 'PERMANENTLY_FAILED', label: t('permanentlyFailed', 'Permanently failed') },
133
+ { id: 'CANCELLED', label: t('cancelled', 'Cancelled') },
134
+ ],
135
+ [t],
136
+ );
137
+
138
+ const filteredTransactions = useMemo(() => {
139
+ const query = debouncedSearchTerm.trim().toLowerCase();
140
+
141
+ return transactions.filter((transaction) => {
142
+ if (!billIdFilter && statusFilter !== 'ALL' && transaction.status !== statusFilter) {
143
+ return false;
144
+ }
145
+
146
+ if (!query || billIdFilter) {
147
+ return true;
148
+ }
149
+
150
+ return [
151
+ transaction.billId,
152
+ transaction.transactionType,
153
+ transaction.status,
154
+ transaction.traderInvoiceNumber,
155
+ transaction.kraInvoiceNumber,
156
+ transaction.cuInvoiceNumber,
157
+ transaction.kraReceiptNumber,
158
+ transaction.resultCode,
159
+ transaction.resultMessage,
160
+ transaction.failureCategory,
161
+ ].some((value) => displayValue(value).toLowerCase().includes(query));
162
+ });
163
+ }, [billIdFilter, debouncedSearchTerm, statusFilter, transactions]);
164
+
165
+ const pagedTransactions = useMemo(() => {
166
+ const start = (page - 1) * pageSize;
167
+ return filteredTransactions.slice(start, start + pageSize);
168
+ }, [filteredTransactions, page, pageSize]);
169
+
170
+ const { pageSizes } = usePaginationInfo(
171
+ DEFAULT_PAGE_SIZE,
172
+ filteredTransactions.length,
173
+ page,
174
+ pagedTransactions.length,
175
+ );
176
+
177
+ const headers: Array<DataTableHeader> = useMemo(
178
+ () => [
179
+ { key: 'billId', header: t('billId', 'Bill ID') },
180
+ { key: 'transactionType', header: t('transactionType', 'Type') },
181
+ { key: 'status', header: t('status', 'Status') },
182
+ { key: 'traderInvoiceNumber', header: t('traderInvoiceNumber', 'Trader invoice') },
183
+ { key: 'kraInvoiceNumber', header: t('kraInvoiceNumber', 'KRA invoice') },
184
+ { key: 'attemptCount', header: t('attemptCount', 'Attempts') },
185
+ { key: 'dateCreated', header: t('dateCreated', 'Date created') },
186
+ { key: 'confirmedAt', header: t('confirmedAt', 'Confirmed at') },
187
+ ],
188
+ [t],
189
+ );
190
+
191
+ const tableRows: Array<TransactionRow> = useMemo(
192
+ () =>
193
+ pagedTransactions.map((transaction, index) => ({
194
+ id: transaction.uuid ?? `transaction-${index}`,
195
+ billId: displayValue(transaction.billId),
196
+ transactionType: displayValue(transaction.transactionType),
197
+ status: displayValue(transaction.status),
198
+ traderInvoiceNumber: displayValue(transaction.traderInvoiceNumber),
199
+ kraInvoiceNumber: displayValue(transaction.kraInvoiceNumber),
200
+ resultMessage: displayValue(transaction.resultMessage),
201
+ attemptCount: displayValue(transaction.attemptCount),
202
+ dateCreated: formatNullableDatetime(transaction.dateCreated),
203
+ confirmedAt: formatNullableDatetime(transaction.confirmedAt),
204
+ })),
205
+ [pagedTransactions],
206
+ );
207
+
208
+ const openDetails = useCallback((transaction: EtimsTransaction) => {
209
+ launchWorkspace2('etims-transaction-details', { transaction });
210
+ }, []);
211
+
212
+ const openConfirmActionModal = useCallback(
213
+ (action: 'retry' | 'reconcile' | 'reactivate', transaction: EtimsTransaction) => {
214
+ const dispose = showModal('etims-confirm-transaction-action-modal', {
215
+ close: () => dispose(),
216
+ action,
217
+ uuid: transaction.uuid,
218
+ displayName: transactionLabel(transaction),
219
+ onComplete: () => mutate(),
220
+ });
221
+ },
222
+ [mutate],
223
+ );
224
+
225
+ const openOverrideModal = useCallback(
226
+ (transaction: EtimsTransaction) => {
227
+ const dispose = showModal('etims-override-reconciliation-modal', {
228
+ close: () => dispose(),
229
+ uuid: transaction.uuid,
230
+ displayName: transactionLabel(transaction),
231
+ kraInvoiceNumber: transaction.kraInvoiceNumber,
232
+ onComplete: () => mutate(),
233
+ });
234
+ },
235
+ [mutate],
236
+ );
237
+
238
+ const handleDownloadTaxInvoice = useCallback(
239
+ async (transaction: EtimsTransaction) => {
240
+ if (!transaction.billId) {
241
+ showSnackbar({
242
+ kind: 'error',
243
+ title: t('taxInvoiceError', 'Unable to download tax invoice'),
244
+ subtitle: t('missingBillId', 'This transaction has no bill ID'),
245
+ });
246
+ return;
247
+ }
248
+
249
+ setIsDownloading(transaction.uuid);
250
+ try {
251
+ await downloadEtimsTaxInvoice(transaction.billId);
252
+ showSnackbar({
253
+ kind: 'success',
254
+ title: t('taxInvoiceDownloaded', 'Tax invoice downloaded'),
255
+ });
256
+ } catch (downloadError: any) {
257
+ showSnackbar({
258
+ kind: 'error',
259
+ title: t('taxInvoiceError', 'Unable to download tax invoice'),
260
+ subtitle: downloadError?.message,
261
+ });
262
+ } finally {
263
+ setIsDownloading(null);
264
+ }
265
+ },
266
+ [t],
267
+ );
268
+
269
+ const handleSearchChange = useCallback((event: React.ChangeEvent<HTMLInputElement> | '', value?: string) => {
270
+ setSearchTerm(typeof event === 'string' ? value ?? '' : event.target.value);
271
+ setPage(1);
272
+ }, []);
273
+
274
+ const handlePaginationChange = useCallback(
275
+ ({ page: nextPage, pageSize: nextSize }: { page: number; pageSize: number }) => {
276
+ if (nextSize !== pageSize) {
277
+ setPageSize(nextSize);
278
+ }
279
+ setPage(nextPage);
280
+ },
281
+ [pageSize],
282
+ );
283
+
284
+ if (isLoading) {
285
+ return (
286
+ <div className={styles.container}>
287
+ <DataTableSkeleton headers={headers} rowCount={5} showHeader showToolbar />
288
+ </div>
289
+ );
290
+ }
291
+
292
+ if (error) {
293
+ return (
294
+ <div className={styles.dataTableContainer}>
295
+ <ErrorCard error={error} headerTitle={t('transactionsError', 'Transactions')} />
296
+ </div>
297
+ );
298
+ }
299
+
300
+ return (
301
+ <div className={styles.container}>
302
+ <DataTable rows={tableRows} headers={headers} size="sm" useZebraStyles isSortable>
303
+ {({
304
+ rows,
305
+ headers: tableHeaders,
306
+ getHeaderProps,
307
+ getRowProps,
308
+ getToolbarProps,
309
+ getTableProps,
310
+ getTableContainerProps,
311
+ }) => (
312
+ <TableContainer
313
+ title={t('transactions', 'Transactions')}
314
+ description={t(
315
+ 'transactionsDescription',
316
+ 'Review the fiscal outbox, retry failed submissions, and reconcile ambiguous KRA results.',
317
+ )}
318
+ {...getTableContainerProps()}>
319
+ <TableToolbar {...getToolbarProps()}>
320
+ <TableToolbarContent>
321
+ <TableToolbarSearch
322
+ persistent
323
+ onChange={handleSearchChange}
324
+ placeholder={t('searchTransactions', 'Search by bill ID, invoice, or result')}
325
+ />
326
+ <Dropdown
327
+ size="sm"
328
+ id="transaction-status-filter"
329
+ className={styles.statusFilter}
330
+ label={t('status', 'Status')}
331
+ titleText=""
332
+ items={statusItems}
333
+ itemToString={(item) => item?.label ?? ''}
334
+ selectedItem={statusItems.find((item) => item.id === statusFilter)}
335
+ onChange={({ selectedItem }) => {
336
+ setStatusFilter((selectedItem?.id as StatusFilter) ?? 'ALL');
337
+ setPage(1);
338
+ }}
339
+ />
340
+ </TableToolbarContent>
341
+ </TableToolbar>
342
+
343
+ <Table {...getTableProps()} aria-label={t('transactions', 'Transactions')}>
344
+ <TableHead>
345
+ <TableRow>
346
+ {tableHeaders.map((header) => (
347
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
348
+ ))}
349
+ <TableHeader aria-label={t('rowActions', 'Row actions')} />
350
+ </TableRow>
351
+ </TableHead>
352
+ <TableBody>
353
+ {rows.map((row) => {
354
+ const transaction = pagedTransactions.find((item) => item.uuid === row.id);
355
+
356
+ if (!transaction) {
357
+ return null;
358
+ }
359
+
360
+ return (
361
+ <TableRow {...getRowProps({ row })}>
362
+ {row.cells.map((cell) => {
363
+ if (cell.info.header === 'status') {
364
+ return (
365
+ <TableCell key={cell.id}>
366
+ <Tag type={statusTagType(cell.value)} size="sm">
367
+ {cell.value}
368
+ </Tag>
369
+ </TableCell>
370
+ );
371
+ }
372
+
373
+ return <TableCell key={cell.id}>{cell.value}</TableCell>;
374
+ })}
375
+ <TableCell className="cds--table-column-menu">
376
+ <OverflowMenu flipped size="sm" aria-label={t('rowActions', 'Row actions')}>
377
+ <OverflowMenuItem
378
+ itemText={t('viewDetails', 'View details')}
379
+ onClick={() => openDetails(transaction)}
380
+ />
381
+ {isRetryable(transaction.status) ? (
382
+ <UserHasAccess privilege="Retry eTIMS Transactions">
383
+ <OverflowMenuItem
384
+ itemText={t('retrySubmission', 'Retry submission')}
385
+ onClick={() => openConfirmActionModal('retry', transaction)}
386
+ />
387
+ </UserHasAccess>
388
+ ) : null}
389
+ {isReactivatable(transaction.status) ? (
390
+ <UserHasAccess privilege="Retry eTIMS Transactions">
391
+ <OverflowMenuItem
392
+ itemText={t('retrySubmission', 'Retry submission')}
393
+ onClick={() => openConfirmActionModal('reactivate', transaction)}
394
+ />
395
+ </UserHasAccess>
396
+ ) : null}
397
+ {isReconcilable(transaction.status) ? (
398
+ <UserHasAccess privilege="Reconcile eTIMS Transactions">
399
+ <OverflowMenuItem
400
+ itemText={t('reconcileWithKra', 'Reconcile with KRA')}
401
+ onClick={() => openConfirmActionModal('reconcile', transaction)}
402
+ />
403
+ </UserHasAccess>
404
+ ) : null}
405
+ {isReconcilable(transaction.status) ? (
406
+ <UserHasAccess privilege="Reconcile eTIMS Transactions">
407
+ <OverflowMenuItem
408
+ itemText={t('recordOverride', 'Record override')}
409
+ onClick={() => openOverrideModal(transaction)}
410
+ />
411
+ </UserHasAccess>
412
+ ) : null}
413
+ {isConfirmed(transaction.status) ? (
414
+ <OverflowMenuItem
415
+ disabled={isDownloading === transaction.uuid}
416
+ itemText={
417
+ isDownloading === transaction.uuid
418
+ ? t('downloading', 'Downloading...')
419
+ : t('downloadTaxInvoice', 'Download tax invoice')
420
+ }
421
+ onClick={() => handleDownloadTaxInvoice(transaction)}
422
+ />
423
+ ) : null}
424
+ </OverflowMenu>
425
+ </TableCell>
426
+ </TableRow>
427
+ );
428
+ })}
429
+ </TableBody>
430
+ </Table>
431
+ </TableContainer>
432
+ )}
433
+ </DataTable>
434
+ <Pagination
435
+ itemsPerPageText={t('itemsPerPage', 'Items per page:')}
436
+ forwardText={t('nextPage', 'Next page')}
437
+ backwardText={t('previousPage', 'Previous page')}
438
+ itemRangeText={(min, max, total) => t('minMaxItems', '{{min}}-{{max}} of {{total}} items', { min, max, total })}
439
+ pageRangeText={(_current, total) => t('pageRangeText', 'of {{count}} pages', { count: total })}
440
+ page={page}
441
+ pageSize={pageSize}
442
+ pageSizes={pageSizes?.length > 0 ? pageSizes : PAGE_SIZE_OPTIONS}
443
+ totalItems={filteredTransactions.length}
444
+ onChange={handlePaginationChange}
445
+ />
446
+ </div>
447
+ );
448
+ };
449
+
450
+ export default EtimsTransactionTable;
@@ -0,0 +1,20 @@
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: 0;
11
+ border: 1px solid colors.$gray-20;
12
+
13
+ :global(.cds--table-toolbar) {
14
+ position: relative !important;
15
+ }
16
+ }
17
+
18
+ .statusFilter {
19
+ min-width: 16rem;
20
+ }
@@ -0,0 +1,180 @@
1
+ import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import useSWR from 'swr';
3
+ import type {
4
+ EtimsReconciliationOverride,
5
+ EtimsReconciliationOverridePayload,
6
+ EtimsTransaction,
7
+ EtimsTransactionStatus,
8
+ EtimsTransactionSummary,
9
+ } from './etims-transaction.types';
10
+
11
+ export const etimsTransactionUrl = `${restBaseUrl}/cashier/etimsTransaction`;
12
+ export const etimsTaxInvoiceUrl = `${restBaseUrl}/cashier/etimsTaxInvoice`;
13
+
14
+ export const RETRYABLE_STATUSES: Array<EtimsTransactionStatus> = ['PENDING', 'RETRY'];
15
+ export const RECONCILABLE_STATUSES: Array<EtimsTransactionStatus> = ['RECONCILIATION_REQUIRED'];
16
+ export const REACTIVATABLE_STATUSES: Array<EtimsTransactionStatus> = ['ACTION_REQUIRED'];
17
+
18
+ type TransactionListResponse = { results: Array<EtimsTransaction> };
19
+
20
+ const extractErrorMessage = async (response: FetchResponse, fallback: string) => {
21
+ const payload =
22
+ (response.data as { error?: string | { message?: string; detail?: string } } | undefined) ??
23
+ (await response.json().catch(() => ({})));
24
+ if (typeof payload?.error === 'string' && payload.error.trim()) {
25
+ return payload.error;
26
+ }
27
+
28
+ if (payload?.error && typeof payload.error === 'object') {
29
+ return payload.error.message ?? payload.error.detail ?? fallback;
30
+ }
31
+
32
+ if (typeof payload === 'string' && payload.trim()) {
33
+ return payload;
34
+ }
35
+
36
+ return fallback;
37
+ };
38
+
39
+ const requireOk = async <T>(response: FetchResponse<T>, fallback: string) => {
40
+ if (!response.ok) {
41
+ throw new Error(await extractErrorMessage(response, fallback));
42
+ }
43
+
44
+ return response.data;
45
+ };
46
+
47
+ export type TransactionListFilters = {
48
+ status?: EtimsTransactionStatus | 'ALL';
49
+ billId?: string;
50
+ };
51
+
52
+ export function useEtimsTransactions(filters: TransactionListFilters = {}) {
53
+ const params = new URLSearchParams({ v: 'default', limit: '100' });
54
+ const billId = filters.billId?.trim();
55
+ const status = filters.status && filters.status !== 'ALL' ? filters.status : undefined;
56
+
57
+ if (billId) {
58
+ params.set('q', billId);
59
+ params.set('billId', billId);
60
+ } else if (status) {
61
+ params.set('q', status);
62
+ params.set('status', status);
63
+ }
64
+
65
+ const { data, error, isLoading, isValidating, mutate } = useSWR<FetchResponse<TransactionListResponse>>(
66
+ `${etimsTransactionUrl}?${params.toString()}`,
67
+ openmrsFetch,
68
+ );
69
+
70
+ return {
71
+ transactions: data?.data?.results ?? [],
72
+ error,
73
+ isLoading,
74
+ isValidating,
75
+ mutate,
76
+ };
77
+ }
78
+
79
+ export function useEtimsTransaction(uuid?: string) {
80
+ const { data, error, isLoading, mutate } = useSWR<FetchResponse<EtimsTransaction>>(
81
+ uuid ? `${etimsTransactionUrl}/${uuid}?v=full` : null,
82
+ openmrsFetch,
83
+ );
84
+
85
+ return {
86
+ transaction: data?.data ?? null,
87
+ error,
88
+ isLoading,
89
+ mutate,
90
+ };
91
+ }
92
+
93
+ export function useEtimsReconciliationOverrides(uuid?: string) {
94
+ const { data, error, isLoading, mutate } = useSWR<FetchResponse<Array<EtimsReconciliationOverride>>>(
95
+ uuid ? `${etimsTransactionUrl}/${uuid}/reconciliationOverride` : null,
96
+ openmrsFetch,
97
+ );
98
+
99
+ return {
100
+ overrides: data?.data ?? [],
101
+ error,
102
+ isLoading,
103
+ mutate,
104
+ };
105
+ }
106
+
107
+ export async function retryEtimsTransaction(uuid: string) {
108
+ const response = await openmrsFetch<EtimsTransactionSummary>(`${etimsTransactionUrl}/${uuid}/retry`, {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify({}),
112
+ });
113
+
114
+ return requireOk(response, 'Failed to retry eTIMS transaction');
115
+ }
116
+
117
+ export async function reactivateEtimsTransaction(uuid: string) {
118
+ const response = await openmrsFetch<EtimsTransactionSummary>(`${etimsTransactionUrl}/${uuid}/reactivate`, {
119
+ method: 'POST',
120
+ headers: { 'Content-Type': 'application/json' },
121
+ body: JSON.stringify({}),
122
+ });
123
+
124
+ return requireOk(response, 'Failed to reactivate eTIMS transaction');
125
+ }
126
+
127
+ export async function reconcileEtimsTransaction(uuid: string) {
128
+ const response = await openmrsFetch<EtimsTransactionSummary>(`${etimsTransactionUrl}/${uuid}/reconciliation`, {
129
+ method: 'POST',
130
+ headers: { 'Content-Type': 'application/json' },
131
+ body: JSON.stringify({}),
132
+ });
133
+
134
+ return requireOk(response, 'Failed to reconcile eTIMS transaction');
135
+ }
136
+
137
+ export async function overrideEtimsReconciliation(uuid: string, payload: EtimsReconciliationOverridePayload) {
138
+ const response = await openmrsFetch<EtimsTransactionSummary>(
139
+ `${etimsTransactionUrl}/${uuid}/reconciliationOverride`,
140
+ {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify(payload),
144
+ },
145
+ );
146
+
147
+ return requireOk(response, 'Failed to record eTIMS reconciliation override');
148
+ }
149
+
150
+ export async function downloadEtimsTaxInvoice(billId: number) {
151
+ const response = await fetch(`${window.openmrsBase}${etimsTaxInvoiceUrl}?billId=${billId}`, {
152
+ credentials: 'same-origin',
153
+ });
154
+
155
+ if (!response.ok) {
156
+ const message = (await response.text()).trim();
157
+ throw new Error(message || 'Failed to download tax invoice');
158
+ }
159
+
160
+ const blob = await response.blob();
161
+ const objectUrl = URL.createObjectURL(blob);
162
+ const link = document.createElement('a');
163
+ link.href = objectUrl;
164
+ link.download = `tax-invoice-${billId}.pdf`;
165
+ document.body.appendChild(link);
166
+ link.click();
167
+ link.remove();
168
+ URL.revokeObjectURL(objectUrl);
169
+ }
170
+
171
+ export const isRetryable = (status?: EtimsTransactionStatus | null) =>
172
+ Boolean(status && RETRYABLE_STATUSES.includes(status));
173
+
174
+ export const isReconcilable = (status?: EtimsTransactionStatus | null) =>
175
+ Boolean(status && RECONCILABLE_STATUSES.includes(status));
176
+
177
+ export const isReactivatable = (status?: EtimsTransactionStatus | null) =>
178
+ Boolean(status && REACTIVATABLE_STATUSES.includes(status));
179
+
180
+ export const isConfirmed = (status?: EtimsTransactionStatus | null) => status === 'CONFIRMED';