@openmrs/esm-billing-app 1.0.2-pre.84 → 1.0.2-pre.849

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