@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.
@@ -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,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, 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 { getCoreTranslation, navigate, showSnackbar, useDebounce, useLayoutType } from '@openmrs/esm-framework';
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
- 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
- };
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
- const DEFAULT_PAYMENT_OPTION = { paymentMode: '', price: 0 };
42
+ interface BillableServiceFormData {
43
+ name: string;
44
+ shortName?: string;
45
+ serviceType: ServiceType | null;
46
+ concept?: ServiceConcept | null;
47
+ payment: PaymentModeForm[];
48
+ }
48
49
 
49
- const AddBillableService: React.FC<{
50
- editingService?: any;
50
+ interface AddBillableServiceProps {
51
+ serviceToEdit?: BillableService;
51
52
  onClose: () => void;
52
53
  onServiceUpdated?: () => void;
53
54
  isModal?: boolean;
54
- }> = ({ editingService, onClose, onServiceUpdated, isModal = false }) => {
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, isLoading: isLoadingPaymentModes } = usePaymentModes();
58
- const { serviceTypes, isLoading: isLoadingServicesTypes } = useServiceTypes();
59
- const [billableServicePayload, setBillableServicePayload] = useState(editingService || {});
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, isValid },
137
+ formState: { errors },
65
138
  setValue,
66
- } = useForm<any>({
139
+ reset,
140
+ } = useForm<BillableServiceFormData>({
67
141
  mode: 'all',
68
142
  defaultValues: {
69
- name: editingService?.name,
70
- serviceShortName: editingService?.shortName,
71
- serviceType: editingService?.serviceType,
72
- conceptsSearch: editingService?.concept,
73
- payment: editingService?.servicePrices || [DEFAULT_PAYMENT_OPTION],
143
+ name: serviceToEdit?.name || '',
144
+ shortName: serviceToEdit?.shortName || '',
145
+ serviceType: serviceToEdit?.serviceType || null,
146
+ concept: serviceToEdit?.concept || null,
147
+ payment: serviceToEdit?.servicePrices?.map((servicePrice: ServicePrice) => ({
148
+ paymentMode: servicePrice.paymentMode?.uuid || '',
149
+ price: servicePrice.price,
150
+ })) || [DEFAULT_PAYMENT_OPTION],
74
151
  },
75
- resolver: zodResolver(paymentFormSchema),
76
- shouldUnregister: !editingService,
152
+ resolver: zodResolver(billableServiceSchema),
77
153
  });
78
- const { fields, remove, append } = useFieldArray({ name: 'payment', control: control });
154
+ const { fields, remove, append } = useFieldArray({ name: 'payment', control });
79
155
 
80
- const handleAppendPaymentMode = useCallback(() => append(DEFAULT_PAYMENT_OPTION), [append]);
81
- const handleRemovePaymentMode = useCallback((index) => remove(index), [remove]);
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 [selectedConcept, setSelectedConcept] = useState<ServiceConcept>(null);
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 (editingService && !isLoadingPaymentModes) {
102
- setBillableServicePayload(editingService);
103
- setValue('serviceName', editingService.name || '');
104
- setValue('shortName', editingService.shortName || '');
105
- setValue('serviceType', editingService.serviceType || '');
106
- setValue(
107
- 'payment',
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
- }, [editingService, isLoadingPaymentModes, paymentModes, serviceTypes, setValue]);
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: billableServicePayload.name.substring(0, MAX_NAME_LENGTH),
126
- shortName: billableServicePayload.shortName.substring(0, MAX_NAME_LENGTH),
127
- serviceType: billableServicePayload.serviceType.uuid,
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: selectedConcept?.uuid,
200
+ concept: data.concept?.uuid,
138
201
  };
139
202
 
140
- const saveAction = editingService
141
- ? updateBillableService(editingService.uuid, payload)
142
- : createBillableService(payload);
203
+ try {
204
+ if (serviceToEdit) {
205
+ await updateBillableService(serviceToEdit.uuid, payload);
206
+ } else {
207
+ await createBillableService(payload);
208
+ }
143
209
 
144
- saveAction.then(
145
- (resp) => {
146
- showSnackbar({
147
- title: t('billableService', 'Billable service'),
148
- subtitle: editingService
149
- ? t('updatedSuccessfully', 'Billable service updated successfully')
150
- : t('createdSuccessfully', 'Billable service created successfully'),
151
- kind: 'success',
152
- timeoutInMs: 3000,
153
- });
154
- if (onServiceUpdated) {
155
- onServiceUpdated();
156
- }
157
- onClose();
158
- handleNavigateToServiceDashboard();
159
- },
160
- (error) => {
161
- showSnackbar({ title: t('billPaymentError', 'Bill payment error'), kind: 'error', subtitle: error?.message });
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 && isLoadingServicesTypes) {
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
- <h4>
187
- {editingService
188
- ? t('editBillableServices', 'Edit Billable Services')
189
- : t('addBillableServices', 'Add Billable Services')}
190
- </h4>
191
- <section>
192
- <Layer>
193
- <TextInput
194
- id="serviceName"
195
- type="text"
196
- labelText={t('serviceName', 'Service Name')}
197
- size="md"
198
- value={billableServicePayload.name || ''}
199
- onChange={(e) => {
200
- const newName = e.target.value.substring(0, MAX_NAME_LENGTH);
201
- setBillableServicePayload({
202
- ...billableServicePayload,
203
- name: newName,
204
- });
205
- }}
206
- placeholder="Enter service name"
207
- maxLength={MAX_NAME_LENGTH}
208
- />
209
- {billableServicePayload.name?.length >= MAX_NAME_LENGTH && (
210
- <span className={styles.errorMessage}>
211
- {t('serviceNameExceedsLimit', 'Service Name exceeds the character limit of {{MAX_NAME_LENGTH}}.', {
212
- MAX_NAME_LENGTH,
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
- </Layer>
217
- </section>
218
- <section>
219
- <Layer>
220
- <TextInput
221
- id="serviceShortName"
222
- type="text"
223
- labelText={t('serviceShortName', 'Short Name')}
224
- size="md"
225
- value={billableServicePayload.shortName || ''}
226
- onChange={(e) => {
227
- const newShortName = e.target.value.substring(0, MAX_NAME_LENGTH);
228
- setBillableServicePayload({
229
- ...billableServicePayload,
230
- shortName: newShortName,
231
- });
232
- }}
233
- placeholder="Enter service short name"
234
- maxLength={MAX_NAME_LENGTH}
280
+ </section>
281
+ <section>
282
+ <Controller
283
+ name="shortName"
284
+ control={control}
285
+ render={({ field }) => (
286
+ <Layer>
287
+ <TextInput
288
+ {...field}
289
+ value={field.value || ''}
290
+ id="serviceShortName"
291
+ type="text"
292
+ labelText={t('serviceShortName', 'Short Name')}
293
+ placeholder={t('enterServiceShortName', 'Enter service short name')}
294
+ maxLength={MAX_NAME_LENGTH}
295
+ invalid={!!errors.shortName}
296
+ invalidText={errors.shortName?.message}
297
+ />
298
+ </Layer>
299
+ )}
235
300
  />
236
- {billableServicePayload.shortName?.length >= MAX_NAME_LENGTH && (
237
- <span className={styles.errorMessage}>
238
- {t('shortNameExceedsLimit', 'Short Name exceeds the character limit of {{MAX_NAME_LENGTH}}.', {
239
- MAX_NAME_LENGTH,
240
- })}
241
- </span>
242
- )}
243
- </Layer>
244
- </section>
245
- <section>
246
- <FormLabel className={styles.conceptLabel}>Associated Concept</FormLabel>
247
- <Controller
248
- name="search"
249
- control={control}
250
- render={({ field: { onChange, value, onBlur } }) => (
251
- <ResponsiveWrapper isTablet={isTablet}>
252
- <Search
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
- if (!debouncedSearchTerm || selectedConcept) return null;
285
- if (isSearching)
286
- return <InlineLoading className={styles.loader} description={t('searching', 'Searching') + '...'} />;
287
- if (searchResults && searchResults.length) {
319
+ {(() => {
320
+ if (!debouncedSearchTerm || selectedConcept) {
321
+ return null;
322
+ }
323
+ if (isSearching) {
324
+ return <InlineLoading className={styles.loader} description={t('searching', 'Searching') + '...'} />;
325
+ }
326
+ if (searchResults && searchResults.length) {
327
+ return (
328
+ <ul className={styles.conceptsList}>
329
+ {searchResults?.map((searchResult) => (
330
+ <li
331
+ role="menuitem"
332
+ className={styles.service}
333
+ key={searchResult.uuid}
334
+ onClick={() => {
335
+ setValue('concept', searchResult);
336
+ setSearchTerm('');
337
+ }}>
338
+ {searchResult.display}
339
+ </li>
340
+ ))}
341
+ </ul>
342
+ );
343
+ }
288
344
  return (
289
- <ul className={styles.conceptsList}>
290
- {/*TODO: use uuid instead of index as the key*/}
291
- {searchResults?.map((searchResult, index) => (
292
- <li
293
- role="menuitem"
294
- className={styles.service}
295
- key={index}
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
- return (
304
- <Layer>
305
- <Tile className={styles.emptyResults}>
306
- <span>
307
- {t('noResultsFor', 'No results for')} <strong>"{debouncedSearchTerm}"</strong>
308
- </span>
309
- </Tile>
310
- </Layer>
311
- );
312
- })()}
313
- </section>
314
- <section>
315
- <Layer>
316
- <ComboBox
317
- id="serviceType"
318
- items={serviceTypes ?? []}
319
- titleText={t('serviceType', 'Service Type')}
320
- itemToString={(item) => item?.display || ''}
321
- selectedItem={billableServicePayload.serviceType || null}
322
- onChange={({ selectedItem }) => {
323
- setBillableServicePayload({
324
- ...billableServicePayload,
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
- </Layer>
333
- </section>
334
- <section>
335
- <div>
336
- {fields.map((field, index) => (
337
- <div key={field.id} className={styles.paymentMethodContainer}>
338
- <Controller
339
- control={control}
340
- name={`payment.${index}.paymentMode`}
341
- render={({ field }) => (
342
- <Layer>
343
- <Dropdown
344
- onChange={({ selectedItem }) => field.onChange(selectedItem.uuid)}
345
- titleText={t('paymentMode', 'Payment Mode')}
346
- label={t('selectPaymentMethod', 'Select payment method')}
347
- items={paymentModes ?? []}
348
- itemToString={(item) => (item ? item.name : '')}
349
- selectedItem={paymentModes.find((mode) => mode.uuid === field.value)}
350
- invalid={!!errors?.payment?.[index]?.paymentMode}
351
- invalidText={errors?.payment?.[index]?.paymentMode?.message}
352
- />
353
- </Layer>
354
- )}
355
- />
356
- <Controller
357
- control={control}
358
- name={`payment.${index}.price`}
359
- render={({ field }) => (
360
- <Layer>
361
- <TextInput
362
- {...field}
363
- invalid={!!errors?.payment?.[index]?.price}
364
- invalidText={errors?.payment?.[index]?.price?.message}
365
- labelText={t('sellingPrice', 'Selling Price')}
366
- placeholder={t('sellingAmount', 'Enter selling price')}
367
- />
368
- </Layer>
369
- )}
370
- />
371
- <div className={styles.removeButtonContainer}>
372
- <TrashCan onClick={() => handleRemovePaymentMode(index)} className={styles.removeButton} size={20} />
377
+ </section>
378
+ <section>
379
+ <div>
380
+ {fields.map((field, index) => (
381
+ <div key={field.id} className={styles.paymentMethodContainer}>
382
+ <Controller
383
+ control={control}
384
+ name={`payment.${index}.paymentMode`}
385
+ render={({ field }) => (
386
+ <Layer>
387
+ <Dropdown
388
+ id={`paymentMode-${index}`}
389
+ onChange={({ selectedItem }) => field.onChange(selectedItem.uuid)}
390
+ titleText={t('paymentMode', 'Payment mode')}
391
+ label={t('selectPaymentMode', 'Select payment mode')}
392
+ items={paymentModes ?? []}
393
+ itemToString={(item) => (item ? item.name : '')}
394
+ selectedItem={paymentModes.find((mode) => mode.uuid === field.value)}
395
+ invalid={!!errors?.payment?.[index]?.paymentMode}
396
+ invalidText={errors?.payment?.[index]?.paymentMode?.message}
397
+ />
398
+ </Layer>
399
+ )}
400
+ />
401
+ <Controller
402
+ control={control}
403
+ name={`payment.${index}.price`}
404
+ render={({ field }) => (
405
+ <Layer>
406
+ {/* FIXME: this should be a NumberInput */}
407
+ <TextInput
408
+ {...field}
409
+ id={`price-${index}`}
410
+ invalid={!!errors?.payment?.[index]?.price}
411
+ invalidText={errors?.payment?.[index]?.price?.message}
412
+ labelText={t('sellingPrice', 'Selling Price')}
413
+ placeholder={t('enterSellingPrice', 'Enter selling price')}
414
+ />
415
+ </Layer>
416
+ )}
417
+ />
418
+ <div className={styles.removeButtonContainer}>
419
+ <TrashCan onClick={() => handleRemovePaymentMode(index)} className={styles.removeButton} size={20} />
420
+ </div>
373
421
  </div>
374
- </div>
375
- ))}
376
- <Button
377
- size="md"
378
- onClick={handleAppendPaymentMode}
379
- className={styles.paymentButtons}
380
- renderIcon={(props) => <Add size={24} {...props} />}
381
- iconDescription="Add">
382
- {t('addPaymentOptions', 'Add payment option')}
383
- </Button>
384
- {getPaymentErrorMessage() && <div className={styles.errorMessage}>{getPaymentErrorMessage()}</div>}
385
- </div>
386
- </section>
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" disabled={!isValid || Object.keys(errors).length > 0}>
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;