@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,284 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import {
3
+ Button,
4
+ DataTable,
5
+ DataTableSkeleton,
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 { Add } from '@carbon/react/icons';
23
+ import {
24
+ ErrorCard,
25
+ formatDatetime,
26
+ launchWorkspace2,
27
+ parseDate,
28
+ useDebounce,
29
+ usePaginationInfo,
30
+ UserHasAccess,
31
+ } from '@openmrs/esm-framework';
32
+ import { useTranslation } from 'react-i18next';
33
+ import type { EtimsTransaction } from '../transaction/etims-transaction.types';
34
+ import { useEtimsCreditNotes } from './etims-credit-note.resource';
35
+ import styles from './etims-credit-note-table.scss';
36
+
37
+ const DEFAULT_PAGE_SIZE = 10;
38
+ const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
39
+
40
+ type CreditNoteRow = {
41
+ id: string;
42
+ billId: string;
43
+ status: string;
44
+ traderInvoiceNumber: string;
45
+ kraInvoiceNumber: string;
46
+ resultMessage: string;
47
+ dateCreated: string;
48
+ confirmedAt: string;
49
+ };
50
+
51
+ const displayValue = (value?: string | number | boolean | null) => {
52
+ if (value === null || value === undefined || value === '') {
53
+ return '--';
54
+ }
55
+
56
+ return String(value);
57
+ };
58
+
59
+ const formatNullableDatetime = (value?: string | null) => {
60
+ if (!value) {
61
+ return '--';
62
+ }
63
+
64
+ try {
65
+ return formatDatetime(parseDate(value), { mode: 'standard', noToday: true });
66
+ } catch {
67
+ return value;
68
+ }
69
+ };
70
+
71
+ const statusTagType = (status?: string | null): 'green' | 'red' | 'blue' | 'cyan' | 'magenta' | 'purple' | 'gray' => {
72
+ if (status === 'CONFIRMED') {
73
+ return 'green';
74
+ }
75
+ if (status === 'PERMANENTLY_FAILED' || status === 'ACTION_REQUIRED') {
76
+ return 'red';
77
+ }
78
+ if (status === 'RECONCILIATION_REQUIRED') {
79
+ return 'magenta';
80
+ }
81
+ if (status === 'RETRY') {
82
+ return 'purple';
83
+ }
84
+ if (status === 'SUBMITTED' || status === 'PROCESSING') {
85
+ return 'cyan';
86
+ }
87
+ if (status === 'PENDING') {
88
+ return 'blue';
89
+ }
90
+ return 'gray';
91
+ };
92
+
93
+ const EtimsCreditNoteTable: React.FC = () => {
94
+ const { t } = useTranslation();
95
+ const [page, setPage] = useState(1);
96
+ const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
97
+ const [searchTerm, setSearchTerm] = useState('');
98
+ const debouncedSearchTerm = useDebounce(searchTerm, 300);
99
+ const { creditNotes, error, isLoading, mutate } = useEtimsCreditNotes();
100
+
101
+ const filteredCreditNotes = useMemo(() => {
102
+ const query = debouncedSearchTerm.trim().toLowerCase();
103
+
104
+ if (!query) {
105
+ return creditNotes;
106
+ }
107
+
108
+ return creditNotes.filter((creditNote) =>
109
+ [
110
+ creditNote.billId,
111
+ creditNote.status,
112
+ creditNote.traderInvoiceNumber,
113
+ creditNote.kraInvoiceNumber,
114
+ creditNote.cuInvoiceNumber,
115
+ creditNote.resultMessage,
116
+ ].some((value) => displayValue(value).toLowerCase().includes(query)),
117
+ );
118
+ }, [creditNotes, debouncedSearchTerm]);
119
+
120
+ const pagedCreditNotes = useMemo(() => {
121
+ const start = (page - 1) * pageSize;
122
+ return filteredCreditNotes.slice(start, start + pageSize);
123
+ }, [filteredCreditNotes, page, pageSize]);
124
+
125
+ const { pageSizes } = usePaginationInfo(DEFAULT_PAGE_SIZE, filteredCreditNotes.length, page, pagedCreditNotes.length);
126
+
127
+ const headers: Array<DataTableHeader> = useMemo(
128
+ () => [
129
+ { key: 'billId', header: t('billId', 'Bill ID') },
130
+ { key: 'status', header: t('status', 'Status') },
131
+ { key: 'traderInvoiceNumber', header: t('traderInvoiceNumber', 'Trader invoice') },
132
+ { key: 'kraInvoiceNumber', header: t('kraInvoiceNumber', 'KRA invoice') },
133
+ { key: 'resultMessage', header: t('resultMessage', 'Last result') },
134
+ { key: 'dateCreated', header: t('dateCreated', 'Date created') },
135
+ { key: 'confirmedAt', header: t('confirmedAt', 'Confirmed at') },
136
+ ],
137
+ [t],
138
+ );
139
+
140
+ const tableRows: Array<CreditNoteRow> = useMemo(
141
+ () =>
142
+ pagedCreditNotes.map((creditNote, index) => ({
143
+ id: creditNote.uuid ?? `credit-note-${index}`,
144
+ billId: displayValue(creditNote.billId),
145
+ status: displayValue(creditNote.status),
146
+ traderInvoiceNumber: displayValue(creditNote.traderInvoiceNumber),
147
+ kraInvoiceNumber: displayValue(creditNote.kraInvoiceNumber),
148
+ resultMessage: displayValue(creditNote.resultMessage),
149
+ dateCreated: formatNullableDatetime(creditNote.dateCreated),
150
+ confirmedAt: formatNullableDatetime(creditNote.confirmedAt),
151
+ })),
152
+ [pagedCreditNotes],
153
+ );
154
+
155
+ const openCreateWorkspace = useCallback(() => {
156
+ launchWorkspace2('etims-credit-note-form', { mutateCreditNotes: mutate });
157
+ }, [mutate]);
158
+
159
+ const openDetails = useCallback((transaction: EtimsTransaction) => {
160
+ launchWorkspace2('etims-transaction-details', { transaction });
161
+ }, []);
162
+
163
+ const handleSearchChange = useCallback((event: React.ChangeEvent<HTMLInputElement> | '', value?: string) => {
164
+ setSearchTerm(typeof event === 'string' ? value ?? '' : event.target.value);
165
+ setPage(1);
166
+ }, []);
167
+
168
+ const handlePaginationChange = useCallback(
169
+ ({ page: nextPage, pageSize: nextSize }: { page: number; pageSize: number }) => {
170
+ if (nextSize !== pageSize) {
171
+ setPageSize(nextSize);
172
+ }
173
+ setPage(nextPage);
174
+ },
175
+ [pageSize],
176
+ );
177
+
178
+ if (isLoading) {
179
+ return (
180
+ <div className={styles.container}>
181
+ <DataTableSkeleton headers={headers} rowCount={5} showHeader showToolbar />
182
+ </div>
183
+ );
184
+ }
185
+
186
+ if (error) {
187
+ return (
188
+ <div className={styles.dataTableContainer}>
189
+ <ErrorCard error={error} headerTitle={t('creditNotes', 'Credit notes')} />
190
+ </div>
191
+ );
192
+ }
193
+
194
+ return (
195
+ <div className={styles.container}>
196
+ <DataTable rows={tableRows} headers={headers} size="sm" useZebraStyles isSortable>
197
+ {({ rows, headers: tableHeaders, getHeaderProps, getRowProps, getTableProps, getTableContainerProps }) => (
198
+ <TableContainer
199
+ title={t('creditNotes', 'Credit notes')}
200
+ description={t(
201
+ 'creditNotesDescription',
202
+ 'Raise credit notes against confirmed eTIMS invoices and track their submission to KRA.',
203
+ )}
204
+ {...getTableContainerProps()}>
205
+ <TableToolbar>
206
+ <TableToolbarContent>
207
+ <TableToolbarSearch
208
+ persistent
209
+ onChange={handleSearchChange}
210
+ placeholder={t('searchCreditNotes', 'Search by bill ID, invoice, or result')}
211
+ />
212
+ <UserHasAccess privilege="Create eTIMS Credit Notes">
213
+ <Button kind="primary" size="sm" renderIcon={Add} onClick={openCreateWorkspace}>
214
+ {t('createCreditNote', 'Create credit note')}
215
+ </Button>
216
+ </UserHasAccess>
217
+ </TableToolbarContent>
218
+ </TableToolbar>
219
+
220
+ <Table {...getTableProps()} aria-label={t('creditNotes', 'Credit notes')}>
221
+ <TableHead>
222
+ <TableRow>
223
+ {tableHeaders.map((header) => (
224
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
225
+ ))}
226
+ <TableHeader aria-label={t('rowActions', 'Row actions')} />
227
+ </TableRow>
228
+ </TableHead>
229
+ <TableBody>
230
+ {rows.map((row) => {
231
+ const creditNote = pagedCreditNotes.find((item) => item.uuid === row.id);
232
+
233
+ if (!creditNote) {
234
+ return null;
235
+ }
236
+
237
+ return (
238
+ <TableRow {...getRowProps({ row })}>
239
+ {row.cells.map((cell) => {
240
+ if (cell.info.header === 'status') {
241
+ return (
242
+ <TableCell key={cell.id}>
243
+ <Tag type={statusTagType(cell.value)} size="sm">
244
+ {cell.value}
245
+ </Tag>
246
+ </TableCell>
247
+ );
248
+ }
249
+
250
+ return <TableCell key={cell.id}>{cell.value}</TableCell>;
251
+ })}
252
+ <TableCell className="cds--table-column-menu">
253
+ <OverflowMenu flipped size="sm" aria-label={t('rowActions', 'Row actions')}>
254
+ <OverflowMenuItem
255
+ itemText={t('viewDetails', 'View details')}
256
+ onClick={() => openDetails(creditNote)}
257
+ />
258
+ </OverflowMenu>
259
+ </TableCell>
260
+ </TableRow>
261
+ );
262
+ })}
263
+ </TableBody>
264
+ </Table>
265
+ </TableContainer>
266
+ )}
267
+ </DataTable>
268
+ <Pagination
269
+ itemsPerPageText={t('itemsPerPage', 'Items per page:')}
270
+ forwardText={t('nextPage', 'Next page')}
271
+ backwardText={t('previousPage', 'Previous page')}
272
+ itemRangeText={(min, max, total) => t('minMaxItems', '{{min}}-{{max}} of {{total}} items', { min, max, total })}
273
+ pageRangeText={(_current, total) => t('pageRangeText', 'of {{count}} pages', { count: total })}
274
+ page={page}
275
+ pageSize={pageSize}
276
+ pageSizes={pageSizes?.length > 0 ? pageSizes : PAGE_SIZE_OPTIONS}
277
+ totalItems={filteredCreditNotes.length}
278
+ onChange={handlePaginationChange}
279
+ />
280
+ </div>
281
+ );
282
+ };
283
+
284
+ export default EtimsCreditNoteTable;
@@ -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: 0;
11
+ border: 1px solid colors.$gray-20;
12
+
13
+ :global(.cds--table-toolbar) {
14
+ position: relative !important;
15
+ }
16
+ }
@@ -0,0 +1,96 @@
1
+ import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import useSWR from 'swr';
3
+ import { useEtimsTransactions } from '../transaction/etims-transaction.resource';
4
+ import type { EtimsTransaction } from '../transaction/etims-transaction.types';
5
+ import type { BillSearchResult, EtimsAmendmentPayload, EtimsAmendmentResult } from './etims-credit-note.types';
6
+
7
+ export const etimsAmendmentUrl = `${restBaseUrl}/cashier/etimsAmendment`;
8
+ export const cashierBillUrl = `${restBaseUrl}/cashier/bill`;
9
+
10
+ const billSearchRep =
11
+ 'custom:(uuid,id,display,receiptNumber,status,dateCreated,patient:(uuid,display),' +
12
+ 'etims:(status,etimsInvoiceNumber,cuInvoiceNumber,qrUrl,receiptAvailable),' +
13
+ 'lineItems:full)';
14
+
15
+ type BillSearchResponse = { results: Array<BillSearchResult> };
16
+
17
+ const extractErrorMessage = async (response: FetchResponse, fallback: string) => {
18
+ const payload =
19
+ (response.data as { error?: string | { message?: string; detail?: string } } | undefined) ??
20
+ (await response.json().catch(() => ({})));
21
+ if (typeof payload?.error === 'string' && payload.error.trim()) {
22
+ return payload.error;
23
+ }
24
+
25
+ if (payload?.error && typeof payload.error === 'object') {
26
+ return payload.error.message ?? payload.error.detail ?? fallback;
27
+ }
28
+
29
+ return fallback;
30
+ };
31
+
32
+ /**
33
+ * Free-text search over cashier bills, scoped to the patient's names and identifiers, that also
34
+ * pulls the eTIMS fiscal state and line items required to enqueue a credit note.
35
+ */
36
+ export function useBillSearch(query?: string) {
37
+ const trimmed = query?.trim();
38
+ const params = new URLSearchParams({ v: billSearchRep, includeVoided: 'true', limit: '20' });
39
+ if (trimmed) {
40
+ params.set('q', trimmed);
41
+ }
42
+
43
+ const { data, error, isLoading, isValidating } = useSWR<FetchResponse<BillSearchResponse>>(
44
+ trimmed ? `${cashierBillUrl}?${params.toString()}` : null,
45
+ openmrsFetch,
46
+ );
47
+
48
+ return {
49
+ bills: data?.data?.results ?? [],
50
+ error,
51
+ isLoading,
52
+ isValidating,
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Lists eTIMS credit-note transactions. The transaction search endpoint filters by status or bill,
58
+ * so the credit-note subset is derived on the client from the full transaction list.
59
+ */
60
+ export function useEtimsCreditNotes() {
61
+ const { transactions, error, isLoading, isValidating, mutate } = useEtimsTransactions({ status: 'ALL' });
62
+
63
+ const creditNotes = (transactions as Array<EtimsTransaction>).filter(
64
+ (transaction) => transaction.transactionType === 'CREDIT_NOTE',
65
+ );
66
+
67
+ return { creditNotes, error, isLoading, isValidating, mutate };
68
+ }
69
+
70
+ export async function createEtimsAmendment(payload: EtimsAmendmentPayload) {
71
+ const response = await openmrsFetch<EtimsAmendmentResult>(etimsAmendmentUrl, {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify(payload),
75
+ });
76
+
77
+ if (!response.ok) {
78
+ throw new Error(await extractErrorMessage(response, 'Failed to enqueue credit note'));
79
+ }
80
+
81
+ return response.data;
82
+ }
83
+
84
+ /**
85
+ * Line item labels come back as "uuid:label" from the cashier REST layer; keep only the label.
86
+ */
87
+ export const parseLineItemName = (value?: string | null) => {
88
+ if (!value) {
89
+ return '';
90
+ }
91
+
92
+ const separatorIndex = value.indexOf(':');
93
+ return separatorIndex === -1 ? value : value.slice(separatorIndex + 1);
94
+ };
95
+
96
+ export const hasConfirmedSale = (bill?: BillSearchResult | null) => bill?.etims?.status === 'CONFIRMED';
@@ -0,0 +1,56 @@
1
+ import type { EtimsTransactionStatus, EtimsTransactionType } from '../transaction/etims-transaction.types';
2
+
3
+ export type EtimsAmendmentType = 'FULL_CREDIT' | 'PARTIAL_CREDIT' | 'REPLACEMENT';
4
+
5
+ export interface EtimsAmendmentPayload {
6
+ type: EtimsAmendmentType;
7
+ billUuid: string;
8
+ reason?: string;
9
+ billLineItemUuid?: string;
10
+ creditNoteUuid?: string;
11
+ }
12
+
13
+ export interface EtimsAmendmentResult {
14
+ uuid: string;
15
+ billId?: number | null;
16
+ transactionType?: EtimsTransactionType | null;
17
+ status?: EtimsTransactionStatus | null;
18
+ kraInvoiceNumber?: string | null;
19
+ traderInvoiceNumber?: string | null;
20
+ resultCode?: string | null;
21
+ resultMessage?: string | null;
22
+ cuInvoiceNumber?: string | null;
23
+ receiptNumber?: string | null;
24
+ confirmedAt?: string | null;
25
+ }
26
+
27
+ export interface BillEtimsState {
28
+ status?: EtimsTransactionStatus | null;
29
+ etimsInvoiceNumber?: string | null;
30
+ cuInvoiceNumber?: string | null;
31
+ qrUrl?: string | null;
32
+ receiptAvailable?: boolean | null;
33
+ }
34
+
35
+ export interface BillLineItem {
36
+ uuid: string;
37
+ display?: string | null;
38
+ item?: string | null;
39
+ billableService?: string | null;
40
+ quantity?: number | null;
41
+ price?: number | null;
42
+ paymentStatus?: string | null;
43
+ voided?: boolean | null;
44
+ }
45
+
46
+ export interface BillSearchResult {
47
+ uuid: string;
48
+ id?: number | null;
49
+ display?: string | null;
50
+ receiptNumber?: string | null;
51
+ status?: string | null;
52
+ dateCreated?: string | null;
53
+ patient?: { uuid: string; display: string } | null;
54
+ etims?: BillEtimsState | null;
55
+ lineItems?: Array<BillLineItem> | null;
56
+ }
@@ -0,0 +1,76 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .form {
6
+ display: flex;
7
+ flex-direction: column;
8
+ justify-content: space-between;
9
+ height: 100%;
10
+ }
11
+
12
+ .formContainer {
13
+ margin: layout.$spacing-05;
14
+ display: flex;
15
+ flex-direction: column;
16
+ justify-content: flex-start;
17
+ gap: layout.$spacing-05;
18
+ }
19
+
20
+ .searchField {
21
+ display: flex;
22
+ flex-direction: column;
23
+ }
24
+
25
+ .searchStatus {
26
+ min-height: layout.$spacing-05;
27
+ margin-top: layout.$spacing-02;
28
+ }
29
+
30
+ .billSummary {
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: layout.$spacing-03;
34
+ padding: layout.$spacing-05;
35
+ background-color: colors.$gray-10;
36
+ border-radius: layout.$spacing-02;
37
+ }
38
+
39
+ .summaryRow {
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: space-between;
43
+ gap: layout.$spacing-03;
44
+ }
45
+
46
+ .summaryLabel {
47
+ @include type.type-style('label-01');
48
+ color: colors.$gray-70;
49
+ }
50
+
51
+ .tablet {
52
+ padding: layout.$spacing-06 layout.$spacing-05;
53
+ background-color: colors.$white;
54
+
55
+ & button {
56
+ max-width: 50% !important;
57
+ }
58
+ }
59
+
60
+ .desktop {
61
+ padding: 0;
62
+
63
+ & button {
64
+ max-width: 50% !important;
65
+ }
66
+ }
67
+
68
+ .buttonContainer {
69
+ max-width: 50%;
70
+ }
71
+
72
+ .inlineLoading {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: layout.$spacing-03;
76
+ }