@openmrs/esm-billing-app 1.0.2-pre.112 → 1.0.2-pre.1124

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 (241) hide show
  1. package/.eslintrc +16 -2
  2. package/README.md +54 -9
  3. package/__mocks__/bills.mock.ts +12 -0
  4. package/__mocks__/react-i18next.js +6 -5
  5. package/dist/1119.js +1 -1
  6. package/dist/1146.js +1 -2
  7. package/dist/1146.js.map +1 -1
  8. package/dist/1197.js +1 -1
  9. package/dist/1537.js +1 -0
  10. package/dist/1537.js.map +1 -0
  11. package/dist/1856.js +1 -0
  12. package/dist/1856.js.map +1 -0
  13. package/dist/2146.js +1 -1
  14. package/dist/2177.js +1 -1
  15. package/dist/2177.js.map +1 -1
  16. package/dist/2524.js +1 -0
  17. package/dist/2524.js.map +1 -0
  18. package/dist/2690.js +1 -1
  19. package/dist/3099.js +1 -1
  20. package/dist/3584.js +1 -1
  21. package/dist/3717.js +2 -0
  22. package/dist/3717.js.map +1 -0
  23. package/dist/4055.js +1 -1
  24. package/dist/4132.js +1 -1
  25. package/dist/4300.js +1 -1
  26. package/dist/4335.js +1 -1
  27. package/dist/439.js +1 -0
  28. package/dist/4618.js +1 -1
  29. package/dist/4652.js +1 -1
  30. package/dist/4724.js +1 -0
  31. package/dist/4724.js.map +1 -0
  32. package/dist/4739.js +1 -1
  33. package/dist/4739.js.map +1 -1
  34. package/dist/4944.js +1 -1
  35. package/dist/5173.js +1 -1
  36. package/dist/5241.js +1 -1
  37. package/dist/5442.js +1 -1
  38. package/dist/5661.js +1 -1
  39. package/dist/6022.js +1 -1
  40. package/dist/6468.js +1 -1
  41. package/dist/6540.js +1 -1
  42. package/dist/6540.js.map +1 -1
  43. package/dist/6589.js +1 -0
  44. package/dist/6679.js +1 -1
  45. package/dist/6840.js +1 -1
  46. package/dist/6859.js +1 -1
  47. package/dist/7097.js +1 -1
  48. package/dist/7159.js +1 -1
  49. package/dist/723.js +1 -1
  50. package/dist/7255.js +1 -1
  51. package/dist/7255.js.map +1 -1
  52. package/dist/7617.js +1 -1
  53. package/dist/795.js +1 -1
  54. package/dist/8163.js +1 -1
  55. package/dist/8349.js +1 -1
  56. package/dist/8371.js +1 -0
  57. package/dist/8572.js +1 -0
  58. package/dist/8572.js.map +1 -0
  59. package/dist/8618.js +1 -1
  60. package/dist/8708.js +2 -0
  61. package/dist/{6557.js.LICENSE.txt → 8708.js.LICENSE.txt} +22 -0
  62. package/dist/8708.js.map +1 -0
  63. package/dist/890.js +1 -1
  64. package/dist/9214.js +1 -1
  65. package/dist/9538.js +1 -1
  66. package/dist/9569.js +1 -1
  67. package/dist/961.js +1 -1
  68. package/dist/961.js.map +1 -1
  69. package/dist/986.js +1 -1
  70. package/dist/9879.js +1 -1
  71. package/dist/9895.js +1 -1
  72. package/dist/9900.js +1 -1
  73. package/dist/9913.js +1 -1
  74. package/dist/main.js +1 -1
  75. package/dist/main.js.map +1 -1
  76. package/dist/openmrs-esm-billing-app.js +1 -1
  77. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +321 -249
  78. package/dist/openmrs-esm-billing-app.js.map +1 -1
  79. package/dist/routes.json +1 -1
  80. package/e2e/README.md +19 -18
  81. package/e2e/commands/billing-operations.ts +96 -0
  82. package/e2e/commands/index.ts +3 -0
  83. package/e2e/commands/patient-operations.ts +82 -0
  84. package/e2e/commands/types.ts +73 -0
  85. package/e2e/core/test.ts +12 -1
  86. package/e2e/fixtures/api.ts +2 -3
  87. package/e2e/pages/billing-dashboard-page.ts +84 -0
  88. package/e2e/pages/billing-form-page.ts +114 -0
  89. package/e2e/pages/index.ts +4 -1
  90. package/e2e/pages/invoice-page.ts +113 -0
  91. package/e2e/pages/payment-page.ts +104 -0
  92. package/e2e/specs/billing-dashboard.spec.ts +203 -0
  93. package/e2e/specs/billing-patient-chart.spec.ts +738 -0
  94. package/e2e/support/github/Dockerfile +1 -1
  95. package/e2e/support/github/run-e2e-docker-env.sh +29 -32
  96. package/example.env +15 -1
  97. package/package.json +19 -16
  98. package/playwright.config.ts +4 -2
  99. package/src/bill-history/bill-history.component.tsx +15 -17
  100. package/src/bill-history/bill-history.scss +4 -94
  101. package/src/bill-history/bill-history.test.tsx +37 -77
  102. package/src/bill-item-actions/bill-item-actions.scss +21 -5
  103. package/src/bill-item-actions/edit-bill-item.modal.tsx +227 -0
  104. package/src/bill-item-actions/edit-bill-item.test.tsx +233 -40
  105. package/src/billable-services/bill-waiver/bill-selection.component.tsx +5 -5
  106. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +37 -39
  107. package/src/billable-services/bill-waiver/patient-bills.component.tsx +7 -7
  108. package/src/billable-services/bill-waiver/utils.ts +13 -3
  109. package/src/billable-services/{create-edit/add-billable-service.scss → billable-service-form/billable-service-form.scss} +32 -64
  110. package/src/billable-services/billable-service-form/billable-service-form.test.tsx +1099 -0
  111. package/src/billable-services/billable-service-form/billable-service-form.workspace.tsx +516 -0
  112. package/src/billable-services/billable-service.resource.ts +71 -27
  113. package/src/billable-services/billable-services-home.component.tsx +13 -42
  114. package/src/billable-services/billable-services-left-panel-link.component.tsx +50 -0
  115. package/src/billable-services/billable-services-left-panel-menu.component.tsx +48 -0
  116. package/src/billable-services/billable-services-menu-item/item.component.tsx +5 -4
  117. package/src/billable-services/billable-services.component.tsx +156 -152
  118. package/src/billable-services/billable-services.scss +29 -0
  119. package/src/billable-services/billable-services.test.tsx +6 -49
  120. package/src/billable-services/cash-point/add-cash-point.modal.tsx +170 -0
  121. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +19 -193
  122. package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
  123. package/src/billable-services/dashboard/dashboard.component.tsx +0 -2
  124. package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +77 -0
  125. package/src/billable-services/payment-modes/payment-mode-form.modal.tsx +131 -0
  126. package/src/billable-services/payment-modes/payment-modes-config.component.tsx +139 -0
  127. package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -4
  128. package/src/billable-services-admin-card-link.component.test.tsx +2 -2
  129. package/src/billable-services-admin-card-link.component.tsx +1 -1
  130. package/src/billing-dashboard/billing-dashboard.scss +1 -1
  131. package/src/billing-form/billing-checkin-form.component.tsx +29 -29
  132. package/src/billing-form/billing-checkin-form.scss +0 -7
  133. package/src/billing-form/billing-checkin-form.test.tsx +100 -27
  134. package/src/billing-form/billing-form.component.tsx +226 -284
  135. package/src/billing-form/billing-form.scss +143 -0
  136. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +47 -41
  137. package/src/billing-form/visit-attributes/visit-attributes-form.scss +16 -3
  138. package/src/billing.resource.test.ts +503 -0
  139. package/src/billing.resource.ts +68 -76
  140. package/src/bills-table/bills-table.component.tsx +17 -24
  141. package/src/bills-table/bills-table.test.tsx +98 -54
  142. package/src/config-schema.ts +84 -31
  143. package/src/dashboard.meta.ts +5 -3
  144. package/src/helpers/functions.ts +5 -4
  145. package/src/index.ts +75 -13
  146. package/src/invoice/invoice-table.component.tsx +36 -70
  147. package/src/invoice/invoice-table.scss +8 -5
  148. package/src/invoice/invoice-table.test.tsx +273 -62
  149. package/src/invoice/invoice.component.tsx +44 -38
  150. package/src/invoice/invoice.scss +11 -4
  151. package/src/invoice/invoice.test.tsx +324 -120
  152. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +9 -9
  153. package/src/invoice/payments/payment-form/payment-form.component.tsx +45 -36
  154. package/src/invoice/payments/payment-form/payment-form.scss +5 -6
  155. package/src/invoice/payments/payment-form/payment-form.test.tsx +217 -67
  156. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  157. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  158. package/src/invoice/payments/payments.component.tsx +56 -68
  159. package/src/invoice/payments/payments.scss +4 -3
  160. package/src/invoice/payments/payments.test.tsx +282 -0
  161. package/src/invoice/payments/utils.ts +15 -27
  162. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -3
  163. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  164. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  165. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  166. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +20 -11
  167. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +95 -16
  168. package/src/invoice/printable-invoice/printable-invoice.component.tsx +21 -35
  169. package/src/left-panel-link.component.tsx +9 -8
  170. package/src/left-panel-link.test.tsx +1 -4
  171. package/src/metrics-cards/metrics-cards.component.tsx +16 -6
  172. package/src/metrics-cards/metrics-cards.scss +4 -0
  173. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  174. package/src/modal/require-payment-modal.test.tsx +27 -22
  175. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +18 -19
  176. package/src/routes.json +44 -20
  177. package/src/types/index.ts +87 -24
  178. package/translations/am.json +140 -80
  179. package/translations/ar.json +141 -81
  180. package/translations/ar_SY.json +141 -81
  181. package/translations/bn.json +143 -83
  182. package/translations/cs.json +230 -0
  183. package/translations/de.json +141 -81
  184. package/translations/en.json +142 -81
  185. package/translations/en_US.json +141 -81
  186. package/translations/es.json +140 -80
  187. package/translations/es_MX.json +141 -81
  188. package/translations/fr.json +224 -164
  189. package/translations/he.json +140 -80
  190. package/translations/hi.json +141 -81
  191. package/translations/hi_IN.json +141 -81
  192. package/translations/id.json +141 -81
  193. package/translations/it.json +166 -106
  194. package/translations/ka.json +141 -81
  195. package/translations/km.json +140 -80
  196. package/translations/ku.json +141 -81
  197. package/translations/ky.json +141 -81
  198. package/translations/lg.json +141 -81
  199. package/translations/ne.json +141 -81
  200. package/translations/pl.json +141 -81
  201. package/translations/pt.json +141 -81
  202. package/translations/pt_BR.json +141 -81
  203. package/translations/qu.json +141 -81
  204. package/translations/ro_RO.json +225 -165
  205. package/translations/ru_RU.json +141 -81
  206. package/translations/si.json +141 -81
  207. package/translations/sq.json +230 -0
  208. package/translations/sw.json +141 -81
  209. package/translations/sw_KE.json +141 -81
  210. package/translations/tr.json +141 -81
  211. package/translations/tr_TR.json +141 -81
  212. package/translations/uk.json +141 -81
  213. package/translations/uz.json +141 -81
  214. package/translations/uz@Latn.json +141 -81
  215. package/translations/uz_UZ.json +141 -81
  216. package/translations/vi.json +141 -81
  217. package/translations/zh.json +141 -81
  218. package/translations/zh_CN.json +171 -111
  219. package/translations/zh_TW.json +230 -0
  220. package/dist/1146.js.LICENSE.txt +0 -21
  221. package/dist/2352.js +0 -1
  222. package/dist/2352.js.map +0 -1
  223. package/dist/246.js +0 -1
  224. package/dist/246.js.map +0 -1
  225. package/dist/4689.js +0 -2
  226. package/dist/4689.js.map +0 -1
  227. package/dist/6557.js +0 -2
  228. package/dist/6557.js.map +0 -1
  229. package/dist/8638.js +0 -1
  230. package/dist/8638.js.map +0 -1
  231. package/dist/9968.js +0 -1
  232. package/dist/9968.js.map +0 -1
  233. package/e2e/pages/home-page.ts +0 -9
  234. package/e2e/specs/sample-test.spec.ts +0 -11
  235. package/src/bill-item-actions/edit-bill-item.component.tsx +0 -221
  236. package/src/billable-services/create-edit/add-billable-service.component.tsx +0 -401
  237. package/src/billable-services/create-edit/add-billable-service.test.tsx +0 -154
  238. package/src/billable-services/dashboard/service-metrics.component.tsx +0 -41
  239. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  240. package/src/invoice/payments/payments.component.test.tsx +0 -121
  241. /package/dist/{4689.js.LICENSE.txt → 3717.js.LICENSE.txt} +0 -0
@@ -1,182 +1,126 @@
1
- import React, { useState, useEffect, useMemo } from 'react';
2
- import fuzzy from 'fuzzy';
3
- import isEmpty from 'lodash-es/isEmpty';
1
+ import React, { useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useSWRConfig } from 'swr';
4
4
  import {
5
5
  Button,
6
6
  ButtonSet,
7
+ ComboBox,
7
8
  Form,
9
+ IconButton,
8
10
  InlineLoading,
9
- RadioButton,
10
- RadioButtonGroup,
11
- Search,
12
- Stack,
13
- Table,
14
- TableBody,
15
- TableCell,
16
- TableHead,
17
- TableHeader,
18
- TableRow,
11
+ InlineNotification,
12
+ NumberInput,
19
13
  } from '@carbon/react';
20
- import { TrashCan } from '@carbon/react/icons';
21
- import { mutate } from 'swr';
22
- import { useTranslation } from 'react-i18next';
23
- import { z } from 'zod';
24
- import { showSnackbar, showToast, useConfig, useDebounce, useLayoutType } from '@openmrs/esm-framework';
14
+ import { useConfig, useLayoutType, showSnackbar, getCoreTranslation, TrashCanIcon } from '@openmrs/esm-framework';
15
+ import { processBillItems, useBillableServices } from '../billing.resource';
16
+ import { calculateTotalAmount, convertToCurrency } from '../helpers/functions';
17
+ import type { BillingConfig } from '../config-schema';
18
+ import type { BillableItem, LineItem, ServicePrice } from '../types';
25
19
  import { apiBasePath } from '../constants';
26
- import { convertToCurrency } from '../helpers';
27
- import { type BillabeItem } from '../types';
28
- import { useFetchSearchResults, processBillItems } from '../billing.resource';
29
20
  import styles from './billing-form.scss';
30
21
 
22
+ interface ExtendedLineItem extends LineItem {
23
+ selectedPaymentMethod?: ServicePrice;
24
+ availablePaymentMethods?: ServicePrice[];
25
+ }
26
+
31
27
  type BillingFormProps = {
32
28
  patientUuid: string;
33
29
  closeWorkspace: () => void;
34
30
  };
35
31
 
36
32
  const BillingForm: React.FC<BillingFormProps> = ({ patientUuid, closeWorkspace }) => {
37
- const { t } = useTranslation();
38
- const { defaultCurrency, postBilledItems } = useConfig();
33
+ const { mutate } = useSWRConfig();
39
34
  const isTablet = useLayoutType() === 'tablet';
40
-
41
- const [grandTotal, setGrandTotal] = useState(0);
42
- const [searchOptions, setSearchOptions] = useState([]);
43
- const [billItems, setBillItems] = useState([]);
44
- const [category, setCategory] = useState('');
45
- const [saveDisabled, setSaveDisabled] = useState<boolean>(false);
35
+ const { t } = useTranslation();
36
+ const { defaultCurrency, postBilledItems } = useConfig<BillingConfig>();
46
37
  const [isSubmitting, setIsSubmitting] = useState(false);
47
- const [addedItems, setAddedItems] = useState([]);
48
- const [searchTerm, setSearchTerm] = useState('');
49
- const debouncedSearchTerm = useDebounce(searchTerm);
50
- const [disableSearch, setDisableSearch] = useState<boolean>(true);
38
+ const [selectedItems, setSelectedItems] = useState<ExtendedLineItem[]>([]);
39
+ const { data, error, isLoading } = useBillableServices();
51
40
 
52
- const toggleSearch = (choiceSelected) => {
53
- if (!isEmpty(choiceSelected)) {
54
- setDisableSearch(false);
41
+ const selectBillableItem = (item: BillableItem) => {
42
+ if (!item) {
43
+ return;
55
44
  }
56
- setCategory(choiceSelected === 'Stock Item' ? 'Stock Item' : 'Service');
57
- };
58
45
 
59
- const billItemSchema = z.object({
60
- Qnty: z.number().min(1, t('quantityGreaterThanZero', 'Quantity must be at least one for all items.')), // zod logic
61
- });
62
-
63
- const calculateTotal = (event, itemName) => {
64
- const quantity = parseInt(event.target.value);
65
- let isValid = true;
66
-
67
- try {
68
- billItemSchema.parse({ Qnty: quantity });
69
- } catch (error) {
70
- isValid = false;
71
- const parsedErrorMessage = JSON.parse(error.message);
72
- showToast({
73
- title: t('billItems', 'Save Bill'),
74
- kind: 'error',
75
- description: parsedErrorMessage[0].message,
76
- });
46
+ const existingItem = selectedItems.find((selectedItem) => selectedItem.uuid === item.uuid);
47
+ if (existingItem) {
48
+ const updatedItem = { ...existingItem, quantity: existingItem.quantity + 1 };
49
+ setSelectedItems(
50
+ [...selectedItems].map((selectedItem) => (selectedItem.uuid === item.uuid ? updatedItem : selectedItem)),
51
+ );
52
+ return;
77
53
  }
78
54
 
79
- const updatedItems = billItems.map((item) => {
80
- if (item.Item.toLowerCase().includes(itemName.toLowerCase())) {
81
- return { ...item, Qnty: quantity, Total: quantity > 0 ? item.Price * quantity : 0 };
82
- }
83
- return item;
84
- });
55
+ const availablePaymentMethods = item.servicePrices || [];
56
+ let defaultPrice = 0;
57
+ let selectedPaymentMethod = null;
85
58
 
86
- const anyInvalidQuantity = updatedItems.some((item) => item.Qnty <= 0);
59
+ if (availablePaymentMethods.length === 1) {
60
+ const price = availablePaymentMethods[0].price;
61
+ defaultPrice = typeof price === 'number' ? price : parseFloat(price);
62
+ selectedPaymentMethod = availablePaymentMethods[0];
63
+ }
87
64
 
88
- setSaveDisabled(!isValid || anyInvalidQuantity);
65
+ const mappedItem: ExtendedLineItem = {
66
+ uuid: item.uuid,
67
+ display: item.name,
68
+ quantity: 1,
69
+ price: defaultPrice,
70
+ billableService: item.uuid,
71
+ paymentStatus: 'PENDING',
72
+ lineItemOrder: 0,
73
+ selectedPaymentMethod: selectedPaymentMethod,
74
+ availablePaymentMethods: availablePaymentMethods,
75
+ };
89
76
 
90
- const updatedGrandTotal = updatedItems.reduce((acc, item) => acc + item.Total, 0);
91
- setGrandTotal(updatedGrandTotal);
77
+ setSelectedItems([...selectedItems, mappedItem]);
92
78
  };
93
79
 
94
- const calculateTotalAfterAddBillItem = (items) => {
95
- const sum = items.reduce((acc, item) => acc + item.Price * item.Qnty, 0);
96
- setGrandTotal(sum);
80
+ const updateQuantity = (uuid: string, quantity: number) => {
81
+ const updatedItems = [...selectedItems].map((item) => (item.uuid === uuid ? { ...item, quantity } : item));
82
+ setSelectedItems(updatedItems);
97
83
  };
98
84
 
99
- const addItemToBill = (event, itemid, itemname, itemcategory, itemPrice) => {
100
- const existingItemIndex = billItems.findIndex((item) => item.uuid === itemid);
101
-
102
- let updatedItems = [];
103
- if (existingItemIndex >= 0) {
104
- updatedItems = billItems.map((item, index) => {
105
- if (index === existingItemIndex) {
106
- const updatedQuantity = item.Qnty + 1;
107
- return { ...item, Qnty: updatedQuantity, Total: updatedQuantity * item.Price };
108
- }
109
- return item;
110
- });
111
- } else {
112
- const newItem = {
113
- uuid: itemid,
114
- Item: itemname,
115
- Qnty: 1,
116
- Price: itemPrice,
117
- Total: itemPrice,
118
- category: itemcategory,
119
- };
120
- updatedItems = [...billItems, newItem];
121
- setAddedItems([...addedItems, newItem]);
122
- }
123
-
124
- setBillItems(updatedItems);
125
- calculateTotalAfterAddBillItem(updatedItems);
126
- (document.getElementById('searchField') as HTMLInputElement).value = '';
85
+ const removeSelectedBillableItem = (uuid: string) => {
86
+ const updatedItems = [...selectedItems].filter((item) => item.uuid !== uuid);
87
+ setSelectedItems(updatedItems);
127
88
  };
128
89
 
129
- const removeItemFromBill = (uuid) => {
130
- const updatedItems = billItems.filter((item) => item.uuid !== uuid);
131
- setBillItems(updatedItems);
132
-
133
- // Update the list of added items
134
- setAddedItems(addedItems.filter((item) => item.uuid !== uuid));
135
-
136
- const updatedGrandTotal = updatedItems.reduce((acc, item) => acc + item.Total, 0);
137
- setGrandTotal(updatedGrandTotal);
90
+ const updatePaymentMethod = (itemUuid: string, paymentMethod: ServicePrice) => {
91
+ const updatedItems = [...selectedItems].map((item) =>
92
+ item.uuid === itemUuid
93
+ ? {
94
+ ...item,
95
+ selectedPaymentMethod: paymentMethod,
96
+ price: typeof paymentMethod.price === 'number' ? paymentMethod.price : parseFloat(paymentMethod.price),
97
+ priceName: paymentMethod.name,
98
+ priceUuid: paymentMethod.uuid,
99
+ }
100
+ : item,
101
+ );
102
+ setSelectedItems(updatedItems);
138
103
  };
139
104
 
140
- const { data, error, isLoading, isValidating } = useFetchSearchResults(debouncedSearchTerm, category);
141
-
142
- const handleSearchTermChange = (e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value);
143
-
144
- const filterItems = useMemo(() => {
145
- if (!debouncedSearchTerm || isLoading || error) {
146
- return [];
105
+ const validateSelectedItems = (): boolean => {
106
+ for (const item of selectedItems) {
107
+ if (item.availablePaymentMethods && item.availablePaymentMethods.length > 1 && !item.selectedPaymentMethod) {
108
+ showSnackbar({
109
+ title: t('validationError', 'Validation error'),
110
+ subtitle: t('paymentMethodRequired', 'Payment method is required for all items'),
111
+ kind: 'error',
112
+ });
113
+ return false;
114
+ }
147
115
  }
116
+ return true;
117
+ };
148
118
 
149
- const res = data as { results: BillabeItem[] };
150
- const existingItemUuids = new Set(billItems.map((item) => item.uuid));
151
-
152
- const preprocessedData = res?.results
153
- ?.map((item) => {
154
- return {
155
- uuid: item.uuid || '',
156
- Item: item.commonName ? item.commonName : item.name,
157
- Qnty: 1,
158
- Price: item.commonName ? 10 : item.servicePrices[0]?.price,
159
- Total: item.commonName ? 10 : item.servicePrices[0]?.price,
160
- category: item.commonName ? 'StockItem' : 'Service',
161
- };
162
- })
163
- .filter((item) => !existingItemUuids.has(item.uuid));
164
-
165
- return debouncedSearchTerm
166
- ? fuzzy
167
- .filter(debouncedSearchTerm, preprocessedData, {
168
- extract: (o) => `${o.Item}`,
169
- })
170
- .sort((r1, r2) => r1.score - r2.score)
171
- .map((result) => result.original)
172
- : searchOptions;
173
- }, [debouncedSearchTerm, isLoading, error, data, billItems, searchOptions]);
174
-
175
- useEffect(() => {
176
- setSearchOptions(filterItems);
177
- }, [filterItems]);
119
+ const postBillItems = async () => {
120
+ if (!validateSelectedItems()) {
121
+ return;
122
+ }
178
123
 
179
- const postBillItems = () => {
180
124
  setIsSubmitting(true);
181
125
  const bill = {
182
126
  cashPoint: postBilledItems.cashPoint,
@@ -187,165 +131,163 @@ const BillingForm: React.FC<BillingFormProps> = ({ patientUuid, closeWorkspace }
187
131
  status: 'PENDING',
188
132
  };
189
133
 
190
- billItems.forEach((item) => {
191
- const lineItem: any = {
192
- quantity: parseInt(item.Qnty),
193
- price: item.Price,
194
- priceName: 'Default',
195
- priceUuid: postBilledItems.priceUuid,
134
+ selectedItems.forEach((item) => {
135
+ const lineItem: LineItem = {
136
+ quantity: item.quantity,
137
+ price: item.price,
196
138
  lineItemOrder: 0,
197
139
  paymentStatus: 'PENDING',
140
+ billableService: item.uuid,
198
141
  };
199
142
 
200
- if (item.category === 'StockItem') {
201
- lineItem.item = item.uuid;
202
- } else {
203
- lineItem.billableService = item.uuid;
204
- }
205
-
206
- bill?.lineItems.push(lineItem);
143
+ bill.lineItems.push(lineItem);
207
144
  });
208
145
 
209
146
  const url = `${apiBasePath}bill`;
210
- processBillItems(bill).then(
211
- () => {
212
- setIsSubmitting(false);
147
+ try {
148
+ await processBillItems(bill);
149
+ closeWorkspace();
213
150
 
214
- closeWorkspace();
215
- mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
216
- showSnackbar({
217
- title: t('billItems', 'Save Bill'),
218
- subtitle: 'Bill processing has been successful',
219
- kind: 'success',
220
- timeoutInMs: 3000,
221
- });
222
- },
223
- (error) => {
224
- setIsSubmitting(false);
225
- showSnackbar({ title: 'Bill processing error', kind: 'error', subtitle: error?.message });
226
- },
227
- );
228
- };
151
+ mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
229
152
 
230
- const handleClearSearchTerm = () => {
231
- setSearchOptions([]);
153
+ showSnackbar({
154
+ title: t('billProcessed', 'Bill processed'),
155
+ subtitle: t('billProcessedSuccessfully', 'Bill processed successfully'),
156
+ kind: 'success',
157
+ });
158
+ } catch (error) {
159
+ showSnackbar({
160
+ title: t('billProcessingError', 'Bill processing error'),
161
+ kind: 'error',
162
+ subtitle: error?.message,
163
+ });
164
+ } finally {
165
+ setIsSubmitting(false);
166
+ }
232
167
  };
233
168
 
234
169
  return (
235
170
  <Form className={styles.form}>
236
171
  <div className={styles.grid}>
237
- <Stack>
238
- <RadioButtonGroup
239
- legendText={t('selectCategory', 'Select category')}
240
- name="radio-button-group"
241
- defaultSelected="radio-1"
242
- className={styles.mt2}
243
- onChange={toggleSearch}>
244
- <RadioButton labelText={t('stockItem', 'Stock Item')} value="Stock Item" id="stockItem" />
245
- <RadioButton labelText={t('service', 'Service')} value="Service" id="service" />
246
- </RadioButtonGroup>
247
- </Stack>
248
- <Stack>
249
- <Search
250
- size="lg"
251
- id="searchField"
252
- disabled={disableSearch}
253
- closeButtonLabelText={t('clearSearchInput', 'Clear search input')}
254
- className={styles.mt2}
255
- placeholder={t('searchItems', 'Search items and services')}
256
- labelText={t('searchItems', 'Search items and services')}
257
- onKeyUp={handleSearchTermChange}
258
- onClear={handleClearSearchTerm}
172
+ {isLoading ? (
173
+ <InlineLoading description={getCoreTranslation('loading') + '...'} />
174
+ ) : error ? (
175
+ <InlineNotification
176
+ kind="error"
177
+ lowContrast
178
+ title={t('errorLoadingBillableServices', 'Error loading billable services')}
179
+ subtitle={error?.message}
259
180
  />
260
- </Stack>
261
- <Stack>
262
- <ul className={styles.searchContent}>
263
- {searchOptions?.length > 0 &&
264
- searchOptions?.map((row) => (
265
- <li key={row.uuid} className={styles.searchItem}>
266
- <Button
267
- id={row.uuid}
268
- onClick={(e) => addItemToBill(e, row.uuid, row.Item, row.category, row.Price)}
269
- style={{ background: 'inherit', color: 'black' }}>
270
- {row.Item} Qnty.{row.Qnty} {defaultCurrency}.{row.Price}
271
- </Button>
272
- </li>
273
- ))}
274
-
275
- {searchOptions?.length === 0 && !isLoading && !!debouncedSearchTerm && (
276
- <p>{t('noResultsFound', 'No results found')}</p>
277
- )}
278
- </ul>
279
- </Stack>
280
- <Stack>
281
- <Table aria-label="sample table" className={styles.mt2}>
282
- <TableHead>
283
- <TableRow>
284
- <TableHeader>Item</TableHeader>
285
- <TableHeader>Quantity</TableHeader>
286
- <TableHeader>Price</TableHeader>
287
- <TableHeader>Total</TableHeader>
288
- <TableHeader>Action</TableHeader>
289
- </TableRow>
290
- </TableHead>
291
- <TableBody>
292
- {billItems && Array.isArray(billItems) ? (
293
- billItems.map((row) => (
294
- <TableRow>
295
- <TableCell>{row.Item}</TableCell>
296
- <TableCell>
297
- <input
298
- type="number"
299
- className={`form-control ${row.Qnty <= 0 ? styles.invalidInput : ''}`}
300
- id={row.Item}
301
- min={0}
302
- max={100}
303
- value={row.Qnty}
304
- onChange={(e) => {
305
- calculateTotal(e, row.Item);
306
- row.Qnty = e.target.value;
181
+ ) : (
182
+ <ComboBox
183
+ id="searchItems"
184
+ onChange={({ selectedItem: item }: { selectedItem: BillableItem }) => selectBillableItem(item)}
185
+ itemToString={(item: BillableItem) => item?.name || ''}
186
+ items={data ?? []}
187
+ titleText={t('searchItems', 'Search items and services')}
188
+ />
189
+ )}
190
+ {selectedItems && selectedItems.length > 0 && (
191
+ <div className={styles.selectedItemsContainer}>
192
+ <h4>{t('selectedItems', 'Selected items')}</h4>
193
+ {selectedItems.map((item) => (
194
+ <div key={item.uuid} className={styles.itemCard}>
195
+ <div className={styles.itemHeader}>
196
+ <span className={styles.itemName}>{item.display}</span>
197
+ <IconButton
198
+ align="top-end"
199
+ kind="ghost"
200
+ label={t('remove', 'Remove')}
201
+ onClick={() => removeSelectedBillableItem(item.uuid)}>
202
+ <TrashCanIcon size={16} />
203
+ </IconButton>
204
+ </div>
205
+
206
+ <div className={styles.itemControls}>
207
+ {item.availablePaymentMethods && item.availablePaymentMethods.length > 1 ? (
208
+ <div className={styles.controlSection}>
209
+ <label>{t('selectPaymentMethod', 'Select payment method')}</label>
210
+ <ComboBox
211
+ id={`payment-method-${item.uuid}`}
212
+ items={item.availablePaymentMethods}
213
+ itemToString={(method: ServicePrice) =>
214
+ method
215
+ ? `${method.name} - ${convertToCurrency(
216
+ typeof method.price === 'number' ? method.price : parseFloat(method.price),
217
+ defaultCurrency,
218
+ )}`
219
+ : ''
220
+ }
221
+ selectedItem={item.selectedPaymentMethod}
222
+ onChange={({ selectedItem }) => {
223
+ if (selectedItem) {
224
+ updatePaymentMethod(item.uuid, selectedItem);
225
+ }
307
226
  }}
227
+ placeholder={t('selectPaymentMethod', 'Select payment method')}
228
+ titleText=""
308
229
  />
309
- </TableCell>
310
- <TableCell id={row.Item + 'Price'}>{row.Price}</TableCell>
311
- <TableCell id={row.Item + 'Total'} className="totalValue">
312
- {row.Total}
313
- </TableCell>
314
- <TableCell>
315
- <TrashCan onClick={() => removeItemFromBill(row.uuid)} className={styles.removeButton} />
316
- </TableCell>
317
- </TableRow>
318
- ))
319
- ) : (
320
- <p>Loading...</p>
321
- )}
322
- <TableRow>
323
- <TableCell colSpan={3}></TableCell>
324
- <TableCell style={{ fontWeight: 'bold' }}>{t('grandTotal', 'Grand total')}:</TableCell>
325
- <TableCell id="GrandTotalSum">{convertToCurrency(grandTotal, defaultCurrency)}</TableCell>
326
- </TableRow>
327
- </TableBody>
328
- </Table>
329
- </Stack>
330
-
331
- <ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
332
- <Button className={styles.button} kind="secondary" disabled={isSubmitting} onClick={closeWorkspace}>
333
- {t('discard', 'Discard')}
334
- </Button>
335
- <Button
336
- className={styles.button}
337
- kind="primary"
338
- onClick={postBillItems}
339
- disabled={isSubmitting || saveDisabled}
340
- type="submit">
341
- {isSubmitting ? (
342
- <InlineLoading description={t('saving', 'Saving') + '...'} />
343
- ) : (
344
- t('saveAndClose', 'Save and close')
345
- )}
346
- </Button>
347
- </ButtonSet>
230
+ </div>
231
+ ) : (
232
+ <div className={styles.controlSection}>
233
+ <label>{t('unitPrice', 'Unit price')}</label>
234
+ <span className={styles.priceDisplay}>{convertToCurrency(item.price, defaultCurrency)}</span>
235
+ </div>
236
+ )}
237
+
238
+ <div className={styles.controlSection}>
239
+ <label>{t('quantity', 'Quantity')}</label>
240
+ <NumberInput
241
+ allowEmpty
242
+ disableWheel
243
+ hideSteppers
244
+ id={`quantity-${item.uuid}`}
245
+ min={1}
246
+ onChange={(_, { value }) => {
247
+ const number = parseFloat(String(value));
248
+ updateQuantity(item.uuid, isNaN(number) ? 1 : number);
249
+ }}
250
+ value={item.quantity}
251
+ />
252
+ </div>
253
+
254
+ <div className={styles.controlSection}>
255
+ <label>{t('total', 'Total')}</label>
256
+ <span className={styles.totalDisplay}>
257
+ {convertToCurrency(item.price * item.quantity, defaultCurrency)}
258
+ </span>
259
+ </div>
260
+ </div>
261
+ </div>
262
+ ))}
263
+
264
+ <div className={styles.grandTotal}>
265
+ <strong>
266
+ {t('grandTotal', 'Grand total')}:{' '}
267
+ {convertToCurrency(calculateTotalAmount(selectedItems), defaultCurrency)}
268
+ </strong>
269
+ </div>
270
+ </div>
271
+ )}
348
272
  </div>
273
+
274
+ <ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
275
+ <Button className={styles.button} kind="secondary" disabled={isSubmitting} onClick={closeWorkspace}>
276
+ {t('discard', 'Discard')}
277
+ </Button>
278
+ <Button
279
+ className={styles.button}
280
+ kind="primary"
281
+ onClick={postBillItems}
282
+ disabled={isSubmitting || selectedItems.length === 0}
283
+ type="submit">
284
+ {isSubmitting ? (
285
+ <InlineLoading description={t('saving', 'Saving') + '...'} />
286
+ ) : (
287
+ t('saveAndClose', 'Save and close')
288
+ )}
289
+ </Button>
290
+ </ButtonSet>
349
291
  </Form>
350
292
  );
351
293
  };