@openmrs/esm-billing-app 1.0.2-pre.749 → 1.0.2-pre.753
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.
- package/dist/4300.js +1 -1
- package/dist/4739.js +1 -1
- package/dist/4739.js.map +1 -1
- package/dist/942.js +1 -1
- package/dist/942.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-billing-app.js.buildmanifest.json +12 -12
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/bill-item-actions/edit-bill-item.modal.tsx +0 -1
- package/src/bill-item-actions/edit-bill-item.test.tsx +0 -1
- package/src/billable-services/billable-service.resource.ts +13 -6
- package/src/billable-services/billable-services.component.tsx +47 -33
- package/src/billable-services/cash-point/cash-point-configuration.component.tsx +2 -1
- package/src/billable-services/create-edit/add-billable-service.component.tsx +336 -293
- package/src/billable-services/create-edit/add-billable-service.scss +3 -1
- package/src/billable-services/create-edit/add-billable-service.test.tsx +36 -35
- package/src/billable-services/create-edit/edit-billable-service.modal.tsx +6 -5
- package/src/billable-services/payment-modes/payment-modes-config.component.tsx +44 -44
- package/src/billable-services/payment-modes/payment-modes-config.scss +0 -3
- package/src/billing-form/billing-checkin-form.test.tsx +97 -22
- package/src/billing-form/billing-form.component.tsx +7 -4
- package/src/invoice/payments/payments.component.tsx +0 -1
- package/src/modal/require-payment-modal.test.tsx +2 -2
- package/src/types/index.ts +14 -6
- package/translations/en.json +15 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useRef, useState, useEffect, useMemo } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Button,
|
|
4
4
|
ComboBox,
|
|
@@ -8,15 +8,17 @@ 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
|
|
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 { getCoreTranslation, navigate, showSnackbar, useDebounce
|
|
21
|
+
import { getCoreTranslation, navigate, ResponsiveWrapper, showSnackbar, useDebounce } from '@openmrs/esm-framework';
|
|
20
22
|
import {
|
|
21
23
|
createBillableService,
|
|
22
24
|
updateBillableService,
|
|
@@ -24,73 +26,142 @@ import {
|
|
|
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
|
-
|
|
32
|
+
interface ServiceType {
|
|
33
|
+
uuid: string;
|
|
34
|
+
display: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface PaymentModeForm {
|
|
31
38
|
paymentMode: string;
|
|
32
39
|
price: string | number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const servicePriceSchema = z.object({
|
|
36
|
-
paymentMode: z.string().refine((value) => !!value, 'Payment method is required'),
|
|
37
|
-
price: z.union([
|
|
38
|
-
z.number().refine((value) => !!value, 'Price is required'),
|
|
39
|
-
z.string().refine((value) => !!value, 'Price is required'),
|
|
40
|
-
]),
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const paymentFormSchema = z.object({
|
|
44
|
-
payment: z.array(servicePriceSchema).min(1, 'At least one payment option is required'),
|
|
45
|
-
});
|
|
40
|
+
}
|
|
46
41
|
|
|
47
|
-
|
|
42
|
+
interface BillableServiceFormData {
|
|
43
|
+
name: string;
|
|
44
|
+
shortName?: string;
|
|
45
|
+
serviceType: ServiceType | null;
|
|
46
|
+
concept?: ServiceConcept | null;
|
|
47
|
+
payment: PaymentModeForm[];
|
|
48
|
+
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
interface AddBillableServiceProps {
|
|
51
|
+
serviceToEdit?: BillableService;
|
|
51
52
|
onClose: () => void;
|
|
52
53
|
onServiceUpdated?: () => void;
|
|
53
54
|
isModal?: boolean;
|
|
54
|
-
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DEFAULT_PAYMENT_OPTION: PaymentModeForm = { paymentMode: '', price: 0 };
|
|
58
|
+
const MAX_NAME_LENGTH = 19;
|
|
59
|
+
|
|
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
|
+
});
|
|
82
|
+
|
|
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
|
+
};
|
|
120
|
+
|
|
121
|
+
const AddBillableService: React.FC<AddBillableServiceProps> = ({
|
|
122
|
+
serviceToEdit,
|
|
123
|
+
onClose,
|
|
124
|
+
onServiceUpdated,
|
|
125
|
+
isModal = false,
|
|
126
|
+
}) => {
|
|
55
127
|
const { t } = useTranslation();
|
|
56
128
|
|
|
57
|
-
const { paymentModes,
|
|
58
|
-
const { serviceTypes,
|
|
59
|
-
|
|
129
|
+
const { paymentModes, isLoadingPaymentModes } = usePaymentModes();
|
|
130
|
+
const { serviceTypes, isLoadingServiceTypes } = useServiceTypes();
|
|
131
|
+
|
|
132
|
+
const billableServiceSchema = useMemo(() => createBillableServiceSchema(t), [t]);
|
|
60
133
|
|
|
61
134
|
const {
|
|
62
135
|
control,
|
|
63
136
|
handleSubmit,
|
|
64
|
-
formState: { errors
|
|
137
|
+
formState: { errors },
|
|
65
138
|
setValue,
|
|
66
|
-
|
|
139
|
+
reset,
|
|
140
|
+
} = useForm<BillableServiceFormData>({
|
|
67
141
|
mode: 'all',
|
|
68
142
|
defaultValues: {
|
|
69
|
-
name:
|
|
70
|
-
|
|
71
|
-
serviceType:
|
|
72
|
-
|
|
73
|
-
payment:
|
|
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],
|
|
74
151
|
},
|
|
75
|
-
resolver: zodResolver(
|
|
76
|
-
shouldUnregister: !editingService,
|
|
152
|
+
resolver: zodResolver(billableServiceSchema),
|
|
77
153
|
});
|
|
78
|
-
const { fields, remove, append } = useFieldArray({ name: 'payment', control
|
|
154
|
+
const { fields, remove, append } = useFieldArray({ name: 'payment', control });
|
|
79
155
|
|
|
80
|
-
const handleAppendPaymentMode =
|
|
81
|
-
const handleRemovePaymentMode =
|
|
156
|
+
const handleAppendPaymentMode = () => append(DEFAULT_PAYMENT_OPTION);
|
|
157
|
+
const handleRemovePaymentMode = (index: number) => remove(index);
|
|
82
158
|
|
|
83
|
-
const isTablet = useLayoutType() === 'tablet';
|
|
84
159
|
const searchInputRef = useRef(null);
|
|
85
|
-
const handleSearchTermChange = (event: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(event.target.value);
|
|
86
160
|
|
|
87
|
-
const
|
|
161
|
+
const selectedConcept = useWatch({ control, name: 'concept' });
|
|
88
162
|
const [searchTerm, setSearchTerm] = useState('');
|
|
89
|
-
const debouncedSearchTerm = useDebounce(searchTerm);
|
|
163
|
+
const debouncedSearchTerm = useDebounce(searchTerm.trim());
|
|
90
164
|
const { searchResults, isSearching } = useConceptsSearch(debouncedSearchTerm);
|
|
91
|
-
const handleConceptChange = useCallback((selectedConcept: any) => {
|
|
92
|
-
setSelectedConcept(selectedConcept);
|
|
93
|
-
}, []);
|
|
94
165
|
|
|
95
166
|
const handleNavigateToServiceDashboard = () =>
|
|
96
167
|
navigate({
|
|
@@ -98,69 +169,63 @@ const AddBillableService: React.FC<{
|
|
|
98
169
|
});
|
|
99
170
|
|
|
100
171
|
useEffect(() => {
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
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) => ({
|
|
109
179
|
paymentMode: payment.paymentMode?.uuid || '',
|
|
110
180
|
price: payment.price,
|
|
111
181
|
})),
|
|
112
|
-
);
|
|
113
|
-
setValue('conceptsSearch', editingService.concept);
|
|
114
|
-
|
|
115
|
-
if (editingService.concept) {
|
|
116
|
-
setSelectedConcept(editingService.concept);
|
|
117
|
-
}
|
|
182
|
+
});
|
|
118
183
|
}
|
|
119
|
-
}, [
|
|
120
|
-
|
|
121
|
-
const MAX_NAME_LENGTH = 19;
|
|
184
|
+
}, [serviceToEdit, isLoadingPaymentModes, reset, isLoadingServiceTypes]);
|
|
122
185
|
|
|
123
|
-
const onSubmit = (data) => {
|
|
186
|
+
const onSubmit = async (data: BillableServiceFormData) => {
|
|
124
187
|
const payload = {
|
|
125
|
-
name:
|
|
126
|
-
shortName:
|
|
127
|
-
serviceType:
|
|
188
|
+
name: data.name,
|
|
189
|
+
shortName: data.shortName || '',
|
|
190
|
+
serviceType: data.serviceType!.uuid,
|
|
128
191
|
servicePrices: data.payment.map((payment) => {
|
|
129
192
|
const mode = paymentModes.find((m) => m.uuid === payment.paymentMode);
|
|
130
193
|
return {
|
|
131
194
|
paymentMode: payment.paymentMode,
|
|
132
195
|
name: mode?.name || 'Unknown',
|
|
133
|
-
price: parseFloat(payment.price),
|
|
196
|
+
price: typeof payment.price === 'string' ? parseFloat(payment.price) : payment.price,
|
|
134
197
|
};
|
|
135
198
|
}),
|
|
136
199
|
serviceStatus: 'ENABLED',
|
|
137
|
-
concept:
|
|
200
|
+
concept: data.concept?.uuid,
|
|
138
201
|
};
|
|
139
202
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
203
|
+
try {
|
|
204
|
+
if (serviceToEdit) {
|
|
205
|
+
await updateBillableService(serviceToEdit.uuid, payload);
|
|
206
|
+
} else {
|
|
207
|
+
await createBillableService(payload);
|
|
208
|
+
}
|
|
143
209
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
);
|
|
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
|
+
});
|
|
217
|
+
|
|
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
|
+
}
|
|
164
229
|
};
|
|
165
230
|
|
|
166
231
|
const getPaymentErrorMessage = () => {
|
|
@@ -171,7 +236,7 @@ const AddBillableService: React.FC<{
|
|
|
171
236
|
return null;
|
|
172
237
|
};
|
|
173
238
|
|
|
174
|
-
if (isLoadingPaymentModes
|
|
239
|
+
if (isLoadingPaymentModes || isLoadingServiceTypes) {
|
|
175
240
|
return (
|
|
176
241
|
<InlineLoading
|
|
177
242
|
status="active"
|
|
@@ -183,223 +248,201 @@ const AddBillableService: React.FC<{
|
|
|
183
248
|
|
|
184
249
|
return (
|
|
185
250
|
<Form id="billable-service-form" className={styles.form} onSubmit={handleSubmit(onSubmit)}>
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
</span>
|
|
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
|
+
/>
|
|
215
279
|
)}
|
|
216
|
-
</
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
)}
|
|
235
300
|
/>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
ref={searchInputRef}
|
|
254
|
-
size="md"
|
|
255
|
-
id="conceptsSearch"
|
|
256
|
-
labelText={t('enterConcept', 'Associated concept')}
|
|
257
|
-
placeholder={t('searchConcepts', 'Search associated concept')}
|
|
258
|
-
className={errors?.search && styles.serviceError}
|
|
259
|
-
onChange={(e) => {
|
|
260
|
-
setSearchTerm(e.target.value);
|
|
261
|
-
onChange(e);
|
|
262
|
-
handleSearchTermChange(e);
|
|
263
|
-
}}
|
|
264
|
-
renderIcon={errors?.search && <WarningFilled />}
|
|
265
|
-
onBlur={onBlur}
|
|
266
|
-
onClear={() => {
|
|
267
|
-
setSearchTerm('');
|
|
268
|
-
setSelectedConcept(null);
|
|
269
|
-
}}
|
|
270
|
-
value={(() => {
|
|
271
|
-
if (selectedConcept) {
|
|
272
|
-
return selectedConcept.display;
|
|
273
|
-
}
|
|
274
|
-
if (debouncedSearchTerm) {
|
|
275
|
-
return value;
|
|
276
|
-
}
|
|
277
|
-
})()}
|
|
278
|
-
/>
|
|
279
|
-
</ResponsiveWrapper>
|
|
280
|
-
)}
|
|
281
|
-
/>
|
|
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>
|
|
282
318
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
}
|
|
288
344
|
return (
|
|
289
|
-
<
|
|
290
|
-
{
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
onClick={() => handleConceptChange(searchResult)}>
|
|
297
|
-
{searchResult.display}
|
|
298
|
-
</li>
|
|
299
|
-
))}
|
|
300
|
-
</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>
|
|
301
352
|
);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
display: selectedItem?.display,
|
|
326
|
-
serviceType: selectedItem,
|
|
327
|
-
});
|
|
328
|
-
}}
|
|
329
|
-
placeholder="Select service type"
|
|
330
|
-
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
|
+
)}
|
|
331
376
|
/>
|
|
332
|
-
</
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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>
|
|
373
421
|
</div>
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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>
|
|
387
436
|
{!isModal && (
|
|
388
437
|
<section>
|
|
389
438
|
<Button kind="secondary" onClick={onClose}>
|
|
390
439
|
{getCoreTranslation('cancel')}
|
|
391
440
|
</Button>
|
|
392
|
-
<Button type="submit"
|
|
393
|
-
{getCoreTranslation('save')}
|
|
394
|
-
</Button>
|
|
441
|
+
<Button type="submit">{getCoreTranslation('save')}</Button>
|
|
395
442
|
</section>
|
|
396
443
|
)}
|
|
397
444
|
</Form>
|
|
398
445
|
);
|
|
399
446
|
};
|
|
400
447
|
|
|
401
|
-
function ResponsiveWrapper({ children, isTablet }: { children: React.ReactNode; isTablet: boolean }) {
|
|
402
|
-
return isTablet ? <Layer>{children} </Layer> : <>{children}</>;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
448
|
export default AddBillableService;
|