@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,267 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import {
3
+ Button,
4
+ ButtonSet,
5
+ ComboBox,
6
+ Form,
7
+ InlineLoading,
8
+ InlineNotification,
9
+ RadioButton,
10
+ RadioButtonGroup,
11
+ Tag,
12
+ TextArea,
13
+ } from '@carbon/react';
14
+ import {
15
+ showSnackbar,
16
+ useDebounce,
17
+ useLayoutType,
18
+ Workspace2,
19
+ type Workspace2DefinitionProps,
20
+ } from '@openmrs/esm-framework';
21
+ import classNames from 'classnames';
22
+ import { useTranslation } from 'react-i18next';
23
+ import { createEtimsAmendment, hasConfirmedSale, parseLineItemName, useBillSearch } from './etims-credit-note.resource';
24
+ import type { BillLineItem, BillSearchResult, EtimsAmendmentType } from './etims-credit-note.types';
25
+ import styles from './etims-credit-note.workspace.scss';
26
+
27
+ type EtimsCreditNoteWorkspaceProps = {
28
+ bill?: BillSearchResult | null;
29
+ mutateCreditNotes?: () => void;
30
+ };
31
+
32
+ const billToString = (bill?: BillSearchResult | null) => {
33
+ if (!bill) {
34
+ return '';
35
+ }
36
+
37
+ const parts = [bill.receiptNumber ?? `#${bill.id ?? ''}`, bill.patient?.display].filter(Boolean);
38
+ return parts.join(' · ');
39
+ };
40
+
41
+ const lineItemToString = (lineItem?: BillLineItem | null) => {
42
+ if (!lineItem) {
43
+ return '';
44
+ }
45
+
46
+ const name = parseLineItemName(lineItem.billableService || lineItem.item) || lineItem.display || lineItem.uuid;
47
+ const quantity = lineItem.quantity ?? 1;
48
+ const price = lineItem.price ?? 0;
49
+ const suffix = `${quantity} × ${price}`;
50
+ return `${name} (${suffix})`;
51
+ };
52
+
53
+ const EtimsCreditNoteWorkspace: React.FC<Workspace2DefinitionProps<EtimsCreditNoteWorkspaceProps, {}, {}>> = ({
54
+ closeWorkspace,
55
+ workspaceProps,
56
+ }) => {
57
+ const { t } = useTranslation();
58
+ const isTablet = useLayoutType() === 'tablet';
59
+
60
+ const [searchTerm, setSearchTerm] = useState('');
61
+ const debouncedSearchTerm = useDebounce(searchTerm, 300);
62
+ const { bills, isLoading: isSearching } = useBillSearch(debouncedSearchTerm);
63
+
64
+ const [selectedBill, setSelectedBill] = useState<BillSearchResult | null>(workspaceProps?.bill ?? null);
65
+ const [creditType, setCreditType] = useState<EtimsAmendmentType>('FULL_CREDIT');
66
+ const [selectedLineItem, setSelectedLineItem] = useState<BillLineItem | null>(null);
67
+ const [reason, setReason] = useState('');
68
+ const [isSubmitting, setIsSubmitting] = useState(false);
69
+
70
+ const mutateCreditNotes = workspaceProps?.mutateCreditNotes;
71
+ const lineItems = useMemo(() => selectedBill?.lineItems ?? [], [selectedBill]);
72
+ const isConfirmedSale = hasConfirmedSale(selectedBill);
73
+ const isPartial = creditType === 'PARTIAL_CREDIT';
74
+
75
+ const mergedBills = useMemo(() => {
76
+ if (!selectedBill || bills.some((bill) => bill.uuid === selectedBill.uuid)) {
77
+ return bills;
78
+ }
79
+ return [selectedBill, ...bills];
80
+ }, [bills, selectedBill]);
81
+
82
+ const canSubmit =
83
+ Boolean(selectedBill) &&
84
+ isConfirmedSale &&
85
+ reason.trim().length > 0 &&
86
+ (!isPartial || Boolean(selectedLineItem)) &&
87
+ !isSubmitting;
88
+
89
+ const handleBillSelect = (bill: BillSearchResult | null) => {
90
+ setSelectedBill(bill);
91
+ setSelectedLineItem(null);
92
+ };
93
+
94
+ const onSubmit = async (event: React.FormEvent) => {
95
+ event.preventDefault();
96
+ if (!selectedBill || !canSubmit) {
97
+ return;
98
+ }
99
+
100
+ setIsSubmitting(true);
101
+ try {
102
+ const result = await createEtimsAmendment({
103
+ type: creditType,
104
+ billUuid: selectedBill.uuid,
105
+ reason: reason.trim(),
106
+ billLineItemUuid: isPartial ? selectedLineItem?.uuid : undefined,
107
+ });
108
+
109
+ showSnackbar({
110
+ kind: 'success',
111
+ title: t('creditNoteEnqueued', 'Credit note enqueued'),
112
+ subtitle: t('creditNoteEnqueuedMessage', 'Credit note for bill {{billId}} is now {{status}}.', {
113
+ billId: result?.billId ?? selectedBill.id ?? '',
114
+ status: result?.status ?? 'PENDING',
115
+ }),
116
+ });
117
+ mutateCreditNotes?.();
118
+ closeWorkspace({ discardUnsavedChanges: true });
119
+ } catch (error) {
120
+ showSnackbar({
121
+ kind: 'error',
122
+ title: t('creditNoteError', 'Unable to create credit note'),
123
+ subtitle: (error as Error)?.message,
124
+ });
125
+ } finally {
126
+ setIsSubmitting(false);
127
+ }
128
+ };
129
+
130
+ const hasUnsavedChanges = Boolean(selectedBill) || reason.trim().length > 0;
131
+
132
+ return (
133
+ <Workspace2 title={t('createCreditNote', 'Create credit note')} hasUnsavedChanges={hasUnsavedChanges}>
134
+ <Form onSubmit={onSubmit} className={styles.form}>
135
+ <div className={styles.formContainer}>
136
+ <InlineNotification
137
+ kind="info"
138
+ lowContrast
139
+ hideCloseButton
140
+ title={t('creditNoteInfo', 'Credit note')}
141
+ subtitle={t(
142
+ 'creditNoteInfoMessage',
143
+ 'Search for the patient whose confirmed eTIMS invoice you want to credit. The scheduler submits the credit note to KRA once it is enqueued.',
144
+ )}
145
+ />
146
+
147
+ <div className={styles.searchField}>
148
+ <ComboBox<BillSearchResult>
149
+ id="creditNoteBill"
150
+ titleText={t('bill', 'Bill')}
151
+ placeholder={t('searchBillsByPatient', 'Search by patient name or identifier')}
152
+ items={mergedBills}
153
+ itemToString={billToString}
154
+ selectedItem={selectedBill}
155
+ shouldFilterItem={() => true}
156
+ onInputChange={(value) => {
157
+ if (value !== billToString(selectedBill)) {
158
+ setSearchTerm(value);
159
+ }
160
+ }}
161
+ onChange={({ selectedItem }) => handleBillSelect(selectedItem ?? null)}
162
+ />
163
+ <div className={styles.searchStatus}>
164
+ {isSearching ? <InlineLoading description={t('searching', 'Searching...')} /> : null}
165
+ </div>
166
+ </div>
167
+
168
+ {selectedBill ? (
169
+ <div className={styles.billSummary}>
170
+ <div className={styles.summaryRow}>
171
+ <span className={styles.summaryLabel}>{t('patient', 'Patient')}</span>
172
+ <span>{selectedBill.patient?.display ?? '--'}</span>
173
+ </div>
174
+ <div className={styles.summaryRow}>
175
+ <span className={styles.summaryLabel}>{t('receiptNumber', 'Receipt number')}</span>
176
+ <span>{selectedBill.receiptNumber ?? '--'}</span>
177
+ </div>
178
+ <div className={styles.summaryRow}>
179
+ <span className={styles.summaryLabel}>{t('etimsStatus', 'eTIMS status')}</span>
180
+ <Tag type={isConfirmedSale ? 'green' : 'red'} size="sm">
181
+ {selectedBill.etims?.status ?? t('noEtimsSale', 'No eTIMS sale')}
182
+ </Tag>
183
+ </div>
184
+ </div>
185
+ ) : null}
186
+
187
+ {selectedBill && !isConfirmedSale ? (
188
+ <InlineNotification
189
+ kind="warning"
190
+ lowContrast
191
+ hideCloseButton
192
+ title={t('noConfirmedSale', 'No confirmed sale')}
193
+ subtitle={t(
194
+ 'noConfirmedSaleMessage',
195
+ 'A credit note can only be raised against a bill whose eTIMS sale is CONFIRMED.',
196
+ )}
197
+ />
198
+ ) : null}
199
+
200
+ <RadioButtonGroup
201
+ legendText={t('creditType', 'Credit type')}
202
+ name="creditType"
203
+ valueSelected={creditType}
204
+ onChange={(value) => {
205
+ setCreditType(value as EtimsAmendmentType);
206
+ setSelectedLineItem(null);
207
+ }}>
208
+ <RadioButton id="credit-full" labelText={t('fullCredit', 'Full credit')} value="FULL_CREDIT" />
209
+ <RadioButton id="credit-partial" labelText={t('partialCredit', 'Partial credit')} value="PARTIAL_CREDIT" />
210
+ </RadioButtonGroup>
211
+
212
+ {isPartial ? (
213
+ <>
214
+ <ComboBox<BillLineItem>
215
+ id="creditNoteLineItem"
216
+ titleText={t('lineItem', 'Line item')}
217
+ placeholder={t('selectLineItem', 'Select the credited line')}
218
+ items={lineItems}
219
+ itemToString={lineItemToString}
220
+ selectedItem={selectedLineItem}
221
+ onChange={({ selectedItem }) => setSelectedLineItem(selectedItem ?? null)}
222
+ disabled={!selectedBill}
223
+ />
224
+ <InlineNotification
225
+ kind="info"
226
+ lowContrast
227
+ hideCloseButton
228
+ title={t('partialCredit', 'Partial credit')}
229
+ subtitle={t(
230
+ 'partialCreditHint',
231
+ 'Partial credit applies to a single line that was voided after the sale was confirmed. No replacement sale follows.',
232
+ )}
233
+ />
234
+ </>
235
+ ) : null}
236
+
237
+ <TextArea
238
+ id="creditNoteReason"
239
+ labelText={t('reason', 'Reason')}
240
+ placeholder={t('creditNoteReasonPlaceholder', 'Why is this credit note being raised?')}
241
+ value={reason}
242
+ onChange={(event) => setReason(event.target.value)}
243
+ rows={3}
244
+ />
245
+ </div>
246
+
247
+ <ButtonSet className={classNames({ [styles.tablet]: isTablet, [styles.desktop]: !isTablet })}>
248
+ <Button className={styles.buttonContainer} kind="secondary" onClick={() => closeWorkspace()}>
249
+ {t('cancel', 'Cancel')}
250
+ </Button>
251
+ <Button className={styles.buttonContainer} disabled={!canSubmit} kind="primary" type="submit">
252
+ {isSubmitting ? (
253
+ <span className={styles.inlineLoading}>
254
+ {t('submitting', 'Submitting...')}
255
+ <InlineLoading status="active" />
256
+ </span>
257
+ ) : (
258
+ t('createCreditNote', 'Create credit note')
259
+ )}
260
+ </Button>
261
+ </ButtonSet>
262
+ </Form>
263
+ </Workspace2>
264
+ );
265
+ };
266
+
267
+ export default EtimsCreditNoteWorkspace;
@@ -0,0 +1,2 @@
1
+ declare module '*.css';
2
+ declare module '*.scss';
@@ -0,0 +1,141 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import {
3
+ Button,
4
+ DataTable,
5
+ DataTableSkeleton,
6
+ Table,
7
+ TableBody,
8
+ TableCell,
9
+ TableContainer,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ TableToolbar,
14
+ TableToolbarContent,
15
+ TableToolbarSearch,
16
+ } from '@carbon/react';
17
+ import { Edit } from '@carbon/react/icons';
18
+ import { ErrorState, launchWorkspace2, useDebounce, UserHasAccess } from '@openmrs/esm-framework';
19
+ import { useTranslation } from 'react-i18next';
20
+ import Header from '../components/header/header.component';
21
+ import { etimsPropertyKeys, useEtimsGlobalProperties } from './etims-global-properties.resource';
22
+ import styles from './etims-global-properties-dashboard.scss';
23
+
24
+ const labels: Record<keyof typeof etimsPropertyKeys, string> = {
25
+ enabled: 'eTIMS enabled',
26
+ environment: 'Environment',
27
+ baseUrl: 'Base URL',
28
+ businessPin: 'Business PIN',
29
+ branchId: 'Branch ID',
30
+ connectTimeout: 'Connect timeout',
31
+ readTimeout: 'Read timeout',
32
+ maxAttempts: 'Maximum attempts',
33
+ retryInterval: 'Retry interval',
34
+ invoiceNumberSource: 'Invoice number source',
35
+ localInvoiceNumberStart: 'Local invoice number start',
36
+ paymentModeMappingFile: 'Payment mode mapping file',
37
+ apiKey: 'API key',
38
+ sandboxValidatedContractVersion: 'Sandbox validated contract version',
39
+ };
40
+
41
+ const EtimsGlobalPropertiesDashboard: React.FC = () => {
42
+ const { t } = useTranslation();
43
+ const { properties, error, isLoading, mutate } = useEtimsGlobalProperties();
44
+ const [searchTerm, setSearchTerm] = useState('');
45
+ const debouncedSearchTerm = useDebounce(searchTerm, 400);
46
+ const rows = useMemo(() => {
47
+ const configs = Object.entries(etimsPropertyKeys).map(([name, property]) => {
48
+ const recordedValue = properties.find((candidate) => candidate.property === property)?.value;
49
+ return {
50
+ id: name,
51
+ name: labels[name as keyof typeof etimsPropertyKeys],
52
+ property,
53
+ value: name === 'apiKey' && recordedValue ? '••••••••' : recordedValue || '—',
54
+ };
55
+ });
56
+
57
+ if (debouncedSearchTerm === '') {
58
+ return configs;
59
+ }
60
+
61
+ return configs.filter((config) => config.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase()));
62
+ }, [properties, debouncedSearchTerm]);
63
+ const headers = [
64
+ { key: 'name', header: t('name', 'Name') },
65
+ { key: 'property', header: t('property', 'Property') },
66
+ { key: 'value', header: t('recordedValue', 'Recorded value') },
67
+ ];
68
+
69
+ const openConfiguration = () =>
70
+ launchWorkspace2('etims-global-properties-form', {
71
+ mutateProperties: mutate,
72
+ });
73
+
74
+ const renderContent = () => {
75
+ if (isLoading) {
76
+ return <DataTableSkeleton headers={headers} rowCount={14} showHeader showToolbar />;
77
+ }
78
+
79
+ if (error) {
80
+ return <ErrorState error={error} headerTitle={t('etimsGlobalProperties', 'eTIMS global properties')} />;
81
+ }
82
+
83
+ return (
84
+ <div className={styles.container}>
85
+ <DataTable rows={rows} headers={headers} size="sm" useZebraStyles>
86
+ {({ rows: tableRows, headers: tableHeaders, getHeaderProps, getRowProps, getTableProps }) => (
87
+ <TableContainer
88
+ title={t('etimsGlobalProperties', 'eTIMS global properties')}
89
+ description={t(
90
+ 'etimsGlobalPropertiesDescription',
91
+ 'Review the global properties used by the eTIMS integration.',
92
+ )}>
93
+ <TableToolbar>
94
+ <TableToolbarContent>
95
+ <TableToolbarSearch
96
+ onChange={(event) => setSearchTerm(typeof event === 'string' ? event : event.target.value)}
97
+ size="sm"
98
+ persistent
99
+ placeholder={t('searchForGlobalProperties', 'Search for global properties')}
100
+ />
101
+ <Button size="sm" renderIcon={Edit} onClick={openConfiguration}>
102
+ {t('configure', 'Configure')}
103
+ </Button>
104
+ </TableToolbarContent>
105
+ </TableToolbar>
106
+ <Table {...getTableProps()} aria-label={t('etimsGlobalProperties', 'eTIMS global properties')}>
107
+ <TableHead>
108
+ <TableRow>
109
+ {tableHeaders.map((header) => (
110
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
111
+ ))}
112
+ </TableRow>
113
+ </TableHead>
114
+ <TableBody>
115
+ {tableRows.map((row) => (
116
+ <TableRow {...getRowProps({ row })}>
117
+ {row.cells.map((cell) => (
118
+ <TableCell key={cell.id}>{cell.value}</TableCell>
119
+ ))}
120
+ </TableRow>
121
+ ))}
122
+ </TableBody>
123
+ </Table>
124
+ </TableContainer>
125
+ )}
126
+ </DataTable>
127
+ </div>
128
+ );
129
+ };
130
+
131
+ return (
132
+ <>
133
+ <Header title={t('etimsGlobalProperties', 'eTIMS global properties')} />
134
+ <UserHasAccess privilege="Manage eTIMS Configuration">
135
+ <div className={styles.dashboard}>{renderContent()}</div>
136
+ </UserHasAccess>
137
+ </>
138
+ );
139
+ };
140
+
141
+ export default EtimsGlobalPropertiesDashboard;
@@ -0,0 +1,62 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ margin: layout.$layout-01;
7
+ border: 1px solid colors.$gray-20;
8
+
9
+ :global(.cds--table-toolbar) {
10
+ position: relative !important;
11
+ }
12
+ }
13
+
14
+ .form {
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: layout.$spacing-05;
18
+ max-width: 64rem;
19
+ }
20
+
21
+ .intro {
22
+ @include type.type-style('body-01');
23
+ color: colors.$gray-70;
24
+ margin: 0;
25
+ }
26
+
27
+ .section {
28
+ background: colors.$white;
29
+ border: 1px solid colors.$gray-20;
30
+ padding: layout.$spacing-05;
31
+ }
32
+
33
+ .sectionTitle {
34
+ @include type.type-style('heading-compact-02');
35
+ margin: 0 0 layout.$spacing-05;
36
+ }
37
+
38
+ .fields {
39
+ display: grid;
40
+ grid-template-columns: repeat(2, minmax(0, 1fr));
41
+ gap: layout.$spacing-05;
42
+ }
43
+
44
+ .fullWidth {
45
+ grid-column: 1 / -1;
46
+ }
47
+
48
+ .actions {
49
+ display: flex;
50
+ justify-content: flex-end;
51
+
52
+ button {
53
+ width: 50%;
54
+ max-width: none;
55
+ }
56
+ }
57
+
58
+ :global(.omrs-breakpoint-lt-tablet) {
59
+ .fields {
60
+ grid-template-columns: 1fr;
61
+ }
62
+ }
@@ -0,0 +1,74 @@
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
+ gap: layout.$spacing-05;
17
+ overflow-y: auto;
18
+ }
19
+
20
+ .intro {
21
+ @include type.type-style('body-01');
22
+ color: colors.$gray-70;
23
+ margin: 0;
24
+ }
25
+
26
+ .section {
27
+ background: colors.$white;
28
+ border: 1px solid colors.$gray-20;
29
+ padding: layout.$spacing-05;
30
+ }
31
+
32
+ .sectionTitle {
33
+ @include type.type-style('heading-compact-02');
34
+ margin: 0 0 layout.$spacing-05;
35
+ }
36
+
37
+ .fields {
38
+ display: grid;
39
+ grid-template-columns: repeat(2, minmax(0, 1fr));
40
+ gap: layout.$spacing-05;
41
+ }
42
+
43
+ .fullWidth {
44
+ grid-column: 1 / -1;
45
+ }
46
+
47
+ .tablet {
48
+ padding: layout.$spacing-06 layout.$spacing-05;
49
+ background-color: colors.$white;
50
+ }
51
+
52
+ .desktop {
53
+ padding: 0;
54
+
55
+ & button {
56
+ max-width: 50% !important;
57
+ }
58
+ }
59
+
60
+ .buttonContainer {
61
+ max-width: 50%;
62
+ }
63
+
64
+ .inlineLoading {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: layout.$spacing-03;
68
+ }
69
+
70
+ :global(.omrs-breakpoint-lt-tablet) {
71
+ .fields {
72
+ grid-template-columns: 1fr;
73
+ }
74
+ }