@openmrs/esm-billing-app 1.0.2-pre.80 → 1.0.2-pre.800

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