@openmrs/esm-billing-app 1.0.2-pre.92 → 1.0.2-pre.933
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/.eslintrc +16 -2
- package/README.md +54 -9
- package/__mocks__/bills.mock.ts +12 -0
- package/__mocks__/react-i18next.js +6 -5
- package/dist/1119.js +1 -1
- package/dist/1146.js +1 -2
- package/dist/1146.js.map +1 -1
- package/dist/1197.js +1 -1
- package/dist/1537.js +1 -0
- package/dist/1537.js.map +1 -0
- package/dist/1856.js +1 -0
- package/dist/1856.js.map +1 -0
- package/dist/2146.js +1 -1
- package/dist/2524.js +1 -0
- package/dist/2524.js.map +1 -0
- package/dist/2690.js +1 -1
- package/dist/3099.js +1 -1
- package/dist/3584.js +1 -1
- package/dist/3717.js +2 -0
- package/dist/3717.js.map +1 -0
- package/dist/4055.js +1 -1
- package/dist/4132.js +1 -1
- package/dist/4300.js +1 -1
- package/dist/4335.js +1 -1
- package/dist/4618.js +1 -1
- package/dist/4652.js +1 -1
- package/dist/4724.js +1 -0
- package/dist/4724.js.map +1 -0
- package/dist/4739.js +1 -1
- package/dist/4739.js.map +1 -1
- package/dist/4944.js +1 -1
- package/dist/5173.js +1 -1
- package/dist/5241.js +1 -1
- package/dist/5442.js +1 -1
- package/dist/5661.js +1 -1
- package/dist/6022.js +1 -1
- package/dist/6468.js +1 -1
- package/dist/6540.js +1 -1
- package/dist/6540.js.map +1 -1
- package/dist/6679.js +1 -1
- package/dist/6840.js +1 -1
- package/dist/6859.js +1 -1
- package/dist/7097.js +1 -1
- package/dist/7159.js +1 -1
- package/dist/723.js +1 -1
- package/dist/7255.js +1 -1
- package/dist/7255.js.map +1 -1
- package/dist/7617.js +1 -1
- package/dist/795.js +1 -1
- package/dist/8163.js +1 -1
- package/dist/8349.js +1 -1
- package/dist/8572.js +1 -0
- package/dist/8572.js.map +1 -0
- package/dist/8618.js +1 -1
- package/dist/8708.js +2 -0
- package/dist/{6557.js.LICENSE.txt → 8708.js.LICENSE.txt} +22 -0
- package/dist/8708.js.map +1 -0
- package/dist/890.js +1 -1
- package/dist/9214.js +1 -1
- package/dist/9538.js +1 -1
- package/dist/9569.js +1 -1
- package/dist/961.js +1 -1
- package/dist/961.js.map +1 -1
- package/dist/986.js +1 -1
- package/dist/9879.js +1 -1
- package/dist/9895.js +1 -1
- package/dist/9900.js +1 -1
- package/dist/9913.js +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-billing-app.js +1 -1
- package/dist/openmrs-esm-billing-app.js.buildmanifest.json +271 -285
- package/dist/openmrs-esm-billing-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/e2e/README.md +19 -18
- package/e2e/core/test.ts +1 -1
- package/e2e/fixtures/api.ts +1 -1
- package/e2e/specs/sample-test.spec.ts +0 -1
- package/e2e/support/github/Dockerfile +1 -1
- package/package.json +18 -15
- package/src/bill-history/bill-history.component.tsx +20 -28
- package/src/bill-history/bill-history.scss +4 -94
- package/src/bill-history/bill-history.test.tsx +37 -78
- package/src/bill-item-actions/bill-item-actions.scss +21 -5
- package/src/bill-item-actions/edit-bill-item.modal.tsx +225 -0
- package/src/bill-item-actions/edit-bill-item.test.tsx +214 -40
- package/src/billable-services/bill-waiver/bill-selection.component.tsx +5 -5
- package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +28 -32
- package/src/billable-services/bill-waiver/patient-bills.component.tsx +7 -7
- package/src/billable-services/bill-waiver/utils.ts +13 -3
- package/src/billable-services/{create-edit/add-billable-service.scss → billable-service-form/billable-service-form.scss} +32 -64
- package/src/billable-services/billable-service-form/billable-service-form.test.tsx +898 -0
- package/src/billable-services/billable-service-form/billable-service-form.workspace.tsx +504 -0
- package/src/billable-services/billable-service.resource.ts +71 -27
- package/src/billable-services/billable-services-home.component.tsx +13 -42
- package/src/billable-services/billable-services-left-panel-link.component.tsx +48 -0
- package/src/billable-services/billable-services-left-panel-menu.component.tsx +46 -0
- package/src/billable-services/billable-services-menu-item/item.component.tsx +5 -4
- package/src/billable-services/billable-services.component.tsx +156 -152
- package/src/billable-services/billable-services.scss +29 -0
- package/src/billable-services/billable-services.test.tsx +6 -49
- package/src/billable-services/cash-point/add-cash-point.modal.tsx +170 -0
- package/src/billable-services/cash-point/cash-point-configuration.component.tsx +19 -193
- package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
- package/src/billable-services/dashboard/dashboard.component.tsx +0 -2
- package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +77 -0
- package/src/billable-services/payment-modes/payment-mode-form.modal.tsx +131 -0
- package/src/billable-services/payment-modes/payment-modes-config.component.tsx +139 -0
- package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -4
- package/src/billable-services-admin-card-link.component.test.tsx +2 -2
- package/src/billable-services-admin-card-link.component.tsx +1 -1
- package/src/billing-dashboard/billing-dashboard.scss +1 -1
- package/src/billing-form/billing-checkin-form.component.tsx +21 -17
- package/src/billing-form/billing-checkin-form.test.tsx +99 -26
- package/src/billing-form/billing-form.component.tsx +222 -292
- package/src/billing-form/billing-form.scss +143 -0
- package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +1 -1
- package/src/billing.resource.ts +69 -74
- package/src/bills-table/bills-table.component.tsx +3 -3
- package/src/bills-table/bills-table.test.tsx +98 -54
- package/src/config-schema.ts +52 -24
- package/src/dashboard.meta.ts +4 -2
- package/src/helpers/functions.ts +5 -4
- package/src/index.ts +71 -9
- package/src/invoice/invoice-table.component.tsx +36 -70
- package/src/invoice/invoice-table.scss +8 -5
- package/src/invoice/invoice-table.test.tsx +273 -62
- package/src/invoice/invoice.component.tsx +39 -32
- package/src/invoice/invoice.scss +11 -4
- package/src/invoice/invoice.test.tsx +324 -120
- package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +9 -9
- package/src/invoice/payments/payment-form/payment-form.component.tsx +43 -34
- package/src/invoice/payments/payment-form/payment-form.scss +5 -6
- package/src/invoice/payments/payment-form/payment-form.test.tsx +216 -66
- package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
- package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
- package/src/invoice/payments/payments.component.tsx +55 -67
- package/src/invoice/payments/payments.scss +4 -3
- package/src/invoice/payments/payments.test.tsx +282 -0
- package/src/invoice/payments/utils.ts +15 -27
- package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
- package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
- package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
- package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
- package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
- package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
- package/src/invoice/printable-invoice/printable-invoice.component.tsx +20 -34
- package/src/left-panel-link.test.tsx +1 -4
- package/src/metrics-cards/metrics-cards.component.tsx +16 -6
- package/src/metrics-cards/metrics-cards.scss +4 -0
- package/src/metrics-cards/metrics-cards.test.tsx +18 -5
- package/src/modal/require-payment-modal.test.tsx +27 -22
- package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +18 -19
- package/src/routes.json +44 -20
- package/src/types/index.ts +86 -23
- package/translations/am.json +132 -77
- package/translations/ar.json +133 -78
- package/translations/ar_SY.json +133 -78
- package/translations/bn.json +135 -80
- package/translations/de.json +133 -78
- package/translations/en.json +134 -79
- package/translations/en_US.json +133 -78
- package/translations/es.json +132 -77
- package/translations/es_MX.json +133 -78
- package/translations/fr.json +138 -83
- package/translations/he.json +132 -77
- package/translations/hi.json +133 -78
- package/translations/hi_IN.json +133 -78
- package/translations/id.json +133 -78
- package/translations/it.json +159 -104
- package/translations/ka.json +133 -78
- package/translations/km.json +132 -77
- package/translations/ku.json +133 -78
- package/translations/ky.json +133 -78
- package/translations/lg.json +133 -78
- package/translations/ne.json +133 -78
- package/translations/pl.json +133 -78
- package/translations/pt.json +133 -78
- package/translations/pt_BR.json +133 -78
- package/translations/qu.json +133 -78
- package/translations/ro_RO.json +220 -165
- package/translations/ru_RU.json +133 -78
- package/translations/si.json +133 -78
- package/translations/sw.json +133 -78
- package/translations/sw_KE.json +133 -78
- package/translations/tr.json +133 -78
- package/translations/tr_TR.json +133 -78
- package/translations/uk.json +133 -78
- package/translations/uz.json +133 -78
- package/translations/uz@Latn.json +133 -78
- package/translations/uz_UZ.json +133 -78
- package/translations/vi.json +133 -78
- package/translations/zh.json +133 -78
- package/translations/zh_CN.json +163 -108
- package/dist/1146.js.LICENSE.txt +0 -21
- package/dist/2352.js +0 -1
- package/dist/2352.js.map +0 -1
- package/dist/246.js +0 -1
- package/dist/246.js.map +0 -1
- package/dist/4689.js +0 -2
- package/dist/4689.js.map +0 -1
- package/dist/6557.js +0 -2
- package/dist/6557.js.map +0 -1
- package/dist/8638.js +0 -1
- package/dist/8638.js.map +0 -1
- package/dist/9968.js +0 -1
- package/dist/9968.js.map +0 -1
- package/src/bill-item-actions/edit-bill-item.component.tsx +0 -221
- package/src/billable-services/create-edit/add-billable-service.component.tsx +0 -401
- package/src/billable-services/create-edit/add-billable-service.test.tsx +0 -154
- package/src/billable-services/dashboard/service-metrics.component.tsx +0 -41
- package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
- package/src/invoice/payments/payments.component.test.tsx +0 -121
- /package/dist/{4689.js.LICENSE.txt → 3717.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
ButtonSet,
|
|
5
|
+
ComboBox,
|
|
6
|
+
Dropdown,
|
|
7
|
+
Form,
|
|
8
|
+
FormGroup,
|
|
9
|
+
FormLabel,
|
|
10
|
+
InlineLoading,
|
|
11
|
+
Layer,
|
|
12
|
+
NumberInput,
|
|
13
|
+
Search,
|
|
14
|
+
Stack,
|
|
15
|
+
TextInput,
|
|
16
|
+
Tile,
|
|
17
|
+
} from '@carbon/react';
|
|
18
|
+
import { type TFunction } from 'i18next';
|
|
19
|
+
import { Controller, useFieldArray, useForm, useWatch } from 'react-hook-form';
|
|
20
|
+
import { useTranslation } from 'react-i18next';
|
|
21
|
+
import { Add, TrashCan } from '@carbon/react/icons';
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
24
|
+
import {
|
|
25
|
+
getCoreTranslation,
|
|
26
|
+
ResponsiveWrapper,
|
|
27
|
+
showSnackbar,
|
|
28
|
+
useDebounce,
|
|
29
|
+
useLayoutType,
|
|
30
|
+
} from '@openmrs/esm-framework';
|
|
31
|
+
import type { BillableService, ServicePrice } from '../../types';
|
|
32
|
+
import {
|
|
33
|
+
createBillableService,
|
|
34
|
+
updateBillableService,
|
|
35
|
+
useConceptsSearch,
|
|
36
|
+
usePaymentModes,
|
|
37
|
+
useServiceTypes,
|
|
38
|
+
} from '../billable-service.resource';
|
|
39
|
+
import styles from './billable-service-form.scss';
|
|
40
|
+
|
|
41
|
+
interface BillableServiceFormWorkspaceProps {
|
|
42
|
+
serviceToEdit?: BillableService;
|
|
43
|
+
closeWorkspace: () => void;
|
|
44
|
+
closeWorkspaceWithSavedChanges?: () => void;
|
|
45
|
+
promptBeforeClosing?: (testFcn: () => boolean) => void;
|
|
46
|
+
onWorkspaceClose?: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface BillableServiceFormData {
|
|
50
|
+
name: string;
|
|
51
|
+
payment: PaymentModeForm[];
|
|
52
|
+
serviceType: ServiceType | null;
|
|
53
|
+
concept?: { uuid: string; display: string } | null;
|
|
54
|
+
shortName?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface PaymentModeForm {
|
|
58
|
+
paymentMode: string;
|
|
59
|
+
price: string | number | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ServiceType {
|
|
63
|
+
uuid: string;
|
|
64
|
+
display: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const DEFAULT_PAYMENT_OPTION: PaymentModeForm = { paymentMode: '', price: '' };
|
|
68
|
+
const MAX_NAME_LENGTH = 255;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Transforms a BillableService into form data structure
|
|
72
|
+
* Centralizes the mapping logic to avoid duplication between defaultValues and reset()
|
|
73
|
+
* Exported for testing
|
|
74
|
+
*/
|
|
75
|
+
export const transformServiceToFormData = (service?: BillableService): BillableServiceFormData => {
|
|
76
|
+
if (!service) {
|
|
77
|
+
return {
|
|
78
|
+
name: '',
|
|
79
|
+
shortName: '',
|
|
80
|
+
serviceType: null,
|
|
81
|
+
concept: null,
|
|
82
|
+
payment: [DEFAULT_PAYMENT_OPTION],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
name: service.name || '',
|
|
88
|
+
shortName: service.shortName || '',
|
|
89
|
+
serviceType: service.serviceType || null,
|
|
90
|
+
concept: service.concept ? { uuid: service.concept.uuid, display: service.concept.display } : null,
|
|
91
|
+
payment: service.servicePrices?.map((servicePrice: ServicePrice) => ({
|
|
92
|
+
paymentMode: servicePrice.paymentMode?.uuid || '',
|
|
93
|
+
price: servicePrice.price ?? '',
|
|
94
|
+
})) || [DEFAULT_PAYMENT_OPTION],
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Normalizes price value from form (string | number | undefined) to number
|
|
100
|
+
* Handles Carbon NumberInput which can return either type
|
|
101
|
+
* Exported for testing
|
|
102
|
+
*/
|
|
103
|
+
export const normalizePrice = (price: string | number | undefined): number => {
|
|
104
|
+
if (typeof price === 'number') {
|
|
105
|
+
return price;
|
|
106
|
+
}
|
|
107
|
+
return parseFloat(String(price));
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const createBillableServiceSchema = (t: TFunction) => {
|
|
111
|
+
const servicePriceSchema = z.object({
|
|
112
|
+
paymentMode: z
|
|
113
|
+
.string({
|
|
114
|
+
required_error: t('paymentModeRequired', 'Payment mode is required'),
|
|
115
|
+
})
|
|
116
|
+
.trim()
|
|
117
|
+
.min(1, t('paymentModeRequired', 'Payment mode is required')),
|
|
118
|
+
price: z.union([z.number(), z.string(), z.undefined()]).superRefine((val, ctx) => {
|
|
119
|
+
if (val === undefined || val === null || val === '') {
|
|
120
|
+
ctx.addIssue({
|
|
121
|
+
code: z.ZodIssueCode.custom,
|
|
122
|
+
message: t('priceIsRequired', 'Price is required'),
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const numValue = typeof val === 'number' ? val : parseFloat(val);
|
|
128
|
+
if (isNaN(numValue) || numValue <= 0) {
|
|
129
|
+
ctx.addIssue({
|
|
130
|
+
code: z.ZodIssueCode.custom,
|
|
131
|
+
message: t('priceMustBePositive', 'Price must be greater than 0'),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return z.object({
|
|
138
|
+
name: z
|
|
139
|
+
.string({
|
|
140
|
+
required_error: t('serviceNameRequired', 'Service name is required'),
|
|
141
|
+
})
|
|
142
|
+
.trim()
|
|
143
|
+
.min(1, t('serviceNameRequired', 'Service name is required'))
|
|
144
|
+
.max(
|
|
145
|
+
MAX_NAME_LENGTH,
|
|
146
|
+
t('serviceNameExceedsLimit', 'Service name cannot exceed {{MAX_NAME_LENGTH}} characters', {
|
|
147
|
+
MAX_NAME_LENGTH,
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
150
|
+
shortName: z
|
|
151
|
+
.string()
|
|
152
|
+
.max(
|
|
153
|
+
MAX_NAME_LENGTH,
|
|
154
|
+
t('shortNameExceedsLimit', 'Short name cannot exceed {{MAX_NAME_LENGTH}} characters', { MAX_NAME_LENGTH }),
|
|
155
|
+
)
|
|
156
|
+
.optional(),
|
|
157
|
+
serviceType: z
|
|
158
|
+
.object({
|
|
159
|
+
uuid: z.string(),
|
|
160
|
+
display: z.string(),
|
|
161
|
+
})
|
|
162
|
+
.nullable()
|
|
163
|
+
.refine((val) => val !== null, t('serviceTypeRequired', 'Service type is required')),
|
|
164
|
+
concept: z
|
|
165
|
+
.object({
|
|
166
|
+
uuid: z.string(),
|
|
167
|
+
display: z.string(),
|
|
168
|
+
})
|
|
169
|
+
.nullable()
|
|
170
|
+
.optional(),
|
|
171
|
+
payment: z.array(servicePriceSchema).min(1, t('paymentOptionRequired', 'At least one payment option is required')),
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const BillableServiceFormWorkspace: React.FC<BillableServiceFormWorkspaceProps> = ({
|
|
176
|
+
serviceToEdit,
|
|
177
|
+
closeWorkspace,
|
|
178
|
+
closeWorkspaceWithSavedChanges,
|
|
179
|
+
onWorkspaceClose,
|
|
180
|
+
}) => {
|
|
181
|
+
const { t } = useTranslation();
|
|
182
|
+
const layout = useLayoutType();
|
|
183
|
+
const isTablet = layout === 'tablet';
|
|
184
|
+
const { paymentModes, isLoadingPaymentModes } = usePaymentModes();
|
|
185
|
+
const { serviceTypes, isLoadingServiceTypes } = useServiceTypes();
|
|
186
|
+
|
|
187
|
+
const billableServiceSchema = useMemo(() => createBillableServiceSchema(t), [t]);
|
|
188
|
+
|
|
189
|
+
const {
|
|
190
|
+
control,
|
|
191
|
+
handleSubmit,
|
|
192
|
+
formState: { errors },
|
|
193
|
+
setValue,
|
|
194
|
+
reset,
|
|
195
|
+
} = useForm<BillableServiceFormData>({
|
|
196
|
+
mode: 'all',
|
|
197
|
+
defaultValues: transformServiceToFormData(serviceToEdit),
|
|
198
|
+
resolver: zodResolver(billableServiceSchema),
|
|
199
|
+
});
|
|
200
|
+
const { fields, remove, append } = useFieldArray({ name: 'payment', control });
|
|
201
|
+
|
|
202
|
+
const handleAppendPaymentMode = () => append(DEFAULT_PAYMENT_OPTION);
|
|
203
|
+
const handleRemovePaymentMode = (index: number) => remove(index);
|
|
204
|
+
|
|
205
|
+
const searchInputRef = useRef(null);
|
|
206
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
207
|
+
|
|
208
|
+
const selectedConcept = useWatch({ control, name: 'concept' });
|
|
209
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
210
|
+
const debouncedSearchTerm = useDebounce(searchTerm.trim());
|
|
211
|
+
const { searchResults, isSearching } = useConceptsSearch(debouncedSearchTerm);
|
|
212
|
+
|
|
213
|
+
// Re-initialize form when editing and dependencies load
|
|
214
|
+
// Needed because serviceTypes/paymentModes may not be available during initial render
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
if (serviceToEdit && !isLoadingPaymentModes && !isLoadingServiceTypes) {
|
|
217
|
+
reset(transformServiceToFormData(serviceToEdit));
|
|
218
|
+
}
|
|
219
|
+
}, [serviceToEdit, isLoadingPaymentModes, isLoadingServiceTypes, reset]);
|
|
220
|
+
|
|
221
|
+
const onSubmit = async (data: BillableServiceFormData) => {
|
|
222
|
+
setIsSubmitting(true);
|
|
223
|
+
|
|
224
|
+
const payload = {
|
|
225
|
+
name: data.name,
|
|
226
|
+
shortName: data.shortName || '',
|
|
227
|
+
serviceType: data.serviceType!.uuid,
|
|
228
|
+
servicePrices: data.payment.map((payment) => {
|
|
229
|
+
const mode = paymentModes.find((m) => m.uuid === payment.paymentMode);
|
|
230
|
+
return {
|
|
231
|
+
paymentMode: payment.paymentMode,
|
|
232
|
+
name: mode?.name || 'Unknown',
|
|
233
|
+
price: normalizePrice(payment.price),
|
|
234
|
+
};
|
|
235
|
+
}),
|
|
236
|
+
serviceStatus: 'ENABLED',
|
|
237
|
+
concept: data.concept?.uuid,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
if (serviceToEdit) {
|
|
242
|
+
await updateBillableService(serviceToEdit.uuid, payload);
|
|
243
|
+
} else {
|
|
244
|
+
await createBillableService(payload);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
showSnackbar({
|
|
248
|
+
title: serviceToEdit
|
|
249
|
+
? t('billableServiceUpdated', 'Billable service updated')
|
|
250
|
+
: t('billableServiceCreated', 'Billable service created'),
|
|
251
|
+
subtitle: serviceToEdit
|
|
252
|
+
? t('billableServiceUpdatedSuccessfully', 'Billable service updated successfully')
|
|
253
|
+
: t('billableServiceCreatedSuccessfully', 'Billable service created successfully'),
|
|
254
|
+
kind: 'success',
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Call onWorkspaceClose callback to refresh data in parent component
|
|
258
|
+
if (onWorkspaceClose) {
|
|
259
|
+
onWorkspaceClose();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Close the workspace
|
|
263
|
+
if (closeWorkspaceWithSavedChanges) {
|
|
264
|
+
closeWorkspaceWithSavedChanges();
|
|
265
|
+
} else {
|
|
266
|
+
closeWorkspace();
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
showSnackbar({
|
|
270
|
+
title: t('billPaymentError', 'Bill payment error'),
|
|
271
|
+
kind: 'error',
|
|
272
|
+
subtitle: error instanceof Error ? error.message : String(error),
|
|
273
|
+
});
|
|
274
|
+
} finally {
|
|
275
|
+
setIsSubmitting(false);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const getPaymentErrorMessage = () => {
|
|
280
|
+
const paymentError = errors.payment;
|
|
281
|
+
if (paymentError && typeof paymentError.message === 'string') {
|
|
282
|
+
return paymentError.message;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
if (isLoadingPaymentModes || isLoadingServiceTypes) {
|
|
288
|
+
return (
|
|
289
|
+
<InlineLoading
|
|
290
|
+
status="active"
|
|
291
|
+
iconDescription={t('loadingDescription', 'Loading')}
|
|
292
|
+
description={t('loading', 'Loading data') + '...'}
|
|
293
|
+
/>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<Form
|
|
299
|
+
aria-label={t('billableServiceForm', 'Billable service form')}
|
|
300
|
+
className={styles.form}
|
|
301
|
+
id="billable-service-form"
|
|
302
|
+
onSubmit={handleSubmit(onSubmit)}>
|
|
303
|
+
<Stack className={styles.stack} gap={5}>
|
|
304
|
+
<FormGroup className={styles.formGroup}>
|
|
305
|
+
{serviceToEdit ? (
|
|
306
|
+
<FormLabel className={styles.serviceNameLabel}>{serviceToEdit.name}</FormLabel>
|
|
307
|
+
) : (
|
|
308
|
+
<Controller
|
|
309
|
+
name="name"
|
|
310
|
+
control={control}
|
|
311
|
+
render={({ field }) => (
|
|
312
|
+
<Layer>
|
|
313
|
+
<TextInput
|
|
314
|
+
{...field}
|
|
315
|
+
enableCounter
|
|
316
|
+
id="serviceName"
|
|
317
|
+
invalid={!!errors.name}
|
|
318
|
+
invalidText={errors.name?.message}
|
|
319
|
+
labelText={t('serviceName', 'Service name')}
|
|
320
|
+
maxCount={MAX_NAME_LENGTH}
|
|
321
|
+
placeholder={t('enterServiceName', 'Enter service name')}
|
|
322
|
+
type="text"
|
|
323
|
+
/>
|
|
324
|
+
</Layer>
|
|
325
|
+
)}
|
|
326
|
+
/>
|
|
327
|
+
)}
|
|
328
|
+
</FormGroup>
|
|
329
|
+
<FormGroup>
|
|
330
|
+
<Controller
|
|
331
|
+
name="shortName"
|
|
332
|
+
control={control}
|
|
333
|
+
render={({ field }) => (
|
|
334
|
+
<Layer>
|
|
335
|
+
<TextInput
|
|
336
|
+
{...field}
|
|
337
|
+
enableCounter
|
|
338
|
+
id="serviceShortName"
|
|
339
|
+
invalid={!!errors.shortName}
|
|
340
|
+
invalidText={errors.shortName?.message}
|
|
341
|
+
labelText={t('shortName', 'Short name')}
|
|
342
|
+
maxCount={MAX_NAME_LENGTH}
|
|
343
|
+
placeholder={t('enterServiceShortName', 'Enter service short name')}
|
|
344
|
+
type="text"
|
|
345
|
+
value={field.value || ''}
|
|
346
|
+
/>
|
|
347
|
+
</Layer>
|
|
348
|
+
)}
|
|
349
|
+
/>
|
|
350
|
+
</FormGroup>
|
|
351
|
+
<FormGroup>
|
|
352
|
+
<FormLabel className={styles.conceptLabel}>{t('associatedConcept', 'Associated concept')}</FormLabel>
|
|
353
|
+
<ResponsiveWrapper>
|
|
354
|
+
<Search
|
|
355
|
+
id="conceptsSearch"
|
|
356
|
+
labelText={t('associatedConcept', 'Associated concept')}
|
|
357
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
|
358
|
+
onClear={() => {
|
|
359
|
+
setSearchTerm('');
|
|
360
|
+
setValue('concept', null);
|
|
361
|
+
}}
|
|
362
|
+
placeholder={t('searchConcepts', 'Search associated concept')}
|
|
363
|
+
ref={searchInputRef}
|
|
364
|
+
value={selectedConcept?.display || searchTerm}
|
|
365
|
+
/>
|
|
366
|
+
</ResponsiveWrapper>
|
|
367
|
+
|
|
368
|
+
{(() => {
|
|
369
|
+
if (!debouncedSearchTerm || selectedConcept) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
if (isSearching) {
|
|
373
|
+
return <InlineLoading className={styles.loader} description={t('searching', 'Searching') + '...'} />;
|
|
374
|
+
}
|
|
375
|
+
if (searchResults && searchResults.length) {
|
|
376
|
+
return (
|
|
377
|
+
<ul className={styles.conceptsList}>
|
|
378
|
+
{searchResults?.map((searchResult) => (
|
|
379
|
+
<li
|
|
380
|
+
className={styles.service}
|
|
381
|
+
key={searchResult.concept.uuid}
|
|
382
|
+
onClick={() => {
|
|
383
|
+
setValue('concept', {
|
|
384
|
+
uuid: searchResult.concept.uuid,
|
|
385
|
+
display: searchResult.display,
|
|
386
|
+
});
|
|
387
|
+
setSearchTerm('');
|
|
388
|
+
}}
|
|
389
|
+
role="menuitem">
|
|
390
|
+
{searchResult.display}
|
|
391
|
+
</li>
|
|
392
|
+
))}
|
|
393
|
+
</ul>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return (
|
|
397
|
+
<Layer>
|
|
398
|
+
<Tile className={styles.emptyResults}>
|
|
399
|
+
<span>{t('noResultsFor', 'No results for {{searchTerm}}', { searchTerm: debouncedSearchTerm })}</span>
|
|
400
|
+
</Tile>
|
|
401
|
+
</Layer>
|
|
402
|
+
);
|
|
403
|
+
})()}
|
|
404
|
+
</FormGroup>
|
|
405
|
+
<FormGroup>
|
|
406
|
+
<Controller
|
|
407
|
+
name="serviceType"
|
|
408
|
+
control={control}
|
|
409
|
+
render={({ field }) => (
|
|
410
|
+
<Layer>
|
|
411
|
+
<ComboBox
|
|
412
|
+
id="serviceType"
|
|
413
|
+
items={serviceTypes ?? []}
|
|
414
|
+
titleText={t('serviceType', 'Service type')}
|
|
415
|
+
itemToString={(item: ServiceType) => item?.display || ''}
|
|
416
|
+
selectedItem={field.value}
|
|
417
|
+
onChange={({ selectedItem }: { selectedItem: ServiceType | null }) => {
|
|
418
|
+
field.onChange(selectedItem);
|
|
419
|
+
}}
|
|
420
|
+
placeholder={t('selectServiceType', 'Select service type')}
|
|
421
|
+
invalid={!!errors.serviceType}
|
|
422
|
+
invalidText={errors.serviceType?.message}
|
|
423
|
+
/>
|
|
424
|
+
</Layer>
|
|
425
|
+
)}
|
|
426
|
+
/>
|
|
427
|
+
</FormGroup>
|
|
428
|
+
<section>
|
|
429
|
+
<div>
|
|
430
|
+
{fields.map((field, index) => (
|
|
431
|
+
<div key={field.id} className={styles.paymentMethodContainer}>
|
|
432
|
+
<Controller
|
|
433
|
+
control={control}
|
|
434
|
+
name={`payment.${index}.paymentMode`}
|
|
435
|
+
render={({ field }) => (
|
|
436
|
+
<Layer>
|
|
437
|
+
<Dropdown
|
|
438
|
+
id={`paymentMode-${index}`}
|
|
439
|
+
invalid={!!errors?.payment?.[index]?.paymentMode}
|
|
440
|
+
invalidText={errors?.payment?.[index]?.paymentMode?.message}
|
|
441
|
+
items={paymentModes ?? []}
|
|
442
|
+
itemToString={(item) => (item ? item.name : '')}
|
|
443
|
+
label={t('selectPaymentMode', 'Select payment mode')}
|
|
444
|
+
onChange={({ selectedItem }) => field.onChange(selectedItem.uuid)}
|
|
445
|
+
selectedItem={paymentModes.find((mode) => mode.uuid === field.value)}
|
|
446
|
+
titleText={t('paymentMode', 'Payment mode')}
|
|
447
|
+
/>
|
|
448
|
+
</Layer>
|
|
449
|
+
)}
|
|
450
|
+
/>
|
|
451
|
+
<Controller
|
|
452
|
+
control={control}
|
|
453
|
+
name={`payment.${index}.price`}
|
|
454
|
+
render={({ field }) => (
|
|
455
|
+
<Layer>
|
|
456
|
+
<NumberInput
|
|
457
|
+
allowEmpty
|
|
458
|
+
disableWheel
|
|
459
|
+
id={`price-${index}`}
|
|
460
|
+
invalid={!!errors?.payment?.[index]?.price}
|
|
461
|
+
invalidText={errors?.payment?.[index]?.price?.message}
|
|
462
|
+
label={t('sellingPrice', 'Selling price')}
|
|
463
|
+
min={0}
|
|
464
|
+
onChange={(_, { value }) => {
|
|
465
|
+
field.onChange(value === '' || value === undefined ? '' : value);
|
|
466
|
+
}}
|
|
467
|
+
placeholder={t('enterSellingPrice', 'Enter selling price')}
|
|
468
|
+
step={0.01}
|
|
469
|
+
value={field.value === undefined || field.value === null ? '' : field.value}
|
|
470
|
+
/>
|
|
471
|
+
</Layer>
|
|
472
|
+
)}
|
|
473
|
+
/>
|
|
474
|
+
<div className={styles.removeButtonContainer}>
|
|
475
|
+
<TrashCan onClick={() => handleRemovePaymentMode(index)} className={styles.removeButton} size={20} />
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
))}
|
|
479
|
+
<Button
|
|
480
|
+
className={styles.paymentButtons}
|
|
481
|
+
iconDescription={t('add', 'Add')}
|
|
482
|
+
kind="tertiary"
|
|
483
|
+
onClick={handleAppendPaymentMode}
|
|
484
|
+
renderIcon={(props) => <Add size={24} {...props} />}
|
|
485
|
+
type="button">
|
|
486
|
+
{t('addPaymentOption', 'Add payment option')}
|
|
487
|
+
</Button>
|
|
488
|
+
{getPaymentErrorMessage() && <div className={styles.errorMessage}>{getPaymentErrorMessage()}</div>}
|
|
489
|
+
</div>
|
|
490
|
+
</section>
|
|
491
|
+
</Stack>
|
|
492
|
+
<ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
|
|
493
|
+
<Button className={styles.button} kind="secondary" disabled={isSubmitting} onClick={closeWorkspace}>
|
|
494
|
+
{getCoreTranslation('cancel')}
|
|
495
|
+
</Button>
|
|
496
|
+
<Button className={styles.button} kind="primary" disabled={isSubmitting} type="submit">
|
|
497
|
+
{isSubmitting ? <InlineLoading description={t('saving', 'Saving') + '...'} /> : getCoreTranslation('save')}
|
|
498
|
+
</Button>
|
|
499
|
+
</ButtonSet>
|
|
500
|
+
</Form>
|
|
501
|
+
);
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
export default BillableServiceFormWorkspace;
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import useSWR from 'swr';
|
|
2
2
|
import { type OpenmrsResource, openmrsFetch, restBaseUrl, useOpenmrsFetchAll, useConfig } from '@openmrs/esm-framework';
|
|
3
|
-
import { type ServiceConcept } from '../types';
|
|
4
3
|
import { apiBasePath } from '../constants';
|
|
5
|
-
import
|
|
4
|
+
import type {
|
|
5
|
+
BillableService,
|
|
6
|
+
ConceptSearchResult,
|
|
7
|
+
CreateBillableServicePayload,
|
|
8
|
+
PaymentModePayload,
|
|
9
|
+
UpdateBillableServicePayload,
|
|
10
|
+
} from '../types';
|
|
11
|
+
import type { BillingConfig } from '../config-schema';
|
|
6
12
|
|
|
7
13
|
type ResponseObject = {
|
|
8
14
|
results: Array<OpenmrsResource>;
|
|
9
15
|
};
|
|
10
16
|
|
|
11
17
|
export const useBillableServices = () => {
|
|
12
|
-
const url = `${apiBasePath}billableService?v=custom:(uuid,name,shortName,serviceStatus,concept:(uuid,display,name:(name)),serviceType:(display),servicePrices:(uuid,name,price,paymentMode:(uuid,name)))`;
|
|
13
|
-
const { data, isLoading, isValidating, error, mutate } = useOpenmrsFetchAll<BillableService
|
|
18
|
+
const url = `${apiBasePath}billableService?v=custom:(uuid,name,shortName,serviceStatus,concept:(uuid,display,name:(name)),serviceType:(display,uuid),servicePrices:(uuid,name,price,paymentMode:(uuid,name)))`;
|
|
19
|
+
const { data, isLoading, isValidating, error, mutate } = useOpenmrsFetchAll<BillableService>(url);
|
|
14
20
|
|
|
15
21
|
return {
|
|
16
22
|
billableServices: data ?? [],
|
|
@@ -22,46 +28,51 @@ export const useBillableServices = () => {
|
|
|
22
28
|
};
|
|
23
29
|
|
|
24
30
|
export function useServiceTypes() {
|
|
25
|
-
const
|
|
26
|
-
const serviceConceptUuid =
|
|
31
|
+
const { serviceTypes } = useConfig<BillingConfig>();
|
|
32
|
+
const serviceConceptUuid = serviceTypes.billableService;
|
|
27
33
|
const url = `${restBaseUrl}/concept/${serviceConceptUuid}?v=custom:(setMembers:(uuid,display))`;
|
|
28
34
|
|
|
29
|
-
const { data, error, isLoading } = useSWR<{ data }>(
|
|
35
|
+
const { data, error, isLoading } = useSWR<{ data: { setMembers: Array<{ uuid: string; display: string }> } }>(
|
|
36
|
+
url,
|
|
37
|
+
openmrsFetch,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const sortedServiceTypes = data?.data.setMembers
|
|
41
|
+
? [...data.data.setMembers].sort((a, b) => a.display.localeCompare(b.display))
|
|
42
|
+
: [];
|
|
30
43
|
|
|
31
44
|
return {
|
|
32
|
-
serviceTypes:
|
|
45
|
+
serviceTypes: sortedServiceTypes,
|
|
33
46
|
error,
|
|
34
|
-
isLoading,
|
|
47
|
+
isLoadingServiceTypes: isLoading,
|
|
35
48
|
};
|
|
36
49
|
}
|
|
37
50
|
|
|
51
|
+
export interface PaymentMode extends OpenmrsResource {
|
|
52
|
+
name: string;
|
|
53
|
+
description: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
export const usePaymentModes = () => {
|
|
39
57
|
const url = `${apiBasePath}paymentMode`;
|
|
40
58
|
|
|
41
|
-
const { data, error, isLoading } = useSWR<{ data:
|
|
59
|
+
const { data, error, isLoading, mutate } = useSWR<{ data: { results: PaymentMode[] } }>(url, openmrsFetch);
|
|
60
|
+
const sortedPaymentModes = data?.data.results
|
|
61
|
+
? [...data.data.results].sort((a, b) => a.name.localeCompare(b.name))
|
|
62
|
+
: [];
|
|
42
63
|
|
|
43
64
|
return {
|
|
44
|
-
paymentModes:
|
|
65
|
+
paymentModes: sortedPaymentModes as PaymentMode[],
|
|
45
66
|
error,
|
|
46
|
-
isLoading,
|
|
67
|
+
isLoadingPaymentModes: isLoading,
|
|
68
|
+
mutate,
|
|
47
69
|
};
|
|
48
70
|
};
|
|
49
71
|
|
|
50
|
-
export const createBillableSerice = (payload: any) => {
|
|
51
|
-
const url = `${apiBasePath}api/billable-service`;
|
|
52
|
-
return openmrsFetch(url, {
|
|
53
|
-
method: 'POST',
|
|
54
|
-
body: payload,
|
|
55
|
-
headers: {
|
|
56
|
-
'Content-Type': 'application/json',
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
|
|
61
72
|
export function useConceptsSearch(conceptToLookup: string) {
|
|
62
73
|
const conditionsSearchUrl = `${restBaseUrl}/conceptsearch?q=${conceptToLookup}`;
|
|
63
74
|
|
|
64
|
-
const { data, error, isLoading } = useSWR<{ data: { results: Array<
|
|
75
|
+
const { data, error, isLoading } = useSWR<{ data: { results: Array<ConceptSearchResult> } }, Error>(
|
|
65
76
|
conceptToLookup ? conditionsSearchUrl : null,
|
|
66
77
|
openmrsFetch,
|
|
67
78
|
);
|
|
@@ -73,11 +84,44 @@ export function useConceptsSearch(conceptToLookup: string) {
|
|
|
73
84
|
};
|
|
74
85
|
}
|
|
75
86
|
|
|
76
|
-
export const
|
|
77
|
-
const url = `${apiBasePath}/
|
|
87
|
+
export const createBillableService = (payload: CreateBillableServicePayload) => {
|
|
88
|
+
const url = `${apiBasePath}api/billable-service`;
|
|
78
89
|
return openmrsFetch(url, {
|
|
79
90
|
method: 'POST',
|
|
80
|
-
body:
|
|
91
|
+
body: payload,
|
|
92
|
+
headers: {
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const updateBillableService = (uuid: string, payload: UpdateBillableServicePayload) => {
|
|
99
|
+
const url = `${apiBasePath}billableService/${uuid}`;
|
|
100
|
+
return openmrsFetch(url, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
body: payload,
|
|
103
|
+
headers: {
|
|
104
|
+
'Content-Type': 'application/json',
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const createPaymentMode = (payload: PaymentModePayload) => {
|
|
110
|
+
const url = `${restBaseUrl}/billing/paymentMode`;
|
|
111
|
+
return openmrsFetch(url, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
body: payload,
|
|
114
|
+
headers: {
|
|
115
|
+
'Content-Type': 'application/json',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const updatePaymentMode = (uuid: string, payload: PaymentModePayload) => {
|
|
121
|
+
const url = `${restBaseUrl}/billing/paymentMode/${uuid}`;
|
|
122
|
+
return openmrsFetch(url, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
body: payload,
|
|
81
125
|
headers: {
|
|
82
126
|
'Content-Type': 'application/json',
|
|
83
127
|
},
|