@digihmis/esm-administration-app 1.0.0 → 1.0.1
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/package.json +1 -1
- package/release-version.js +4 -4
- package/src/billing-management/billing-charge-items/billing-charge-items-modal/billing-charge-items.resource.ts +2 -1
- package/src/billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-items.resource.ts +30 -7
- package/src/billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-service.workspace.tsx +79 -107
- package/src/billing-management/billing-charge-items/billing-charge-items-workspace/components/price.component.scss +16 -0
- package/src/billing-management/billing-charge-items/billing-charge-items-workspace/components/price.component.tsx +30 -20
- package/src/billing-management/billing-charge-items/billing-charge-items-workspace/schemas/form-schemas.ts +19 -14
- package/src/billing-management/billing-charge-items/billing-charge-items.component.tsx +26 -35
- package/src/billing-management/billing-charge-items/billing-charge-items.resource.ts +1 -1
- package/src/billing-management/billing-charge-items/type/index.ts +10 -10
- package/src/billing-management/billing-charge-items/utils/index.ts +32 -74
- package/src/billing-management/billing-payment-modes/billing-payment-modes.component.tsx +24 -166
- package/src/billing-management/billing-payment-modes/billing-payment-modes.resource.ts +24 -14
- package/src/billing-management/billing-payment-points/billing-payment-point-modal/billing-delete-payment-point.modal.tsx +1 -1
- package/src/billing-management/billing-payment-points/billing-payment-point.resource.ts +1 -1
- package/src/billing-management/billing-payment-points/billing-payment-point.scss +47 -0
- package/src/billing-management/billing-payment-points/billing-payment-points-workspace/billing-payment-point.resource.ts +18 -23
- package/src/billing-management/billing-payment-points/billing-payment-points-workspace/billing-payment-points.workspace.tsx +1 -1
- package/src/config-schema.ts +17 -0
- package/src/dashboard.meta.ts +8 -0
- package/src/document-branding/document-branding-workspace/document-branding.workspace.scss +34 -0
- package/src/document-branding/document-branding-workspace/document-branding.workspace.tsx +160 -0
- package/src/document-branding/document-branding.component.tsx +185 -0
- package/src/document-branding/document-branding.resource.ts +54 -0
- package/src/document-branding/document-branding.scss +69 -0
- package/src/index.ts +15 -9
- package/src/routes.json +34 -13
- package/dist/101.js +0 -1
- package/dist/101.js.map +0 -1
- package/dist/117.js +0 -1
- package/dist/117.js.map +0 -1
- package/dist/132.js +0 -1
- package/dist/132.js.map +0 -1
- package/dist/15.js +0 -1
- package/dist/15.js.map +0 -1
- package/dist/152.js +0 -1
- package/dist/152.js.map +0 -1
- package/dist/153.js +0 -1
- package/dist/153.js.map +0 -1
- package/dist/190.js +0 -1
- package/dist/190.js.map +0 -1
- package/dist/209.js +0 -1
- package/dist/209.js.map +0 -1
- package/dist/266.js +0 -1
- package/dist/266.js.map +0 -1
- package/dist/317.js +0 -1
- package/dist/317.js.map +0 -1
- package/dist/349.js +0 -1
- package/dist/349.js.map +0 -1
- package/dist/371.js +0 -1
- package/dist/371.js.map +0 -1
- package/dist/378.js +0 -1
- package/dist/378.js.map +0 -1
- package/dist/394.js +0 -1
- package/dist/394.js.map +0 -1
- package/dist/442.js +0 -1
- package/dist/442.js.map +0 -1
- package/dist/45.js +0 -1
- package/dist/45.js.map +0 -1
- package/dist/454.js +0 -1
- package/dist/454.js.map +0 -1
- package/dist/466.js +0 -1
- package/dist/466.js.map +0 -1
- package/dist/482.js +0 -1
- package/dist/482.js.map +0 -1
- package/dist/501.js +0 -1
- package/dist/501.js.map +0 -1
- package/dist/508.js +0 -1
- package/dist/508.js.map +0 -1
- package/dist/578.js +0 -1
- package/dist/578.js.map +0 -1
- package/dist/598.js +0 -1
- package/dist/598.js.map +0 -1
- package/dist/606.js +0 -1
- package/dist/606.js.map +0 -1
- package/dist/61.js +0 -1
- package/dist/61.js.map +0 -1
- package/dist/640.js +0 -1
- package/dist/640.js.map +0 -1
- package/dist/685.js +0 -1
- package/dist/685.js.map +0 -1
- package/dist/689.js +0 -1
- package/dist/689.js.map +0 -1
- package/dist/709.js +0 -1
- package/dist/709.js.map +0 -1
- package/dist/712.js +0 -1
- package/dist/712.js.map +0 -1
- package/dist/720.js +0 -1
- package/dist/720.js.map +0 -1
- package/dist/737.js +0 -12
- package/dist/737.js.map +0 -1
- package/dist/742.js +0 -1
- package/dist/742.js.map +0 -1
- package/dist/747.js +0 -1
- package/dist/747.js.map +0 -1
- package/dist/771.js +0 -1
- package/dist/771.js.map +0 -1
- package/dist/806.js +0 -1
- package/dist/806.js.map +0 -1
- package/dist/912.js +0 -1
- package/dist/912.js.map +0 -1
- package/dist/913.js +0 -1
- package/dist/913.js.map +0 -1
- package/dist/916.js +0 -15
- package/dist/916.js.map +0 -1
- package/dist/940.js +0 -1
- package/dist/940.js.map +0 -1
- package/dist/digihmis-esm-administration-app.js +0 -6
- package/dist/digihmis-esm-administration-app.js.buildmanifest.json +0 -1100
- package/dist/digihmis-esm-administration-app.js.map +0 -1
- package/dist/main.js +0 -17
- package/dist/main.js.map +0 -1
- package/dist/routes.json +0 -1
- package/src/billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-item.workspace.tsx +0 -212
- package/src/billing-management/billing-charge-items/billing-charge-items-workspace/components/stock-search.component.tsx +0 -107
package/package.json
CHANGED
package/release-version.js
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
/* tslint:disable */
|
|
4
4
|
export const VERSION = {
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"raw": "
|
|
7
|
-
"hash": "
|
|
6
|
+
"raw": "4d5fb07-dirty",
|
|
7
|
+
"hash": "4d5fb07",
|
|
8
8
|
"distance": null,
|
|
9
9
|
"tag": null,
|
|
10
10
|
"semver": null,
|
|
11
|
-
"suffix": "
|
|
11
|
+
"suffix": "4d5fb07-dirty",
|
|
12
12
|
"semverString": null,
|
|
13
13
|
"version": "1.0.0",
|
|
14
|
-
"buildDate": "2026-07-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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}/
|
|
112
|
-
|
|
117
|
+
handleMutation(`${restBaseUrl}/digihmisbilling/billableitem`);
|
|
113
118
|
closeWorkspaceWithSavedChanges();
|
|
114
119
|
}
|
|
115
120
|
} catch (e) {
|
|
116
|
-
const
|
|
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
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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('
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
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>
|
|
@@ -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 {
|
|
6
|
+
import { type BillableFormSchema } from '../schemas/form-schemas';
|
|
8
7
|
import { usePaymentModes } from '../../../billing-payment-modes/billing-payment-modes.resource';
|
|
9
|
-
import
|
|
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
|
-
(
|
|
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.
|
|
34
|
-
<
|
|
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('
|
|
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
|
-
</
|
|
56
|
-
<
|
|
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
|
-
|
|
64
|
-
|
|
66
|
+
allowEmpty
|
|
67
|
+
hideSteppers
|
|
68
|
+
onChange={(_e, { value }) => field.onChange(value === '' ? undefined : Number(value))}
|
|
65
69
|
label={t('price', 'Price')}
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
</
|
|
74
|
-
<IconButton
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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>;
|
|
@@ -40,16 +40,16 @@ const ChargeItemsTable = () => {
|
|
|
40
40
|
header: t('name', 'Name'),
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
|
-
key: '
|
|
44
|
-
header: t('
|
|
43
|
+
key: 'serviceType',
|
|
44
|
+
header: t('serviceType', 'Service type'),
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
|
-
key: '
|
|
48
|
-
header: t('
|
|
47
|
+
key: 'prices',
|
|
48
|
+
header: t('prices', 'Prices'),
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
|
-
key: '
|
|
52
|
-
header: t('
|
|
51
|
+
key: 'status',
|
|
52
|
+
header: t('status', 'Status'),
|
|
53
53
|
},
|
|
54
54
|
];
|
|
55
55
|
|
|
@@ -58,17 +58,22 @@ const ChargeItemsTable = () => {
|
|
|
58
58
|
return [];
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
const formatMode = (mode: string) =>
|
|
62
|
+
mode ? mode.charAt(0) + mode.slice(1).toLowerCase().replace(/_/g, ' ') : mode;
|
|
63
|
+
|
|
64
|
+
return chargeSummaryItems.map((item) => {
|
|
65
|
+
const perMode = item.prices ?? [];
|
|
66
|
+
return {
|
|
67
|
+
id: item.uuid,
|
|
68
|
+
name: item.name || '-',
|
|
69
|
+
serviceType: item.serviceType?.display || '—',
|
|
70
|
+
prices: perMode.length
|
|
71
|
+
? perMode.map((p) => `${formatMode(p.paymentMode)}: ${p.price}`).join(', ')
|
|
72
|
+
: (item.price ?? '-'),
|
|
73
|
+
status: item.retired ? t('retired', 'Retired') : t('active', 'Active'),
|
|
74
|
+
chargeItem: item,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
72
77
|
}, [chargeSummaryItems, t]);
|
|
73
78
|
|
|
74
79
|
const paginatedRows = useMemo(() => {
|
|
@@ -83,22 +88,11 @@ const ChargeItemsTable = () => {
|
|
|
83
88
|
});
|
|
84
89
|
};
|
|
85
90
|
|
|
86
|
-
const handleAddChargeItem = () => {
|
|
87
|
-
launchWorkspace('billing-charge-item-workspace', {
|
|
88
|
-
workspaceTitle: t('addChargeItem', 'Add Charge Item'),
|
|
89
|
-
});
|
|
90
|
-
};
|
|
91
|
-
|
|
92
91
|
const handleEdit = (service) => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
})
|
|
98
|
-
: launchWorkspace('billing-charge-item-workspace', {
|
|
99
|
-
initialValues: service,
|
|
100
|
-
workspaceTitle: t('editChargeItem', 'Edit Charge Item'),
|
|
101
|
-
});
|
|
92
|
+
launchWorkspace('billing-charge-service-workspace', {
|
|
93
|
+
initialValues: service,
|
|
94
|
+
workspaceTitle: t('editServiceChargeItem', 'Edit Service Charge Item'),
|
|
95
|
+
});
|
|
102
96
|
};
|
|
103
97
|
|
|
104
98
|
const showDeleteChargeItemModal = (chargeItem) => {
|
|
@@ -128,9 +122,6 @@ const ChargeItemsTable = () => {
|
|
|
128
122
|
<Button kind="ghost" renderIcon={(props) => <Add size={16} {...props} />} onClick={handleAddChargeService}>
|
|
129
123
|
{t('addChargeService', 'Add Charge Service')}
|
|
130
124
|
</Button>
|
|
131
|
-
<Button kind="ghost" renderIcon={(props) => <Add size={16} {...props} />} onClick={handleAddChargeItem}>
|
|
132
|
-
{t('addChargeItem', 'Add Charge Item')}
|
|
133
|
-
</Button>
|
|
134
125
|
</div>
|
|
135
126
|
</CardHeader>
|
|
136
127
|
<Layer>
|
|
@@ -3,7 +3,7 @@ import useSWR from 'swr';
|
|
|
3
3
|
import { type ChargeItemsResponse } from './type';
|
|
4
4
|
|
|
5
5
|
export const useChargeItems = () => {
|
|
6
|
-
const url = `${restBaseUrl}/
|
|
6
|
+
const url = `${restBaseUrl}/digihmisbilling/billableitem`;
|
|
7
7
|
|
|
8
8
|
const { data, isLoading, isValidating, error, mutate } = useSWR<FetchResponse<ChargeItemsResponse>>(
|
|
9
9
|
url,
|