@openmrs/esm-billing-app 1.0.2-pre.74 → 1.0.2-pre.749

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 (196) hide show
  1. package/README.md +55 -9
  2. package/__mocks__/bills.mock.ts +12 -0
  3. package/__mocks__/react-i18next.js +6 -5
  4. package/dist/1119.js +1 -1
  5. package/dist/1146.js +1 -2
  6. package/dist/1146.js.map +1 -1
  7. package/dist/1197.js +1 -1
  8. package/dist/1856.js +1 -0
  9. package/dist/1856.js.map +1 -0
  10. package/dist/2146.js +1 -1
  11. package/dist/2177.js +2 -0
  12. package/dist/2177.js.LICENSE.txt +9 -0
  13. package/dist/2177.js.map +1 -0
  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/3041.js +1 -0
  18. package/dist/3041.js.map +1 -0
  19. package/dist/3099.js +1 -1
  20. package/dist/3584.js +1 -1
  21. package/dist/4055.js +1 -1
  22. package/dist/4132.js +1 -1
  23. package/dist/4225.js +1 -0
  24. package/dist/4225.js.map +1 -0
  25. package/dist/4300.js +1 -1
  26. package/dist/4335.js +1 -1
  27. package/dist/4618.js +1 -1
  28. package/dist/4652.js +1 -1
  29. package/dist/4724.js +1 -0
  30. package/dist/4724.js.map +1 -0
  31. package/dist/4739.js +1 -1
  32. package/dist/4739.js.map +1 -1
  33. package/dist/4944.js +1 -1
  34. package/dist/5173.js +1 -1
  35. package/dist/5241.js +1 -1
  36. package/dist/5422.js +1 -0
  37. package/dist/5422.js.map +1 -0
  38. package/dist/5442.js +1 -1
  39. package/dist/5661.js +1 -1
  40. package/dist/6022.js +1 -1
  41. package/dist/6468.js +1 -1
  42. package/dist/6540.js +1 -1
  43. package/dist/6540.js.map +1 -1
  44. package/dist/6606.js +1 -0
  45. package/dist/6606.js.map +1 -0
  46. package/dist/6679.js +1 -1
  47. package/dist/6840.js +1 -1
  48. package/dist/6859.js +1 -1
  49. package/dist/7097.js +1 -1
  50. package/dist/7159.js +1 -1
  51. package/dist/723.js +1 -1
  52. package/dist/7452.js +2 -0
  53. package/dist/7452.js.map +1 -0
  54. package/dist/7617.js +1 -1
  55. package/dist/795.js +1 -0
  56. package/dist/8163.js +1 -1
  57. package/dist/8349.js +1 -1
  58. package/dist/8618.js +1 -1
  59. package/dist/890.js +1 -1
  60. package/dist/8930.js +2 -0
  61. package/dist/{6525.js.LICENSE.txt → 8930.js.LICENSE.txt} +16 -4
  62. package/dist/8930.js.map +1 -0
  63. package/dist/9214.js +1 -1
  64. package/dist/942.js +1 -0
  65. package/dist/942.js.map +1 -0
  66. package/dist/9538.js +1 -1
  67. package/dist/9569.js +1 -0
  68. package/dist/961.js +1 -1
  69. package/dist/961.js.map +1 -1
  70. package/dist/986.js +1 -1
  71. package/dist/9879.js +1 -1
  72. package/dist/9895.js +1 -1
  73. package/dist/9900.js +1 -1
  74. package/dist/9913.js +1 -1
  75. package/dist/main.js +1 -1
  76. package/dist/main.js.map +1 -1
  77. package/dist/openmrs-esm-billing-app.js +1 -1
  78. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +381 -231
  79. package/dist/openmrs-esm-billing-app.js.map +1 -1
  80. package/dist/routes.json +1 -1
  81. package/e2e/README.md +19 -18
  82. package/e2e/specs/sample-test.spec.ts +0 -1
  83. package/package.json +10 -10
  84. package/src/bill-history/bill-history.component.tsx +17 -25
  85. package/src/bill-history/bill-history.scss +4 -94
  86. package/src/bill-history/bill-history.test.tsx +37 -78
  87. package/src/bill-item-actions/bill-item-actions.scss +0 -4
  88. package/src/bill-item-actions/{edit-bill-item.component.tsx → edit-bill-item.modal.tsx} +58 -56
  89. package/src/bill-item-actions/edit-bill-item.test.tsx +22 -24
  90. package/src/billable-services/bill-waiver/bill-selection.component.tsx +2 -2
  91. package/src/billable-services/bill-waiver/patient-bills.component.tsx +3 -3
  92. package/src/billable-services/billable-service.resource.ts +4 -3
  93. package/src/billable-services/billable-services-home.component.tsx +1 -1
  94. package/src/billable-services/billable-services.component.tsx +115 -132
  95. package/src/billable-services/billable-services.scss +3 -0
  96. package/src/billable-services/billable-services.test.tsx +2 -45
  97. package/src/billable-services/cash-point/add-cash-point.modal.tsx +168 -0
  98. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +17 -192
  99. package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
  100. package/src/billable-services/create-edit/add-billable-service.component.tsx +28 -24
  101. package/src/billable-services/create-edit/add-billable-service.scss +2 -5
  102. package/src/billable-services/create-edit/add-billable-service.test.tsx +6 -6
  103. package/src/billable-services/create-edit/edit-billable-service.modal.tsx +50 -0
  104. package/src/billable-services/payment-modes/add-payment-mode.modal.tsx +121 -0
  105. package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +72 -0
  106. package/src/billable-services/payment-modes/payment-modes-config.component.tsx +125 -0
  107. package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -1
  108. package/src/billing-form/billing-checkin-form.component.tsx +2 -3
  109. package/src/billing-form/billing-checkin-form.test.tsx +0 -2
  110. package/src/billing-form/billing-form.component.tsx +210 -268
  111. package/src/billing-form/billing-form.scss +143 -0
  112. package/src/billing.resource.ts +16 -19
  113. package/src/bills-table/bills-table.test.tsx +97 -53
  114. package/src/config-schema.ts +52 -18
  115. package/src/dashboard.meta.ts +4 -2
  116. package/src/helpers/functions.ts +5 -4
  117. package/src/index.ts +17 -6
  118. package/src/invoice/invoice-table.component.tsx +24 -54
  119. package/src/invoice/invoice-table.scss +1 -5
  120. package/src/invoice/invoice-table.test.tsx +21 -47
  121. package/src/invoice/invoice.component.tsx +36 -29
  122. package/src/invoice/invoice.scss +7 -4
  123. package/src/invoice/invoice.test.tsx +22 -48
  124. package/src/invoice/payments/payment-form/payment-form.component.tsx +2 -9
  125. package/src/invoice/payments/payment-form/payment-form.test.tsx +14 -46
  126. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  127. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  128. package/src/invoice/payments/payments.component.tsx +16 -27
  129. package/src/invoice/payments/{payments.component.test.tsx → payments.test.tsx} +24 -10
  130. package/src/invoice/payments/utils.ts +4 -22
  131. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
  132. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  133. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  134. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  135. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
  136. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
  137. package/src/invoice/printable-invoice/printable-invoice.component.tsx +19 -33
  138. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  139. package/src/modal/require-payment-modal.test.tsx +25 -20
  140. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +17 -18
  141. package/src/routes.json +22 -2
  142. package/src/types/index.ts +13 -12
  143. package/translations/am.json +33 -16
  144. package/translations/ar.json +33 -16
  145. package/translations/ar_SY.json +33 -16
  146. package/translations/bn.json +38 -21
  147. package/translations/de.json +33 -16
  148. package/translations/en.json +33 -16
  149. package/translations/en_US.json +187 -0
  150. package/translations/es.json +48 -31
  151. package/translations/es_MX.json +33 -16
  152. package/translations/fr.json +47 -30
  153. package/translations/he.json +33 -16
  154. package/translations/hi.json +33 -16
  155. package/translations/hi_IN.json +33 -16
  156. package/translations/id.json +180 -163
  157. package/translations/it.json +70 -53
  158. package/translations/ka.json +187 -0
  159. package/translations/km.json +33 -16
  160. package/translations/ku.json +33 -16
  161. package/translations/ky.json +33 -16
  162. package/translations/lg.json +33 -16
  163. package/translations/ne.json +33 -16
  164. package/translations/pl.json +33 -16
  165. package/translations/pt.json +33 -16
  166. package/translations/pt_BR.json +33 -16
  167. package/translations/qu.json +33 -16
  168. package/translations/ro_RO.json +182 -165
  169. package/translations/ru_RU.json +33 -16
  170. package/translations/si.json +33 -16
  171. package/translations/sw.json +33 -16
  172. package/translations/sw_KE.json +33 -16
  173. package/translations/tr.json +33 -16
  174. package/translations/tr_TR.json +33 -16
  175. package/translations/uk.json +33 -16
  176. package/translations/uz.json +33 -16
  177. package/translations/uz@Latn.json +33 -16
  178. package/translations/uz_UZ.json +33 -16
  179. package/translations/vi.json +33 -16
  180. package/translations/zh.json +33 -16
  181. package/translations/zh_CN.json +91 -74
  182. package/dist/1146.js.LICENSE.txt +0 -21
  183. package/dist/2352.js +0 -1
  184. package/dist/2352.js.map +0 -1
  185. package/dist/246.js +0 -1
  186. package/dist/246.js.map +0 -1
  187. package/dist/6525.js +0 -2
  188. package/dist/6525.js.map +0 -1
  189. package/dist/8556.js +0 -2
  190. package/dist/8556.js.map +0 -1
  191. package/dist/8638.js +0 -1
  192. package/dist/8638.js.map +0 -1
  193. package/dist/9968.js +0 -1
  194. package/dist/9968.js.map +0 -1
  195. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  196. /package/dist/{8556.js.LICENSE.txt → 7452.js.LICENSE.txt} +0 -0
@@ -1,182 +1,128 @@
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
+ ComboBox,
7
8
  Form,
9
+ NumberInput,
8
10
  InlineLoading,
9
- RadioButton,
10
- RadioButtonGroup,
11
- Search,
12
- Stack,
11
+ InlineNotification,
13
12
  Table,
14
- TableBody,
15
- TableCell,
16
13
  TableHead,
17
- TableHeader,
14
+ TableBody,
18
15
  TableRow,
16
+ TableHeader,
17
+ TableCell,
19
18
  } from '@carbon/react';
20
19
  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';
20
+ import { useConfig, useLayoutType, showSnackbar, getCoreTranslation } from '@openmrs/esm-framework';
21
+ import { processBillItems, useBillableServices } from '../billing.resource';
22
+ import { calculateTotalAmount, convertToCurrency } from '../helpers/functions';
23
+ import type { BillingConfig } from '../config-schema';
24
+ import type { BillableItem, LineItem, ServicePrice } from '../types';
25
25
  import { apiBasePath } from '../constants';
26
- import { convertToCurrency } from '../helpers';
27
- import { type BillabeItem } from '../types';
28
- import { useFetchSearchResults, processBillItems } from '../billing.resource';
29
26
  import styles from './billing-form.scss';
30
27
 
28
+ interface ExtendedLineItem extends LineItem {
29
+ selectedPaymentMethod?: ServicePrice;
30
+ availablePaymentMethods?: ServicePrice[];
31
+ }
32
+
31
33
  type BillingFormProps = {
32
34
  patientUuid: string;
33
35
  closeWorkspace: () => void;
34
36
  };
35
37
 
36
38
  const BillingForm: React.FC<BillingFormProps> = ({ patientUuid, closeWorkspace }) => {
37
- const { t } = useTranslation();
38
- const { defaultCurrency, postBilledItems } = useConfig();
39
39
  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);
40
+ const { t } = useTranslation();
41
+ const { defaultCurrency, postBilledItems } = useConfig<BillingConfig>();
46
42
  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);
43
+ const [selectedItems, setSelectedItems] = useState<ExtendedLineItem[]>([]);
44
+ const { data, error, isLoading } = useBillableServices();
45
+
46
+ const selectBillableItem = (item: BillableItem) => {
47
+ if (!item) return;
48
+
49
+ const existingItem = selectedItems.find((selectedItem) => selectedItem.uuid === item.uuid);
50
+ if (existingItem) {
51
+ const updatedItem = { ...existingItem, quantity: existingItem.quantity + 1 };
52
+ setSelectedItems(
53
+ [...selectedItems].map((selectedItem) => (selectedItem.uuid === item.uuid ? updatedItem : selectedItem)),
54
+ );
55
+ return;
55
56
  }
56
- setCategory(choiceSelected === 'Stock Item' ? 'Stock Item' : 'Service');
57
- };
58
-
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
57
 
63
- const calculateTotal = (event, itemName) => {
64
- const quantity = parseInt(event.target.value);
65
- let isValid = true;
58
+ const availablePaymentMethods = item.servicePrices || [];
59
+ let defaultPrice = 0;
60
+ let selectedPaymentMethod = null;
66
61
 
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
- });
62
+ if (availablePaymentMethods.length === 1) {
63
+ defaultPrice = parseFloat(availablePaymentMethods[0].price);
64
+ selectedPaymentMethod = availablePaymentMethods[0];
77
65
  }
78
66
 
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);
67
+ const mappedItem: ExtendedLineItem = {
68
+ uuid: item.uuid,
69
+ display: item.name,
70
+ quantity: 1,
71
+ price: defaultPrice,
72
+ billableService: item.uuid,
73
+ paymentStatus: 'PENDING',
74
+ lineItemOrder: 0,
75
+ selectedPaymentMethod: selectedPaymentMethod,
76
+ availablePaymentMethods: availablePaymentMethods,
77
+ };
89
78
 
90
- const updatedGrandTotal = updatedItems.reduce((acc, item) => acc + item.Total, 0);
91
- setGrandTotal(updatedGrandTotal);
79
+ setSelectedItems([...selectedItems, mappedItem]);
92
80
  };
93
81
 
94
- const calculateTotalAfterAddBillItem = (items) => {
95
- const sum = items.reduce((acc, item) => acc + item.Price * item.Qnty, 0);
96
- setGrandTotal(sum);
82
+ const updateQuantity = (uuid: string, quantity: number) => {
83
+ const updatedItems = [...selectedItems].map((item) => (item.uuid === uuid ? { ...item, quantity } : item));
84
+ setSelectedItems(updatedItems);
97
85
  };
98
86
 
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 = '';
87
+ const removeSelectedBillableItem = (uuid: string) => {
88
+ const updatedItems = [...selectedItems].filter((item) => item.uuid !== uuid);
89
+ setSelectedItems(updatedItems);
127
90
  };
128
91
 
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);
92
+ const updatePaymentMethod = (itemUuid: string, paymentMethod: ServicePrice) => {
93
+ const updatedItems = [...selectedItems].map((item) =>
94
+ item.uuid === itemUuid
95
+ ? {
96
+ ...item,
97
+ selectedPaymentMethod: paymentMethod,
98
+ price: parseFloat(paymentMethod.price),
99
+ priceName: paymentMethod.name,
100
+ priceUuid: paymentMethod.uuid,
101
+ }
102
+ : item,
103
+ );
104
+ setSelectedItems(updatedItems);
138
105
  };
139
106
 
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 [];
107
+ const validateSelectedItems = (): boolean => {
108
+ for (const item of selectedItems) {
109
+ if (item.availablePaymentMethods && item.availablePaymentMethods.length > 1 && !item.selectedPaymentMethod) {
110
+ showSnackbar({
111
+ title: t('validationError', 'Validation Error'),
112
+ subtitle: t('selectPaymentMethodRequired', 'Please select a payment method for all items'),
113
+ kind: 'error',
114
+ });
115
+ return false;
116
+ }
147
117
  }
148
-
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]);
118
+ return true;
119
+ };
178
120
 
179
121
  const postBillItems = () => {
122
+ if (!validateSelectedItems()) {
123
+ return;
124
+ }
125
+
180
126
  setIsSubmitting(true);
181
127
  const bill = {
182
128
  cashPoint: postBilledItems.cashPoint,
@@ -187,23 +133,16 @@ const BillingForm: React.FC<BillingFormProps> = ({ patientUuid, closeWorkspace }
187
133
  status: 'PENDING',
188
134
  };
189
135
 
190
- billItems.forEach((item) => {
191
- const lineItem: any = {
192
- quantity: parseInt(item.Qnty),
193
- price: item.Price,
194
- priceName: 'Default',
195
- priceUuid: postBilledItems.priceUuid,
136
+ selectedItems.forEach((item) => {
137
+ const lineItem: LineItem = {
138
+ quantity: item.quantity,
139
+ price: item.price,
196
140
  lineItemOrder: 0,
197
141
  paymentStatus: 'PENDING',
142
+ billableService: item.uuid,
198
143
  };
199
144
 
200
- if (item.category === 'StockItem') {
201
- lineItem.item = item.uuid;
202
- } else {
203
- lineItem.billableService = item.uuid;
204
- }
205
-
206
- bill?.lineItems.push(lineItem);
145
+ bill.lineItems.push(lineItem);
207
146
  });
208
147
 
209
148
  const url = `${apiBasePath}bill`;
@@ -214,138 +153,141 @@ const BillingForm: React.FC<BillingFormProps> = ({ patientUuid, closeWorkspace }
214
153
  closeWorkspace();
215
154
  mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
216
155
  showSnackbar({
217
- title: t('billItems', 'Save Bill'),
218
- subtitle: 'Bill processing has been successful',
156
+ title: t('saveBill', 'Save Bill'),
157
+ subtitle: t('billProcessingSuccess', 'Bill processing has been successful'),
219
158
  kind: 'success',
220
159
  timeoutInMs: 3000,
221
160
  });
222
161
  },
223
162
  (error) => {
224
163
  setIsSubmitting(false);
225
- showSnackbar({ title: 'Bill processing error', kind: 'error', subtitle: error?.message });
164
+ showSnackbar({
165
+ title: t('billProcessingError', 'Bill processing error'),
166
+ kind: 'error',
167
+ subtitle: error?.message,
168
+ });
226
169
  },
227
170
  );
228
171
  };
229
172
 
230
- const handleClearSearchTerm = () => {
231
- setSearchOptions([]);
232
- };
233
-
234
173
  return (
235
174
  <Form className={styles.form}>
236
175
  <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}
176
+ {isLoading ? (
177
+ <InlineLoading description={getCoreTranslation('loading') + '...'} />
178
+ ) : error ? (
179
+ <InlineNotification
180
+ kind="error"
181
+ lowContrast
182
+ title={t('billErrorService', 'Bill service error')}
183
+ subtitle={t('errorLoadingBillServices', 'Error loading bill services')}
184
+ />
185
+ ) : (
186
+ <ComboBox
187
+ id="searchItems"
188
+ onChange={({ selectedItem: item }: { selectedItem: BillableItem }) => selectBillableItem(item)}
189
+ itemToString={(item: BillableItem) => item?.name || ''}
190
+ items={data ?? []}
191
+ titleText={t('searchItems', 'Search items and services')}
259
192
  />
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}>
193
+ )}
194
+ {selectedItems && selectedItems.length > 0 && (
195
+ <div className={styles.selectedItemsContainer}>
196
+ <h4>{t('selectedItems', 'Selected Items')}</h4>
197
+ {selectedItems.map((item) => (
198
+ <div key={item.uuid} className={styles.itemCard}>
199
+ <div className={styles.itemHeader}>
200
+ <span className={styles.itemName}>{item.display}</span>
266
201
  <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;
202
+ kind="ghost"
203
+ size="sm"
204
+ renderIcon={TrashCan}
205
+ iconDescription={t('remove', 'Remove')}
206
+ onClick={() => removeSelectedBillableItem(item.uuid)}
207
+ />
208
+ </div>
209
+
210
+ <div className={styles.itemControls}>
211
+ {item.availablePaymentMethods && item.availablePaymentMethods.length > 1 ? (
212
+ <div className={styles.controlSection}>
213
+ <label>{t('selectPaymentMethod', 'Select payment method')}</label>
214
+ <ComboBox
215
+ id={`payment-method-${item.uuid}`}
216
+ items={item.availablePaymentMethods}
217
+ size="md"
218
+ itemToString={(method: ServicePrice) =>
219
+ method
220
+ ? `${method.name} - ${convertToCurrency(parseFloat(method.price), defaultCurrency)}`
221
+ : ''
222
+ }
223
+ selectedItem={item.selectedPaymentMethod}
224
+ onChange={({ selectedItem }) => {
225
+ if (selectedItem) {
226
+ updatePaymentMethod(item.uuid, selectedItem);
227
+ }
307
228
  }}
229
+ placeholder={t('selectPaymentMethod', 'Select payment method')}
230
+ titleText=""
308
231
  />
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>
232
+ </div>
233
+ ) : (
234
+ <div className={styles.controlSection}>
235
+ <label>{t('unitPrice', 'Unit Price')}</label>
236
+ <span className={styles.priceDisplay}>{convertToCurrency(item.price, defaultCurrency)}</span>
237
+ </div>
238
+ )}
239
+
240
+ <div className={styles.controlSection}>
241
+ <label>{t('quantity', 'Quantity')}</label>
242
+ <NumberInput
243
+ id={`quantity-${item.uuid}`}
244
+ min={1}
245
+ value={item.quantity}
246
+ size="md"
247
+ onChange={(_, { value }) => {
248
+ const number = parseFloat(String(value));
249
+ updateQuantity(item.uuid, isNaN(number) ? 1 : number);
250
+ }}
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
  };