@openmrs/esm-billing-app 1.0.2-pre.92 → 1.0.2-pre.933

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 +225 -0
  86. package/src/bill-item-actions/edit-bill-item.test.tsx +214 -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 +28 -32
  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 +898 -0
  93. package/src/billable-services/billable-service-form/billable-service-form.workspace.tsx +504 -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 +222 -292
  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 -32
  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 -2
  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 +12 -11
  146. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
  147. package/src/invoice/printable-invoice/printable-invoice.component.tsx +20 -34
  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 +86 -23
  156. package/translations/am.json +132 -77
  157. package/translations/ar.json +133 -78
  158. package/translations/ar_SY.json +133 -78
  159. package/translations/bn.json +135 -80
  160. package/translations/de.json +133 -78
  161. package/translations/en.json +134 -79
  162. package/translations/en_US.json +133 -78
  163. package/translations/es.json +132 -77
  164. package/translations/es_MX.json +133 -78
  165. package/translations/fr.json +138 -83
  166. package/translations/he.json +132 -77
  167. package/translations/hi.json +133 -78
  168. package/translations/hi_IN.json +133 -78
  169. package/translations/id.json +133 -78
  170. package/translations/it.json +159 -104
  171. package/translations/ka.json +133 -78
  172. package/translations/km.json +132 -77
  173. package/translations/ku.json +133 -78
  174. package/translations/ky.json +133 -78
  175. package/translations/lg.json +133 -78
  176. package/translations/ne.json +133 -78
  177. package/translations/pl.json +133 -78
  178. package/translations/pt.json +133 -78
  179. package/translations/pt_BR.json +133 -78
  180. package/translations/qu.json +133 -78
  181. package/translations/ro_RO.json +220 -165
  182. package/translations/ru_RU.json +133 -78
  183. package/translations/si.json +133 -78
  184. package/translations/sw.json +133 -78
  185. package/translations/sw_KE.json +133 -78
  186. package/translations/tr.json +133 -78
  187. package/translations/tr_TR.json +133 -78
  188. package/translations/uk.json +133 -78
  189. package/translations/uz.json +133 -78
  190. package/translations/uz@Latn.json +133 -78
  191. package/translations/uz_UZ.json +133 -78
  192. package/translations/vi.json +133 -78
  193. package/translations/zh.json +133 -78
  194. package/translations/zh_CN.json +163 -108
  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,115 @@
1
- import React, { useState, useEffect, useMemo } from 'react';
2
- import fuzzy from 'fuzzy';
3
- import isEmpty from 'lodash-es/isEmpty';
4
- import {
5
- Button,
6
- ButtonSet,
7
- Form,
8
- InlineLoading,
9
- RadioButton,
10
- RadioButtonGroup,
11
- Search,
12
- Stack,
13
- Table,
14
- TableBody,
15
- TableCell,
16
- TableHead,
17
- TableHeader,
18
- TableRow,
19
- } from '@carbon/react';
20
- import { TrashCan } from '@carbon/react/icons';
21
- import { mutate } from 'swr';
1
+ import React, { useState } from 'react';
22
2
  import { useTranslation } from 'react-i18next';
23
- import { z } from 'zod';
24
- import { showSnackbar, showToast, useConfig, useDebounce, useLayoutType } from '@openmrs/esm-framework';
3
+ import { mutate } from 'swr';
4
+ import { Button, ButtonSet, ComboBox, Form, NumberInput, InlineLoading, InlineNotification } from '@carbon/react';
5
+ import { TrashCan } from '@carbon/react/icons';
6
+ import { useConfig, useLayoutType, showSnackbar, getCoreTranslation } from '@openmrs/esm-framework';
7
+ import { processBillItems, useBillableServices } from '../billing.resource';
8
+ import { calculateTotalAmount, convertToCurrency } from '../helpers/functions';
9
+ import type { BillingConfig } from '../config-schema';
10
+ import type { BillableItem, LineItem, ServicePrice } from '../types';
25
11
  import { apiBasePath } from '../constants';
26
- import { convertToCurrency } from '../helpers';
27
- import { type BillabeItem } from '../types';
28
- import { useFetchSearchResults, processBillItems } from '../billing.resource';
29
12
  import styles from './billing-form.scss';
30
13
 
14
+ interface ExtendedLineItem extends LineItem {
15
+ selectedPaymentMethod?: ServicePrice;
16
+ availablePaymentMethods?: ServicePrice[];
17
+ }
18
+
31
19
  type BillingFormProps = {
32
20
  patientUuid: string;
33
21
  closeWorkspace: () => void;
34
22
  };
35
23
 
36
24
  const BillingForm: React.FC<BillingFormProps> = ({ patientUuid, closeWorkspace }) => {
37
- const { t } = useTranslation();
38
- const { defaultCurrency, postBilledItems } = useConfig();
39
25
  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);
26
+ const { t } = useTranslation();
27
+ const { defaultCurrency, postBilledItems } = useConfig<BillingConfig>();
46
28
  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);
29
+ const [selectedItems, setSelectedItems] = useState<ExtendedLineItem[]>([]);
30
+ const { data, error, isLoading } = useBillableServices();
31
+
32
+ const selectBillableItem = (item: BillableItem) => {
33
+ if (!item) return;
34
+
35
+ const existingItem = selectedItems.find((selectedItem) => selectedItem.uuid === item.uuid);
36
+ if (existingItem) {
37
+ const updatedItem = { ...existingItem, quantity: existingItem.quantity + 1 };
38
+ setSelectedItems(
39
+ [...selectedItems].map((selectedItem) => (selectedItem.uuid === item.uuid ? updatedItem : selectedItem)),
40
+ );
41
+ return;
55
42
  }
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
43
 
63
- const calculateTotal = (event, itemName) => {
64
- const quantity = parseInt(event.target.value);
65
- let isValid = true;
44
+ const availablePaymentMethods = item.servicePrices || [];
45
+ let defaultPrice = 0;
46
+ let selectedPaymentMethod = null;
66
47
 
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
- });
48
+ if (availablePaymentMethods.length === 1) {
49
+ const price = availablePaymentMethods[0].price;
50
+ defaultPrice = typeof price === 'number' ? price : parseFloat(price);
51
+ selectedPaymentMethod = availablePaymentMethods[0];
77
52
  }
78
53
 
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);
54
+ const mappedItem: ExtendedLineItem = {
55
+ uuid: item.uuid,
56
+ display: item.name,
57
+ quantity: 1,
58
+ price: defaultPrice,
59
+ billableService: item.uuid,
60
+ paymentStatus: 'PENDING',
61
+ lineItemOrder: 0,
62
+ selectedPaymentMethod: selectedPaymentMethod,
63
+ availablePaymentMethods: availablePaymentMethods,
64
+ };
89
65
 
90
- const updatedGrandTotal = updatedItems.reduce((acc, item) => acc + item.Total, 0);
91
- setGrandTotal(updatedGrandTotal);
66
+ setSelectedItems([...selectedItems, mappedItem]);
92
67
  };
93
68
 
94
- const calculateTotalAfterAddBillItem = (items) => {
95
- const sum = items.reduce((acc, item) => acc + item.Price * item.Qnty, 0);
96
- setGrandTotal(sum);
69
+ const updateQuantity = (uuid: string, quantity: number) => {
70
+ const updatedItems = [...selectedItems].map((item) => (item.uuid === uuid ? { ...item, quantity } : item));
71
+ setSelectedItems(updatedItems);
97
72
  };
98
73
 
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 = '';
74
+ const removeSelectedBillableItem = (uuid: string) => {
75
+ const updatedItems = [...selectedItems].filter((item) => item.uuid !== uuid);
76
+ setSelectedItems(updatedItems);
127
77
  };
128
78
 
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);
79
+ const updatePaymentMethod = (itemUuid: string, paymentMethod: ServicePrice) => {
80
+ const updatedItems = [...selectedItems].map((item) =>
81
+ item.uuid === itemUuid
82
+ ? {
83
+ ...item,
84
+ selectedPaymentMethod: paymentMethod,
85
+ price: typeof paymentMethod.price === 'number' ? paymentMethod.price : parseFloat(paymentMethod.price),
86
+ priceName: paymentMethod.name,
87
+ priceUuid: paymentMethod.uuid,
88
+ }
89
+ : item,
90
+ );
91
+ setSelectedItems(updatedItems);
138
92
  };
139
93
 
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 [];
94
+ const validateSelectedItems = (): boolean => {
95
+ for (const item of selectedItems) {
96
+ if (item.availablePaymentMethods && item.availablePaymentMethods.length > 1 && !item.selectedPaymentMethod) {
97
+ showSnackbar({
98
+ title: t('validationError', 'Validation error'),
99
+ subtitle: t('paymentMethodRequired', 'Payment method is required for all items'),
100
+ kind: 'error',
101
+ });
102
+ return false;
103
+ }
147
104
  }
105
+ return true;
106
+ };
148
107
 
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]);
108
+ const postBillItems = async () => {
109
+ if (!validateSelectedItems()) {
110
+ return;
111
+ }
178
112
 
179
- const postBillItems = () => {
180
113
  setIsSubmitting(true);
181
114
  const bill = {
182
115
  cashPoint: postBilledItems.cashPoint,
@@ -187,165 +120,162 @@ const BillingForm: React.FC<BillingFormProps> = ({ patientUuid, closeWorkspace }
187
120
  status: 'PENDING',
188
121
  };
189
122
 
190
- billItems.forEach((item) => {
191
- const lineItem: any = {
192
- quantity: parseInt(item.Qnty),
193
- price: item.Price,
194
- priceName: 'Default',
195
- priceUuid: postBilledItems.priceUuid,
123
+ selectedItems.forEach((item) => {
124
+ const lineItem: LineItem = {
125
+ quantity: item.quantity,
126
+ price: item.price,
196
127
  lineItemOrder: 0,
197
128
  paymentStatus: 'PENDING',
129
+ billableService: item.uuid,
198
130
  };
199
131
 
200
- if (item.category === 'StockItem') {
201
- lineItem.item = item.uuid;
202
- } else {
203
- lineItem.billableService = item.uuid;
204
- }
205
-
206
- bill?.lineItems.push(lineItem);
132
+ bill.lineItems.push(lineItem);
207
133
  });
208
134
 
209
135
  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([]);
136
+ try {
137
+ await processBillItems(bill);
138
+ closeWorkspace();
139
+ mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
140
+ showSnackbar({
141
+ title: t('saveBill', 'Save bill'),
142
+ subtitle: t('billProcessedSuccessfully', 'Bill processed successfully'),
143
+ kind: 'success',
144
+ });
145
+ } catch (error) {
146
+ showSnackbar({
147
+ title: t('billProcessingError', 'Bill processing error'),
148
+ kind: 'error',
149
+ subtitle: error?.message,
150
+ });
151
+ } finally {
152
+ setIsSubmitting(false);
153
+ }
232
154
  };
233
155
 
234
156
  return (
235
157
  <Form className={styles.form}>
236
158
  <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}
159
+ {isLoading ? (
160
+ <InlineLoading description={getCoreTranslation('loading') + '...'} />
161
+ ) : error ? (
162
+ <InlineNotification
163
+ kind="error"
164
+ lowContrast
165
+ title={t('errorLoadingBillableServices', 'Error loading billable services')}
166
+ subtitle={error?.message}
167
+ />
168
+ ) : (
169
+ <ComboBox
170
+ id="searchItems"
171
+ onChange={({ selectedItem: item }: { selectedItem: BillableItem }) => selectBillableItem(item)}
172
+ itemToString={(item: BillableItem) => item?.name || ''}
173
+ items={data ?? []}
174
+ titleText={t('searchItems', 'Search items and services')}
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}>
176
+ )}
177
+ {selectedItems && selectedItems.length > 0 && (
178
+ <div className={styles.selectedItemsContainer}>
179
+ <h4>{t('selectedItems', 'Selected items')}</h4>
180
+ {selectedItems.map((item) => (
181
+ <div key={item.uuid} className={styles.itemCard}>
182
+ <div className={styles.itemHeader}>
183
+ <span className={styles.itemName}>{item.display}</span>
266
184
  <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;
185
+ kind="ghost"
186
+ size="sm"
187
+ renderIcon={TrashCan}
188
+ iconDescription={t('remove', 'Remove')}
189
+ onClick={() => removeSelectedBillableItem(item.uuid)}
190
+ />
191
+ </div>
192
+
193
+ <div className={styles.itemControls}>
194
+ {item.availablePaymentMethods && item.availablePaymentMethods.length > 1 ? (
195
+ <div className={styles.controlSection}>
196
+ <label>{t('selectPaymentMethod', 'Select payment method')}</label>
197
+ <ComboBox
198
+ id={`payment-method-${item.uuid}`}
199
+ items={item.availablePaymentMethods}
200
+ size="md"
201
+ itemToString={(method: ServicePrice) =>
202
+ method
203
+ ? `${method.name} - ${convertToCurrency(
204
+ typeof method.price === 'number' ? method.price : parseFloat(method.price),
205
+ defaultCurrency,
206
+ )}`
207
+ : ''
208
+ }
209
+ selectedItem={item.selectedPaymentMethod}
210
+ onChange={({ selectedItem }) => {
211
+ if (selectedItem) {
212
+ updatePaymentMethod(item.uuid, selectedItem);
213
+ }
307
214
  }}
215
+ placeholder={t('selectPaymentMethod', 'Select payment method')}
216
+ titleText=""
308
217
  />
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>
218
+ </div>
219
+ ) : (
220
+ <div className={styles.controlSection}>
221
+ <label>{t('unitPrice', 'Unit price')}</label>
222
+ <span className={styles.priceDisplay}>{convertToCurrency(item.price, defaultCurrency)}</span>
223
+ </div>
224
+ )}
225
+
226
+ <div className={styles.controlSection}>
227
+ <label>{t('quantity', 'Quantity')}</label>
228
+ <NumberInput
229
+ disableWheel
230
+ hideSteppers
231
+ id={`quantity-${item.uuid}`}
232
+ min={1}
233
+ value={item.quantity}
234
+ size="md"
235
+ onChange={(_, { value }) => {
236
+ const number = parseFloat(String(value));
237
+ updateQuantity(item.uuid, isNaN(number) ? 1 : number);
238
+ }}
239
+ />
240
+ </div>
241
+
242
+ <div className={styles.controlSection}>
243
+ <label>{t('total', 'Total')}</label>
244
+ <span className={styles.totalDisplay}>
245
+ {convertToCurrency(item.price * item.quantity, defaultCurrency)}
246
+ </span>
247
+ </div>
248
+ </div>
249
+ </div>
250
+ ))}
251
+
252
+ <div className={styles.grandTotal}>
253
+ <strong>
254
+ {t('grandTotal', 'Grand total')}:{' '}
255
+ {convertToCurrency(calculateTotalAmount(selectedItems), defaultCurrency)}
256
+ </strong>
257
+ </div>
258
+ </div>
259
+ )}
348
260
  </div>
261
+
262
+ <ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
263
+ <Button className={styles.button} kind="secondary" disabled={isSubmitting} onClick={closeWorkspace}>
264
+ {t('discard', 'Discard')}
265
+ </Button>
266
+ <Button
267
+ className={styles.button}
268
+ kind="primary"
269
+ onClick={postBillItems}
270
+ disabled={isSubmitting || selectedItems.length === 0}
271
+ type="submit">
272
+ {isSubmitting ? (
273
+ <InlineLoading description={t('saving', 'Saving') + '...'} />
274
+ ) : (
275
+ t('saveAndClose', 'Save and close')
276
+ )}
277
+ </Button>
278
+ </ButtonSet>
349
279
  </Form>
350
280
  );
351
281
  };