@openmrs/esm-billing-app 1.0.2-pre.90 → 1.0.2-pre.905
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/4692.js +1 -0
- package/dist/4692.js.map +1 -0
- 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/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 +282 -296
- 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 +893 -0
- package/src/billable-services/billable-service-form/billable-service-form.workspace.tsx +504 -0
- package/src/billable-services/billable-service.resource.ts +42 -26
- 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/add-payment-mode.modal.tsx +121 -0
- package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +74 -0
- package/src/billable-services/payment-modes/payment-modes-config.component.tsx +125 -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 +81 -23
- package/translations/am.json +132 -75
- package/translations/ar.json +133 -76
- package/translations/ar_SY.json +133 -76
- package/translations/bn.json +135 -78
- package/translations/de.json +133 -76
- package/translations/en.json +133 -78
- package/translations/en_US.json +133 -76
- package/translations/es.json +132 -75
- package/translations/es_MX.json +133 -76
- package/translations/fr.json +138 -81
- package/translations/he.json +132 -75
- package/translations/hi.json +133 -76
- package/translations/hi_IN.json +133 -76
- package/translations/id.json +133 -76
- package/translations/it.json +159 -102
- package/translations/ka.json +133 -76
- package/translations/km.json +132 -75
- package/translations/ku.json +133 -76
- package/translations/ky.json +133 -76
- package/translations/lg.json +133 -76
- package/translations/ne.json +133 -76
- package/translations/pl.json +133 -76
- package/translations/pt.json +133 -76
- package/translations/pt_BR.json +133 -76
- package/translations/qu.json +133 -76
- package/translations/ro_RO.json +222 -165
- package/translations/ru_RU.json +133 -76
- package/translations/si.json +133 -76
- package/translations/sw.json +133 -76
- package/translations/sw_KE.json +133 -76
- package/translations/tr.json +133 -76
- package/translations/tr_TR.json +133 -76
- package/translations/uk.json +133 -76
- package/translations/uz.json +133 -76
- package/translations/uz@Latn.json +133 -76
- package/translations/uz_UZ.json +133 -76
- package/translations/vi.json +133 -76
- package/translations/zh.json +133 -76
- package/translations/zh_CN.json +164 -107
- 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
|
@@ -1,46 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
3
|
-
import { render, screen,
|
|
4
|
-
import { showModal } from '@openmrs/esm-framework';
|
|
5
|
-
import InvoiceTable from './invoice-table.component';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import { getDefaultsFromConfigSchema, showModal, useConfig } from '@openmrs/esm-framework';
|
|
6
5
|
import { type MappedBill } from '../types';
|
|
6
|
+
import { configSchema, type BillingConfig } from '../config-schema';
|
|
7
|
+
import InvoiceTable from './invoice-table.component';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
jest.
|
|
10
|
-
useTranslation: jest.fn(() => ({
|
|
11
|
-
t: jest.fn((key, fallback) => fallback || key),
|
|
12
|
-
i18n: { language: 'en' },
|
|
13
|
-
})),
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
jest.mock('@openmrs/esm-framework', () => ({
|
|
17
|
-
showModal: jest.fn(),
|
|
18
|
-
useConfig: jest.fn(() => ({
|
|
19
|
-
defaultCurrency: 'USD',
|
|
20
|
-
showEditBillButton: true,
|
|
21
|
-
})),
|
|
22
|
-
useDebounce: jest.fn((value) => value),
|
|
23
|
-
useLayoutType: jest.fn(() => 'desktop'),
|
|
24
|
-
isDesktop: jest.fn(() => true),
|
|
25
|
-
}));
|
|
9
|
+
const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
|
|
10
|
+
const mockShowModal = jest.mocked(showModal);
|
|
26
11
|
|
|
27
12
|
jest.mock('../helpers', () => ({
|
|
28
13
|
convertToCurrency: jest.fn((price) => `USD ${price}`),
|
|
29
14
|
}));
|
|
30
15
|
|
|
31
16
|
describe('InvoiceTable', () => {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
(useTranslation as jest.Mock).mockReturnValue({ t: mockT, i18n: { language: 'en' } });
|
|
36
|
-
jest.useFakeTimers();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
jest.useRealTimers();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const bill: MappedBill = {
|
|
17
|
+
const defaultBill: MappedBill = {
|
|
44
18
|
uuid: 'bill-uuid',
|
|
45
19
|
id: 123,
|
|
46
20
|
patientUuid: 'patient-uuid',
|
|
@@ -95,57 +69,294 @@ describe('InvoiceTable', () => {
|
|
|
95
69
|
tenderedAmount: 300,
|
|
96
70
|
};
|
|
97
71
|
|
|
98
|
-
|
|
99
|
-
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
mockUseConfig.mockReturnValue({
|
|
74
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
75
|
+
defaultCurrency: 'USD',
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should render table headers correctly', () => {
|
|
80
|
+
render(<InvoiceTable bill={defaultBill} />);
|
|
81
|
+
|
|
82
|
+
expect(screen.getByText(/line items/i)).toBeInTheDocument();
|
|
83
|
+
expect(screen.getByText(/items to be billed/i)).toBeInTheDocument();
|
|
84
|
+
expect(screen.getByRole('columnheader', { name: /number/i })).toBeInTheDocument();
|
|
85
|
+
expect(screen.getByRole('columnheader', { name: /bill item/i })).toBeInTheDocument();
|
|
86
|
+
expect(screen.getByRole('columnheader', { name: /bill code/i })).toBeInTheDocument();
|
|
87
|
+
expect(screen.getByRole('columnheader', { name: /status/i })).toBeInTheDocument();
|
|
88
|
+
expect(screen.getByRole('columnheader', { name: /quantity/i })).toBeInTheDocument();
|
|
89
|
+
expect(screen.getByRole('columnheader', { name: /price/i })).toBeInTheDocument();
|
|
90
|
+
expect(screen.getByRole('columnheader', { name: /total/i })).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should render line items correctly', () => {
|
|
94
|
+
render(<InvoiceTable bill={defaultBill} />);
|
|
100
95
|
|
|
101
96
|
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
|
102
97
|
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
|
103
98
|
expect(screen.getByTestId('receipt-number-0')).toHaveTextContent('12345');
|
|
99
|
+
expect(screen.getByTestId('receipt-number-1')).toHaveTextContent('12345');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should display loading skeleton when bill is loading', () => {
|
|
103
|
+
render(<InvoiceTable bill={defaultBill} isLoadingBill={true} />);
|
|
104
|
+
|
|
105
|
+
expect(screen.getByTestId('loader')).toBeInTheDocument();
|
|
106
|
+
expect(screen.queryByText(/line items/i)).not.toBeInTheDocument();
|
|
104
107
|
});
|
|
105
108
|
|
|
106
|
-
it('
|
|
107
|
-
render(<InvoiceTable bill={
|
|
109
|
+
it('should display payment status for each line item', () => {
|
|
110
|
+
render(<InvoiceTable bill={defaultBill} />);
|
|
111
|
+
|
|
112
|
+
expect(screen.getByText('PAID')).toBeInTheDocument();
|
|
113
|
+
expect(screen.getByText('PENDING')).toBeInTheDocument();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should display correct quantities', () => {
|
|
117
|
+
render(<InvoiceTable bill={defaultBill} />);
|
|
118
|
+
|
|
119
|
+
// Item 1 has quantity 1, Item 2 has quantity 2
|
|
120
|
+
const rows = screen.getAllByRole('row');
|
|
121
|
+
expect(rows).toHaveLength(3); // Header row + 2 data rows
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should calculate and display line item totals correctly', () => {
|
|
125
|
+
const billWithCalculation: MappedBill = {
|
|
126
|
+
...defaultBill,
|
|
127
|
+
lineItems: [
|
|
128
|
+
{
|
|
129
|
+
uuid: '1',
|
|
130
|
+
item: 'Service A',
|
|
131
|
+
paymentStatus: 'PENDING',
|
|
132
|
+
quantity: 3,
|
|
133
|
+
price: 100,
|
|
134
|
+
display: '',
|
|
135
|
+
voided: false,
|
|
136
|
+
voidReason: '',
|
|
137
|
+
billableService: 'Service A',
|
|
138
|
+
priceName: '',
|
|
139
|
+
priceUuid: '',
|
|
140
|
+
lineItemOrder: 0,
|
|
141
|
+
resourceVersion: '',
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
render(<InvoiceTable bill={billWithCalculation} />);
|
|
147
|
+
|
|
148
|
+
// Total should be 3 * 100 = 300
|
|
149
|
+
expect(screen.getByText('USD 300')).toBeInTheDocument();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should render edit buttons for all line items', () => {
|
|
153
|
+
render(<InvoiceTable bill={defaultBill} />);
|
|
154
|
+
|
|
155
|
+
const editButton1 = screen.getByTestId('edit-button-1');
|
|
156
|
+
const editButton2 = screen.getByTestId('edit-button-2');
|
|
157
|
+
|
|
158
|
+
expect(editButton1).toBeInTheDocument();
|
|
159
|
+
expect(editButton2).toBeInTheDocument();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should open edit modal when edit button is clicked', async () => {
|
|
163
|
+
const user = userEvent.setup();
|
|
164
|
+
render(<InvoiceTable bill={defaultBill} />);
|
|
108
165
|
|
|
109
166
|
const editButton = screen.getByTestId('edit-button-1');
|
|
110
|
-
|
|
111
|
-
|
|
167
|
+
await user.click(editButton);
|
|
168
|
+
|
|
169
|
+
expect(mockShowModal).toHaveBeenCalledTimes(1);
|
|
170
|
+
expect(mockShowModal).toHaveBeenCalledWith(
|
|
171
|
+
'edit-bill-line-item-modal',
|
|
172
|
+
expect.objectContaining({
|
|
173
|
+
bill: defaultBill,
|
|
174
|
+
item: expect.objectContaining({ uuid: '1' }),
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
112
177
|
});
|
|
113
178
|
|
|
114
|
-
it('
|
|
115
|
-
|
|
179
|
+
it('should filter line items based on search term', async () => {
|
|
180
|
+
const user = userEvent.setup();
|
|
181
|
+
render(<InvoiceTable bill={defaultBill} />);
|
|
116
182
|
|
|
117
|
-
|
|
183
|
+
const searchInput = screen.getByPlaceholderText(/search this table/i);
|
|
184
|
+
await user.type(searchInput, 'Item 2');
|
|
185
|
+
|
|
186
|
+
await waitFor(() => {
|
|
187
|
+
expect(screen.queryByText('Item 1')).not.toBeInTheDocument();
|
|
188
|
+
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
|
189
|
+
});
|
|
118
190
|
});
|
|
119
191
|
|
|
120
|
-
it('
|
|
121
|
-
|
|
122
|
-
|
|
192
|
+
it('should show all items when search is cleared', async () => {
|
|
193
|
+
const user = userEvent.setup();
|
|
194
|
+
render(<InvoiceTable bill={defaultBill} />);
|
|
123
195
|
|
|
124
|
-
|
|
196
|
+
const searchInput = screen.getByPlaceholderText(/search this table/i);
|
|
125
197
|
|
|
126
|
-
|
|
127
|
-
|
|
198
|
+
// Search for Item 1
|
|
199
|
+
await user.type(searchInput, 'Item 1');
|
|
200
|
+
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
expect(screen.queryByText('Item 2')).not.toBeInTheDocument();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Clear search
|
|
206
|
+
await user.clear(searchInput);
|
|
207
|
+
|
|
208
|
+
await waitFor(() => {
|
|
209
|
+
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
|
210
|
+
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
|
211
|
+
});
|
|
128
212
|
});
|
|
129
213
|
|
|
130
|
-
it('
|
|
131
|
-
const
|
|
132
|
-
|
|
214
|
+
it('should display empty state when no line items exist', () => {
|
|
215
|
+
const emptyBill: MappedBill = {
|
|
216
|
+
...defaultBill,
|
|
217
|
+
lineItems: [],
|
|
218
|
+
};
|
|
133
219
|
|
|
134
|
-
|
|
135
|
-
fireEvent.click(checkboxes[0]);
|
|
220
|
+
render(<InvoiceTable bill={emptyBill} />);
|
|
136
221
|
|
|
137
|
-
expect(
|
|
222
|
+
expect(screen.getByText(/no matching items to display/i)).toBeInTheDocument();
|
|
223
|
+
expect(screen.getByText(/check the filters above/i)).toBeInTheDocument();
|
|
138
224
|
});
|
|
139
225
|
|
|
140
|
-
it('
|
|
141
|
-
|
|
226
|
+
it('should show empty state when search has no results', async () => {
|
|
227
|
+
const user = userEvent.setup();
|
|
228
|
+
render(<InvoiceTable bill={defaultBill} />);
|
|
229
|
+
|
|
230
|
+
const searchInput = screen.getByPlaceholderText(/search this table/i);
|
|
231
|
+
await user.type(searchInput, 'NonexistentItem');
|
|
142
232
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
jest.advanceTimersByTime(1000);
|
|
233
|
+
await waitFor(() => {
|
|
234
|
+
expect(screen.getByText(/no matching items to display/i)).toBeInTheDocument();
|
|
235
|
+
expect(screen.getByText(/check the filters above/i)).toBeInTheDocument();
|
|
147
236
|
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle line items with zero price', () => {
|
|
240
|
+
const billWithZeroPrice: MappedBill = {
|
|
241
|
+
...defaultBill,
|
|
242
|
+
lineItems: [
|
|
243
|
+
{
|
|
244
|
+
uuid: '1',
|
|
245
|
+
item: 'Free Service',
|
|
246
|
+
paymentStatus: 'PAID',
|
|
247
|
+
quantity: 1,
|
|
248
|
+
price: 0,
|
|
249
|
+
display: '',
|
|
250
|
+
voided: false,
|
|
251
|
+
voidReason: '',
|
|
252
|
+
billableService: 'Free Service',
|
|
253
|
+
priceName: '',
|
|
254
|
+
priceUuid: '',
|
|
255
|
+
lineItemOrder: 0,
|
|
256
|
+
resourceVersion: '',
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
render(<InvoiceTable bill={billWithZeroPrice} />);
|
|
262
|
+
|
|
263
|
+
// USD 0 appears for both price and total
|
|
264
|
+
expect(screen.getAllByText('USD 0').length).toBeGreaterThan(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should handle line items with zero quantity', () => {
|
|
268
|
+
const billWithZeroQuantity: MappedBill = {
|
|
269
|
+
...defaultBill,
|
|
270
|
+
lineItems: [
|
|
271
|
+
{
|
|
272
|
+
uuid: '1',
|
|
273
|
+
item: 'Service',
|
|
274
|
+
paymentStatus: 'PENDING',
|
|
275
|
+
quantity: 0,
|
|
276
|
+
price: 100,
|
|
277
|
+
display: '',
|
|
278
|
+
voided: false,
|
|
279
|
+
voidReason: '',
|
|
280
|
+
billableService: 'Service',
|
|
281
|
+
priceName: '',
|
|
282
|
+
priceUuid: '',
|
|
283
|
+
lineItemOrder: 0,
|
|
284
|
+
resourceVersion: '',
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
render(<InvoiceTable bill={billWithZeroQuantity} />);
|
|
290
|
+
|
|
291
|
+
// Total should be 0 * 100 = 0
|
|
292
|
+
expect(screen.getByText('USD 0')).toBeInTheDocument();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should use billableService name when available, otherwise use item name', () => {
|
|
296
|
+
const billWithBillableService: MappedBill = {
|
|
297
|
+
...defaultBill,
|
|
298
|
+
lineItems: [
|
|
299
|
+
{
|
|
300
|
+
uuid: '1',
|
|
301
|
+
item: 'Item Name',
|
|
302
|
+
billableService: 'Billable Service Name',
|
|
303
|
+
paymentStatus: 'PAID',
|
|
304
|
+
quantity: 1,
|
|
305
|
+
price: 100,
|
|
306
|
+
display: '',
|
|
307
|
+
voided: false,
|
|
308
|
+
voidReason: '',
|
|
309
|
+
priceName: '',
|
|
310
|
+
priceUuid: '',
|
|
311
|
+
lineItemOrder: 0,
|
|
312
|
+
resourceVersion: '',
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
uuid: '2',
|
|
316
|
+
item: 'Item Without Billable',
|
|
317
|
+
billableService: '',
|
|
318
|
+
paymentStatus: 'PENDING',
|
|
319
|
+
quantity: 1,
|
|
320
|
+
price: 200,
|
|
321
|
+
display: '',
|
|
322
|
+
voided: false,
|
|
323
|
+
voidReason: '',
|
|
324
|
+
priceName: '',
|
|
325
|
+
priceUuid: '',
|
|
326
|
+
lineItemOrder: 1,
|
|
327
|
+
resourceVersion: '',
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
render(<InvoiceTable bill={billWithBillableService} />);
|
|
333
|
+
|
|
334
|
+
expect(screen.getByText('Billable Service Name')).toBeInTheDocument();
|
|
335
|
+
expect(screen.getByText('Item Without Billable')).toBeInTheDocument();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should display line item numbers starting from 1', () => {
|
|
339
|
+
render(<InvoiceTable bill={defaultBill} />);
|
|
340
|
+
|
|
341
|
+
// Check the table body for numbered rows
|
|
342
|
+
const rows = screen.getAllByRole('row');
|
|
343
|
+
// First row is header, so data rows start at index 1
|
|
344
|
+
expect(rows.length).toBeGreaterThan(2);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should pass correct currency to convertToCurrency helper', () => {
|
|
348
|
+
render(<InvoiceTable bill={defaultBill} />);
|
|
349
|
+
|
|
350
|
+
// Verify prices are formatted with USD - multiple occurrences expected
|
|
351
|
+
expect(screen.getAllByText('USD 100').length).toBeGreaterThan(0);
|
|
352
|
+
expect(screen.getAllByText('USD 200').length).toBeGreaterThan(0);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should render search input in expanded state', () => {
|
|
356
|
+
render(<InvoiceTable bill={defaultBill} />);
|
|
148
357
|
|
|
149
|
-
|
|
358
|
+
const searchInput = screen.getByPlaceholderText(/search this table/i);
|
|
359
|
+
expect(searchInput).toBeInTheDocument();
|
|
360
|
+
expect(searchInput).toBeVisible();
|
|
150
361
|
});
|
|
151
362
|
});
|
|
@@ -4,15 +4,15 @@ import { Printer } from '@carbon/react/icons';
|
|
|
4
4
|
import { useParams } from 'react-router-dom';
|
|
5
5
|
import { useReactToPrint } from 'react-to-print';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { ExtensionSlot, useConfig, usePatient } from '@openmrs/esm-framework';
|
|
8
|
-
import { ErrorState } from '@openmrs/esm-patient-common-lib';
|
|
9
|
-
import { convertToCurrency } from '../helpers';
|
|
10
|
-
import { type LineItem } from '../types';
|
|
11
|
-
import { useBill } from '../billing.resource';
|
|
7
|
+
import { ExtensionSlot, showSnackbar, useConfig, usePatient } from '@openmrs/esm-framework';
|
|
12
8
|
import InvoiceTable from './invoice-table.component';
|
|
13
9
|
import Payments from './payments/payments.component';
|
|
14
10
|
import PrintReceipt from './printable-invoice/print-receipt.component';
|
|
15
11
|
import PrintableInvoice from './printable-invoice/printable-invoice.component';
|
|
12
|
+
import { ErrorState } from '@openmrs/esm-patient-common-lib';
|
|
13
|
+
import { convertToCurrency } from '../helpers';
|
|
14
|
+
import { useBill, useDefaultFacility } from '../billing.resource';
|
|
15
|
+
import type { BillingConfig } from '../config-schema';
|
|
16
16
|
import styles from './invoice.scss';
|
|
17
17
|
|
|
18
18
|
interface InvoiceDetailsProps {
|
|
@@ -22,25 +22,20 @@ interface InvoiceDetailsProps {
|
|
|
22
22
|
|
|
23
23
|
const Invoice: React.FC = () => {
|
|
24
24
|
const { t } = useTranslation();
|
|
25
|
+
const { data } = useDefaultFacility();
|
|
25
26
|
const { billUuid, patientUuid } = useParams();
|
|
26
27
|
const { patient, isLoading: isLoadingPatient } = usePatient(patientUuid);
|
|
27
28
|
const { bill, isLoading: isLoadingBill, error, mutate } = useBill(billUuid);
|
|
28
29
|
const [isPrinting, setIsPrinting] = useState(false);
|
|
29
|
-
const [selectedLineItems, setSelectedLineItems] = useState<LineItem[]>([]);
|
|
30
30
|
const componentRef = useRef<HTMLDivElement>(null);
|
|
31
31
|
const onBeforeGetContentResolve = useRef<(() => void) | null>(null);
|
|
32
|
-
const { defaultCurrency } = useConfig();
|
|
33
|
-
const handleSelectItem = (lineItems: LineItem[]) => {
|
|
34
|
-
setSelectedLineItems(lineItems);
|
|
35
|
-
};
|
|
32
|
+
const { defaultCurrency } = useConfig<BillingConfig>();
|
|
36
33
|
|
|
37
34
|
const handleAfterPrint = useCallback(() => {
|
|
38
35
|
onBeforeGetContentResolve.current = null;
|
|
39
36
|
setIsPrinting(false);
|
|
40
37
|
}, []);
|
|
41
38
|
|
|
42
|
-
const reactToPrintContent = useCallback(() => componentRef.current, []);
|
|
43
|
-
|
|
44
39
|
const handleOnBeforeGetContent = useCallback(() => {
|
|
45
40
|
return new Promise<void>((resolve) => {
|
|
46
41
|
if (patient && bill) {
|
|
@@ -51,11 +46,17 @@ const Invoice: React.FC = () => {
|
|
|
51
46
|
}, [bill, patient]);
|
|
52
47
|
|
|
53
48
|
const handlePrint = useReactToPrint({
|
|
54
|
-
|
|
49
|
+
contentRef: componentRef,
|
|
55
50
|
documentTitle: `Invoice ${bill?.receiptNumber} - ${patient?.name?.[0]?.given?.join(' ')} ${patient?.name?.[0].family}`,
|
|
56
|
-
|
|
51
|
+
onBeforePrint: handleOnBeforeGetContent,
|
|
57
52
|
onAfterPrint: handleAfterPrint,
|
|
58
|
-
|
|
53
|
+
preserveAfterPrint: false,
|
|
54
|
+
onPrintError: (_, error) =>
|
|
55
|
+
showSnackbar({
|
|
56
|
+
title: t('errorPrintingInvoice', 'Error printing invoice'),
|
|
57
|
+
kind: 'error',
|
|
58
|
+
subtitle: error.message,
|
|
59
|
+
}),
|
|
59
60
|
});
|
|
60
61
|
|
|
61
62
|
useEffect(() => {
|
|
@@ -64,11 +65,14 @@ const Invoice: React.FC = () => {
|
|
|
64
65
|
}
|
|
65
66
|
}, [isPrinting]);
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
// Do not remove this comment. Adds the translation keys for the invoice details
|
|
69
|
+
/**
|
|
70
|
+
* t('totalAmount', 'Total amount')
|
|
71
|
+
* t('amountTendered', 'Amount tendered')
|
|
72
|
+
* t('invoiceNumber', 'Invoice #')
|
|
73
|
+
* t('dateAndTime', 'Date and time')
|
|
74
|
+
* t('invoiceStatus', 'Invoice status')
|
|
75
|
+
*/
|
|
72
76
|
const invoiceDetails = {
|
|
73
77
|
'Total Amount': convertToCurrency(bill?.totalAmount, defaultCurrency),
|
|
74
78
|
'Amount Tendered': convertToCurrency(bill?.tenderedAmount, defaultCurrency),
|
|
@@ -77,14 +81,12 @@ const Invoice: React.FC = () => {
|
|
|
77
81
|
'Invoice Status': bill?.status,
|
|
78
82
|
};
|
|
79
83
|
|
|
80
|
-
if (isLoadingPatient
|
|
84
|
+
if (isLoadingPatient || isLoadingBill) {
|
|
81
85
|
return (
|
|
82
86
|
<div className={styles.invoiceContainer}>
|
|
83
87
|
<InlineLoading
|
|
84
88
|
className={styles.loader}
|
|
85
|
-
|
|
86
|
-
iconDescription="Loading"
|
|
87
|
-
description="Loading patient header..."
|
|
89
|
+
description={`${t('loadingBillInfo', 'Loading bill information')}...`}
|
|
88
90
|
/>
|
|
89
91
|
</div>
|
|
90
92
|
);
|
|
@@ -109,10 +111,10 @@ const Invoice: React.FC = () => {
|
|
|
109
111
|
</section>
|
|
110
112
|
<div>
|
|
111
113
|
<Button
|
|
112
|
-
disabled={isPrinting}
|
|
114
|
+
disabled={isPrinting || isLoadingPatient || isLoadingBill}
|
|
113
115
|
onClick={handlePrint}
|
|
114
116
|
renderIcon={(props) => <Printer size={24} {...props} />}
|
|
115
|
-
iconDescription=
|
|
117
|
+
iconDescription={t('printBill', 'Print bill')}
|
|
116
118
|
size="md">
|
|
117
119
|
{t('printBill', 'Print bill')}
|
|
118
120
|
</Button>
|
|
@@ -120,20 +122,25 @@ const Invoice: React.FC = () => {
|
|
|
120
122
|
</div>
|
|
121
123
|
</div>
|
|
122
124
|
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<div className={styles.printContainer} ref={componentRef}>
|
|
127
|
-
{isPrinting && <PrintableInvoice bill={bill} patient={patient} isLoading={isLoadingPatient} />}
|
|
125
|
+
<div className={styles.invoiceContent}>
|
|
126
|
+
<InvoiceTable bill={bill} isLoadingBill={isLoadingBill} />
|
|
127
|
+
<Payments bill={bill} mutate={mutate} />
|
|
128
128
|
</div>
|
|
129
|
+
|
|
130
|
+
{bill && patient && (
|
|
131
|
+
<div className={styles.printContainer}>
|
|
132
|
+
<PrintableInvoice bill={bill} patient={patient} defaultFacility={data} componentRef={componentRef} />
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
129
135
|
</div>
|
|
130
136
|
);
|
|
131
137
|
};
|
|
132
138
|
|
|
133
139
|
function InvoiceDetails({ label, value }: InvoiceDetailsProps) {
|
|
140
|
+
const { t } = useTranslation();
|
|
134
141
|
return (
|
|
135
142
|
<div>
|
|
136
|
-
<h1 className={styles.label}>{label}</h1>
|
|
143
|
+
<h1 className={styles.label}>{t(label)}</h1>
|
|
137
144
|
<span className={styles.value}>{value}</span>
|
|
138
145
|
</div>
|
|
139
146
|
);
|
package/src/invoice/invoice.scss
CHANGED
|
@@ -73,21 +73,28 @@
|
|
|
73
73
|
flex-direction: row;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
.invoiceContent {
|
|
77
|
+
margin: layout.$spacing-05;
|
|
78
|
+
}
|
|
79
|
+
|
|
76
80
|
.billDetail {
|
|
77
81
|
font-weight: bold;
|
|
78
82
|
color: colors.$cool-gray-90;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
.printContainer {
|
|
86
|
+
background-color: colors.$white;
|
|
87
|
+
display: none;
|
|
88
|
+
|
|
89
|
+
@media print {
|
|
90
|
+
display: block !important;
|
|
85
91
|
}
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
@media print {
|
|
89
95
|
html,
|
|
90
96
|
body {
|
|
97
|
+
display: block !important;
|
|
91
98
|
background-color: colors.$white !important;
|
|
92
99
|
}
|
|
93
100
|
}
|