@openmrs/esm-billing-app 1.0.2-pre.98 → 1.0.2-pre.980

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 (214) 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/2524.js +1 -0
  15. package/dist/2524.js.map +1 -0
  16. package/dist/2690.js +1 -1
  17. package/dist/3099.js +1 -1
  18. package/dist/3584.js +1 -1
  19. package/dist/3717.js +2 -0
  20. package/dist/3717.js.map +1 -0
  21. package/dist/4055.js +1 -1
  22. package/dist/4132.js +1 -1
  23. package/dist/4300.js +1 -1
  24. package/dist/4335.js +1 -1
  25. package/dist/4618.js +1 -1
  26. package/dist/4652.js +1 -1
  27. package/dist/4724.js +1 -0
  28. package/dist/4724.js.map +1 -0
  29. package/dist/4739.js +1 -1
  30. package/dist/4739.js.map +1 -1
  31. package/dist/4944.js +1 -1
  32. package/dist/5173.js +1 -1
  33. package/dist/5241.js +1 -1
  34. package/dist/5442.js +1 -1
  35. package/dist/5661.js +1 -1
  36. package/dist/6022.js +1 -1
  37. package/dist/6468.js +1 -1
  38. package/dist/6540.js +1 -1
  39. package/dist/6540.js.map +1 -1
  40. package/dist/6679.js +1 -1
  41. package/dist/6840.js +1 -1
  42. package/dist/6859.js +1 -1
  43. package/dist/7097.js +1 -1
  44. package/dist/7159.js +1 -1
  45. package/dist/723.js +1 -1
  46. package/dist/7255.js +1 -1
  47. package/dist/7255.js.map +1 -1
  48. package/dist/7617.js +1 -1
  49. package/dist/795.js +1 -1
  50. package/dist/8163.js +1 -1
  51. package/dist/8349.js +1 -1
  52. package/dist/8572.js +1 -0
  53. package/dist/8572.js.map +1 -0
  54. package/dist/8618.js +1 -1
  55. package/dist/8708.js +2 -0
  56. package/dist/{6557.js.LICENSE.txt → 8708.js.LICENSE.txt} +22 -0
  57. package/dist/8708.js.map +1 -0
  58. package/dist/890.js +1 -1
  59. package/dist/9214.js +1 -1
  60. package/dist/9538.js +1 -1
  61. package/dist/9569.js +1 -1
  62. package/dist/961.js +1 -1
  63. package/dist/961.js.map +1 -1
  64. package/dist/986.js +1 -1
  65. package/dist/9879.js +1 -1
  66. package/dist/9895.js +1 -1
  67. package/dist/9900.js +1 -1
  68. package/dist/9913.js +1 -1
  69. package/dist/main.js +1 -1
  70. package/dist/main.js.map +1 -1
  71. package/dist/openmrs-esm-billing-app.js +1 -1
  72. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +271 -285
  73. package/dist/openmrs-esm-billing-app.js.map +1 -1
  74. package/dist/routes.json +1 -1
  75. package/e2e/README.md +19 -18
  76. package/e2e/core/test.ts +1 -1
  77. package/e2e/fixtures/api.ts +1 -1
  78. package/e2e/specs/sample-test.spec.ts +0 -1
  79. package/e2e/support/github/Dockerfile +1 -1
  80. package/package.json +18 -15
  81. package/src/bill-history/bill-history.component.tsx +20 -28
  82. package/src/bill-history/bill-history.scss +4 -94
  83. package/src/bill-history/bill-history.test.tsx +37 -78
  84. package/src/bill-item-actions/bill-item-actions.scss +21 -5
  85. package/src/bill-item-actions/edit-bill-item.modal.tsx +226 -0
  86. package/src/bill-item-actions/edit-bill-item.test.tsx +233 -40
  87. package/src/billable-services/bill-waiver/bill-selection.component.tsx +5 -5
  88. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +34 -37
  89. package/src/billable-services/bill-waiver/patient-bills.component.tsx +7 -7
  90. package/src/billable-services/bill-waiver/utils.ts +13 -3
  91. package/src/billable-services/{create-edit/add-billable-service.scss → billable-service-form/billable-service-form.scss} +32 -64
  92. package/src/billable-services/billable-service-form/billable-service-form.test.tsx +1048 -0
  93. package/src/billable-services/billable-service-form/billable-service-form.workspace.tsx +515 -0
  94. package/src/billable-services/billable-service.resource.ts +71 -27
  95. package/src/billable-services/billable-services-home.component.tsx +13 -42
  96. package/src/billable-services/billable-services-left-panel-link.component.tsx +48 -0
  97. package/src/billable-services/billable-services-left-panel-menu.component.tsx +46 -0
  98. package/src/billable-services/billable-services-menu-item/item.component.tsx +5 -4
  99. package/src/billable-services/billable-services.component.tsx +156 -152
  100. package/src/billable-services/billable-services.scss +29 -0
  101. package/src/billable-services/billable-services.test.tsx +6 -49
  102. package/src/billable-services/cash-point/add-cash-point.modal.tsx +170 -0
  103. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +19 -193
  104. package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
  105. package/src/billable-services/dashboard/dashboard.component.tsx +0 -2
  106. package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +77 -0
  107. package/src/billable-services/payment-modes/payment-mode-form.modal.tsx +131 -0
  108. package/src/billable-services/payment-modes/payment-modes-config.component.tsx +139 -0
  109. package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -4
  110. package/src/billable-services-admin-card-link.component.test.tsx +2 -2
  111. package/src/billable-services-admin-card-link.component.tsx +1 -1
  112. package/src/billing-dashboard/billing-dashboard.scss +1 -1
  113. package/src/billing-form/billing-checkin-form.component.tsx +21 -17
  114. package/src/billing-form/billing-checkin-form.test.tsx +99 -26
  115. package/src/billing-form/billing-form.component.tsx +226 -289
  116. package/src/billing-form/billing-form.scss +143 -0
  117. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +1 -1
  118. package/src/billing.resource.ts +69 -74
  119. package/src/bills-table/bills-table.component.tsx +3 -3
  120. package/src/bills-table/bills-table.test.tsx +98 -54
  121. package/src/config-schema.ts +52 -24
  122. package/src/dashboard.meta.ts +4 -2
  123. package/src/helpers/functions.ts +5 -4
  124. package/src/index.ts +71 -9
  125. package/src/invoice/invoice-table.component.tsx +36 -70
  126. package/src/invoice/invoice-table.scss +8 -5
  127. package/src/invoice/invoice-table.test.tsx +273 -62
  128. package/src/invoice/invoice.component.tsx +39 -33
  129. package/src/invoice/invoice.scss +11 -4
  130. package/src/invoice/invoice.test.tsx +324 -120
  131. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +9 -9
  132. package/src/invoice/payments/payment-form/payment-form.component.tsx +43 -34
  133. package/src/invoice/payments/payment-form/payment-form.scss +5 -6
  134. package/src/invoice/payments/payment-form/payment-form.test.tsx +216 -66
  135. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  136. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  137. package/src/invoice/payments/payments.component.tsx +55 -67
  138. package/src/invoice/payments/payments.scss +4 -3
  139. package/src/invoice/payments/payments.test.tsx +282 -0
  140. package/src/invoice/payments/utils.ts +15 -27
  141. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -3
  142. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  143. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  144. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  145. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +20 -11
  146. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +95 -16
  147. package/src/invoice/printable-invoice/printable-invoice.component.tsx +21 -35
  148. package/src/left-panel-link.test.tsx +1 -4
  149. package/src/metrics-cards/metrics-cards.component.tsx +16 -6
  150. package/src/metrics-cards/metrics-cards.scss +4 -0
  151. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  152. package/src/modal/require-payment-modal.test.tsx +27 -22
  153. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +18 -19
  154. package/src/routes.json +44 -20
  155. package/src/types/index.ts +87 -24
  156. package/translations/am.json +135 -78
  157. package/translations/ar.json +136 -79
  158. package/translations/ar_SY.json +136 -79
  159. package/translations/bn.json +138 -81
  160. package/translations/de.json +136 -79
  161. package/translations/en.json +136 -79
  162. package/translations/en_US.json +136 -79
  163. package/translations/es.json +135 -78
  164. package/translations/es_MX.json +136 -79
  165. package/translations/fr.json +141 -84
  166. package/translations/he.json +135 -78
  167. package/translations/hi.json +136 -79
  168. package/translations/hi_IN.json +136 -79
  169. package/translations/id.json +136 -79
  170. package/translations/it.json +162 -105
  171. package/translations/ka.json +136 -79
  172. package/translations/km.json +135 -78
  173. package/translations/ku.json +136 -79
  174. package/translations/ky.json +136 -79
  175. package/translations/lg.json +136 -79
  176. package/translations/ne.json +136 -79
  177. package/translations/pl.json +136 -79
  178. package/translations/pt.json +136 -79
  179. package/translations/pt_BR.json +136 -79
  180. package/translations/qu.json +136 -79
  181. package/translations/ro_RO.json +222 -165
  182. package/translations/ru_RU.json +136 -79
  183. package/translations/si.json +136 -79
  184. package/translations/sw.json +136 -79
  185. package/translations/sw_KE.json +136 -79
  186. package/translations/tr.json +136 -79
  187. package/translations/tr_TR.json +136 -79
  188. package/translations/uk.json +136 -79
  189. package/translations/uz.json +136 -79
  190. package/translations/uz@Latn.json +136 -79
  191. package/translations/uz_UZ.json +136 -79
  192. package/translations/vi.json +136 -79
  193. package/translations/zh.json +136 -79
  194. package/translations/zh_CN.json +166 -109
  195. package/dist/1146.js.LICENSE.txt +0 -21
  196. package/dist/2352.js +0 -1
  197. package/dist/2352.js.map +0 -1
  198. package/dist/246.js +0 -1
  199. package/dist/246.js.map +0 -1
  200. package/dist/4689.js +0 -2
  201. package/dist/4689.js.map +0 -1
  202. package/dist/6557.js +0 -2
  203. package/dist/6557.js.map +0 -1
  204. package/dist/8638.js +0 -1
  205. package/dist/8638.js.map +0 -1
  206. package/dist/9968.js +0 -1
  207. package/dist/9968.js.map +0 -1
  208. package/src/bill-item-actions/edit-bill-item.component.tsx +0 -221
  209. package/src/billable-services/create-edit/add-billable-service.component.tsx +0 -401
  210. package/src/billable-services/create-edit/add-billable-service.test.tsx +0 -154
  211. package/src/billable-services/dashboard/service-metrics.component.tsx +0 -41
  212. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  213. package/src/invoice/payments/payments.component.test.tsx +0 -121
  214. /package/dist/{4689.js.LICENSE.txt → 3717.js.LICENSE.txt} +0 -0
@@ -1,182 +1,123 @@
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 { mutate } from 'swr';
4
4
  import {
5
5
  Button,
6
6
  ButtonSet,
7
- Form,
7
+ ComboBox,
8
+ IconButton,
8
9
  InlineLoading,
9
- RadioButton,
10
- RadioButtonGroup,
11
- Search,
12
- Stack,
13
- Table,
14
- TableBody,
15
- TableCell,
16
- TableHead,
17
- TableHeader,
18
- TableRow,
10
+ InlineNotification,
11
+ Form,
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();
39
33
  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);
34
+ const { t } = useTranslation();
35
+ const { defaultCurrency, postBilledItems } = useConfig<BillingConfig>();
46
36
  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);
51
-
52
- const toggleSearch = (choiceSelected) => {
53
- if (!isEmpty(choiceSelected)) {
54
- setDisableSearch(false);
37
+ const [selectedItems, setSelectedItems] = useState<ExtendedLineItem[]>([]);
38
+ const { data, error, isLoading } = useBillableServices();
39
+
40
+ const selectBillableItem = (item: BillableItem) => {
41
+ if (!item) return;
42
+
43
+ const existingItem = selectedItems.find((selectedItem) => selectedItem.uuid === item.uuid);
44
+ if (existingItem) {
45
+ const updatedItem = { ...existingItem, quantity: existingItem.quantity + 1 };
46
+ setSelectedItems(
47
+ [...selectedItems].map((selectedItem) => (selectedItem.uuid === item.uuid ? updatedItem : selectedItem)),
48
+ );
49
+ return;
55
50
  }
56
- setCategory(choiceSelected === 'Stock Item' ? 'Stock Item' : 'Service');
57
- };
58
51
 
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
- });
52
+ const availablePaymentMethods = item.servicePrices || [];
53
+ let defaultPrice = 0;
54
+ let selectedPaymentMethod = null;
62
55
 
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
- });
56
+ if (availablePaymentMethods.length === 1) {
57
+ const price = availablePaymentMethods[0].price;
58
+ defaultPrice = typeof price === 'number' ? price : parseFloat(price);
59
+ selectedPaymentMethod = availablePaymentMethods[0];
77
60
  }
78
61
 
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
- });
85
-
86
- const anyInvalidQuantity = updatedItems.some((item) => item.Qnty <= 0);
87
-
88
- setSaveDisabled(!isValid || anyInvalidQuantity);
62
+ const mappedItem: ExtendedLineItem = {
63
+ uuid: item.uuid,
64
+ display: item.name,
65
+ quantity: 1,
66
+ price: defaultPrice,
67
+ billableService: item.uuid,
68
+ paymentStatus: 'PENDING',
69
+ lineItemOrder: 0,
70
+ selectedPaymentMethod: selectedPaymentMethod,
71
+ availablePaymentMethods: availablePaymentMethods,
72
+ };
89
73
 
90
- const updatedGrandTotal = updatedItems.reduce((acc, item) => acc + item.Total, 0);
91
- setGrandTotal(updatedGrandTotal);
74
+ setSelectedItems([...selectedItems, mappedItem]);
92
75
  };
93
76
 
94
- const calculateTotalAfterAddBillItem = (items) => {
95
- const sum = items.reduce((acc, item) => acc + item.Price * item.Qnty, 0);
96
- setGrandTotal(sum);
77
+ const updateQuantity = (uuid: string, quantity: number) => {
78
+ const updatedItems = [...selectedItems].map((item) => (item.uuid === uuid ? { ...item, quantity } : item));
79
+ setSelectedItems(updatedItems);
97
80
  };
98
81
 
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 = '';
82
+ const removeSelectedBillableItem = (uuid: string) => {
83
+ const updatedItems = [...selectedItems].filter((item) => item.uuid !== uuid);
84
+ setSelectedItems(updatedItems);
127
85
  };
128
86
 
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);
87
+ const updatePaymentMethod = (itemUuid: string, paymentMethod: ServicePrice) => {
88
+ const updatedItems = [...selectedItems].map((item) =>
89
+ item.uuid === itemUuid
90
+ ? {
91
+ ...item,
92
+ selectedPaymentMethod: paymentMethod,
93
+ price: typeof paymentMethod.price === 'number' ? paymentMethod.price : parseFloat(paymentMethod.price),
94
+ priceName: paymentMethod.name,
95
+ priceUuid: paymentMethod.uuid,
96
+ }
97
+ : item,
98
+ );
99
+ setSelectedItems(updatedItems);
138
100
  };
139
101
 
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 [];
102
+ const validateSelectedItems = (): boolean => {
103
+ for (const item of selectedItems) {
104
+ if (item.availablePaymentMethods && item.availablePaymentMethods.length > 1 && !item.selectedPaymentMethod) {
105
+ showSnackbar({
106
+ title: t('validationError', 'Validation error'),
107
+ subtitle: t('paymentMethodRequired', 'Payment method is required for all items'),
108
+ kind: 'error',
109
+ });
110
+ return false;
111
+ }
147
112
  }
113
+ return true;
114
+ };
148
115
 
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]);
116
+ const postBillItems = async () => {
117
+ if (!validateSelectedItems()) {
118
+ return;
119
+ }
178
120
 
179
- const postBillItems = () => {
180
121
  setIsSubmitting(true);
181
122
  const bill = {
182
123
  cashPoint: postBilledItems.cashPoint,
@@ -187,165 +128,161 @@ const BillingForm: React.FC<BillingFormProps> = ({ patientUuid, closeWorkspace }
187
128
  status: 'PENDING',
188
129
  };
189
130
 
190
- billItems.forEach((item) => {
191
- const lineItem: any = {
192
- quantity: parseInt(item.Qnty),
193
- price: item.Price,
194
- priceName: 'Default',
195
- priceUuid: postBilledItems.priceUuid,
131
+ selectedItems.forEach((item) => {
132
+ const lineItem: LineItem = {
133
+ quantity: item.quantity,
134
+ price: item.price,
196
135
  lineItemOrder: 0,
197
136
  paymentStatus: 'PENDING',
137
+ billableService: item.uuid,
198
138
  };
199
139
 
200
- if (item.category === 'StockItem') {
201
- lineItem.item = item.uuid;
202
- } else {
203
- lineItem.billableService = item.uuid;
204
- }
205
-
206
- bill?.lineItems.push(lineItem);
140
+ bill.lineItems.push(lineItem);
207
141
  });
208
142
 
209
143
  const url = `${apiBasePath}bill`;
210
- processBillItems(bill).then(
211
- () => {
212
- setIsSubmitting(false);
213
-
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
- };
229
-
230
- const handleClearSearchTerm = () => {
231
- setSearchOptions([]);
144
+ try {
145
+ await processBillItems(bill);
146
+ closeWorkspace();
147
+ mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
148
+ showSnackbar({
149
+ title: t('saveBill', 'Save bill'),
150
+ subtitle: t('billProcessedSuccessfully', 'Bill processed successfully'),
151
+ kind: 'success',
152
+ });
153
+ } catch (error) {
154
+ showSnackbar({
155
+ title: t('billProcessingError', 'Bill processing error'),
156
+ kind: 'error',
157
+ subtitle: error?.message,
158
+ });
159
+ } finally {
160
+ setIsSubmitting(false);
161
+ }
232
162
  };
233
163
 
234
164
  return (
235
165
  <Form className={styles.form}>
236
166
  <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}
167
+ {isLoading ? (
168
+ <InlineLoading description={getCoreTranslation('loading') + '...'} />
169
+ ) : error ? (
170
+ <InlineNotification
171
+ kind="error"
172
+ lowContrast
173
+ title={t('errorLoadingBillableServices', 'Error loading billable services')}
174
+ subtitle={error?.message}
259
175
  />
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;
176
+ ) : (
177
+ <ComboBox
178
+ id="searchItems"
179
+ onChange={({ selectedItem: item }: { selectedItem: BillableItem }) => selectBillableItem(item)}
180
+ itemToString={(item: BillableItem) => item?.name || ''}
181
+ items={data ?? []}
182
+ titleText={t('searchItems', 'Search items and services')}
183
+ />
184
+ )}
185
+ {selectedItems && selectedItems.length > 0 && (
186
+ <div className={styles.selectedItemsContainer}>
187
+ <h4>{t('selectedItems', 'Selected items')}</h4>
188
+ {selectedItems.map((item) => (
189
+ <div key={item.uuid} className={styles.itemCard}>
190
+ <div className={styles.itemHeader}>
191
+ <span className={styles.itemName}>{item.display}</span>
192
+ <IconButton
193
+ align="top-end"
194
+ kind="ghost"
195
+ label={t('remove', 'Remove')}
196
+ onClick={() => removeSelectedBillableItem(item.uuid)}>
197
+ <TrashCanIcon size={16} />
198
+ </IconButton>
199
+ </div>
200
+
201
+ <div className={styles.itemControls}>
202
+ {item.availablePaymentMethods && item.availablePaymentMethods.length > 1 ? (
203
+ <div className={styles.controlSection}>
204
+ <label>{t('selectPaymentMethod', 'Select payment method')}</label>
205
+ <ComboBox
206
+ id={`payment-method-${item.uuid}`}
207
+ items={item.availablePaymentMethods}
208
+ itemToString={(method: ServicePrice) =>
209
+ method
210
+ ? `${method.name} - ${convertToCurrency(
211
+ typeof method.price === 'number' ? method.price : parseFloat(method.price),
212
+ defaultCurrency,
213
+ )}`
214
+ : ''
215
+ }
216
+ selectedItem={item.selectedPaymentMethod}
217
+ onChange={({ selectedItem }) => {
218
+ if (selectedItem) {
219
+ updatePaymentMethod(item.uuid, selectedItem);
220
+ }
307
221
  }}
222
+ placeholder={t('selectPaymentMethod', 'Select payment method')}
223
+ titleText=""
308
224
  />
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>
225
+ </div>
226
+ ) : (
227
+ <div className={styles.controlSection}>
228
+ <label>{t('unitPrice', 'Unit price')}</label>
229
+ <span className={styles.priceDisplay}>{convertToCurrency(item.price, defaultCurrency)}</span>
230
+ </div>
231
+ )}
232
+
233
+ <div className={styles.controlSection}>
234
+ <label>{t('quantity', 'Quantity')}</label>
235
+ <NumberInput
236
+ allowEmpty
237
+ disableWheel
238
+ hideSteppers
239
+ id={`quantity-${item.uuid}`}
240
+ min={1}
241
+ onChange={(_, { value }) => {
242
+ const number = parseFloat(String(value));
243
+ updateQuantity(item.uuid, isNaN(number) ? 1 : number);
244
+ }}
245
+ value={item.quantity}
246
+ />
247
+ </div>
248
+
249
+ <div className={styles.controlSection}>
250
+ <label>{t('total', 'Total')}</label>
251
+ <span className={styles.totalDisplay}>
252
+ {convertToCurrency(item.price * item.quantity, defaultCurrency)}
253
+ </span>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ ))}
258
+
259
+ <div className={styles.grandTotal}>
260
+ <strong>
261
+ {t('grandTotal', 'Grand total')}:{' '}
262
+ {convertToCurrency(calculateTotalAmount(selectedItems), defaultCurrency)}
263
+ </strong>
264
+ </div>
265
+ </div>
266
+ )}
348
267
  </div>
268
+
269
+ <ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
270
+ <Button className={styles.button} kind="secondary" disabled={isSubmitting} onClick={closeWorkspace}>
271
+ {t('discard', 'Discard')}
272
+ </Button>
273
+ <Button
274
+ className={styles.button}
275
+ kind="primary"
276
+ onClick={postBillItems}
277
+ disabled={isSubmitting || selectedItems.length === 0}
278
+ type="submit">
279
+ {isSubmitting ? (
280
+ <InlineLoading description={t('saving', 'Saving') + '...'} />
281
+ ) : (
282
+ t('saveAndClose', 'Save and close')
283
+ )}
284
+ </Button>
285
+ </ButtonSet>
349
286
  </Form>
350
287
  );
351
288
  };