@digihmis/esm-administration-app 1.0.0 → 1.0.2

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 (67) hide show
  1. package/dist/145.js +1 -0
  2. package/dist/145.js.map +1 -0
  3. package/dist/153.js +1 -1
  4. package/dist/153.js.map +1 -1
  5. package/dist/402.js +1 -0
  6. package/dist/402.js.map +1 -0
  7. package/dist/594.js +1 -0
  8. package/dist/594.js.map +1 -0
  9. package/dist/598.js +1 -1
  10. package/dist/598.js.map +1 -1
  11. package/dist/625.js +15 -0
  12. package/dist/{916.js.map → 625.js.map} +1 -1
  13. package/dist/{685.js → 631.js} +1 -1
  14. package/dist/631.js.map +1 -0
  15. package/dist/681.js +1 -0
  16. package/dist/681.js.map +1 -0
  17. package/dist/737.js +1 -1
  18. package/dist/737.js.map +1 -1
  19. package/dist/{266.js → 947.js} +1 -1
  20. package/dist/947.js.map +1 -0
  21. package/dist/digihmis-esm-administration-app.js +2 -2
  22. package/dist/digihmis-esm-administration-app.js.buildmanifest.json +140 -140
  23. package/dist/digihmis-esm-administration-app.js.map +1 -1
  24. package/dist/main.js +6 -6
  25. package/dist/main.js.map +1 -1
  26. package/dist/routes.json +1 -1
  27. package/package.json +3 -2
  28. package/release-version.js +4 -4
  29. package/src/billing-management/billing-charge-items/billing-charge-items-modal/billing-charge-items.resource.ts +2 -1
  30. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-items.resource.ts +30 -7
  31. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-service.workspace.tsx +79 -107
  32. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/components/price.component.scss +16 -0
  33. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/components/price.component.tsx +30 -20
  34. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/schemas/form-schemas.ts +19 -14
  35. package/src/billing-management/billing-charge-items/billing-charge-items.component.tsx +26 -35
  36. package/src/billing-management/billing-charge-items/billing-charge-items.resource.ts +1 -1
  37. package/src/billing-management/billing-charge-items/type/index.ts +10 -10
  38. package/src/billing-management/billing-charge-items/utils/index.ts +32 -74
  39. package/src/billing-management/billing-payment-modes/billing-payment-modes.component.tsx +24 -166
  40. package/src/billing-management/billing-payment-modes/billing-payment-modes.resource.ts +24 -14
  41. package/src/billing-management/billing-payment-points/billing-payment-point-modal/billing-delete-payment-point.modal.tsx +1 -1
  42. package/src/billing-management/billing-payment-points/billing-payment-point.resource.ts +1 -1
  43. package/src/billing-management/billing-payment-points/billing-payment-point.scss +47 -0
  44. package/src/billing-management/billing-payment-points/billing-payment-points-workspace/billing-payment-point.resource.ts +18 -23
  45. package/src/billing-management/billing-payment-points/billing-payment-points-workspace/billing-payment-points.workspace.tsx +1 -1
  46. package/src/config-schema.ts +17 -0
  47. package/src/dashboard.meta.ts +8 -0
  48. package/src/document-branding/document-branding-workspace/document-branding.workspace.scss +34 -0
  49. package/src/document-branding/document-branding-workspace/document-branding.workspace.tsx +160 -0
  50. package/src/document-branding/document-branding.component.tsx +185 -0
  51. package/src/document-branding/document-branding.resource.ts +54 -0
  52. package/src/document-branding/document-branding.scss +69 -0
  53. package/src/index.ts +15 -9
  54. package/src/routes.json +34 -13
  55. package/dist/101.js +0 -1
  56. package/dist/101.js.map +0 -1
  57. package/dist/190.js +0 -1
  58. package/dist/190.js.map +0 -1
  59. package/dist/266.js.map +0 -1
  60. package/dist/606.js +0 -1
  61. package/dist/606.js.map +0 -1
  62. package/dist/685.js.map +0 -1
  63. package/dist/747.js +0 -1
  64. package/dist/747.js.map +0 -1
  65. package/dist/916.js +0 -15
  66. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-item.workspace.tsx +0 -212
  67. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/components/stock-search.component.tsx +0 -107
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":"^2.24.0"},"extensions":[{"name":"system-info-management-dashboard-link","slot":"administration-dashboard-slot","component":"systemInfoLeftPanelLink","meta":{"name":"system-info-management","slot":"system-info-management-homepage-dashboard-slot","title":"System Info"}},{"name":"queue-management-dashboard-link","slot":"administration-dashboard-slot","component":"queueLeftPanelLink","meta":{"name":"queue-management","slot":"queue-management-homepage-dashboard-slot","title":"Queue Management"}},{"name":"location-management-dashboard-link","slot":"administration-dashboard-slot","component":"locationLeftPanelLink","meta":{"name":"location-management","slot":"location-management-homepage-dashboard-slot","title":"Location Management"}},{"name":"bed-management-dashboard-link","slot":"administration-dashboard-slot","component":"bedLeftPanelLink","meta":{"name":"bed-management","slot":"bed-management-homepage-dashboard-slot","title":"Bed Management"}},{"name":"form-builder-dashboard-link","slot":"administration-dashboard-slot","component":"formBuilderLeftPanelLink","meta":{"name":"form-builder","slot":"form-builder-homepage-dashboard-slot","title":"Form Builder"}},{"name":"billing-management-dashboard-link","slot":"administration-dashboard-slot","component":"billingLeftPanelLink","meta":{"name":"billing-management","slot":"billing-management-homepage-dashboard-slot","title":"Billing Management"}},{"name":"queue-management-dashboard-content","slot":"queue-management-homepage-dashboard-slot","component":"queueManagementDashboard"},{"name":"location-management-dashboard-content","slot":"location-management-homepage-dashboard-slot","component":"locationManagementDashboard"},{"name":"bed-management-dashboard-content","slot":"bed-management-homepage-dashboard-slot","component":"bedManagementDashboard"},{"name":"form-builder-dashboard-content","slot":"form-builder-homepage-dashboard-slot","component":"formBuilderDashboard"},{"name":"billing-management-dashboard-content","slot":"billing-management-homepage-dashboard-slot","component":"billingManagementDashboard"},{"name":"system-info-management-dashboard-content","slot":"system-info-management-homepage-dashboard-slot","component":"systemInfoDashboard"}],"workspaces":[{"name":"service-queues-service-form","title":"addNewQueueService","component":"addNewQueueServiceWorkspace","type":"service-queues"},{"name":"service-queues-room-workspace","title":"addNewQueueServiceRoom","component":"addNewQueueServiceRoomWorkspace","type":"service-queues"},{"name":"add-location-workspace","title":"addNewLocationWorkspace","component":"addNewLocationWorkspace","type":"location-management"},{"name":"search-location-workspace","title":"searchLocationWorkspace","component":"searchLocationWorkspace","type":"location-management"},{"name":"payment-mode-workspace","title":"paymentModeWorkspace","component":"paymentModeWorkspace","type":"billing-management"},{"name":"payment-point-workspace","title":"paymentPointWorkspace","component":"paymentPointWorkspace","type":"billing-management"},{"name":"billing-charge-service-workspace","title":"addServiceItem","component":"billableServiceForm","type":"billing-management"},{"name":"billing-charge-item-workspace","title":"addCommodityItem","component":"commodityForm","type":"billing-management"}],"modals":[{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"delete-payment-point-modal","component":"deletePaymentPointModal"},{"name":"delete-billable-service-modal","component":"deleteBillableService"}],"version":"1.0.0"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":">=2.24.0"},"extensions":[{"name":"system-info-management-dashboard-link","slot":"administration-dashboard-slot","component":"systemInfoLeftPanelLink","meta":{"name":"system-info-management","slot":"system-info-management-homepage-dashboard-slot","title":"System Info"}},{"name":"queue-management-dashboard-link","slot":"administration-dashboard-slot","component":"queueLeftPanelLink","meta":{"name":"queue-management","slot":"queue-management-homepage-dashboard-slot","title":"Queue Management"}},{"name":"location-management-dashboard-link","slot":"administration-dashboard-slot","component":"locationLeftPanelLink","meta":{"name":"location-management","slot":"location-management-homepage-dashboard-slot","title":"Location Management"}},{"name":"bed-management-dashboard-link","slot":"administration-dashboard-slot","component":"bedLeftPanelLink","meta":{"name":"bed-management","slot":"bed-management-homepage-dashboard-slot","title":"Bed Management"}},{"name":"form-builder-dashboard-link","slot":"administration-dashboard-slot","component":"formBuilderLeftPanelLink","meta":{"name":"form-builder","slot":"form-builder-homepage-dashboard-slot","title":"Form Builder"}},{"name":"billing-management-dashboard-link","slot":"administration-dashboard-slot","component":"billingLeftPanelLink","meta":{"name":"billing-management","slot":"billing-management-homepage-dashboard-slot","title":"Billing Management"}},{"name":"document-branding-dashboard-link","slot":"administration-dashboard-slot","component":"documentBrandingLeftPanelLink","meta":{"name":"document-branding","slot":"document-branding-homepage-dashboard-slot","title":"Document Branding"}},{"name":"queue-management-dashboard-content","slot":"queue-management-homepage-dashboard-slot","component":"queueManagementDashboard"},{"name":"location-management-dashboard-content","slot":"location-management-homepage-dashboard-slot","component":"locationManagementDashboard"},{"name":"bed-management-dashboard-content","slot":"bed-management-homepage-dashboard-slot","component":"bedManagementDashboard"},{"name":"form-builder-dashboard-content","slot":"form-builder-homepage-dashboard-slot","component":"formBuilderDashboard"},{"name":"billing-management-dashboard-content","slot":"billing-management-homepage-dashboard-slot","component":"billingManagementDashboard"},{"name":"system-info-management-dashboard-content","slot":"system-info-management-homepage-dashboard-slot","component":"systemInfoDashboard"},{"name":"document-branding-dashboard-content","slot":"document-branding-homepage-dashboard-slot","component":"documentBrandingDashboard"}],"workspaces":[{"name":"service-queues-service-form","title":"addNewQueueService","component":"addNewQueueServiceWorkspace","type":"service-queues"},{"name":"service-queues-room-workspace","title":"addNewQueueServiceRoom","component":"addNewQueueServiceRoomWorkspace","type":"service-queues"},{"name":"add-location-workspace","title":"addNewLocationWorkspace","component":"addNewLocationWorkspace","type":"location-management"},{"name":"search-location-workspace","title":"searchLocationWorkspace","component":"searchLocationWorkspace","type":"location-management"},{"name":"payment-mode-workspace","title":"paymentModeWorkspace","component":"paymentModeWorkspace","type":"billing-management"},{"name":"payment-point-workspace","title":"paymentPointWorkspace","component":"paymentPointWorkspace","type":"billing-management"},{"name":"billing-charge-service-workspace","title":"addServiceItem","component":"billableServiceForm","type":"billing-management"},{"name":"document-branding-workspace","title":"configureLetterhead","component":"documentBrandingWorkspace","type":"document-branding"}],"modals":[{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"delete-payment-point-modal","component":"deletePaymentPointModal"},{"name":"delete-billable-service-modal","component":"deleteBillableService"}],"version":"1.0.2"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digihmis/esm-administration-app",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Administration microfrontend for the OpenMRS SPA",
5
5
  "browser": "dist/digihmis-esm-administration-app.js",
6
6
  "main": "src/index.ts",
@@ -18,7 +18,8 @@
18
18
  "start": "openmrs develop",
19
19
  "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests",
20
20
  "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js",
21
- "typescript": "tsc"
21
+ "typescript": "tsc",
22
+ "prepublishOnly": "rspack --mode=production"
22
23
  },
23
24
  "browserslist": [
24
25
  "extends browserslist-config-openmrs"
@@ -3,14 +3,14 @@
3
3
  /* tslint:disable */
4
4
  export const VERSION = {
5
5
  "dirty": true,
6
- "raw": "3342d5c-dirty",
7
- "hash": "3342d5c",
6
+ "raw": "4d5fb07-dirty",
7
+ "hash": "4d5fb07",
8
8
  "distance": null,
9
9
  "tag": null,
10
10
  "semver": null,
11
- "suffix": "3342d5c-dirty",
11
+ "suffix": "4d5fb07-dirty",
12
12
  "semverString": null,
13
13
  "version": "1.0.0",
14
- "buildDate": "2026-07-27T23:03:38.929Z"
14
+ "buildDate": "2026-07-27T22:13:37.742Z"
15
15
  };
16
16
  /* tslint:enable */
@@ -1,7 +1,8 @@
1
1
  import { openmrsFetch } from '@openmrs/esm-framework';
2
2
 
3
3
  export const deleteBillableService = (uuid: string) => {
4
- const url = `/ws/rest/v1/cashier/billableService/${uuid}`;
4
+ // The DigiHMIS billing module retires a billable item via DELETE with an optional reason.
5
+ const url = `/ws/rest/v1/digihmisbilling/billableitem/${uuid}?reason=Retired via billing administration`;
5
6
  return openmrsFetch(url, {
6
7
  method: 'DELETE',
7
8
  headers: {
@@ -2,17 +2,40 @@ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
2
  import useSWR from 'swr';
3
3
  import { ServiceConcept, ServiceTypesResponse, StockItemResponse } from '../type';
4
4
 
5
- export const createBillableService = (payload: any, uuid?: string) => {
6
- const url = `${restBaseUrl}/cashier/billableService/${uuid ? `${uuid}` : ''}`;
7
- return openmrsFetch(url, {
5
+ /** Set (or replace) a billable item's price override for one payment mode. */
6
+ export const setBillableItemPrice = (uuid: string, mode: string, price: number | string) => {
7
+ const params = new URLSearchParams({ mode, price: String(price) });
8
+ return openmrsFetch(`${restBaseUrl}/digihmisbilling/billableitem/${uuid}/price?${params.toString()}`, {
8
9
  method: 'POST',
9
- body: payload,
10
- headers: {
11
- 'Content-Type': 'application/json',
12
- },
13
10
  });
14
11
  };
15
12
 
13
+ /** Remove a payment-mode override so that mode falls back to the item's default price. */
14
+ export const removeBillableItemPrice = (uuid: string, mode: string) => {
15
+ return openmrsFetch(`${restBaseUrl}/digihmisbilling/billableitem/${uuid}/price?mode=${mode}`, { method: 'DELETE' });
16
+ };
17
+
18
+ export const createBillableService = (
19
+ payload: { name: string; concept?: string; serviceType?: string; price: string | number; description?: string },
20
+ uuid?: string,
21
+ ) => {
22
+ // The DigiHMIS billing module takes billable-item fields as request params (not a JSON body).
23
+ const params = new URLSearchParams({ name: payload.name ?? '', price: String(payload.price ?? '0') });
24
+ if (payload.description) {
25
+ params.set('description', payload.description);
26
+ }
27
+ if (payload.serviceType !== undefined) {
28
+ params.set('serviceType', payload.serviceType);
29
+ }
30
+ // Create requires the concept; the update endpoint (POST /{uuid}) does not accept one.
31
+ if (!uuid && payload.concept) {
32
+ params.set('concept', payload.concept);
33
+ }
34
+ const base = `${restBaseUrl}/digihmisbilling/billableitem`;
35
+ const url = uuid ? `${base}/${uuid}?${params.toString()}` : `${base}?${params.toString()}`;
36
+ return openmrsFetch(url, { method: 'POST' });
37
+ };
38
+
16
39
  export function useConceptsSearch(conceptToLookup: string) {
17
40
  const conditionsSearchUrl = `${restBaseUrl}/conceptsearch?q=${conceptToLookup}`;
18
41
 
@@ -1,17 +1,8 @@
1
- import React, { useState, useMemo, useEffect } from 'react';
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import {
4
- ButtonSet,
5
- Button,
6
- Stack,
7
- TextInput,
8
- ComboBox,
9
- Toggle,
10
- InlineNotification,
11
- InlineLoading,
12
- } from '@carbon/react';
3
+ import { ButtonSet, Button, Stack, TextInput, TextArea, ComboBox, InlineNotification, InlineLoading } from '@carbon/react';
13
4
  import { Add } from '@carbon/react/icons';
14
- import { Controller, useFieldArray, useForm, FormProvider } from 'react-hook-form';
5
+ import { Controller, useForm, useFieldArray, FormProvider } from 'react-hook-form';
15
6
  import { zodResolver } from '@hookform/resolvers/zod';
16
7
 
17
8
  import { type DefaultWorkspaceProps, useLayoutType, useDebounce, ResponsiveWrapper, showSnackbar, restBaseUrl } from '@openmrs/esm-framework';
@@ -20,9 +11,16 @@ import classNames from 'classnames';
20
11
  import styles from './global/billing-charge-items.scss';
21
12
  import { billableFormSchema, BillableFormSchema } from './schemas/form-schemas';
22
13
  import { handleMutation } from '../../billing-payment-modes/billing-payment-modes-workspace/billing-payment-modes.resource';
23
- import PriceField from './components/price.component';
24
14
  import ConceptSearch from './components/concept-search.component';
25
- import { createBillableService, useConceptsSearch, useServiceTypes } from './billing-charge-items.resource';
15
+ import PriceField from './components/price.component';
16
+ import {
17
+ createBillableService,
18
+ useConceptsSearch,
19
+ useServiceTypes,
20
+ setBillableItemPrice,
21
+ removeBillableItemPrice,
22
+ } from './billing-charge-items.resource';
23
+ import { usePaymentModes } from '../../billing-payment-modes/billing-payment-modes.resource';
26
24
  import { formatBillableServicePayloadForSubmission, mapInputToPayloadSchema } from '../utils';
27
25
 
28
26
  interface AddServiceFormProps extends DefaultWorkspaceProps {
@@ -42,18 +40,18 @@ const AddServiceForm: React.FC<AddServiceFormProps> = ({
42
40
  const [selectedConcept, setSelectedConcept] = useState<any>(null);
43
41
  const inEditMode = !!initialValues;
44
42
 
45
- const { isLoading: isLoadingServiceTypes, serviceTypes } = useServiceTypes();
46
43
  const { isSearching, searchResults: concepts } = useConceptsSearch(debouncedConceptToLookup);
44
+ const { paymentModes } = usePaymentModes(false);
45
+ const { serviceTypes, isLoading: isLoadingServiceTypes } = useServiceTypes();
47
46
  const formMethods = useForm<BillableFormSchema>({
48
47
  resolver: zodResolver(billableFormSchema),
49
48
  defaultValues: initialValues
50
49
  ? mapInputToPayloadSchema(initialValues)
51
50
  : {
52
51
  name: '',
53
- shortName: '',
54
52
  concept: null,
55
53
  serviceType: null,
56
- serviceStatus: 'ENABLED',
54
+ description: '',
57
55
  servicePrices: [],
58
56
  },
59
57
  });
@@ -66,6 +64,12 @@ const AddServiceForm: React.FC<AddServiceFormProps> = ({
66
64
  formState: { errors, isDirty, defaultValues, isSubmitting },
67
65
  } = formMethods;
68
66
 
67
+ const {
68
+ fields: servicePriceFields,
69
+ append: appendServicePrice,
70
+ remove: removeServicePrice,
71
+ } = useFieldArray({ control, name: 'servicePrices' });
72
+
69
73
  useEffect(() => {
70
74
  if (initialValues) {
71
75
  setConceptToLookup(initialValues.concept?.concept?.display);
@@ -73,18 +77,9 @@ const AddServiceForm: React.FC<AddServiceFormProps> = ({
73
77
  }
74
78
  }, [initialValues, trigger]);
75
79
 
76
- const {
77
- fields: servicePriceFields,
78
- append: appendServicePrice,
79
- remove: removeServicePrice,
80
- } = useFieldArray({
81
- control,
82
- name: 'servicePrices',
83
- });
84
-
85
80
  const handleSelectConcept = (concept) => {
86
81
  setSelectedConcept(concept);
87
- setValue('concept', concept);
82
+ setValue('concept', concept, { shouldDirty: true, shouldValidate: true });
88
83
  setConceptToLookup('');
89
84
  };
90
85
 
@@ -97,29 +92,36 @@ const AddServiceForm: React.FC<AddServiceFormProps> = ({
97
92
  try {
98
93
  const response = await createBillableService(formPayload, initialValues?.['uuid']);
99
94
  if (response.ok) {
95
+ // Sync the per-payment-mode prices onto the saved item: set the chosen modes, and clear any
96
+ // mode no longer priced so it falls back to the (first-mode) default.
97
+ const uuid = response?.data?.uuid ?? initialValues?.['uuid'];
98
+ if (uuid) {
99
+ const chosen = new Set(data.servicePrices.map((servicePrice) => servicePrice.paymentMode.uuid));
100
+ await Promise.all([
101
+ ...data.servicePrices.map((servicePrice) =>
102
+ setBillableItemPrice(uuid, servicePrice.paymentMode.uuid, Number(servicePrice.price)),
103
+ ),
104
+ ...(paymentModes ?? [])
105
+ .filter((mode) => !chosen.has(mode.uuid))
106
+ .map((mode) => removeBillableItemPrice(uuid, mode.uuid)),
107
+ ]);
108
+ }
100
109
  showSnackbar({
101
110
  title: inEditMode
102
111
  ? t('serviceUpdatedSuccessfully', 'Service updated successfully')
103
112
  : t('serviceCreated', 'Service created successfully'),
104
113
  kind: 'success',
105
- subtitle: inEditMode
106
- ? t('serviceUpdatedSuccessfully', 'Service updated successfully')
107
- : t('serviceCreatedSuccessfully', 'Service created successfully'),
108
114
  isLowContrast: true,
109
115
  timeoutInMs: 5000,
110
116
  });
111
- handleMutation(`${restBaseUrl}/cashier/billableService?v`);
112
-
117
+ handleMutation(`${restBaseUrl}/digihmisbilling/billableitem`);
113
118
  closeWorkspaceWithSavedChanges();
114
119
  }
115
120
  } catch (e) {
116
- const formSchemaError = JSON.stringify(e, null, 2);
117
- const errorMessage = e?.servicePrices?.root?.message || 'Unknown error occurred';
121
+ const errorMessage = e?.responseBody?.error?.message || e?.message || 'Unknown error occurred';
118
122
  showSnackbar({
119
123
  title: t('serviceCreationFailed', 'Service creation failed'),
120
- subtitle: t('serviceCreationFailedSubtitle', 'The service creation failed: {{errorMessage}}', {
121
- errorMessage,
122
- }),
124
+ subtitle: t('serviceCreationFailedSubtitle', 'The service creation failed: {{errorMessage}}', { errorMessage }),
123
125
  kind: 'error',
124
126
  isLowContrast: true,
125
127
  timeoutInMs: 5000,
@@ -127,6 +129,19 @@ const AddServiceForm: React.FC<AddServiceFormProps> = ({
127
129
  }
128
130
  };
129
131
 
132
+ const handleError = (err) => {
133
+ const errorMessage = Object.entries(err as Record<string, { message: string }>)
134
+ .map(([field, error]) => `${field}: ${error?.message}`)
135
+ .join('; ');
136
+ showSnackbar({
137
+ title: t('serviceCreationFailed', 'Service creation failed'),
138
+ subtitle: t('serviceCreationFailedSubtitle', 'The service creation failed: {{errorMessage}}', { errorMessage }),
139
+ kind: 'error',
140
+ isLowContrast: true,
141
+ timeoutInMs: 5000,
142
+ });
143
+ };
144
+
130
145
  const renderServicePriceFields = useMemo(
131
146
  () =>
132
147
  servicePriceFields.map((field, index) => (
@@ -142,23 +157,6 @@ const AddServiceForm: React.FC<AddServiceFormProps> = ({
142
157
  [servicePriceFields, control, removeServicePrice, errors],
143
158
  );
144
159
 
145
- const handleError = (err) => {
146
- console.error(JSON.stringify(err, null, 2));
147
- const errorMessage = Object.entries(err as Record<string, { message: string }>)
148
- .map(([field, error]) => `${field}: ${error.message}`)
149
- .join('; ');
150
-
151
- showSnackbar({
152
- title: t('serviceCreationFailed', 'Service creation failed'),
153
- subtitle: t('serviceCreationFailedSubtitle', 'The service creation failed: {{errorMessage}}', {
154
- errorMessage,
155
- }),
156
- kind: 'error',
157
- isLowContrast: true,
158
- timeoutInMs: 5000,
159
- });
160
- };
161
-
162
160
  return (
163
161
  <FormProvider {...formMethods}>
164
162
  <form onSubmit={handleSubmit(onSubmit, handleError)} className={styles.form}>
@@ -168,7 +166,7 @@ const AddServiceForm: React.FC<AddServiceFormProps> = ({
168
166
  <InlineNotification
169
167
  kind="error"
170
168
  title={t('conceptMissing', 'Concept missing')}
171
- subtitle={t('conceptMissingSubtitle', 'Please select a stock item')}
169
+ subtitle={t('conceptMissingSubtitle', 'Please select a concept')}
172
170
  />
173
171
  )}
174
172
  <ResponsiveWrapper>
@@ -187,22 +185,6 @@ const AddServiceForm: React.FC<AddServiceFormProps> = ({
187
185
  )}
188
186
  />
189
187
  </ResponsiveWrapper>
190
- <ResponsiveWrapper>
191
- <Controller
192
- name="shortName"
193
- control={control}
194
- render={({ field }) => (
195
- <TextInput
196
- id="serviceShortName"
197
- {...field}
198
- type="text"
199
- labelText={t('serviceShortName', 'Service short name')}
200
- invalid={!!errors.shortName}
201
- invalidText={errors?.shortName?.message}
202
- />
203
- )}
204
- />
205
- </ResponsiveWrapper>
206
188
 
207
189
  <ConceptSearch
208
190
  selectedConcept={selectedConcept}
@@ -219,57 +201,47 @@ const AddServiceForm: React.FC<AddServiceFormProps> = ({
219
201
  <Controller
220
202
  name="serviceType"
221
203
  control={control}
222
- render={({ field }) => {
223
- return (
224
- <ComboBox
225
- id="serviceType"
226
- onChange={({ selectedItem }) => field.onChange(selectedItem)}
227
- titleText={t('serviceType', 'Service type')}
228
- items={serviceTypes ?? []}
229
- itemToString={(item) => (item ? item.display : '')}
230
- placeholder={t('selectServiceType', 'Select service type')}
231
- disabled={isLoadingServiceTypes}
232
- initialSelectedItem={field.value}
233
- invalid={!!errors.serviceType}
234
- invalidText={errors?.serviceType?.message}
235
- itemToElement={(item) => (
236
- <div role="option" aria-selected={field.value?.uuid === item?.uuid}>
237
- {item?.display}
238
- </div>
239
- )}
240
- />
241
- );
242
- }}
204
+ render={({ field }) => (
205
+ <ComboBox
206
+ id="serviceType"
207
+ onChange={({ selectedItem }) => field.onChange(selectedItem)}
208
+ titleText={t('serviceType', 'Service type')}
209
+ items={serviceTypes ?? []}
210
+ itemToString={(item) => (item ? item.display : '')}
211
+ placeholder={t('selectServiceType', 'Select service type')}
212
+ disabled={isLoadingServiceTypes}
213
+ initialSelectedItem={field.value}
214
+ />
215
+ )}
243
216
  />
244
217
  </ResponsiveWrapper>
218
+
245
219
  <ResponsiveWrapper>
246
220
  <Controller
221
+ name="description"
247
222
  control={control}
248
- name="serviceStatus"
249
223
  render={({ field }) => (
250
- <Toggle
251
- labelText={t('status', 'Status')}
252
- labelA="Off"
253
- labelB="On"
254
- defaultToggled={field.value === 'ENABLED'}
255
- id="serviceStatus"
256
- onToggle={(value) => (value ? field.onChange('ENABLED') : field.onChange('DISABLED'))}
224
+ <TextArea
225
+ id="description"
226
+ {...field}
227
+ labelText={t('description', 'Description')}
228
+ placeholder={t('optional', 'Optional')}
229
+ rows={2}
257
230
  />
258
231
  )}
259
232
  />
260
233
  </ResponsiveWrapper>
234
+
261
235
  {renderServicePriceFields}
262
- <Button size="sm" kind="tertiary" renderIcon={Add} onClick={() => appendServicePrice({})}>
263
- {t('addPaymentMethod', 'Add payment method')}
236
+ <Button size="sm" kind="tertiary" renderIcon={Add} onClick={() => appendServicePrice({} as any)}>
237
+ {t('addPaymentMode', 'Add payment mode')}
264
238
  </Button>
265
239
  {!!errors.servicePrices && (
266
240
  <InlineNotification
267
- aria-label="closes notification"
268
241
  kind="error"
269
- lowContrast={true}
270
- statusIconDescription="notification"
271
- title={t('paymentMethodRequired', 'Payment method required')}
272
- subtitle={t('atLeastOnePriceRequired', 'At least one price is required')}
242
+ lowContrast
243
+ title={t('paymentModeRequired', 'Payment mode required')}
244
+ subtitle={t('atLeastOnePriceRequired', 'Add at least one payment mode price')}
273
245
  />
274
246
  )}
275
247
  </Stack>
@@ -0,0 +1,16 @@
1
+ .priceRow {
2
+ display: flex;
3
+ flex-direction: row;
4
+ align-items: flex-end;
5
+ gap: 0.5rem;
6
+ width: 100%;
7
+ }
8
+
9
+ .field {
10
+ flex: 1;
11
+ min-width: 0;
12
+ }
13
+
14
+ .removeButton {
15
+ flex: 0 0 auto;
16
+ }
@@ -1,12 +1,11 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import { Controller, useFormContext, type Control } from 'react-hook-form';
3
3
  import { useTranslation } from 'react-i18next';
4
- import styles from '../global/billing-charge-items.scss';
5
4
  import { ComboBox, NumberInput, IconButton } from '@carbon/react';
6
5
  import { TrashCan } from '@carbon/react/icons';
7
- import { ResponsiveWrapper } from '@openmrs/esm-framework';
6
+ import { type BillableFormSchema } from '../schemas/form-schemas';
8
7
  import { usePaymentModes } from '../../../billing-payment-modes/billing-payment-modes.resource';
9
- import { BillableFormSchema } from '../schemas/form-schemas';
8
+ import styles from './price.component.scss';
10
9
 
11
10
  interface PriceFieldProps {
12
11
  field: Record<string, any>;
@@ -16,22 +15,28 @@ interface PriceFieldProps {
16
15
  errors: Record<string, any>;
17
16
  }
18
17
 
18
+ /**
19
+ * One payment-mode price row -- mode + price + remove, laid out on a single line. Modes already
20
+ * chosen in other rows are filtered out so a mode is priced at most once.
21
+ */
19
22
  const PriceField: React.FC<PriceFieldProps> = ({ field, index, control, removeServicePrice, errors }) => {
20
23
  const { t } = useTranslation();
21
- const { paymentModes, isLoading } = usePaymentModes();
22
- const { watch } = useFormContext();
24
+ const { paymentModes, isLoading } = usePaymentModes(false);
25
+ const { watch } = useFormContext<BillableFormSchema>();
23
26
  const servicePrices = watch('servicePrices');
27
+
24
28
  const availablePaymentModes = useMemo(
25
29
  () =>
26
30
  paymentModes?.filter(
27
- (paymentMode) => !servicePrices?.some((servicePrice) => servicePrice.paymentMode?.uuid === paymentMode.uuid),
31
+ (mode) =>
32
+ !servicePrices?.some((servicePrice, i) => i !== index && servicePrice?.paymentMode?.uuid === mode.uuid),
28
33
  ),
29
- [paymentModes, servicePrices],
34
+ [paymentModes, servicePrices, index],
30
35
  );
31
36
 
32
37
  return (
33
- <div key={field.id} className={styles.paymentMethods}>
34
- <ResponsiveWrapper>
38
+ <div key={field.id} className={styles.priceRow}>
39
+ <div className={styles.field}>
35
40
  <Controller
36
41
  name={`servicePrices.${index}.paymentMode`}
37
42
  control={control}
@@ -39,9 +44,7 @@ const PriceField: React.FC<PriceFieldProps> = ({ field, index, control, removeSe
39
44
  <ComboBox
40
45
  id={`servicePrices.${index}.paymentMode`}
41
46
  onChange={({ selectedItem }) => field.onChange(selectedItem)}
42
- titleText={t('paymentMethodDescription', 'Payment method {{methodName}}', {
43
- methodName: servicePrices[index]?.paymentMode?.name ?? '',
44
- })}
47
+ titleText={t('paymentMode', 'Payment mode')}
45
48
  items={availablePaymentModes ?? []}
46
49
  itemToString={(item) => (item ? item.name : '')}
47
50
  placeholder={t('selectPaymentMode', 'Select payment mode')}
@@ -52,26 +55,33 @@ const PriceField: React.FC<PriceFieldProps> = ({ field, index, control, removeSe
52
55
  />
53
56
  )}
54
57
  />
55
- </ResponsiveWrapper>
56
- <ResponsiveWrapper>
58
+ </div>
59
+ <div className={styles.field}>
57
60
  <Controller
58
61
  name={`servicePrices.${index}.price`}
59
62
  control={control}
60
63
  render={({ field }) => (
61
64
  <NumberInput
62
65
  id={`servicePrices.${index}.price`}
63
- onChange={(e, { value }) => field.onChange(parseFloat(value.toString()))}
64
- type="number"
66
+ allowEmpty
67
+ hideSteppers
68
+ onChange={(_e, { value }) => field.onChange(value === '' ? undefined : Number(value))}
65
69
  label={t('price', 'Price')}
66
- placeholder={t('enterPrice', 'Enter price')}
67
- defaultValue={field.value}
70
+ min={0}
71
+ step={0.01}
72
+ value={field.value ?? ''}
68
73
  invalid={!!errors?.servicePrices?.[index]?.price}
69
74
  invalidText={errors?.servicePrices?.[index]?.price?.message}
70
75
  />
71
76
  )}
72
77
  />
73
- </ResponsiveWrapper>
74
- <IconButton kind="tertiary" size="md" label={t('delete', 'Delete')} onClick={() => removeServicePrice(index)}>
78
+ </div>
79
+ <IconButton
80
+ className={styles.removeButton}
81
+ kind="tertiary"
82
+ size="md"
83
+ label={t('delete', 'Delete')}
84
+ onClick={() => removeServicePrice(index)}>
75
85
  <TrashCan />
76
86
  </IconButton>
77
87
  </div>
@@ -5,28 +5,33 @@ const ServiceConceptSchema = z.object({
5
5
  uuid: z.string(),
6
6
  display: z.string(),
7
7
  }),
8
- conceptName: z.object({
9
- uuid: z.string(),
10
- display: z.string(),
11
- }),
12
- display: z.string(),
8
+ conceptName: z
9
+ .object({
10
+ uuid: z.string(),
11
+ display: z.string(),
12
+ })
13
+ .optional(),
14
+ display: z.string().optional(),
15
+ });
16
+
17
+ const paymentModeSchema = z.object({
18
+ uuid: z.string({ required_error: 'Select a payment mode' }),
19
+ name: z.string().optional(),
13
20
  });
14
21
 
15
22
  export const servicePriceSchema = z.object({
16
- price: z.number().gt(0.01, 'Price must be greater than 0').min(0.01, 'Price must be at least 0.01'),
17
- paymentMode: z.object({ uuid: z.string(), name: z.string() }),
23
+ paymentMode: paymentModeSchema,
24
+ // A price of 0 is valid (a free or fully-waived mode); only a negative price is rejected.
25
+ price: z.coerce.number().min(0, 'Price cannot be negative'),
18
26
  });
19
27
 
28
+ // A billable item is a concept plus one price per payment mode -- there is no separate default price.
20
29
  export const billableFormSchema = z.object({
21
30
  name: z.string().min(1, 'Service name is required'),
22
- shortName: z
23
- .string()
24
- .refine((value) => value.length !== 1, { message: 'Short name must be at least 1 character long' }),
25
- serviceType: z.object({ uuid: z.string(), display: z.string() }),
26
- servicePrices: z.array(servicePriceSchema).min(1, 'At least one price is required'),
27
- serviceStatus: z.enum(['ENABLED', 'DISABLED']),
28
31
  concept: ServiceConceptSchema,
29
- stockItem: z.string().nullable().optional(),
32
+ serviceType: z.object({ uuid: z.string(), display: z.string() }).nullable().optional(),
33
+ description: z.string().optional(),
34
+ servicePrices: z.array(servicePriceSchema).min(1, 'Add at least one payment mode price'),
30
35
  });
31
36
 
32
37
  export type BillableFormSchema = z.infer<typeof billableFormSchema>;