@openmrs/esm-billing-app 1.0.2-pre.76 → 1.0.2-pre.761

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 (198) 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 -1
  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 -1
  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 +368 -262
  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} +57 -56
  89. package/src/bill-item-actions/edit-bill-item.test.tsx +22 -25
  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 +17 -9
  93. package/src/billable-services/billable-services-home.component.tsx +1 -1
  94. package/src/billable-services/billable-services.component.tsx +142 -145
  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 +18 -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 +345 -298
  101. package/src/billable-services/create-edit/add-billable-service.scss +5 -6
  102. package/src/billable-services/create-edit/add-billable-service.test.tsx +37 -36
  103. package/src/billable-services/create-edit/edit-billable-service.modal.tsx +51 -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 -4
  108. package/src/billing-form/billing-checkin-form.component.tsx +2 -3
  109. package/src/billing-form/billing-checkin-form.test.tsx +97 -24
  110. package/src/billing-form/billing-form.component.tsx +214 -269
  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 +29 -29
  125. package/src/invoice/payments/payment-form/payment-form.scss +5 -6
  126. package/src/invoice/payments/payment-form/payment-form.test.tsx +215 -67
  127. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  128. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  129. package/src/invoice/payments/payments.component.tsx +53 -65
  130. package/src/invoice/payments/payments.test.tsx +282 -0
  131. package/src/invoice/payments/utils.ts +5 -23
  132. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
  133. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  134. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  135. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  136. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
  137. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
  138. package/src/invoice/printable-invoice/printable-invoice.component.tsx +19 -33
  139. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  140. package/src/modal/require-payment-modal.test.tsx +27 -22
  141. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +17 -18
  142. package/src/routes.json +22 -2
  143. package/src/types/index.ts +26 -17
  144. package/translations/am.json +60 -18
  145. package/translations/ar.json +60 -18
  146. package/translations/ar_SY.json +60 -18
  147. package/translations/bn.json +65 -23
  148. package/translations/de.json +60 -18
  149. package/translations/en.json +60 -18
  150. package/translations/en_US.json +60 -18
  151. package/translations/es.json +60 -18
  152. package/translations/es_MX.json +60 -18
  153. package/translations/fr.json +73 -31
  154. package/translations/he.json +60 -18
  155. package/translations/hi.json +60 -18
  156. package/translations/hi_IN.json +60 -18
  157. package/translations/id.json +61 -19
  158. package/translations/it.json +96 -54
  159. package/translations/ka.json +61 -19
  160. package/translations/km.json +60 -18
  161. package/translations/ku.json +60 -18
  162. package/translations/ky.json +60 -18
  163. package/translations/lg.json +60 -18
  164. package/translations/ne.json +60 -18
  165. package/translations/pl.json +60 -18
  166. package/translations/pt.json +60 -18
  167. package/translations/pt_BR.json +60 -18
  168. package/translations/qu.json +60 -18
  169. package/translations/ro_RO.json +206 -164
  170. package/translations/ru_RU.json +60 -18
  171. package/translations/si.json +60 -18
  172. package/translations/sw.json +60 -18
  173. package/translations/sw_KE.json +60 -18
  174. package/translations/tr.json +60 -18
  175. package/translations/tr_TR.json +60 -18
  176. package/translations/uk.json +60 -18
  177. package/translations/uz.json +60 -18
  178. package/translations/uz@Latn.json +60 -18
  179. package/translations/uz_UZ.json +60 -18
  180. package/translations/vi.json +60 -18
  181. package/translations/zh.json +60 -18
  182. package/translations/zh_CN.json +117 -75
  183. package/dist/1146.js.LICENSE.txt +0 -21
  184. package/dist/2352.js +0 -1
  185. package/dist/2352.js.map +0 -1
  186. package/dist/246.js +0 -1
  187. package/dist/246.js.map +0 -1
  188. package/dist/6525.js +0 -2
  189. package/dist/6525.js.map +0 -1
  190. package/dist/8556.js +0 -2
  191. package/dist/8556.js.map +0 -1
  192. package/dist/8638.js +0 -1
  193. package/dist/8638.js.map +0 -1
  194. package/dist/9968.js +0 -1
  195. package/dist/9968.js.map +0 -1
  196. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  197. package/src/invoice/payments/payments.component.test.tsx +0 -121
  198. /package/dist/{8556.js.LICENSE.txt → 7452.js.LICENSE.txt} +0 -0
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useRef, useState, useEffect } from 'react';
1
+ import React, { useRef, useState, useEffect, useMemo } from 'react';
2
2
  import {
3
3
  Button,
4
4
  ComboBox,
@@ -8,88 +8,160 @@ import {
8
8
  InlineLoading,
9
9
  Layer,
10
10
  Search,
11
+ Stack,
11
12
  TextInput,
12
13
  Tile,
13
14
  } from '@carbon/react';
14
- import { Add, TrashCan, WarningFilled } from '@carbon/react/icons';
15
- import { Controller, useFieldArray, useForm } from 'react-hook-form';
15
+ import { Add, TrashCan } from '@carbon/react/icons';
16
+ import { Controller, useFieldArray, useForm, useWatch } from 'react-hook-form';
16
17
  import { useTranslation } from 'react-i18next';
18
+ import { type TFunction } from 'i18next';
17
19
  import { z } from 'zod';
18
20
  import { zodResolver } from '@hookform/resolvers/zod';
19
- import { navigate, showSnackbar, useDebounce, useLayoutType } from '@openmrs/esm-framework';
21
+ import { getCoreTranslation, navigate, ResponsiveWrapper, showSnackbar, useDebounce } from '@openmrs/esm-framework';
20
22
  import {
21
- createBillableSerice,
23
+ createBillableService,
22
24
  updateBillableService,
23
25
  useConceptsSearch,
24
26
  usePaymentModes,
25
27
  useServiceTypes,
26
28
  } from '../billable-service.resource';
27
- import { type ServiceConcept } from '../../types';
29
+ import { type BillableService, type ServiceConcept, type ServicePrice } from '../../types';
28
30
  import styles from './add-billable-service.scss';
29
31
 
30
- type PaymentMode = {
32
+ interface ServiceType {
33
+ uuid: string;
34
+ display: string;
35
+ }
36
+
37
+ interface PaymentModeForm {
31
38
  paymentMode: string;
32
39
  price: string | number;
33
- };
40
+ }
34
41
 
35
- type PaymentModeFormValue = {
36
- payment: Array<PaymentMode>;
37
- };
42
+ interface BillableServiceFormData {
43
+ name: string;
44
+ shortName?: string;
45
+ serviceType: ServiceType | null;
46
+ concept?: ServiceConcept | null;
47
+ payment: PaymentModeForm[];
48
+ }
49
+
50
+ interface AddBillableServiceProps {
51
+ serviceToEdit?: BillableService;
52
+ onClose: () => void;
53
+ onServiceUpdated?: () => void;
54
+ isModal?: boolean;
55
+ }
38
56
 
39
- const servicePriceSchema = z.object({
40
- paymentMode: z.string().refine((value) => !!value, 'Payment method is required'),
41
- price: z.union([
42
- z.number().refine((value) => !!value, 'Price is required'),
43
- z.string().refine((value) => !!value, 'Price is required'),
44
- ]),
45
- });
57
+ const DEFAULT_PAYMENT_OPTION: PaymentModeForm = { paymentMode: '', price: 0 };
58
+ const MAX_NAME_LENGTH = 19;
46
59
 
47
- const paymentFormSchema = z.object({
48
- payment: z.array(servicePriceSchema).min(1, 'At least one payment option is required'),
49
- });
60
+ const createBillableServiceSchema = (t: TFunction) => {
61
+ const servicePriceSchema = z.object({
62
+ paymentMode: z
63
+ .string({
64
+ required_error: t('paymentModeRequired', 'Payment mode is required'),
65
+ })
66
+ .trim()
67
+ .min(1, t('paymentModeRequired', 'Payment mode is required')),
68
+ price: z.union([
69
+ z.number().positive(t('priceMustBeGreaterThanZero', 'Price must be greater than 0')),
70
+ z
71
+ .string({
72
+ required_error: t('priceIsRequired', 'Price is required'),
73
+ })
74
+ .trim()
75
+ .min(1, t('priceIsRequired', 'Price is required'))
76
+ .refine(
77
+ (val) => !isNaN(parseFloat(val)) && parseFloat(val) > 0,
78
+ t('priceMustBeValidPositiveNumber', 'Price must be a valid positive number'),
79
+ ),
80
+ ]),
81
+ });
50
82
 
51
- const DEFAULT_PAYMENT_OPTION = { paymentMode: '', price: 0 };
83
+ return z.object({
84
+ name: z
85
+ .string({
86
+ required_error: t('serviceNameRequired', 'Service name is required'),
87
+ })
88
+ .trim()
89
+ .min(1, t('serviceNameRequired', 'Service name is required'))
90
+ .max(
91
+ MAX_NAME_LENGTH,
92
+ t('serviceNameExceedsLimit', 'Service name cannot exceed {{MAX_NAME_LENGTH}} characters', {
93
+ MAX_NAME_LENGTH,
94
+ }),
95
+ ),
96
+ shortName: z
97
+ .string()
98
+ .max(
99
+ MAX_NAME_LENGTH,
100
+ t('shortNameExceedsLimit', 'Short name cannot exceed {{MAX_NAME_LENGTH}} characters', { MAX_NAME_LENGTH }),
101
+ )
102
+ .optional(),
103
+ serviceType: z
104
+ .object({
105
+ uuid: z.string(),
106
+ display: z.string(),
107
+ })
108
+ .nullable()
109
+ .refine((val) => val !== null, t('serviceTypeRequired', 'Service type is required')),
110
+ concept: z
111
+ .object({
112
+ uuid: z.string(),
113
+ display: z.string(),
114
+ })
115
+ .nullable()
116
+ .optional(),
117
+ payment: z.array(servicePriceSchema).min(1, t('paymentOptionRequired', 'At least one payment option is required')),
118
+ });
119
+ };
52
120
 
53
- const AddBillableService: React.FC<{ editingService?: any; onClose: () => void }> = ({ editingService, onClose }) => {
121
+ const AddBillableService: React.FC<AddBillableServiceProps> = ({
122
+ serviceToEdit,
123
+ onClose,
124
+ onServiceUpdated,
125
+ isModal = false,
126
+ }) => {
54
127
  const { t } = useTranslation();
55
128
 
56
- const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentModes();
57
- const { serviceTypes, isLoading: isLoadingServicesTypes } = useServiceTypes();
58
- const [billableServicePayload, setBillableServicePayload] = useState(editingService || {});
129
+ const { paymentModes, isLoadingPaymentModes } = usePaymentModes();
130
+ const { serviceTypes, isLoadingServiceTypes } = useServiceTypes();
131
+
132
+ const billableServiceSchema = useMemo(() => createBillableServiceSchema(t), [t]);
59
133
 
60
134
  const {
61
135
  control,
62
136
  handleSubmit,
63
- formState: { errors, isValid },
137
+ formState: { errors },
64
138
  setValue,
65
- } = useForm<any>({
139
+ reset,
140
+ } = useForm<BillableServiceFormData>({
66
141
  mode: 'all',
67
142
  defaultValues: {
68
- name: editingService?.name,
69
- serviceShortName: editingService?.shortName,
70
- serviceType: editingService?.serviceType,
71
- conceptsSearch: editingService?.concept,
72
- payment: editingService?.servicePrices || [DEFAULT_PAYMENT_OPTION],
143
+ name: serviceToEdit?.name || '',
144
+ shortName: serviceToEdit?.shortName || '',
145
+ serviceType: serviceToEdit?.serviceType || null,
146
+ concept: serviceToEdit?.concept || null,
147
+ payment: serviceToEdit?.servicePrices?.map((servicePrice: ServicePrice) => ({
148
+ paymentMode: servicePrice.paymentMode?.uuid || '',
149
+ price: servicePrice.price,
150
+ })) || [DEFAULT_PAYMENT_OPTION],
73
151
  },
74
- resolver: zodResolver(paymentFormSchema),
75
- shouldUnregister: !editingService,
152
+ resolver: zodResolver(billableServiceSchema),
76
153
  });
77
- const { fields, remove, append } = useFieldArray({ name: 'payment', control: control });
154
+ const { fields, remove, append } = useFieldArray({ name: 'payment', control });
78
155
 
79
- const handleAppendPaymentMode = useCallback(() => append(DEFAULT_PAYMENT_OPTION), [append]);
80
- const handleRemovePaymentMode = useCallback((index) => remove(index), [remove]);
156
+ const handleAppendPaymentMode = () => append(DEFAULT_PAYMENT_OPTION);
157
+ const handleRemovePaymentMode = (index: number) => remove(index);
81
158
 
82
- const isTablet = useLayoutType() === 'tablet';
83
159
  const searchInputRef = useRef(null);
84
- const handleSearchTermChange = (event: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(event.target.value);
85
160
 
86
- const [selectedConcept, setSelectedConcept] = useState<ServiceConcept>(null);
161
+ const selectedConcept = useWatch({ control, name: 'concept' });
87
162
  const [searchTerm, setSearchTerm] = useState('');
88
- const debouncedSearchTerm = useDebounce(searchTerm);
163
+ const debouncedSearchTerm = useDebounce(searchTerm.trim());
89
164
  const { searchResults, isSearching } = useConceptsSearch(debouncedSearchTerm);
90
- const handleConceptChange = useCallback((selectedConcept: any) => {
91
- setSelectedConcept(selectedConcept);
92
- }, []);
93
165
 
94
166
  const handleNavigateToServiceDashboard = () =>
95
167
  navigate({
@@ -97,66 +169,63 @@ const AddBillableService: React.FC<{ editingService?: any; onClose: () => void }
97
169
  });
98
170
 
99
171
  useEffect(() => {
100
- if (editingService && !isLoadingPaymentModes) {
101
- setBillableServicePayload(editingService);
102
- setValue('serviceName', editingService.name || '');
103
- setValue('shortName', editingService.shortName || '');
104
- setValue('serviceType', editingService.serviceType || '');
105
- setValue(
106
- 'payment',
107
- editingService.servicePrices.map((payment) => ({
172
+ if (serviceToEdit && !isLoadingPaymentModes && !isLoadingServiceTypes) {
173
+ reset({
174
+ name: serviceToEdit.name || '',
175
+ shortName: serviceToEdit.shortName || '',
176
+ serviceType: serviceToEdit.serviceType || null,
177
+ concept: serviceToEdit.concept || null,
178
+ payment: serviceToEdit.servicePrices.map((payment: ServicePrice) => ({
108
179
  paymentMode: payment.paymentMode?.uuid || '',
109
180
  price: payment.price,
110
181
  })),
111
- );
112
- setValue('conceptsSearch', editingService.concept);
113
-
114
- if (editingService.concept) {
115
- setSelectedConcept(editingService.concept);
116
- }
182
+ });
117
183
  }
118
- }, [editingService, isLoadingPaymentModes, paymentModes, serviceTypes, setValue]);
184
+ }, [serviceToEdit, isLoadingPaymentModes, reset, isLoadingServiceTypes]);
119
185
 
120
- const MAX_NAME_LENGTH = 19;
121
-
122
- const onSubmit = (data) => {
186
+ const onSubmit = async (data: BillableServiceFormData) => {
123
187
  const payload = {
124
- name: billableServicePayload.name.substring(0, MAX_NAME_LENGTH),
125
- shortName: billableServicePayload.shortName.substring(0, MAX_NAME_LENGTH),
126
- serviceType: billableServicePayload.serviceType.uuid,
188
+ name: data.name,
189
+ shortName: data.shortName || '',
190
+ serviceType: data.serviceType!.uuid,
127
191
  servicePrices: data.payment.map((payment) => {
128
192
  const mode = paymentModes.find((m) => m.uuid === payment.paymentMode);
129
193
  return {
130
194
  paymentMode: payment.paymentMode,
131
195
  name: mode?.name || 'Unknown',
132
- price: parseFloat(payment.price),
196
+ price: typeof payment.price === 'string' ? parseFloat(payment.price) : payment.price,
133
197
  };
134
198
  }),
135
199
  serviceStatus: 'ENABLED',
136
- concept: selectedConcept?.uuid,
200
+ concept: data.concept?.uuid,
137
201
  };
138
202
 
139
- const saveAction = editingService
140
- ? updateBillableService(editingService.uuid, payload)
141
- : createBillableSerice(payload);
203
+ try {
204
+ if (serviceToEdit) {
205
+ await updateBillableService(serviceToEdit.uuid, payload);
206
+ } else {
207
+ await createBillableService(payload);
208
+ }
209
+
210
+ showSnackbar({
211
+ title: t('billableService', 'Billable service'),
212
+ subtitle: serviceToEdit
213
+ ? t('updatedSuccessfully', 'Billable service updated successfully')
214
+ : t('createdSuccessfully', 'Billable service created successfully'),
215
+ kind: 'success',
216
+ });
142
217
 
143
- saveAction.then(
144
- (resp) => {
145
- showSnackbar({
146
- title: t('billableService', 'Billable service'),
147
- subtitle: editingService
148
- ? t('updatedSuccessfully', 'Billable service updated successfully')
149
- : t('createdSuccessfully', 'Billable service created successfully'),
150
- kind: 'success',
151
- timeoutInMs: 3000,
152
- });
153
- onClose();
154
- handleNavigateToServiceDashboard();
155
- },
156
- (error) => {
157
- showSnackbar({ title: t('billPaymentError', 'Bill payment error'), kind: 'error', subtitle: error?.message });
158
- },
159
- );
218
+ if (onServiceUpdated) {
219
+ onServiceUpdated();
220
+ }
221
+ handleNavigateToServiceDashboard();
222
+ } catch (error) {
223
+ showSnackbar({
224
+ title: t('billPaymentError', 'Bill payment error'),
225
+ kind: 'error',
226
+ subtitle: error instanceof Error ? error.message : String(error),
227
+ });
228
+ }
160
229
  };
161
230
 
162
231
  const getPaymentErrorMessage = () => {
@@ -167,235 +236,213 @@ const AddBillableService: React.FC<{ editingService?: any; onClose: () => void }
167
236
  return null;
168
237
  };
169
238
 
170
- if (isLoadingPaymentModes && isLoadingServicesTypes) {
239
+ if (isLoadingPaymentModes || isLoadingServiceTypes) {
171
240
  return (
172
241
  <InlineLoading
173
242
  status="active"
174
243
  iconDescription={t('loadingDescription', 'Loading')}
175
- description={t('loading', 'Loading data...')}
244
+ description={t('loadingData', 'Loading data') + '...'}
176
245
  />
177
246
  );
178
247
  }
179
248
 
180
249
  return (
181
- <Form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
182
- <h4>
183
- {editingService
184
- ? t('editBillableServices', 'Edit Billable Services')
185
- : t('addBillableServices', 'Add Billable Services')}
186
- </h4>
187
- <section className={styles.section}>
188
- <Layer>
189
- <TextInput
190
- id="serviceName"
191
- type="text"
192
- labelText={t('serviceName', 'Service Name')}
193
- size="md"
194
- value={billableServicePayload.name || ''}
195
- onChange={(e) => {
196
- const newName = e.target.value.substring(0, MAX_NAME_LENGTH);
197
- setBillableServicePayload({
198
- ...billableServicePayload,
199
- name: newName,
200
- });
201
- }}
202
- placeholder="Enter service name"
203
- maxLength={MAX_NAME_LENGTH}
204
- />
205
- {billableServicePayload.name?.length >= MAX_NAME_LENGTH && (
206
- <span className={styles.errorMessage}>
207
- {t('serviceNameExceedsLimit', 'Service Name exceeds the character limit of {{MAX_NAME_LENGTH}}.', {
208
- MAX_NAME_LENGTH,
209
- })}
210
- </span>
250
+ <Form id="billable-service-form" className={styles.form} onSubmit={handleSubmit(onSubmit)}>
251
+ <Stack gap={5}>
252
+ <h4>
253
+ {serviceToEdit
254
+ ? t('editBillableServices', 'Edit Billable Services')
255
+ : t('addBillableServices', 'Add Billable Services')}
256
+ </h4>
257
+ <section>
258
+ {serviceToEdit ? (
259
+ <FormLabel className={styles.serviceNameLabel}>{serviceToEdit.name}</FormLabel>
260
+ ) : (
261
+ <Controller
262
+ name="name"
263
+ control={control}
264
+ render={({ field }) => (
265
+ <Layer>
266
+ <TextInput
267
+ {...field}
268
+ id="serviceName"
269
+ type="text"
270
+ labelText={t('serviceName', 'Service name')}
271
+ placeholder={t('enterServiceName', 'Enter service name')}
272
+ maxLength={MAX_NAME_LENGTH}
273
+ invalid={!!errors.name}
274
+ invalidText={errors.name?.message}
275
+ />
276
+ </Layer>
277
+ )}
278
+ />
211
279
  )}
212
- </Layer>
213
- </section>
214
- <section className={styles.section}>
215
- <Layer>
216
- <TextInput
217
- id="serviceShortName"
218
- type="text"
219
- labelText={t('serviceShortName', 'Short Name')}
220
- size="md"
221
- value={billableServicePayload.shortName || ''}
222
- onChange={(e) => {
223
- const newShortName = e.target.value.substring(0, MAX_NAME_LENGTH);
224
- setBillableServicePayload({
225
- ...billableServicePayload,
226
- shortName: newShortName,
227
- });
228
- }}
229
- placeholder="Enter service short name"
230
- maxLength={MAX_NAME_LENGTH}
280
+ </section>
281
+ <section>
282
+ <Controller
283
+ name="shortName"
284
+ control={control}
285
+ render={({ field }) => (
286
+ <Layer>
287
+ <TextInput
288
+ {...field}
289
+ value={field.value || ''}
290
+ id="serviceShortName"
291
+ type="text"
292
+ labelText={t('serviceShortName', 'Short Name')}
293
+ placeholder={t('enterServiceShortName', 'Enter service short name')}
294
+ maxLength={MAX_NAME_LENGTH}
295
+ invalid={!!errors.shortName}
296
+ invalidText={errors.shortName?.message}
297
+ />
298
+ </Layer>
299
+ )}
231
300
  />
232
- {billableServicePayload.shortName?.length >= MAX_NAME_LENGTH && (
233
- <span className={styles.errorMessage}>
234
- {t('shortNameExceedsLimit', 'Short Name exceeds the character limit of {{MAX_NAME_LENGTH}}.', {
235
- MAX_NAME_LENGTH,
236
- })}
237
- </span>
238
- )}
239
- </Layer>
240
- </section>
241
- <section>
242
- <FormLabel className={styles.conceptLabel}>Associated Concept</FormLabel>
243
- <Controller
244
- name="search"
245
- control={control}
246
- render={({ field: { onChange, value, onBlur } }) => (
247
- <ResponsiveWrapper isTablet={isTablet}>
248
- <Search
249
- ref={searchInputRef}
250
- size="md"
251
- id="conceptsSearch"
252
- labelText={t('enterConcept', 'Associated concept')}
253
- placeholder={t('searchConcepts', 'Search associated concept')}
254
- className={errors?.search && styles.serviceError}
255
- onChange={(e) => {
256
- setSearchTerm(e.target.value);
257
- onChange(e);
258
- handleSearchTermChange(e);
259
- }}
260
- renderIcon={errors?.search && <WarningFilled />}
261
- onBlur={onBlur}
262
- onClear={() => {
263
- setSearchTerm('');
264
- setSelectedConcept(null);
265
- }}
266
- value={(() => {
267
- if (selectedConcept) {
268
- return selectedConcept.display;
269
- }
270
- if (debouncedSearchTerm) {
271
- return value;
272
- }
273
- })()}
274
- />
275
- </ResponsiveWrapper>
276
- )}
277
- />
301
+ </section>
302
+ <section>
303
+ <FormLabel className={styles.conceptLabel}>{t('associatedConcept', 'Associated concept')}</FormLabel>
304
+ <ResponsiveWrapper>
305
+ <Search
306
+ ref={searchInputRef}
307
+ id="conceptsSearch"
308
+ labelText={t('enterConcept', 'Associated concept')}
309
+ placeholder={t('searchConcepts', 'Search associated concept')}
310
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
311
+ onClear={() => {
312
+ setSearchTerm('');
313
+ setValue('concept', null);
314
+ }}
315
+ value={selectedConcept?.display || searchTerm}
316
+ />
317
+ </ResponsiveWrapper>
278
318
 
279
- {(() => {
280
- if (!debouncedSearchTerm || selectedConcept) return null;
281
- if (isSearching)
282
- return <InlineLoading className={styles.loader} description={t('searching', 'Searching') + '...'} />;
283
- if (searchResults && searchResults.length) {
319
+ {(() => {
320
+ if (!debouncedSearchTerm || selectedConcept) {
321
+ return null;
322
+ }
323
+ if (isSearching) {
324
+ return <InlineLoading className={styles.loader} description={t('searching', 'Searching') + '...'} />;
325
+ }
326
+ if (searchResults && searchResults.length) {
327
+ return (
328
+ <ul className={styles.conceptsList}>
329
+ {searchResults?.map((searchResult) => (
330
+ <li
331
+ role="menuitem"
332
+ className={styles.service}
333
+ key={searchResult.uuid}
334
+ onClick={() => {
335
+ setValue('concept', searchResult);
336
+ setSearchTerm('');
337
+ }}>
338
+ {searchResult.display}
339
+ </li>
340
+ ))}
341
+ </ul>
342
+ );
343
+ }
284
344
  return (
285
- <ul className={styles.conceptsList}>
286
- {/*TODO: use uuid instead of index as the key*/}
287
- {searchResults?.map((searchResult, index) => (
288
- <li
289
- role="menuitem"
290
- className={styles.service}
291
- key={index}
292
- onClick={() => handleConceptChange(searchResult)}>
293
- {searchResult.display}
294
- </li>
295
- ))}
296
- </ul>
345
+ <Layer>
346
+ <Tile className={styles.emptyResults}>
347
+ <span>
348
+ {t('noResultsFor', 'No results for')} <strong>"{debouncedSearchTerm}"</strong>
349
+ </span>
350
+ </Tile>
351
+ </Layer>
297
352
  );
298
- }
299
- return (
300
- <Layer>
301
- <Tile className={styles.emptyResults}>
302
- <span>
303
- {t('noResultsFor', 'No results for')} <strong>"{debouncedSearchTerm}"</strong>
304
- </span>
305
- </Tile>
306
- </Layer>
307
- );
308
- })()}
309
- </section>
310
- <section className={styles.section}>
311
- <Layer>
312
- <ComboBox
313
- id="serviceType"
314
- items={serviceTypes ?? []}
315
- titleText={t('serviceType', 'Service Type')}
316
- itemToString={(item) => item?.display || ''}
317
- selectedItem={billableServicePayload.serviceType || null}
318
- onChange={({ selectedItem }) => {
319
- setBillableServicePayload({
320
- ...billableServicePayload,
321
- display: selectedItem?.display,
322
- serviceType: selectedItem,
323
- });
324
- }}
325
- placeholder="Select service type"
326
- required
353
+ })()}
354
+ </section>
355
+ <section>
356
+ <Controller
357
+ name="serviceType"
358
+ control={control}
359
+ render={({ field }) => (
360
+ <Layer>
361
+ <ComboBox
362
+ id="serviceType"
363
+ items={serviceTypes ?? []}
364
+ titleText={t('serviceType', 'Service type')}
365
+ itemToString={(item: ServiceType) => item?.display || ''}
366
+ selectedItem={field.value}
367
+ onChange={({ selectedItem }: { selectedItem: ServiceType | null }) => {
368
+ field.onChange(selectedItem);
369
+ }}
370
+ placeholder={t('selectServiceType', 'Select service type')}
371
+ invalid={!!errors.serviceType}
372
+ invalidText={errors.serviceType?.message}
373
+ />
374
+ </Layer>
375
+ )}
327
376
  />
328
- </Layer>
329
- </section>
330
-
331
- <section>
332
- <div className={styles.container}>
333
- {fields.map((field, index) => (
334
- <div key={field.id} className={styles.paymentMethodContainer}>
335
- <Controller
336
- control={control}
337
- name={`payment.${index}.paymentMode`}
338
- render={({ field }) => (
339
- <Layer>
340
- <Dropdown
341
- onChange={({ selectedItem }) => field.onChange(selectedItem.uuid)}
342
- titleText={t('paymentMode', 'Payment Mode')}
343
- label={t('selectPaymentMethod', 'Select payment method')}
344
- items={paymentModes ?? []}
345
- itemToString={(item) => (item ? item.name : '')}
346
- selectedItem={paymentModes.find((mode) => mode.uuid === field.value)}
347
- invalid={!!errors?.payment?.[index]?.paymentMode}
348
- invalidText={errors?.payment?.[index]?.paymentMode?.message}
349
- />
350
- </Layer>
351
- )}
352
- />
353
- <Controller
354
- control={control}
355
- name={`payment.${index}.price`}
356
- render={({ field }) => (
357
- <Layer>
358
- <TextInput
359
- {...field}
360
- invalid={!!errors?.payment?.[index]?.price}
361
- invalidText={errors?.payment?.[index]?.price?.message}
362
- labelText={t('sellingPrice', 'Selling Price')}
363
- placeholder={t('sellingAmount', 'Enter selling price')}
364
- />
365
- </Layer>
366
- )}
367
- />
368
- <div className={styles.removeButtonContainer}>
369
- <TrashCan onClick={() => handleRemovePaymentMode(index)} className={styles.removeButton} size={20} />
377
+ </section>
378
+ <section>
379
+ <div>
380
+ {fields.map((field, index) => (
381
+ <div key={field.id} className={styles.paymentMethodContainer}>
382
+ <Controller
383
+ control={control}
384
+ name={`payment.${index}.paymentMode`}
385
+ render={({ field }) => (
386
+ <Layer>
387
+ <Dropdown
388
+ id={`paymentMode-${index}`}
389
+ onChange={({ selectedItem }) => field.onChange(selectedItem.uuid)}
390
+ titleText={t('paymentMode', 'Payment mode')}
391
+ label={t('selectPaymentMode', 'Select payment mode')}
392
+ items={paymentModes ?? []}
393
+ itemToString={(item) => (item ? item.name : '')}
394
+ selectedItem={paymentModes.find((mode) => mode.uuid === field.value)}
395
+ invalid={!!errors?.payment?.[index]?.paymentMode}
396
+ invalidText={errors?.payment?.[index]?.paymentMode?.message}
397
+ />
398
+ </Layer>
399
+ )}
400
+ />
401
+ <Controller
402
+ control={control}
403
+ name={`payment.${index}.price`}
404
+ render={({ field }) => (
405
+ <Layer>
406
+ {/* FIXME: this should be a NumberInput */}
407
+ <TextInput
408
+ {...field}
409
+ id={`price-${index}`}
410
+ invalid={!!errors?.payment?.[index]?.price}
411
+ invalidText={errors?.payment?.[index]?.price?.message}
412
+ labelText={t('sellingPrice', 'Selling Price')}
413
+ placeholder={t('enterSellingPrice', 'Enter selling price')}
414
+ />
415
+ </Layer>
416
+ )}
417
+ />
418
+ <div className={styles.removeButtonContainer}>
419
+ <TrashCan onClick={() => handleRemovePaymentMode(index)} className={styles.removeButton} size={20} />
420
+ </div>
370
421
  </div>
371
- </div>
372
- ))}
373
- <Button
374
- size="md"
375
- onClick={handleAppendPaymentMode}
376
- className={styles.paymentButtons}
377
- renderIcon={(props) => <Add size={24} {...props} />}
378
- iconDescription="Add">
379
- {t('addPaymentOptions', 'Add payment option')}
422
+ ))}
423
+ <Button
424
+ kind="tertiary"
425
+ type="button"
426
+ onClick={handleAppendPaymentMode}
427
+ className={styles.paymentButtons}
428
+ renderIcon={(props) => <Add size={24} {...props} />}
429
+ iconDescription={t('add', 'Add')}>
430
+ {t('addPaymentOption', 'Add payment option')}
431
+ </Button>
432
+ {getPaymentErrorMessage() && <div className={styles.errorMessage}>{getPaymentErrorMessage()}</div>}
433
+ </div>
434
+ </section>
435
+ </Stack>
436
+ {!isModal && (
437
+ <section>
438
+ <Button kind="secondary" onClick={onClose}>
439
+ {getCoreTranslation('cancel')}
380
440
  </Button>
381
- {getPaymentErrorMessage() && <div className={styles.errorMessage}>{getPaymentErrorMessage()}</div>}
382
- </div>
383
- </section>
384
-
385
- <section>
386
- <Button kind="secondary" onClick={onClose}>
387
- {t('cancel', 'Cancel')}
388
- </Button>
389
- <Button type="submit" disabled={!isValid || Object.keys(errors).length > 0}>
390
- {t('save', 'Save')}
391
- </Button>
392
- </section>
441
+ <Button type="submit">{getCoreTranslation('save')}</Button>
442
+ </section>
443
+ )}
393
444
  </Form>
394
445
  );
395
446
  };
396
447
 
397
- function ResponsiveWrapper({ children, isTablet }: { children: React.ReactNode; isTablet: boolean }) {
398
- return isTablet ? <Layer>{children} </Layer> : <>{children}</>;
399
- }
400
-
401
448
  export default AddBillableService;