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

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