@openmrs/esm-billing-app 1.0.2-pre.863 → 1.0.2-pre.866

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.
@@ -591,9 +591,9 @@
591
591
  "initial": false,
592
592
  "entry": false,
593
593
  "recorded": false,
594
- "size": 1178398,
594
+ "size": 1178229,
595
595
  "sizes": {
596
- "javascript": 1178356,
596
+ "javascript": 1178187,
597
597
  "consume-shared": 42
598
598
  },
599
599
  "names": [],
@@ -607,7 +607,7 @@
607
607
  "auxiliaryFiles": [
608
608
  "4344.js.map"
609
609
  ],
610
- "hash": "b5185f69a329da9d",
610
+ "hash": "29bf4b30203bd689",
611
611
  "childrenByOrder": {}
612
612
  },
613
613
  {
@@ -1236,10 +1236,10 @@
1236
1236
  "initial": true,
1237
1237
  "entry": true,
1238
1238
  "recorded": false,
1239
- "size": 5461852,
1239
+ "size": 5461683,
1240
1240
  "sizes": {
1241
1241
  "consume-shared": 210,
1242
- "javascript": 5439196,
1242
+ "javascript": 5439027,
1243
1243
  "share-init": 336,
1244
1244
  "runtime": 22110
1245
1245
  },
@@ -1256,7 +1256,7 @@
1256
1256
  "auxiliaryFiles": [
1257
1257
  "main.js.map"
1258
1258
  ],
1259
- "hash": "59c20b061c048abc",
1259
+ "hash": "bc8a8b57973d7d6d",
1260
1260
  "childrenByOrder": {}
1261
1261
  },
1262
1262
  {
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.24.0","fhir2":">=1.2"},"pages":[{"component":"billableServicesHome","route":"billable-services"}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"},"featureFlag":"billing"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing history"},"featureFlag":"billing"},{"name":"billable-services-app-menu-item","component":"billableServicesAppMenuItem","slot":"app-menu-item-slot","meta":{"name":"Billable Services"}},{"name":"billing-checkin-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm","featureFlag":"billing"},{"slot":"system-admin-page-card-link-slot","component":"billableServicesCardLink","name":"billable-services-admin-card-link"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"billing-home-tiles-ext","slot":"billing-home-tiles-slot","component":"serviceMetrics"}],"modals":[{"name":"add-cash-point-modal","component":"addCashPointModal"},{"name":"add-payment-mode-modal","component":"addPaymentModeModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"edit-bill-item-modal","component":"editBillLineItemModal"},{"name":"edit-bill-line-item-modal","component":"editBillLineItemModal"},{"name":"edit-billable-service-modal","component":"editBillableServiceModal"},{"name":"require-billing-modal","component":"requirePaymentModal"}],"workspaces":[{"name":"billing-form-workspace","title":"billingForm","component":"billingFormWorkspace","type":"form"}],"featureFlags":[{"flagName":"billing","label":"Billing module","description":"This feature introduces navigation links on the patient chart and home page to allow accessing the billing module features"}],"version":"1.0.2-pre.863"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.24.0","fhir2":">=1.2"},"pages":[{"component":"billableServicesHome","route":"billable-services"}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"},"featureFlag":"billing"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing history"},"featureFlag":"billing"},{"name":"billable-services-app-menu-item","component":"billableServicesAppMenuItem","slot":"app-menu-item-slot","meta":{"name":"Billable Services"}},{"name":"billing-checkin-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm","featureFlag":"billing"},{"slot":"system-admin-page-card-link-slot","component":"billableServicesCardLink","name":"billable-services-admin-card-link"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"billing-home-tiles-ext","slot":"billing-home-tiles-slot","component":"serviceMetrics"}],"modals":[{"name":"add-cash-point-modal","component":"addCashPointModal"},{"name":"add-payment-mode-modal","component":"addPaymentModeModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"edit-bill-item-modal","component":"editBillLineItemModal"},{"name":"edit-bill-line-item-modal","component":"editBillLineItemModal"},{"name":"edit-billable-service-modal","component":"editBillableServiceModal"},{"name":"require-billing-modal","component":"requirePaymentModal"}],"workspaces":[{"name":"billing-form-workspace","title":"billingForm","component":"billingFormWorkspace","type":"form"}],"featureFlags":[{"flagName":"billing","label":"Billing module","description":"This feature introduces navigation links on the patient chart and home page to allow accessing the billing module features"}],"version":"1.0.2-pre.866"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-billing-app",
3
- "version": "1.0.2-pre.863",
3
+ "version": "1.0.2-pre.866",
4
4
  "description": "O3 frontend module for handling billing concerns in healthcare settings",
5
5
  "browser": "dist/openmrs-esm-billing-app.js",
6
6
  "main": "src/index.ts",
@@ -20,7 +20,7 @@ import { Add, TrashCan } from '@carbon/react/icons';
20
20
  import { z } from 'zod';
21
21
  import { zodResolver } from '@hookform/resolvers/zod';
22
22
  import { getCoreTranslation, navigate, ResponsiveWrapper, showSnackbar, useDebounce } from '@openmrs/esm-framework';
23
- import type { BillableService, ConceptSearchResult, ServicePrice } from '../../types';
23
+ import type { BillableService, ServicePrice } from '../../types';
24
24
  import {
25
25
  createBillableService,
26
26
  updateBillableService,
@@ -30,34 +30,74 @@ import {
30
30
  } from '../billable-service.resource';
31
31
  import styles from './add-billable-service.scss';
32
32
 
33
- interface ServiceType {
34
- uuid: string;
35
- display: string;
36
- }
37
-
38
- interface PaymentModeForm {
39
- paymentMode: string;
40
- price: string | number | undefined;
33
+ interface AddBillableServiceProps {
34
+ serviceToEdit?: BillableService;
35
+ onClose: () => void;
36
+ onServiceUpdated?: () => void;
37
+ isModal?: boolean;
41
38
  }
42
39
 
43
40
  interface BillableServiceFormData {
44
41
  name: string;
45
- shortName?: string;
42
+ payment: PaymentModeForm[];
46
43
  serviceType: ServiceType | null;
47
44
  concept?: { uuid: string; display: string } | null;
48
- payment: PaymentModeForm[];
45
+ shortName?: string;
49
46
  }
50
47
 
51
- interface AddBillableServiceProps {
52
- serviceToEdit?: BillableService;
53
- onClose: () => void;
54
- onServiceUpdated?: () => void;
55
- isModal?: boolean;
48
+ interface PaymentModeForm {
49
+ paymentMode: string;
50
+ price: string | number | undefined;
56
51
  }
57
52
 
58
- const DEFAULT_PAYMENT_OPTION: PaymentModeForm = { paymentMode: '', price: undefined };
53
+ interface ServiceType {
54
+ uuid: string;
55
+ display: string;
56
+ }
57
+
58
+ const DEFAULT_PAYMENT_OPTION: PaymentModeForm = { paymentMode: '', price: '' };
59
59
  const MAX_NAME_LENGTH = 255;
60
60
 
61
+ /**
62
+ * Transforms a BillableService into form data structure
63
+ * Centralizes the mapping logic to avoid duplication between defaultValues and reset()
64
+ * Exported for testing
65
+ */
66
+ export const transformServiceToFormData = (service?: BillableService): BillableServiceFormData => {
67
+ if (!service) {
68
+ return {
69
+ name: '',
70
+ shortName: '',
71
+ serviceType: null,
72
+ concept: null,
73
+ payment: [DEFAULT_PAYMENT_OPTION],
74
+ };
75
+ }
76
+
77
+ return {
78
+ name: service.name || '',
79
+ shortName: service.shortName || '',
80
+ serviceType: service.serviceType || null,
81
+ concept: service.concept ? { uuid: service.concept.uuid, display: service.concept.display } : null,
82
+ payment: service.servicePrices?.map((servicePrice: ServicePrice) => ({
83
+ paymentMode: servicePrice.paymentMode?.uuid || '',
84
+ price: servicePrice.price ?? '',
85
+ })) || [DEFAULT_PAYMENT_OPTION],
86
+ };
87
+ };
88
+
89
+ /**
90
+ * Normalizes price value from form (string | number | undefined) to number
91
+ * Handles Carbon NumberInput which can return either type
92
+ * Exported for testing
93
+ */
94
+ export const normalizePrice = (price: string | number | undefined): number => {
95
+ if (typeof price === 'number') {
96
+ return price;
97
+ }
98
+ return parseFloat(String(price));
99
+ };
100
+
61
101
  const createBillableServiceSchema = (t: TFunction) => {
62
102
  const servicePriceSchema = z.object({
63
103
  paymentMode: z
@@ -143,18 +183,7 @@ const AddBillableService: React.FC<AddBillableServiceProps> = ({
143
183
  reset,
144
184
  } = useForm<BillableServiceFormData>({
145
185
  mode: 'all',
146
- defaultValues: {
147
- name: serviceToEdit?.name || '',
148
- shortName: serviceToEdit?.shortName || '',
149
- serviceType: serviceToEdit?.serviceType || null,
150
- concept: serviceToEdit?.concept
151
- ? { uuid: serviceToEdit.concept.uuid, display: serviceToEdit.concept.display }
152
- : null,
153
- payment: serviceToEdit?.servicePrices?.map((servicePrice: ServicePrice) => ({
154
- paymentMode: servicePrice.paymentMode?.uuid || '',
155
- price: servicePrice.price || '',
156
- })) || [DEFAULT_PAYMENT_OPTION],
157
- },
186
+ defaultValues: transformServiceToFormData(serviceToEdit),
158
187
  resolver: zodResolver(billableServiceSchema),
159
188
  });
160
189
  const { fields, remove, append } = useFieldArray({ name: 'payment', control });
@@ -174,22 +203,13 @@ const AddBillableService: React.FC<AddBillableServiceProps> = ({
174
203
  to: window.getOpenmrsSpaBase() + 'billable-services',
175
204
  });
176
205
 
206
+ // Re-initialize form when editing and dependencies load
207
+ // Needed because serviceTypes/paymentModes may not be available during initial render
177
208
  useEffect(() => {
178
209
  if (serviceToEdit && !isLoadingPaymentModes && !isLoadingServiceTypes) {
179
- reset({
180
- name: serviceToEdit.name || '',
181
- shortName: serviceToEdit.shortName || '',
182
- serviceType: serviceToEdit.serviceType || null,
183
- concept: serviceToEdit.concept
184
- ? { uuid: serviceToEdit.concept.uuid, display: serviceToEdit.concept.display }
185
- : null,
186
- payment: serviceToEdit.servicePrices.map((payment: ServicePrice) => ({
187
- paymentMode: payment.paymentMode?.uuid || '',
188
- price: payment.price || '',
189
- })),
190
- });
210
+ reset(transformServiceToFormData(serviceToEdit));
191
211
  }
192
- }, [serviceToEdit, isLoadingPaymentModes, reset, isLoadingServiceTypes]);
212
+ }, [serviceToEdit, isLoadingPaymentModes, isLoadingServiceTypes, reset]);
193
213
 
194
214
  const onSubmit = async (data: BillableServiceFormData) => {
195
215
  const payload = {
@@ -201,7 +221,7 @@ const AddBillableService: React.FC<AddBillableServiceProps> = ({
201
221
  return {
202
222
  paymentMode: payment.paymentMode,
203
223
  name: mode?.name || 'Unknown',
204
- price: typeof payment.price === 'string' ? parseFloat(payment.price) : payment.price,
224
+ price: normalizePrice(payment.price),
205
225
  };
206
226
  }),
207
227
  serviceStatus: 'ENABLED',
@@ -277,13 +297,14 @@ const AddBillableService: React.FC<AddBillableServiceProps> = ({
277
297
  <Layer>
278
298
  <TextInput
279
299
  {...field}
300
+ enableCounter
280
301
  id="serviceName"
281
- type="text"
282
- labelText={t('serviceName', 'Service name')}
283
- placeholder={t('enterServiceName', 'Enter service name')}
284
- maxLength={MAX_NAME_LENGTH}
285
302
  invalid={!!errors.name}
286
303
  invalidText={errors.name?.message}
304
+ labelText={t('serviceName', 'Service name')}
305
+ maxCount={MAX_NAME_LENGTH}
306
+ placeholder={t('enterServiceName', 'Enter service name')}
307
+ type="text"
287
308
  />
288
309
  </Layer>
289
310
  )}
@@ -298,14 +319,15 @@ const AddBillableService: React.FC<AddBillableServiceProps> = ({
298
319
  <Layer>
299
320
  <TextInput
300
321
  {...field}
301
- value={field.value || ''}
322
+ enableCounter
302
323
  id="serviceShortName"
303
- type="text"
304
- labelText={t('shortName', 'Short name')}
305
- placeholder={t('enterServiceShortName', 'Enter service short name')}
306
- maxLength={MAX_NAME_LENGTH}
307
324
  invalid={!!errors.shortName}
308
325
  invalidText={errors.shortName?.message}
326
+ labelText={t('shortName', 'Short name')}
327
+ maxCount={MAX_NAME_LENGTH}
328
+ placeholder={t('enterServiceShortName', 'Enter service short name')}
329
+ type="text"
330
+ value={field.value || ''}
309
331
  />
310
332
  </Layer>
311
333
  )}
@@ -315,15 +337,15 @@ const AddBillableService: React.FC<AddBillableServiceProps> = ({
315
337
  <FormLabel className={styles.conceptLabel}>{t('associatedConcept', 'Associated concept')}</FormLabel>
316
338
  <ResponsiveWrapper>
317
339
  <Search
318
- ref={searchInputRef}
319
340
  id="conceptsSearch"
320
341
  labelText={t('associatedConcept', 'Associated concept')}
321
- placeholder={t('searchConcepts', 'Search associated concept')}
322
342
  onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
323
343
  onClear={() => {
324
344
  setSearchTerm('');
325
345
  setValue('concept', null);
326
346
  }}
347
+ placeholder={t('searchConcepts', 'Search associated concept')}
348
+ ref={searchInputRef}
327
349
  value={selectedConcept?.display || searchTerm}
328
350
  />
329
351
  </ResponsiveWrapper>
@@ -340,7 +362,6 @@ const AddBillableService: React.FC<AddBillableServiceProps> = ({
340
362
  <ul className={styles.conceptsList}>
341
363
  {searchResults?.map((searchResult) => (
342
364
  <li
343
- role="menuitem"
344
365
  className={styles.service}
345
366
  key={searchResult.concept.uuid}
346
367
  onClick={() => {
@@ -349,7 +370,8 @@ const AddBillableService: React.FC<AddBillableServiceProps> = ({
349
370
  display: searchResult.display,
350
371
  });
351
372
  setSearchTerm('');
352
- }}>
373
+ }}
374
+ role="menuitem">
353
375
  {searchResult.display}
354
376
  </li>
355
377
  ))}
@@ -399,14 +421,14 @@ const AddBillableService: React.FC<AddBillableServiceProps> = ({
399
421
  <Layer>
400
422
  <Dropdown
401
423
  id={`paymentMode-${index}`}
402
- onChange={({ selectedItem }) => field.onChange(selectedItem.uuid)}
403
- titleText={t('paymentMode', 'Payment mode')}
404
- label={t('selectPaymentMode', 'Select payment mode')}
424
+ invalid={!!errors?.payment?.[index]?.paymentMode}
425
+ invalidText={errors?.payment?.[index]?.paymentMode?.message}
405
426
  items={paymentModes ?? []}
406
427
  itemToString={(item) => (item ? item.name : '')}
428
+ label={t('selectPaymentMode', 'Select payment mode')}
429
+ onChange={({ selectedItem }) => field.onChange(selectedItem.uuid)}
407
430
  selectedItem={paymentModes.find((mode) => mode.uuid === field.value)}
408
- invalid={!!errors?.payment?.[index]?.paymentMode}
409
- invalidText={errors?.payment?.[index]?.paymentMode?.message}
431
+ titleText={t('paymentMode', 'Payment mode')}
410
432
  />
411
433
  </Layer>
412
434
  )}
@@ -424,12 +446,11 @@ const AddBillableService: React.FC<AddBillableServiceProps> = ({
424
446
  label={t('sellingPrice', 'Selling price')}
425
447
  min={0}
426
448
  onChange={(_, { value }) => {
427
- const numValue = value === '' || value === undefined ? undefined : Number(value);
428
- field.onChange(numValue);
449
+ field.onChange(value === '' || value === undefined ? '' : value);
429
450
  }}
430
451
  placeholder={t('enterSellingPrice', 'Enter selling price')}
431
452
  step={0.01}
432
- value={field.value ?? ''}
453
+ value={field.value === undefined || field.value === null ? '' : field.value}
433
454
  />
434
455
  </Layer>
435
456
  )}
@@ -440,12 +461,12 @@ const AddBillableService: React.FC<AddBillableServiceProps> = ({
440
461
  </div>
441
462
  ))}
442
463
  <Button
464
+ className={styles.paymentButtons}
465
+ iconDescription={t('add', 'Add')}
443
466
  kind="tertiary"
444
- type="button"
445
467
  onClick={handleAppendPaymentMode}
446
- className={styles.paymentButtons}
447
468
  renderIcon={(props) => <Add size={24} {...props} />}
448
- iconDescription={t('add', 'Add')}>
469
+ type="button">
449
470
  {t('addPaymentOption', 'Add payment option')}
450
471
  </Button>
451
472
  {getPaymentErrorMessage() && <div className={styles.errorMessage}>{getPaymentErrorMessage()}</div>}