@openmrs/esm-billing-app 1.1.1 → 1.1.2-pre.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/.turbo/cache/53e233a916ffe7d2-meta.json +1 -0
- package/.turbo/cache/53e233a916ffe7d2.tar.zst +0 -0
- package/.turbo/turbo-build.log +44 -0
- package/__mocks__/bills.mock.ts +6 -5
- package/dist/1119.js +1 -1
- package/dist/1197.js +1 -1
- package/dist/1435.js +1 -0
- package/dist/1435.js.map +1 -0
- package/dist/1807.js +1 -0
- package/dist/1807.js.map +1 -0
- package/dist/2146.js +1 -1
- package/dist/2177.js +1 -1
- package/dist/2177.js.map +1 -1
- package/dist/2690.js +1 -1
- package/dist/2704.js +1 -0
- package/dist/2704.js.map +1 -0
- package/dist/3002.js +1 -0
- package/dist/3002.js.map +1 -0
- package/dist/3041.js +1 -1
- package/dist/3041.js.map +1 -1
- package/dist/3099.js +1 -1
- package/dist/3184.js +1 -1
- package/dist/3184.js.map +1 -1
- package/dist/3584.js +1 -1
- package/dist/4055.js +1 -1
- package/dist/4132.js +1 -1
- package/dist/4225.js +1 -1
- package/dist/4225.js.map +1 -1
- package/dist/4300.js +1 -1
- package/dist/4335.js +1 -1
- package/dist/439.js +1 -1
- package/dist/4618.js +1 -1
- package/dist/4652.js +1 -1
- package/dist/4944.js +1 -1
- package/dist/5173.js +1 -1
- package/dist/5241.js +1 -1
- package/dist/5422.js +1 -1
- package/dist/5422.js.map +1 -1
- package/dist/5442.js +1 -1
- package/dist/5661.js +1 -1
- package/dist/6022.js +1 -1
- package/dist/6404.js +1 -0
- package/dist/6404.js.map +1 -0
- package/dist/6468.js +1 -1
- package/dist/6540.js +1 -1
- package/dist/6540.js.map +1 -1
- package/dist/6589.js +1 -1
- package/dist/6606.js +1 -1
- package/dist/6606.js.map +1 -1
- package/dist/6679.js +1 -1
- package/dist/6792.js +1 -0
- package/dist/6792.js.map +1 -0
- 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/8341.js +2 -0
- package/dist/{1907.js.LICENSE.txt → 8341.js.LICENSE.txt} +0 -15
- package/dist/8341.js.map +1 -0
- package/dist/8349.js +1 -1
- package/dist/8371.js +1 -1
- package/dist/8421.js +1 -0
- package/dist/8421.js.map +1 -0
- package/dist/8618.js +1 -1
- 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.LICENSE.txt +0 -15
- 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 +284 -259
- package/dist/openmrs-esm-billing-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/e2e/commands/patient-operations.ts +1 -1
- package/e2e/pages/billing-dashboard-page.ts +3 -1
- package/e2e/pages/billing-form-page.ts +5 -0
- package/e2e/pages/invoice-page.ts +10 -0
- package/e2e/specs/billing-dashboard.spec.ts +126 -3
- package/e2e/specs/billing-patient-chart.spec.ts +95 -9
- package/package.json +3 -6
- package/src/bill-history/bill-action-menu.component.tsx +41 -0
- package/src/bill-history/bill-action-menu.scss +3 -0
- package/src/bill-history/bill-history.component.tsx +15 -5
- package/src/bill-history/bill-history.scss +0 -1
- package/src/bill-history/bill-history.test.tsx +78 -1
- package/src/bill-item-actions/edit-bill-item.modal.tsx +1 -1
- package/src/bill-item-actions/edit-bill-item.test.tsx +40 -0
- package/src/billable-services/bill-waiver/bill-waiver.component.tsx +3 -1
- package/src/billing-dashboard/billing-dashboard.component.tsx +3 -16
- package/src/billing-form/billing-checkin-form.component.tsx +116 -57
- package/src/billing-form/billing-checkin-form.scss +26 -2
- package/src/billing-form/billing-checkin-form.test.tsx +51 -1
- package/src/billing-form/billing-form.resource.test.ts +87 -0
- package/src/billing-form/billing-form.resource.ts +33 -0
- package/src/billing-form/billing-form.scss +54 -7
- package/src/billing-form/billing-form.test.tsx +547 -0
- package/src/billing-form/billing-form.workspace.tsx +150 -45
- package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +25 -2
- package/src/billing-form/visit-attributes/visit-attributes-form.scss +29 -0
- package/src/billing-header/billing-header.component.tsx +1 -34
- package/src/billing-header/billing-header.scss +0 -50
- package/src/billing.resource.test.ts +11 -11
- package/src/billing.resource.ts +42 -12
- package/src/bills-table/bills-table.component.tsx +16 -12
- package/src/bills-table/bills-table.test.tsx +84 -7
- package/src/index.ts +5 -0
- package/src/invoice/invoice.component.tsx +46 -16
- package/src/invoice/invoice.scss +9 -8
- package/src/invoice/invoice.test.tsx +128 -7
- package/src/invoice/line-item-action-menu.component.tsx +2 -2
- package/src/invoice/payments/payments.component.tsx +2 -2
- package/src/invoice/payments/payments.test.tsx +31 -2
- package/src/metrics-cards/metrics.resource.ts +3 -4
- package/src/modal/finalize-bill-confirmation.modal.test.tsx +209 -0
- package/src/modal/finalize-bill-confirmation.modal.tsx +86 -0
- package/src/modal/require-payment.modal.tsx +2 -1
- package/src/routes.json +4 -0
- package/src/types/index.ts +10 -1
- package/tools/setup-tests.ts +7 -6
- package/translations/am.json +28 -0
- package/translations/ar.json +28 -0
- package/translations/ar_SY.json +28 -0
- package/translations/bn.json +28 -0
- package/translations/cs.json +28 -0
- package/translations/de.json +266 -238
- package/translations/en.json +29 -0
- package/translations/en_US.json +28 -0
- package/translations/es.json +28 -0
- package/translations/es_MX.json +28 -0
- package/translations/fr.json +28 -0
- package/translations/he.json +28 -0
- package/translations/hi.json +28 -0
- package/translations/hi_IN.json +28 -0
- package/translations/id.json +28 -0
- package/translations/it.json +28 -0
- package/translations/ka.json +28 -0
- package/translations/km.json +28 -0
- package/translations/ku.json +28 -0
- package/translations/ky.json +28 -0
- package/translations/lg.json +28 -0
- package/translations/ne.json +28 -0
- package/translations/pl.json +28 -0
- package/translations/pt.json +28 -0
- package/translations/pt_BR.json +28 -0
- package/translations/qu.json +28 -0
- package/translations/ro_RO.json +28 -0
- package/translations/ru_RU.json +28 -0
- package/translations/si.json +28 -0
- package/translations/sq.json +28 -0
- package/translations/sw.json +28 -0
- package/translations/sw_KE.json +28 -0
- package/translations/tr.json +28 -0
- package/translations/tr_TR.json +28 -0
- package/translations/uk.json +28 -0
- package/translations/uz.json +28 -0
- package/translations/uz@Latn.json +28 -0
- package/translations/uz_UZ.json +28 -0
- package/translations/vi.json +28 -0
- package/translations/zh.json +268 -240
- package/translations/zh_CN.json +30 -2
- package/translations/zh_TW.json +28 -0
- package/turbo.json +29 -0
- package/dist/1537.js +0 -1
- package/dist/1537.js.map +0 -1
- package/dist/1907.js +0 -2
- package/dist/1907.js.map +0 -1
- package/dist/1981.js +0 -1
- package/dist/1981.js.map +0 -1
- package/dist/2820.js +0 -1
- package/dist/2820.js.map +0 -1
- package/dist/8025.js +0 -1
- package/dist/8025.js.map +0 -1
- package/dist/9727.js +0 -2
- package/dist/9727.js.LICENSE.txt +0 -14
- package/dist/9727.js.map +0 -1
- package/dist/9756.js +0 -1
- package/dist/9756.js.map +0 -1
- package/src/hooks/selectedDateContext.ts +0 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import {
|
|
4
4
|
Button,
|
|
@@ -19,10 +19,12 @@ import {
|
|
|
19
19
|
type Workspace2DefinitionProps,
|
|
20
20
|
Workspace2,
|
|
21
21
|
} from '@openmrs/esm-framework';
|
|
22
|
-
import { processBillItems, useBillableServices } from '../billing.resource';
|
|
22
|
+
import { processBillItems, updateBillItems, useBill, useBillableServices } from '../billing.resource';
|
|
23
|
+
import { useBillableServices as useBillableServicesList } from '../billable-services/billable-service.resource';
|
|
24
|
+
import { getBillableServiceUuid } from '../invoice/payments/utils';
|
|
23
25
|
import { calculateTotalAmount, convertToCurrency } from '../helpers/functions';
|
|
24
26
|
import type { BillingConfig } from '../config-schema';
|
|
25
|
-
import type
|
|
27
|
+
import { BillStatus, type BillableItem, type LineItem, type ServicePrice } from '../types';
|
|
26
28
|
import styles from './billing-form.scss';
|
|
27
29
|
|
|
28
30
|
interface ExtendedLineItem extends LineItem {
|
|
@@ -34,10 +36,11 @@ type BillingFormProps = {
|
|
|
34
36
|
patientUuid: string;
|
|
35
37
|
closeWorkspace: () => void;
|
|
36
38
|
onMutate?: () => void;
|
|
39
|
+
billUuid?: string;
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
|
|
40
|
-
workspaceProps: { patientUuid, onMutate },
|
|
43
|
+
workspaceProps: { patientUuid, onMutate, billUuid },
|
|
41
44
|
closeWorkspace,
|
|
42
45
|
}) => {
|
|
43
46
|
const isTablet = useLayoutType() === 'tablet';
|
|
@@ -46,6 +49,27 @@ const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
|
|
|
46
49
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
47
50
|
const [selectedItems, setSelectedItems] = useState<ExtendedLineItem[]>([]);
|
|
48
51
|
const { data, error, isLoading } = useBillableServices();
|
|
52
|
+
const { bill, isLoading: isLoadingBill, error: billError } = useBill(billUuid);
|
|
53
|
+
const {
|
|
54
|
+
billableServices,
|
|
55
|
+
isLoading: isLoadingBillableServices,
|
|
56
|
+
error: billableServicesError,
|
|
57
|
+
} = useBillableServicesList();
|
|
58
|
+
const isEditMode = !!billUuid && !!bill;
|
|
59
|
+
const existingItemsTotal = useMemo(
|
|
60
|
+
() => (isEditMode ? calculateTotalAmount(bill.lineItems) : 0),
|
|
61
|
+
[isEditMode, bill?.lineItems],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const availableBillableItems = useMemo(() => {
|
|
65
|
+
if (!data) return [];
|
|
66
|
+
if (!isEditMode) return data;
|
|
67
|
+
const lineItems = bill.lineItems ?? [];
|
|
68
|
+
const existingNames = new Set(
|
|
69
|
+
lineItems.flatMap((lineItem) => [lineItem.billableService, lineItem.item].filter(Boolean)),
|
|
70
|
+
);
|
|
71
|
+
return data.filter((item) => item.name && !existingNames.has(item.name));
|
|
72
|
+
}, [data, isEditMode, bill?.lineItems]);
|
|
49
73
|
|
|
50
74
|
const selectBillableItem = (item: BillableItem) => {
|
|
51
75
|
if (!item) {
|
|
@@ -77,7 +101,7 @@ const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
|
|
|
77
101
|
quantity: 1,
|
|
78
102
|
price: defaultPrice,
|
|
79
103
|
billableService: item.uuid,
|
|
80
|
-
paymentStatus:
|
|
104
|
+
paymentStatus: BillStatus.PENDING,
|
|
81
105
|
lineItemOrder: 0,
|
|
82
106
|
selectedPaymentMethod: selectedPaymentMethod,
|
|
83
107
|
availablePaymentMethods: availablePaymentMethods,
|
|
@@ -135,49 +159,84 @@ const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
|
|
|
135
159
|
if (isSubmitting || selectedItems.length === 0) {
|
|
136
160
|
return;
|
|
137
161
|
}
|
|
162
|
+
if (isEditMode && (isLoadingBillableServices || billableServicesError)) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
138
165
|
if (!validateSelectedItems()) {
|
|
139
166
|
return;
|
|
140
167
|
}
|
|
141
168
|
|
|
142
169
|
setIsSubmitting(true);
|
|
143
|
-
const bill = {
|
|
144
|
-
cashPoint: postBilledItems.cashPoint,
|
|
145
|
-
cashier: postBilledItems.cashier,
|
|
146
|
-
lineItems: [],
|
|
147
|
-
payments: [],
|
|
148
|
-
patient: patientUuid,
|
|
149
|
-
status: 'PENDING',
|
|
150
|
-
};
|
|
151
170
|
|
|
152
|
-
selectedItems.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
bill.lineItems.push(lineItem);
|
|
162
|
-
});
|
|
171
|
+
const newLineItems: Array<LineItem> = selectedItems.map((item) => ({
|
|
172
|
+
quantity: item.quantity,
|
|
173
|
+
price: item.price,
|
|
174
|
+
lineItemOrder: 0,
|
|
175
|
+
paymentStatus: BillStatus.PENDING,
|
|
176
|
+
billableService: item.uuid,
|
|
177
|
+
}));
|
|
163
178
|
|
|
164
179
|
try {
|
|
165
|
-
|
|
166
|
-
|
|
180
|
+
if (isEditMode) {
|
|
181
|
+
const existingLineItems = bill.lineItems.map((item) => {
|
|
182
|
+
const serviceUuid = getBillableServiceUuid(billableServices, item.billableService || item.item);
|
|
183
|
+
if (!serviceUuid) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
t('serviceResolutionError', 'Could not resolve service "{{service}}"', {
|
|
186
|
+
service: item.billableService || item.item,
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
uuid: item.uuid,
|
|
192
|
+
quantity: item.quantity,
|
|
193
|
+
price: item.price,
|
|
194
|
+
lineItemOrder: item.lineItemOrder,
|
|
195
|
+
paymentStatus: item.paymentStatus,
|
|
196
|
+
billableService: serviceUuid,
|
|
197
|
+
priceName: item.priceName,
|
|
198
|
+
priceUuid: item.priceUuid,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
167
201
|
|
|
168
|
-
|
|
202
|
+
const payload = {
|
|
203
|
+
cashPoint: bill.cashPointUuid,
|
|
204
|
+
cashier: bill.cashier.uuid,
|
|
205
|
+
lineItems: [...existingLineItems, ...newLineItems],
|
|
206
|
+
patient: bill.patientUuid,
|
|
207
|
+
status: bill.status,
|
|
208
|
+
uuid: bill.uuid,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
await updateBillItems(payload);
|
|
212
|
+
} else {
|
|
213
|
+
const payload = {
|
|
214
|
+
cashPoint: postBilledItems.cashPoint,
|
|
215
|
+
cashier: postBilledItems.cashier,
|
|
216
|
+
lineItems: newLineItems,
|
|
217
|
+
payments: [],
|
|
218
|
+
patient: patientUuid,
|
|
219
|
+
status: BillStatus.PENDING,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
await processBillItems(payload);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
closeWorkspace({ discardUnsavedChanges: true });
|
|
169
226
|
onMutate?.();
|
|
170
227
|
|
|
171
228
|
showSnackbar({
|
|
172
|
-
title: t('billProcessed', 'Bill processed'),
|
|
173
|
-
subtitle:
|
|
229
|
+
title: isEditMode ? t('itemsAddedToBill', 'Items added to bill') : t('billProcessed', 'Bill processed'),
|
|
230
|
+
subtitle: isEditMode
|
|
231
|
+
? t('itemsAddedToBillSuccessfully', 'Items have been added to the bill successfully')
|
|
232
|
+
: t('billProcessedSuccessfully', 'Bill processed successfully'),
|
|
174
233
|
kind: 'success',
|
|
175
234
|
});
|
|
176
|
-
} catch (
|
|
235
|
+
} catch (err) {
|
|
177
236
|
showSnackbar({
|
|
178
237
|
title: t('billProcessingError', 'Bill processing error'),
|
|
179
238
|
kind: 'error',
|
|
180
|
-
subtitle:
|
|
239
|
+
subtitle: err instanceof Error ? err.message : t('unknownBillError', 'An unexpected error occurred'),
|
|
181
240
|
});
|
|
182
241
|
} finally {
|
|
183
242
|
setIsSubmitting(false);
|
|
@@ -190,30 +249,76 @@ const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
|
|
|
190
249
|
};
|
|
191
250
|
|
|
192
251
|
return (
|
|
193
|
-
<Workspace2
|
|
252
|
+
<Workspace2
|
|
253
|
+
title={isEditMode ? t('addItemsToBill', 'Add items to bill') : t('addBillItems', 'Add bill items')}
|
|
254
|
+
hasUnsavedChanges={selectedItems.length > 0}>
|
|
194
255
|
<Form className={styles.form} onSubmit={handleSubmit}>
|
|
195
256
|
<div className={styles.grid}>
|
|
196
|
-
{
|
|
257
|
+
{billUuid && isLoadingBill ? (
|
|
197
258
|
<InlineLoading description={getCoreTranslation('loading') + '...'} />
|
|
198
|
-
) :
|
|
259
|
+
) : billUuid && billError ? (
|
|
260
|
+
<InlineNotification
|
|
261
|
+
kind="error"
|
|
262
|
+
lowContrast
|
|
263
|
+
title={t('errorLoadingBill', 'Error loading bill')}
|
|
264
|
+
subtitle={billError?.message}
|
|
265
|
+
/>
|
|
266
|
+
) : isEditMode && billableServicesError ? (
|
|
199
267
|
<InlineNotification
|
|
200
268
|
kind="error"
|
|
201
269
|
lowContrast
|
|
202
270
|
title={t('errorLoadingBillableServices', 'Error loading billable services')}
|
|
203
|
-
subtitle={
|
|
271
|
+
subtitle={billableServicesError?.message}
|
|
204
272
|
/>
|
|
205
273
|
) : (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
274
|
+
<>
|
|
275
|
+
{isEditMode && (
|
|
276
|
+
<div className={styles.existingItemsContainer}>
|
|
277
|
+
<h4 className={styles.sectionHeading}>{t('existingItems', 'Existing items')}</h4>
|
|
278
|
+
{bill.lineItems.map((item) => (
|
|
279
|
+
<div key={item.uuid} className={styles.existingItemRow}>
|
|
280
|
+
<span className={styles.existingItemName}>
|
|
281
|
+
{item.billableService || item.item || item.display}
|
|
282
|
+
</span>
|
|
283
|
+
<span className={styles.existingItemDetail}>
|
|
284
|
+
{item.quantity} x {convertToCurrency(item.price, defaultCurrency)}
|
|
285
|
+
</span>
|
|
286
|
+
<span className={styles.existingItemTotal}>
|
|
287
|
+
{convertToCurrency(item.price * item.quantity, defaultCurrency)}
|
|
288
|
+
</span>
|
|
289
|
+
</div>
|
|
290
|
+
))}
|
|
291
|
+
<div className={styles.existingItemsSubtotal}>
|
|
292
|
+
<strong>
|
|
293
|
+
{t('subtotal', 'Subtotal')}: {convertToCurrency(existingItemsTotal, defaultCurrency)}
|
|
294
|
+
</strong>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
{isEditMode && <h4 className={styles.sectionHeading}>{t('newItems', 'New items')}</h4>}
|
|
299
|
+
{isLoading ? (
|
|
300
|
+
<InlineLoading description={getCoreTranslation('loading') + '...'} />
|
|
301
|
+
) : error ? (
|
|
302
|
+
<InlineNotification
|
|
303
|
+
kind="error"
|
|
304
|
+
lowContrast
|
|
305
|
+
title={t('errorLoadingBillableServices', 'Error loading billable services')}
|
|
306
|
+
subtitle={error?.message}
|
|
307
|
+
/>
|
|
308
|
+
) : (
|
|
309
|
+
<ComboBox
|
|
310
|
+
id="searchItems"
|
|
311
|
+
onChange={({ selectedItem: item }: { selectedItem: BillableItem }) => selectBillableItem(item)}
|
|
312
|
+
itemToString={(item: BillableItem) => item?.name || ''}
|
|
313
|
+
items={availableBillableItems}
|
|
314
|
+
titleText={t('searchItems', 'Search items and services')}
|
|
315
|
+
/>
|
|
316
|
+
)}
|
|
317
|
+
</>
|
|
213
318
|
)}
|
|
214
319
|
{selectedItems && selectedItems.length > 0 && (
|
|
215
320
|
<div className={styles.selectedItemsContainer}>
|
|
216
|
-
<h4>{t('selectedItems', 'Selected items')}</h4>
|
|
321
|
+
<h4 className={styles.sectionHeading}>{t('selectedItems', 'Selected items')}</h4>
|
|
217
322
|
{selectedItems.map((item) => (
|
|
218
323
|
<div key={item.uuid} className={styles.itemCard}>
|
|
219
324
|
<div className={styles.itemHeader}>
|
|
@@ -289,7 +394,7 @@ const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
|
|
|
289
394
|
<div className={styles.grandTotal}>
|
|
290
395
|
<strong>
|
|
291
396
|
{t('grandTotal', 'Grand total')}:{' '}
|
|
292
|
-
{convertToCurrency(calculateTotalAmount(selectedItems), defaultCurrency)}
|
|
397
|
+
{convertToCurrency(existingItemsTotal + calculateTotalAmount(selectedItems), defaultCurrency)}
|
|
293
398
|
</strong>
|
|
294
399
|
</div>
|
|
295
400
|
</div>
|
|
@@ -307,7 +412,7 @@ const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
|
|
|
307
412
|
<Button
|
|
308
413
|
className={styles.button}
|
|
309
414
|
kind="primary"
|
|
310
|
-
disabled={isSubmitting || selectedItems.length === 0}
|
|
415
|
+
disabled={isSubmitting || selectedItems.length === 0 || (isEditMode && isLoadingBillableServices)}
|
|
311
416
|
type="submit">
|
|
312
417
|
{isSubmitting ? (
|
|
313
418
|
<InlineLoading description={t('saving', 'Saving') + '...'} />
|
|
@@ -3,7 +3,18 @@ import { z } from 'zod';
|
|
|
3
3
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
4
4
|
import { Controller, useForm } from 'react-hook-form';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ComboBox,
|
|
8
|
+
InlineLoading,
|
|
9
|
+
RadioButton,
|
|
10
|
+
RadioButtonGroup,
|
|
11
|
+
Stack,
|
|
12
|
+
TextInput,
|
|
13
|
+
Toggletip,
|
|
14
|
+
ToggletipButton,
|
|
15
|
+
ToggletipContent,
|
|
16
|
+
} from '@carbon/react';
|
|
17
|
+
import { Information } from '@carbon/react/icons';
|
|
7
18
|
import { useConfig } from '@openmrs/esm-framework';
|
|
8
19
|
import { usePaymentMethods } from '../billing-form.resource';
|
|
9
20
|
import styles from './visit-attributes-form.scss';
|
|
@@ -121,7 +132,19 @@ const VisitAttributesForm: React.FC<VisitAttributesFormProps> = ({ setAttributes
|
|
|
121
132
|
render={({ field }) => (
|
|
122
133
|
<RadioButtonGroup
|
|
123
134
|
className={styles.radioButtonGroup}
|
|
124
|
-
legendText={
|
|
135
|
+
legendText={
|
|
136
|
+
<div className={styles.paymentDetailsLegend}>
|
|
137
|
+
{t('paymentDetails', 'Payment details')}
|
|
138
|
+
<Toggletip autoAlign align="bottom">
|
|
139
|
+
<ToggletipButton label={t('showInformation', 'Show information')}>
|
|
140
|
+
<Information />
|
|
141
|
+
</ToggletipButton>
|
|
142
|
+
<ToggletipContent>
|
|
143
|
+
<p>{t('nonPayingInfo', 'Any services rendered to non-paying patients will not be billed')}</p>
|
|
144
|
+
</ToggletipContent>
|
|
145
|
+
</Toggletip>
|
|
146
|
+
</div>
|
|
147
|
+
}
|
|
125
148
|
name="payment-details"
|
|
126
149
|
onChange={(selected) => field.onChange(selected)}
|
|
127
150
|
orientation="vertical">
|
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
.radioButtonGroup {
|
|
22
|
+
overflow: visible;
|
|
23
|
+
|
|
22
24
|
:global(.cds--radio-button-group) {
|
|
25
|
+
overflow: visible;
|
|
26
|
+
|
|
23
27
|
:global(.cds--radio-button-wrapper) {
|
|
24
28
|
margin-bottom: layout.$spacing-03;
|
|
25
29
|
|
|
@@ -28,8 +32,33 @@
|
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
}
|
|
35
|
+
|
|
36
|
+
:global(fieldset.cds--fieldset) {
|
|
37
|
+
overflow: visible;
|
|
38
|
+
}
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
.stack {
|
|
34
42
|
margin-bottom: layout.$spacing-05;
|
|
43
|
+
overflow: visible;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.paymentDetailsLegend {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: layout.$spacing-03;
|
|
50
|
+
overflow: visible;
|
|
51
|
+
|
|
52
|
+
:global(.cds--toggletip) {
|
|
53
|
+
overflow: visible;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
:global(.cds--popover-content) {
|
|
57
|
+
z-index: 9100;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
:global(.cds--toggletip-content) {
|
|
61
|
+
min-width: 16rem;
|
|
62
|
+
white-space: normal;
|
|
63
|
+
}
|
|
35
64
|
}
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import dayjs from 'dayjs';
|
|
3
|
-
import { DatePickerInput, DatePicker } from '@carbon/react';
|
|
1
|
+
import React from 'react';
|
|
4
2
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import { Location, UserFollow } from '@carbon/react/icons';
|
|
6
|
-
import { useSession } from '@openmrs/esm-framework';
|
|
7
|
-
import { omrsDateFormat } from '../constants';
|
|
8
3
|
import BillingIllustration from './billing-illustration.component';
|
|
9
|
-
import SelectedDateContext from '../hooks/selectedDateContext';
|
|
10
4
|
import styles from './billing-header.scss';
|
|
11
5
|
|
|
12
6
|
interface BillingHeaderProps {
|
|
@@ -15,9 +9,6 @@ interface BillingHeaderProps {
|
|
|
15
9
|
|
|
16
10
|
const BillingHeader: React.FC<BillingHeaderProps> = ({ title }) => {
|
|
17
11
|
const { t } = useTranslation();
|
|
18
|
-
const session = useSession();
|
|
19
|
-
const location = session?.sessionLocation?.display;
|
|
20
|
-
const { selectedDate, setSelectedDate } = useContext(SelectedDateContext);
|
|
21
12
|
|
|
22
13
|
return (
|
|
23
14
|
<div className={styles.header} data-testid="billing-header">
|
|
@@ -28,30 +19,6 @@ const BillingHeader: React.FC<BillingHeaderProps> = ({ title }) => {
|
|
|
28
19
|
<p className={styles['page-name']}>{title}</p>
|
|
29
20
|
</div>
|
|
30
21
|
</div>
|
|
31
|
-
<div className={styles['right-justified-items']}>
|
|
32
|
-
<div className={styles.userContainer}>
|
|
33
|
-
<p>{session?.user?.person?.display}</p>
|
|
34
|
-
<UserFollow size={16} className={styles.userIcon} />
|
|
35
|
-
</div>
|
|
36
|
-
<div className={styles['date-and-location']}>
|
|
37
|
-
<Location size={16} />
|
|
38
|
-
<span className={styles.value}>{location}</span>
|
|
39
|
-
<span className={styles.middot}>·</span>
|
|
40
|
-
<DatePicker
|
|
41
|
-
onChange={([date]) => setSelectedDate(dayjs(date).startOf('day').format(omrsDateFormat))}
|
|
42
|
-
value={dayjs(selectedDate).format('DD MMM YYYY')}
|
|
43
|
-
dateFormat="d-M-Y"
|
|
44
|
-
datePickerType="single">
|
|
45
|
-
<DatePickerInput
|
|
46
|
-
style={{ cursor: 'pointer', backgroundColor: 'transparent', border: 'none', maxWidth: '10rem' }}
|
|
47
|
-
id="appointment-date-picker"
|
|
48
|
-
placeholder="DD-MMM-YYYY"
|
|
49
|
-
labelText=""
|
|
50
|
-
type="text"
|
|
51
|
-
/>
|
|
52
|
-
</DatePicker>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
22
|
</div>
|
|
56
23
|
);
|
|
57
24
|
};
|
|
@@ -18,15 +18,6 @@
|
|
|
18
18
|
flex-direction: row;
|
|
19
19
|
align-items: center;
|
|
20
20
|
cursor: pointer;
|
|
21
|
-
align-items: center;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.right-justified-items {
|
|
25
|
-
@include type.type-style('body-compact-02');
|
|
26
|
-
color: $text-02;
|
|
27
|
-
display: flex;
|
|
28
|
-
flex-direction: column;
|
|
29
|
-
justify-content: space-between;
|
|
30
21
|
}
|
|
31
22
|
|
|
32
23
|
.page-name {
|
|
@@ -40,44 +31,3 @@
|
|
|
40
31
|
margin-bottom: layout.$spacing-02;
|
|
41
32
|
}
|
|
42
33
|
}
|
|
43
|
-
|
|
44
|
-
.date-and-location {
|
|
45
|
-
display: flex;
|
|
46
|
-
justify-content: flex-end;
|
|
47
|
-
align-items: center;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.userContainer {
|
|
51
|
-
display: flex;
|
|
52
|
-
justify-content: flex-end;
|
|
53
|
-
gap: layout.$spacing-05;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.value {
|
|
57
|
-
margin-left: layout.$spacing-02;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.middot {
|
|
61
|
-
margin: 0 layout.$spacing-03;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.view {
|
|
65
|
-
@include type.type-style('label-01');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Overriding styles for RTL support
|
|
69
|
-
html[dir='rtl'] {
|
|
70
|
-
.date-and-location {
|
|
71
|
-
& > svg {
|
|
72
|
-
order: -1;
|
|
73
|
-
}
|
|
74
|
-
& > span:nth-child(2) {
|
|
75
|
-
order: -2;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.userIcon {
|
|
81
|
-
fill: $ui-05;
|
|
82
|
-
margin: layout.$spacing-01;
|
|
83
|
-
}
|
|
@@ -194,8 +194,8 @@ describe('mapBillProperties', () => {
|
|
|
194
194
|
patient: createPatient({ display: '12345 - John Doe' }),
|
|
195
195
|
});
|
|
196
196
|
const result = mapBillProperties(bill);
|
|
197
|
-
expect(result.identifier).toBe('12345
|
|
198
|
-
expect(result.patientName).toBe('
|
|
197
|
+
expect(result.identifier).toBe('12345');
|
|
198
|
+
expect(result.patientName).toBe('John Doe');
|
|
199
199
|
expect(result.patientUuid).toBe('patient-uuid');
|
|
200
200
|
});
|
|
201
201
|
|
|
@@ -204,8 +204,8 @@ describe('mapBillProperties', () => {
|
|
|
204
204
|
patient: createPatient({ display: 'John Doe' }),
|
|
205
205
|
});
|
|
206
206
|
const result = mapBillProperties(bill);
|
|
207
|
-
expect(result.identifier).toBe('
|
|
208
|
-
expect(result.patientName).
|
|
207
|
+
expect(result.identifier).toBe('');
|
|
208
|
+
expect(result.patientName).toBe('John Doe');
|
|
209
209
|
});
|
|
210
210
|
|
|
211
211
|
it('Handles patient display with multiple dashes', () => {
|
|
@@ -213,9 +213,9 @@ describe('mapBillProperties', () => {
|
|
|
213
213
|
patient: createPatient({ display: '12345 - John - Doe - Jr' }),
|
|
214
214
|
});
|
|
215
215
|
const result = mapBillProperties(bill);
|
|
216
|
-
expect(result.identifier).toBe('12345
|
|
216
|
+
expect(result.identifier).toBe('12345');
|
|
217
217
|
// Note: split('-')[1] only takes the second element, not everything after first dash
|
|
218
|
-
expect(result.patientName).toBe('
|
|
218
|
+
expect(result.patientName).toBe('John - Doe - Jr');
|
|
219
219
|
});
|
|
220
220
|
|
|
221
221
|
it('Handles empty patient display string', () => {
|
|
@@ -224,7 +224,7 @@ describe('mapBillProperties', () => {
|
|
|
224
224
|
});
|
|
225
225
|
const result = mapBillProperties(bill);
|
|
226
226
|
expect(result.identifier).toBe('');
|
|
227
|
-
expect(result.patientName).
|
|
227
|
+
expect(result.patientName).toBe('');
|
|
228
228
|
});
|
|
229
229
|
|
|
230
230
|
it('Handles undefined patient gracefully', () => {
|
|
@@ -233,8 +233,8 @@ describe('mapBillProperties', () => {
|
|
|
233
233
|
});
|
|
234
234
|
const result = mapBillProperties(bill);
|
|
235
235
|
expect(result.patientUuid).toBeUndefined();
|
|
236
|
-
expect(result.identifier).
|
|
237
|
-
expect(result.patientName).
|
|
236
|
+
expect(result.identifier).toBe('');
|
|
237
|
+
expect(result.patientName).toBe('');
|
|
238
238
|
});
|
|
239
239
|
|
|
240
240
|
it('Handles patient with undefined display', () => {
|
|
@@ -245,8 +245,8 @@ describe('mapBillProperties', () => {
|
|
|
245
245
|
},
|
|
246
246
|
});
|
|
247
247
|
const result = mapBillProperties(bill);
|
|
248
|
-
expect(result.identifier).
|
|
249
|
-
expect(result.patientName).
|
|
248
|
+
expect(result.identifier).toBe('');
|
|
249
|
+
expect(result.patientName).toBe('');
|
|
250
250
|
});
|
|
251
251
|
});
|
|
252
252
|
|
package/src/billing.resource.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import useSWR from 'swr';
|
|
2
2
|
import sortBy from 'lodash-es/sortBy';
|
|
3
3
|
import {
|
|
4
|
-
formatDate,
|
|
5
|
-
parseDate,
|
|
6
4
|
openmrsFetch,
|
|
7
5
|
useSession,
|
|
8
6
|
useVisit,
|
|
@@ -11,26 +9,47 @@ import {
|
|
|
11
9
|
useOpenmrsPagination,
|
|
12
10
|
} from '@openmrs/esm-framework';
|
|
13
11
|
import { apiBasePath } from './constants';
|
|
14
|
-
import
|
|
15
|
-
MappedBill,
|
|
16
|
-
PatientInvoice,
|
|
17
|
-
BillableItem,
|
|
18
|
-
PaymentRequestPayload,
|
|
19
|
-
CreateBillPayload,
|
|
20
|
-
UpdateBillPayload,
|
|
12
|
+
import {
|
|
13
|
+
type MappedBill,
|
|
14
|
+
type PatientInvoice,
|
|
15
|
+
type BillableItem,
|
|
16
|
+
type PaymentRequestPayload,
|
|
17
|
+
type CreateBillPayload,
|
|
18
|
+
type UpdateBillPayload,
|
|
19
|
+
BillStatus,
|
|
21
20
|
} from './types';
|
|
22
21
|
|
|
22
|
+
const parsePatientDisplay = (display: string | undefined): { identifier: string; name: string } => {
|
|
23
|
+
if (!display) {
|
|
24
|
+
return { identifier: '', name: '' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const separator = ' - ';
|
|
28
|
+
const index = display.indexOf(separator);
|
|
29
|
+
|
|
30
|
+
if (index === -1) {
|
|
31
|
+
return { identifier: '', name: display.trim() };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
identifier: display.substring(0, index).trim(),
|
|
36
|
+
name: display.substring(index + separator.length).trim(),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
23
40
|
export const mapBillProperties = (bill: PatientInvoice): MappedBill => {
|
|
24
|
-
const activeLineItems = bill?.lineItems?.filter((item) => !item.voided)
|
|
41
|
+
const activeLineItems = bill?.lineItems?.filter((item) => !item.voided) ?? [];
|
|
42
|
+
const { identifier, name } = parsePatientDisplay(bill?.patient?.display);
|
|
25
43
|
|
|
26
44
|
return {
|
|
27
45
|
...bill,
|
|
28
|
-
patientName:
|
|
29
|
-
identifier:
|
|
46
|
+
patientName: name,
|
|
47
|
+
identifier: identifier,
|
|
30
48
|
patientUuid: bill?.patient?.uuid,
|
|
31
49
|
cashPointUuid: bill?.cashPoint?.uuid,
|
|
32
50
|
cashPointName: bill?.cashPoint?.name,
|
|
33
51
|
cashPointLocation: bill?.cashPoint?.location?.display,
|
|
52
|
+
status: bill.status as BillStatus,
|
|
34
53
|
lineItems: activeLineItems,
|
|
35
54
|
billingService: activeLineItems.map((lineItem) => lineItem?.item || lineItem?.billableService || '--').join(' '),
|
|
36
55
|
totalAmount: activeLineItems
|
|
@@ -175,6 +194,17 @@ export const updateBillItems = (payload: UpdateBillPayload) => {
|
|
|
175
194
|
});
|
|
176
195
|
};
|
|
177
196
|
|
|
197
|
+
export const finalizeBill = (billUuid: string) => {
|
|
198
|
+
const url = `${apiBasePath}bill/${billUuid}`;
|
|
199
|
+
return openmrsFetch(url, {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
body: { status: BillStatus.POSTED },
|
|
202
|
+
headers: {
|
|
203
|
+
'Content-Type': 'application/json',
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
178
208
|
export const deleteBillItem = (itemUuid: string, voidReason: string) => {
|
|
179
209
|
const url = `${apiBasePath}billLineItem/${itemUuid}?reason=${encodeURIComponent(voidReason)}`;
|
|
180
210
|
|