@openmrs/esm-billing-app 1.0.2-pre.112 → 1.0.2-pre.1124
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/2177.js +1 -1
- package/dist/2177.js.map +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/439.js +1 -0
- 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/6589.js +1 -0
- 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/8371.js +1 -0
- 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 +321 -249
- package/dist/openmrs-esm-billing-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/e2e/README.md +19 -18
- package/e2e/commands/billing-operations.ts +96 -0
- package/e2e/commands/index.ts +3 -0
- package/e2e/commands/patient-operations.ts +82 -0
- package/e2e/commands/types.ts +73 -0
- package/e2e/core/test.ts +12 -1
- package/e2e/fixtures/api.ts +2 -3
- package/e2e/pages/billing-dashboard-page.ts +84 -0
- package/e2e/pages/billing-form-page.ts +114 -0
- package/e2e/pages/index.ts +4 -1
- package/e2e/pages/invoice-page.ts +113 -0
- package/e2e/pages/payment-page.ts +104 -0
- package/e2e/specs/billing-dashboard.spec.ts +203 -0
- package/e2e/specs/billing-patient-chart.spec.ts +738 -0
- package/e2e/support/github/Dockerfile +1 -1
- package/e2e/support/github/run-e2e-docker-env.sh +29 -32
- package/example.env +15 -1
- package/package.json +19 -16
- package/playwright.config.ts +4 -2
- package/src/bill-history/bill-history.component.tsx +15 -17
- package/src/bill-history/bill-history.scss +4 -94
- package/src/bill-history/bill-history.test.tsx +37 -77
- package/src/bill-item-actions/bill-item-actions.scss +21 -5
- package/src/bill-item-actions/edit-bill-item.modal.tsx +227 -0
- package/src/bill-item-actions/edit-bill-item.test.tsx +233 -40
- package/src/billable-services/bill-waiver/bill-selection.component.tsx +5 -5
- package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +37 -39
- 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 +1099 -0
- package/src/billable-services/billable-service-form/billable-service-form.workspace.tsx +516 -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 +50 -0
- package/src/billable-services/billable-services-left-panel-menu.component.tsx +48 -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 +29 -29
- package/src/billing-form/billing-checkin-form.scss +0 -7
- package/src/billing-form/billing-checkin-form.test.tsx +100 -27
- package/src/billing-form/billing-form.component.tsx +226 -284
- package/src/billing-form/billing-form.scss +143 -0
- package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +47 -41
- package/src/billing-form/visit-attributes/visit-attributes-form.scss +16 -3
- package/src/billing.resource.test.ts +503 -0
- package/src/billing.resource.ts +68 -76
- package/src/bills-table/bills-table.component.tsx +17 -24
- package/src/bills-table/bills-table.test.tsx +98 -54
- package/src/config-schema.ts +84 -31
- package/src/dashboard.meta.ts +5 -3
- package/src/helpers/functions.ts +5 -4
- package/src/index.ts +75 -13
- 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 +44 -38
- 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 +45 -36
- package/src/invoice/payments/payment-form/payment-form.scss +5 -6
- package/src/invoice/payments/payment-form/payment-form.test.tsx +217 -67
- 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 +56 -68
- 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 -3
- 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 +20 -11
- package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +95 -16
- package/src/invoice/printable-invoice/printable-invoice.component.tsx +21 -35
- package/src/left-panel-link.component.tsx +9 -8
- 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 +87 -24
- package/translations/am.json +140 -80
- package/translations/ar.json +141 -81
- package/translations/ar_SY.json +141 -81
- package/translations/bn.json +143 -83
- package/translations/cs.json +230 -0
- package/translations/de.json +141 -81
- package/translations/en.json +142 -81
- package/translations/en_US.json +141 -81
- package/translations/es.json +140 -80
- package/translations/es_MX.json +141 -81
- package/translations/fr.json +224 -164
- package/translations/he.json +140 -80
- package/translations/hi.json +141 -81
- package/translations/hi_IN.json +141 -81
- package/translations/id.json +141 -81
- package/translations/it.json +166 -106
- package/translations/ka.json +141 -81
- package/translations/km.json +140 -80
- package/translations/ku.json +141 -81
- package/translations/ky.json +141 -81
- package/translations/lg.json +141 -81
- package/translations/ne.json +141 -81
- package/translations/pl.json +141 -81
- package/translations/pt.json +141 -81
- package/translations/pt_BR.json +141 -81
- package/translations/qu.json +141 -81
- package/translations/ro_RO.json +225 -165
- package/translations/ru_RU.json +141 -81
- package/translations/si.json +141 -81
- package/translations/sq.json +230 -0
- package/translations/sw.json +141 -81
- package/translations/sw_KE.json +141 -81
- package/translations/tr.json +141 -81
- package/translations/tr_TR.json +141 -81
- package/translations/uk.json +141 -81
- package/translations/uz.json +141 -81
- package/translations/uz@Latn.json +141 -81
- package/translations/uz_UZ.json +141 -81
- package/translations/vi.json +141 -81
- package/translations/zh.json +141 -81
- package/translations/zh_CN.json +171 -111
- package/translations/zh_TW.json +230 -0
- 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/e2e/pages/home-page.ts +0 -9
- package/e2e/specs/sample-test.spec.ts +0 -11
- 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
|
@@ -1,11 +1,7 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
1
2
|
@use '@carbon/layout';
|
|
2
3
|
@use '@carbon/type';
|
|
3
|
-
@use '@carbon/colors';
|
|
4
4
|
|
|
5
|
-
.section {
|
|
6
|
-
margin: layout.$spacing-03;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
5
|
.sectionTitle {
|
|
10
6
|
@include type.type-style('heading-compact-02');
|
|
11
7
|
color: colors.$gray-70;
|
|
@@ -16,6 +12,16 @@
|
|
|
16
12
|
padding-bottom: layout.$spacing-05;
|
|
17
13
|
}
|
|
18
14
|
|
|
15
|
+
.billInfo {
|
|
16
|
+
@include type.type-style('body-compact-01');
|
|
17
|
+
color: colors.$gray-70;
|
|
18
|
+
|
|
19
|
+
.separator {
|
|
20
|
+
margin: 0 layout.$spacing-02;
|
|
21
|
+
color: colors.$gray-50;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
.label {
|
|
20
26
|
@include type.type-style('heading-compact-01');
|
|
21
27
|
margin-bottom: layout.$spacing-05;
|
|
@@ -24,3 +30,13 @@
|
|
|
24
30
|
.controlField {
|
|
25
31
|
margin-bottom: layout.$spacing-05;
|
|
26
32
|
}
|
|
33
|
+
|
|
34
|
+
.loader {
|
|
35
|
+
&:global(.cds--inline-loading) {
|
|
36
|
+
min-height: layout.$spacing-05;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:global(.cds--inline-loading__text) {
|
|
40
|
+
font-size: unset;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Form,
|
|
5
|
+
InlineLoading,
|
|
6
|
+
ModalBody,
|
|
7
|
+
ModalFooter,
|
|
8
|
+
ModalHeader,
|
|
9
|
+
NumberInput,
|
|
10
|
+
Stack,
|
|
11
|
+
TextInput,
|
|
12
|
+
} from '@carbon/react';
|
|
13
|
+
import { useTranslation } from 'react-i18next';
|
|
14
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
15
|
+
import { useSWRConfig } from 'swr';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
18
|
+
import { getCoreTranslation, showSnackbar, useConfig } from '@openmrs/esm-framework';
|
|
19
|
+
import { apiBasePath } from '../constants';
|
|
20
|
+
import { getBillableServiceUuid } from '../invoice/payments/utils';
|
|
21
|
+
import { type LineItem, type MappedBill } from '../types';
|
|
22
|
+
import { updateBillItems } from '../billing.resource';
|
|
23
|
+
import { useBillableServices } from '../billable-services/billable-service.resource';
|
|
24
|
+
import { type BillingConfig } from '../config-schema';
|
|
25
|
+
import { convertToCurrency } from '../helpers';
|
|
26
|
+
import styles from './bill-item-actions.scss';
|
|
27
|
+
|
|
28
|
+
interface EditBillLineItemModalProps {
|
|
29
|
+
bill: MappedBill;
|
|
30
|
+
closeModal: () => void;
|
|
31
|
+
item: LineItem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const EditBillLineItemModal: React.FC<EditBillLineItemModalProps> = ({ bill, closeModal, item }) => {
|
|
35
|
+
const { t } = useTranslation();
|
|
36
|
+
const { defaultCurrency } = useConfig<BillingConfig>();
|
|
37
|
+
const { billableServices } = useBillableServices();
|
|
38
|
+
const [total, setTotal] = useState(0);
|
|
39
|
+
const { mutate } = useSWRConfig();
|
|
40
|
+
|
|
41
|
+
const schema = useMemo(
|
|
42
|
+
() =>
|
|
43
|
+
z.object({
|
|
44
|
+
// NOTE: Frontend-only validation - quantities <1 or >100 can still be submitted via API.
|
|
45
|
+
// Backend (BillServiceImpl.java:100) has empty validate() method.
|
|
46
|
+
// TODO: Add server-side validation to enforce data integrity
|
|
47
|
+
quantity: z.coerce
|
|
48
|
+
.number({
|
|
49
|
+
required_error: t('quantityRequired', 'Quantity is required'),
|
|
50
|
+
invalid_type_error: t('quantityMustBeNumber', 'Quantity must be a valid number'),
|
|
51
|
+
})
|
|
52
|
+
.int(t('quantityMustBeInteger', 'Quantity must be a whole number'))
|
|
53
|
+
.min(1, t('quantityMustBeAtLeastOne', 'Quantity must be at least 1'))
|
|
54
|
+
.max(100, t('quantityCannotExceed100', 'Quantity cannot exceed 100')),
|
|
55
|
+
price: z.coerce
|
|
56
|
+
.number({
|
|
57
|
+
required_error: t('priceIsRequired', 'Price is required'),
|
|
58
|
+
invalid_type_error: t('priceMustBeNumber', 'Price must be a valid number'),
|
|
59
|
+
})
|
|
60
|
+
.positive(t('priceMustBePositive', 'Price must be greater than 0')),
|
|
61
|
+
}),
|
|
62
|
+
[t],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
type BillLineItemForm = z.infer<typeof schema>;
|
|
66
|
+
|
|
67
|
+
const {
|
|
68
|
+
control,
|
|
69
|
+
handleSubmit,
|
|
70
|
+
formState: { isSubmitting, errors },
|
|
71
|
+
watch,
|
|
72
|
+
} = useForm<BillLineItemForm>({
|
|
73
|
+
defaultValues: {
|
|
74
|
+
quantity: item.quantity,
|
|
75
|
+
price: item.price,
|
|
76
|
+
},
|
|
77
|
+
resolver: zodResolver(schema),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const quantity = watch('quantity');
|
|
81
|
+
const price = watch('price');
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const quantityNum = typeof quantity === 'number' ? quantity : parseFloat(quantity) || 0;
|
|
85
|
+
const priceNum = typeof price === 'number' ? price : parseFloat(price) || 0;
|
|
86
|
+
const newTotal = quantityNum * priceNum;
|
|
87
|
+
setTotal(isNaN(newTotal) ? 0 : newTotal);
|
|
88
|
+
}, [quantity, price]);
|
|
89
|
+
|
|
90
|
+
const onSubmit = async (data: BillLineItemForm) => {
|
|
91
|
+
const url = `${apiBasePath}bill`;
|
|
92
|
+
|
|
93
|
+
const newItem = {
|
|
94
|
+
...item,
|
|
95
|
+
quantity: data.quantity,
|
|
96
|
+
price: data.price,
|
|
97
|
+
billableService: getBillableServiceUuid(billableServices, item.billableService),
|
|
98
|
+
item: item?.item,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const previousLineitems = bill?.lineItems
|
|
102
|
+
.filter((currItem) => currItem.uuid !== item?.uuid)
|
|
103
|
+
.map((currItem) => ({
|
|
104
|
+
...currItem,
|
|
105
|
+
billableService: getBillableServiceUuid(billableServices, currItem.billableService),
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
const updatedLineItems = previousLineitems.concat(newItem);
|
|
109
|
+
|
|
110
|
+
const payload = {
|
|
111
|
+
cashPoint: bill.cashPointUuid,
|
|
112
|
+
cashier: bill.cashier.uuid,
|
|
113
|
+
lineItems: updatedLineItems,
|
|
114
|
+
patient: bill.patientUuid,
|
|
115
|
+
status: bill.status,
|
|
116
|
+
uuid: bill.uuid,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await updateBillItems(payload);
|
|
121
|
+
mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
|
|
122
|
+
showSnackbar({
|
|
123
|
+
title: t('lineItemUpdated', 'Line item updated'),
|
|
124
|
+
subtitle: t('lineItemUpdateSuccess', 'The bill line item has been updated successfully'),
|
|
125
|
+
kind: 'success',
|
|
126
|
+
});
|
|
127
|
+
closeModal();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
showSnackbar({
|
|
130
|
+
title: t('lineItemUpdateFailed', 'Failed to update line item'),
|
|
131
|
+
subtitle:
|
|
132
|
+
error?.message || t('lineItemUpdateErrorDefault', 'Unable to update the bill line item. Please try again.'),
|
|
133
|
+
kind: 'error',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (Object.keys(bill)?.length === 0) {
|
|
139
|
+
return <ModalHeader closeModal={closeModal} title={t('billLineItemEmpty', 'This bill has no line items')} />;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<>
|
|
144
|
+
<ModalHeader closeModal={closeModal} title={t('editBillLineItem', 'Edit bill line item')} />
|
|
145
|
+
<Form onSubmit={handleSubmit(onSubmit)}>
|
|
146
|
+
<ModalBody>
|
|
147
|
+
<Stack gap={5}>
|
|
148
|
+
<div className={styles.modalBody}>
|
|
149
|
+
<div className={styles.billInfo}>
|
|
150
|
+
{bill?.patientName}
|
|
151
|
+
<span className={styles.separator}>·</span>
|
|
152
|
+
{bill?.cashPointName}
|
|
153
|
+
<span className={styles.separator}>·</span>
|
|
154
|
+
{bill?.receiptNumber}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
<section>
|
|
158
|
+
<p className={styles.label}>
|
|
159
|
+
{t('item', 'Item')}: {item?.billableService ? item?.billableService : item?.item}
|
|
160
|
+
</p>
|
|
161
|
+
<p className={styles.label}>
|
|
162
|
+
{t('currentPrice', 'Current price')}: {convertToCurrency(item?.price, defaultCurrency)}
|
|
163
|
+
</p>
|
|
164
|
+
<p className={styles.label}>
|
|
165
|
+
{t('serviceStatus', 'Service status')}: {item?.paymentStatus}
|
|
166
|
+
</p>
|
|
167
|
+
<Controller
|
|
168
|
+
name="quantity"
|
|
169
|
+
control={control}
|
|
170
|
+
render={({ field: { onChange, value } }) => (
|
|
171
|
+
<NumberInput
|
|
172
|
+
allowEmpty
|
|
173
|
+
disableWheel
|
|
174
|
+
className={styles.controlField}
|
|
175
|
+
hideSteppers
|
|
176
|
+
id="quantityInput"
|
|
177
|
+
invalid={!!errors.quantity}
|
|
178
|
+
invalidText={errors.quantity?.message}
|
|
179
|
+
label={t('quantity', 'Quantity')}
|
|
180
|
+
onChange={(_event, state: { value: number | string; direction: string }) => {
|
|
181
|
+
onChange(state.value);
|
|
182
|
+
}}
|
|
183
|
+
value={value}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
/>
|
|
187
|
+
|
|
188
|
+
<Controller
|
|
189
|
+
name="price"
|
|
190
|
+
control={control}
|
|
191
|
+
render={({ field: { value } }) => (
|
|
192
|
+
<TextInput
|
|
193
|
+
className={styles.controlField}
|
|
194
|
+
helperText={t('unitPriceHelperText', 'This is the unit price for this item')}
|
|
195
|
+
id="priceInput"
|
|
196
|
+
labelText={t('unitPrice', 'Unit price')}
|
|
197
|
+
readOnly
|
|
198
|
+
value={value}
|
|
199
|
+
/>
|
|
200
|
+
)}
|
|
201
|
+
/>
|
|
202
|
+
<p className={styles.label} aria-live="polite">
|
|
203
|
+
{t('total', 'Total')}: {convertToCurrency(total, defaultCurrency)}
|
|
204
|
+
</p>
|
|
205
|
+
</section>
|
|
206
|
+
</Stack>
|
|
207
|
+
</ModalBody>
|
|
208
|
+
<ModalFooter>
|
|
209
|
+
<Button kind="secondary" onClick={closeModal}>
|
|
210
|
+
{getCoreTranslation('cancel')}
|
|
211
|
+
</Button>
|
|
212
|
+
<Button type="submit" disabled={isSubmitting}>
|
|
213
|
+
{isSubmitting ? (
|
|
214
|
+
<div className={styles.inline}>
|
|
215
|
+
<InlineLoading className={styles.loader} description={`${t('submitting', 'Submitting')}...`} />
|
|
216
|
+
</div>
|
|
217
|
+
) : (
|
|
218
|
+
getCoreTranslation('save')
|
|
219
|
+
)}
|
|
220
|
+
</Button>
|
|
221
|
+
</ModalFooter>
|
|
222
|
+
</Form>
|
|
223
|
+
</>
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export default EditBillLineItemModal;
|
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import { type FetchResponse, getDefaultsFromConfigSchema, showSnackbar, useConfig } from '@openmrs/esm-framework';
|
|
5
|
+
import { configSchema, type BillingConfig } from '../config-schema';
|
|
4
6
|
import { type MappedBill } from '../types';
|
|
5
7
|
import { updateBillItems } from '../billing.resource';
|
|
6
|
-
import
|
|
8
|
+
import EditBillLineItemModal from './edit-bill-item.modal';
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
jest.
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const mockUpdateBillItems = jest.mocked(updateBillItems);
|
|
11
|
+
const mockShowSnackbar = jest.mocked(showSnackbar);
|
|
12
|
+
const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
|
|
13
|
+
|
|
14
|
+
const mockBillableServices = [
|
|
15
|
+
{ name: 'X-Ray Service', uuid: 'xray-uuid-123' },
|
|
16
|
+
{ name: 'Lab Test Service', uuid: 'lab-uuid-456' },
|
|
17
|
+
{ name: 'Consultation Service', uuid: 'consult-uuid-789' },
|
|
18
|
+
];
|
|
12
19
|
|
|
13
|
-
jest.mock('
|
|
14
|
-
|
|
20
|
+
jest.mock('../billing.resource', () => ({
|
|
21
|
+
updateBillItems: jest.fn().mockResolvedValue({}),
|
|
15
22
|
}));
|
|
16
23
|
|
|
17
24
|
jest.mock('../billable-services/billable-service.resource', () => ({
|
|
18
25
|
useBillableServices: jest.fn(() => ({
|
|
19
|
-
billableServices:
|
|
26
|
+
billableServices: mockBillableServices,
|
|
20
27
|
})),
|
|
21
28
|
}));
|
|
22
29
|
|
|
@@ -41,7 +48,7 @@ const mockBill: MappedBill = {
|
|
|
41
48
|
voided: false,
|
|
42
49
|
voidReason: null,
|
|
43
50
|
priceName: 'Service Price',
|
|
44
|
-
billableService: '
|
|
51
|
+
billableService: 'X-Ray Service',
|
|
45
52
|
priceUuid: 'price-uuid',
|
|
46
53
|
lineItemOrder: 1,
|
|
47
54
|
resourceVersion: '1.0',
|
|
@@ -62,7 +69,7 @@ const mockItem = {
|
|
|
62
69
|
uuid: 'item-uuid',
|
|
63
70
|
quantity: 2,
|
|
64
71
|
price: 100,
|
|
65
|
-
billableService: '
|
|
72
|
+
billableService: 'X-Ray Service',
|
|
66
73
|
paymentStatus: 'UNPAID',
|
|
67
74
|
item: 'Test Service',
|
|
68
75
|
display: 'Test Service',
|
|
@@ -74,64 +81,250 @@ const mockItem = {
|
|
|
74
81
|
resourceVersion: '1.0',
|
|
75
82
|
};
|
|
76
83
|
|
|
77
|
-
describe('
|
|
78
|
-
const closeModalMock = jest.fn();
|
|
79
|
-
|
|
84
|
+
describe('EditBillItem', () => {
|
|
80
85
|
beforeEach(() => {
|
|
81
|
-
|
|
86
|
+
mockUseConfig.mockReturnValue({
|
|
87
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
88
|
+
defaultCurrency: 'USD',
|
|
89
|
+
});
|
|
82
90
|
});
|
|
83
91
|
|
|
92
|
+
const mockCloseModal = jest.fn();
|
|
93
|
+
|
|
84
94
|
test('renders the form with correct fields and default values', () => {
|
|
85
|
-
render(<
|
|
95
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
86
96
|
|
|
87
|
-
expect(screen.getByText(
|
|
88
|
-
expect(screen.getByText(
|
|
89
|
-
expect(screen.
|
|
90
|
-
expect(screen.
|
|
91
|
-
expect(screen.
|
|
97
|
+
expect(screen.getByText(/edit bill line item/i)).toBeInTheDocument();
|
|
98
|
+
expect(screen.getByText(/John Doe/)).toBeInTheDocument();
|
|
99
|
+
expect(screen.getByText(/Main Cashpoint/)).toBeInTheDocument();
|
|
100
|
+
expect(screen.getByText(/123456/)).toBeInTheDocument();
|
|
101
|
+
expect(screen.getByRole('spinbutton', { name: /quantity/i })).toHaveValue(2);
|
|
102
|
+
expect(screen.getByLabelText(/unit price/i)).toHaveValue('100');
|
|
103
|
+
expect(screen.getByText(/total/i)).toHaveTextContent(/200/);
|
|
92
104
|
});
|
|
93
105
|
|
|
94
|
-
test('updates total when quantity is changed', () => {
|
|
95
|
-
|
|
106
|
+
test('updates total when quantity is changed', async () => {
|
|
107
|
+
const user = userEvent.setup();
|
|
108
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
96
109
|
|
|
97
|
-
const quantityInput = screen.getByRole('spinbutton', { name: /
|
|
98
|
-
|
|
110
|
+
const quantityInput = screen.getByRole('spinbutton', { name: /quantity/i });
|
|
111
|
+
await user.clear(quantityInput);
|
|
112
|
+
await user.type(quantityInput, '3');
|
|
99
113
|
|
|
100
|
-
expect(screen.getByText(/
|
|
114
|
+
expect(screen.getByText(/total/i)).toHaveTextContent(/300/);
|
|
101
115
|
});
|
|
102
116
|
|
|
103
117
|
test('submits the form and shows a success notification', async () => {
|
|
104
|
-
|
|
118
|
+
const user = userEvent.setup();
|
|
119
|
+
mockUpdateBillItems.mockResolvedValueOnce({} as FetchResponse<any>);
|
|
105
120
|
|
|
106
|
-
render(<
|
|
121
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
107
122
|
|
|
108
|
-
|
|
123
|
+
await user.click(screen.getByText(/save/i));
|
|
109
124
|
|
|
110
125
|
await waitFor(() => {
|
|
111
|
-
expect(
|
|
126
|
+
expect(mockUpdateBillItems).toHaveBeenCalled();
|
|
112
127
|
expect(showSnackbar).toHaveBeenCalledWith({
|
|
113
|
-
title: '
|
|
114
|
-
subtitle: '
|
|
128
|
+
title: 'Line item updated',
|
|
129
|
+
subtitle: 'The bill line item has been updated successfully',
|
|
115
130
|
kind: 'success',
|
|
116
|
-
timeoutInMs: 3000,
|
|
117
131
|
});
|
|
118
|
-
expect(
|
|
132
|
+
expect(mockCloseModal).toHaveBeenCalled();
|
|
119
133
|
});
|
|
120
134
|
});
|
|
121
135
|
|
|
122
136
|
test('shows error notification when submission fails', async () => {
|
|
123
|
-
|
|
137
|
+
const user = userEvent.setup();
|
|
138
|
+
mockUpdateBillItems.mockRejectedValueOnce({ message: 'Error occurred' });
|
|
124
139
|
|
|
125
|
-
render(<
|
|
140
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
126
141
|
|
|
127
|
-
|
|
142
|
+
await user.click(screen.getByText(/Save/));
|
|
128
143
|
|
|
129
144
|
await waitFor(() => {
|
|
130
|
-
expect(
|
|
131
|
-
title: '
|
|
145
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith({
|
|
146
|
+
title: 'Failed to update line item',
|
|
132
147
|
kind: 'error',
|
|
133
148
|
subtitle: 'Error occurred',
|
|
134
149
|
});
|
|
135
150
|
});
|
|
136
151
|
});
|
|
152
|
+
|
|
153
|
+
test('preserves billable service UUIDs for other line items when editing', async () => {
|
|
154
|
+
const user = userEvent.setup();
|
|
155
|
+
mockUpdateBillItems.mockResolvedValueOnce({} as FetchResponse<any>);
|
|
156
|
+
|
|
157
|
+
// Bill with multiple line items with different billable services
|
|
158
|
+
const billWithMultipleItems: MappedBill = {
|
|
159
|
+
...mockBill,
|
|
160
|
+
lineItems: [
|
|
161
|
+
{
|
|
162
|
+
uuid: 'item-1',
|
|
163
|
+
quantity: 1,
|
|
164
|
+
price: 100,
|
|
165
|
+
billableService: 'X-Ray Service',
|
|
166
|
+
paymentStatus: 'PENDING',
|
|
167
|
+
item: 'X-Ray',
|
|
168
|
+
display: 'X-Ray',
|
|
169
|
+
voided: false,
|
|
170
|
+
voidReason: null,
|
|
171
|
+
priceName: 'X-Ray Price',
|
|
172
|
+
priceUuid: 'xray-price-uuid',
|
|
173
|
+
lineItemOrder: 1,
|
|
174
|
+
resourceVersion: '1.0',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
uuid: 'item-2',
|
|
178
|
+
quantity: 2,
|
|
179
|
+
price: 50,
|
|
180
|
+
billableService: 'Lab Test Service',
|
|
181
|
+
paymentStatus: 'PENDING',
|
|
182
|
+
item: 'Lab Test',
|
|
183
|
+
display: 'Lab Test',
|
|
184
|
+
voided: false,
|
|
185
|
+
voidReason: null,
|
|
186
|
+
priceName: 'Lab Price',
|
|
187
|
+
priceUuid: 'lab-price-uuid',
|
|
188
|
+
lineItemOrder: 2,
|
|
189
|
+
resourceVersion: '1.0',
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
uuid: 'item-3',
|
|
193
|
+
quantity: 1,
|
|
194
|
+
price: 200,
|
|
195
|
+
billableService: 'Consultation Service',
|
|
196
|
+
paymentStatus: 'PENDING',
|
|
197
|
+
item: 'Consultation',
|
|
198
|
+
display: 'Consultation',
|
|
199
|
+
voided: false,
|
|
200
|
+
voidReason: null,
|
|
201
|
+
priceName: 'Consult Price',
|
|
202
|
+
priceUuid: 'consult-price-uuid',
|
|
203
|
+
lineItemOrder: 3,
|
|
204
|
+
resourceVersion: '1.0',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Editing the Lab Test item (item-2)
|
|
210
|
+
const itemToEdit = billWithMultipleItems.lineItems[1];
|
|
211
|
+
|
|
212
|
+
render(<EditBillLineItemModal bill={billWithMultipleItems} item={itemToEdit} closeModal={mockCloseModal} />);
|
|
213
|
+
|
|
214
|
+
await user.click(screen.getByText(/Save/));
|
|
215
|
+
|
|
216
|
+
await waitFor(() => {
|
|
217
|
+
expect(mockUpdateBillItems).toHaveBeenCalled();
|
|
218
|
+
const payload = mockUpdateBillItems.mock.calls[0][0];
|
|
219
|
+
|
|
220
|
+
// Verify that each line item has the correct billable service UUID
|
|
221
|
+
const xrayItem = payload.lineItems.find((li) => li.uuid === 'item-1');
|
|
222
|
+
const consultItem = payload.lineItems.find((li) => li.uuid === 'item-3');
|
|
223
|
+
|
|
224
|
+
// These should NOT have the Lab Test UUID (lab-uuid-456)
|
|
225
|
+
// They should keep their original UUIDs
|
|
226
|
+
expect(xrayItem?.billableService).toBe('xray-uuid-123');
|
|
227
|
+
expect(consultItem?.billableService).toBe('consult-uuid-789');
|
|
228
|
+
|
|
229
|
+
// The edited item should have the Lab Test UUID
|
|
230
|
+
const labItem = payload.lineItems.find((li) => li.uuid === 'item-2');
|
|
231
|
+
expect(labItem?.billableService).toBe('lab-uuid-456');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('shows validation error for quantity less than 1', async () => {
|
|
236
|
+
const user = userEvent.setup();
|
|
237
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
238
|
+
|
|
239
|
+
const quantityInput = screen.getByRole('spinbutton', { name: /Quantity/ });
|
|
240
|
+
await user.clear(quantityInput);
|
|
241
|
+
await user.type(quantityInput, '0');
|
|
242
|
+
|
|
243
|
+
// Try to submit
|
|
244
|
+
await user.click(screen.getByText(/Save/));
|
|
245
|
+
|
|
246
|
+
// Should show validation error
|
|
247
|
+
await waitFor(() => {
|
|
248
|
+
expect(screen.getByText(/Quantity must be at least 1/)).toBeInTheDocument();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Should NOT call the update function
|
|
252
|
+
expect(mockUpdateBillItems).not.toHaveBeenCalled();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('shows validation error for quantity greater than 100', async () => {
|
|
256
|
+
const user = userEvent.setup();
|
|
257
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
258
|
+
|
|
259
|
+
const quantityInput = screen.getByRole('spinbutton', { name: /Quantity/ });
|
|
260
|
+
await user.clear(quantityInput);
|
|
261
|
+
await user.type(quantityInput, '101');
|
|
262
|
+
|
|
263
|
+
await user.click(screen.getByText(/Save/));
|
|
264
|
+
|
|
265
|
+
await waitFor(() => {
|
|
266
|
+
expect(screen.getByText(/Quantity cannot exceed 100/)).toBeInTheDocument();
|
|
267
|
+
});
|
|
268
|
+
expect(mockUpdateBillItems).not.toHaveBeenCalled();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('shows validation error for non-integer quantity', async () => {
|
|
272
|
+
const user = userEvent.setup();
|
|
273
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
274
|
+
|
|
275
|
+
const quantityInput = screen.getByRole('spinbutton', { name: /Quantity/ });
|
|
276
|
+
await user.clear(quantityInput);
|
|
277
|
+
await user.type(quantityInput, '2.5');
|
|
278
|
+
|
|
279
|
+
await user.click(screen.getByText(/Save/));
|
|
280
|
+
|
|
281
|
+
await waitFor(() => {
|
|
282
|
+
expect(screen.getByText(/Quantity must be a whole number/)).toBeInTheDocument();
|
|
283
|
+
});
|
|
284
|
+
expect(mockUpdateBillItems).not.toHaveBeenCalled();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('clears validation error when valid quantity is entered', async () => {
|
|
288
|
+
const user = userEvent.setup();
|
|
289
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
290
|
+
|
|
291
|
+
const quantityInput = screen.getByRole('spinbutton', { name: /Quantity/ });
|
|
292
|
+
|
|
293
|
+
// Enter invalid value
|
|
294
|
+
await user.clear(quantityInput);
|
|
295
|
+
await user.type(quantityInput, '0');
|
|
296
|
+
await user.click(screen.getByText(/Save/));
|
|
297
|
+
|
|
298
|
+
await waitFor(() => {
|
|
299
|
+
expect(screen.getByText(/Quantity must be at least 1/)).toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Fix it
|
|
303
|
+
await user.clear(quantityInput);
|
|
304
|
+
await user.type(quantityInput, '5');
|
|
305
|
+
|
|
306
|
+
// Error should disappear
|
|
307
|
+
await waitFor(() => {
|
|
308
|
+
expect(screen.queryByText(/Quantity must be at least 1/)).not.toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('shows validation error when quantity field is left empty', async () => {
|
|
313
|
+
const user = userEvent.setup();
|
|
314
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
315
|
+
|
|
316
|
+
const quantityInput = screen.getByRole('spinbutton', { name: /Quantity/ });
|
|
317
|
+
await user.clear(quantityInput);
|
|
318
|
+
|
|
319
|
+
// Try to submit with empty field
|
|
320
|
+
await user.click(screen.getByText(/save/i));
|
|
321
|
+
|
|
322
|
+
// Should show validation error (empty string coerces to 0, triggering min validation)
|
|
323
|
+
await waitFor(() => {
|
|
324
|
+
expect(screen.getByText(/Quantity must be at least 1/)).toBeInTheDocument();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Should NOT call the update function
|
|
328
|
+
expect(mockUpdateBillItems).not.toHaveBeenCalled();
|
|
329
|
+
});
|
|
137
330
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Checkbox,
|
|
4
4
|
Layer,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
StructuredListWrapper,
|
|
10
10
|
} from '@carbon/react';
|
|
11
11
|
import { useTranslation } from 'react-i18next';
|
|
12
|
-
import { useConfig } from '@openmrs/esm-framework';
|
|
12
|
+
import { getCoreTranslation, useConfig } from '@openmrs/esm-framework';
|
|
13
13
|
import { convertToCurrency } from '../../helpers';
|
|
14
14
|
import { type MappedBill, type LineItem } from '../../types';
|
|
15
15
|
import BillWaiverForm from './bill-waiver-form.component';
|
|
@@ -20,7 +20,7 @@ const PatientBillsSelections: React.FC<{ bills: MappedBill; setPatientUuid: (pat
|
|
|
20
20
|
setPatientUuid,
|
|
21
21
|
}) => {
|
|
22
22
|
const { t } = useTranslation();
|
|
23
|
-
const [selectedBills, setSelectedBills] =
|
|
23
|
+
const [selectedBills, setSelectedBills] = useState<Array<LineItem>>([]);
|
|
24
24
|
const { defaultCurrency } = useConfig();
|
|
25
25
|
|
|
26
26
|
const checkBoxLabel = (lineItem) => {
|
|
@@ -42,9 +42,9 @@ const PatientBillsSelections: React.FC<{ bills: MappedBill; setPatientUuid: (pat
|
|
|
42
42
|
<StructuredListRow head>
|
|
43
43
|
<StructuredListCell head>{t('billItem', 'Bill item')}</StructuredListCell>
|
|
44
44
|
<StructuredListCell head>{t('quantity', 'Quantity')}</StructuredListCell>
|
|
45
|
-
<StructuredListCell head>{t('unitPrice', 'Unit
|
|
45
|
+
<StructuredListCell head>{t('unitPrice', 'Unit price')}</StructuredListCell>
|
|
46
46
|
<StructuredListCell head>{t('total', 'Total')}</StructuredListCell>
|
|
47
|
-
<StructuredListCell head>{
|
|
47
|
+
<StructuredListCell head>{getCoreTranslation('actions')}</StructuredListCell>
|
|
48
48
|
</StructuredListRow>
|
|
49
49
|
</StructuredListHead>
|
|
50
50
|
<StructuredListBody>
|