@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
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type LayoutType,
|
|
32
32
|
} from '@openmrs/esm-framework';
|
|
33
33
|
import { usePaginatedBills } from '../billing.resource';
|
|
34
|
+
import { BillStatus } from '../types';
|
|
34
35
|
import type { MappedBill } from '../types';
|
|
35
36
|
import type { BillingConfig } from '../config-schema';
|
|
36
37
|
import styles from './bills-table.scss';
|
|
@@ -42,9 +43,9 @@ interface BillDisplayItem extends Omit<MappedBill, 'id'> {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
interface BillPaymentStatusFilterItem {
|
|
45
|
-
id:
|
|
46
|
+
id: BillStatus | '';
|
|
46
47
|
text: string;
|
|
47
|
-
status:
|
|
48
|
+
status: BillStatus | '';
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const mapLineItems = (bill: MappedBill) =>
|
|
@@ -64,13 +65,20 @@ const BillsTable: React.FC = () => {
|
|
|
64
65
|
const billPaymentStatusFilterItems: BillPaymentStatusFilterItem[] = useMemo(
|
|
65
66
|
() => [
|
|
66
67
|
{ id: '', text: t('allBills', 'All bills'), status: '' },
|
|
67
|
-
{
|
|
68
|
-
|
|
68
|
+
{
|
|
69
|
+
id: BillStatus.PENDING,
|
|
70
|
+
text: t('pendingConfirmationBills', 'Pending confirmation'),
|
|
71
|
+
status: BillStatus.PENDING,
|
|
72
|
+
},
|
|
73
|
+
{ id: BillStatus.POSTED, text: t('pendingPaymentBills', 'Pending payment'), status: BillStatus.POSTED },
|
|
74
|
+
{ id: BillStatus.PAID, text: t('paidBills', 'Paid bills'), status: BillStatus.PAID },
|
|
69
75
|
],
|
|
70
76
|
[t],
|
|
71
77
|
);
|
|
78
|
+
|
|
79
|
+
// Default to 'POSTED' (pending payment) so cashiers see bills awaiting payment on load
|
|
72
80
|
const [billPaymentStatus, setBillPaymentStatus] = useState<BillPaymentStatusFilterItem>(
|
|
73
|
-
billPaymentStatusFilterItems[
|
|
81
|
+
() => billPaymentStatusFilterItems.find((item) => item.id === BillStatus.POSTED) ?? billPaymentStatusFilterItems[0],
|
|
74
82
|
);
|
|
75
83
|
const [searchString, setSearchString] = useState('');
|
|
76
84
|
const debouncedSearchString = useDebounce(searchString, 500);
|
|
@@ -127,9 +135,6 @@ const BillsTable: React.FC = () => {
|
|
|
127
135
|
return mappedBills;
|
|
128
136
|
}, [bills]);
|
|
129
137
|
|
|
130
|
-
// Server-side search is now handled by the API, so we just use the bills directly
|
|
131
|
-
const searchResults = billList;
|
|
132
|
-
|
|
133
138
|
// Check if user has applied any filters (not "All bills") or search
|
|
134
139
|
const hasActiveFiltersOrSearch = searchString.trim() !== '' || billPaymentStatus.id !== '';
|
|
135
140
|
|
|
@@ -156,7 +161,6 @@ const BillsTable: React.FC = () => {
|
|
|
156
161
|
className={styles.filterDropdown}
|
|
157
162
|
direction="bottom"
|
|
158
163
|
id="bill-payment-status-filter"
|
|
159
|
-
initialSelectedItem={billPaymentStatusFilterItems[1]}
|
|
160
164
|
selectedItem={billPaymentStatus}
|
|
161
165
|
items={billPaymentStatusFilterItems}
|
|
162
166
|
itemToString={(item: BillPaymentStatusFilterItem) => (item ? item.text : '')}
|
|
@@ -196,10 +200,10 @@ const BillsTable: React.FC = () => {
|
|
|
196
200
|
/>
|
|
197
201
|
<DataTable
|
|
198
202
|
isSortable
|
|
199
|
-
rows={
|
|
203
|
+
rows={billList}
|
|
200
204
|
headers={headerData}
|
|
201
205
|
size={responsiveSize}
|
|
202
|
-
useZebraStyles={
|
|
206
|
+
useZebraStyles={billList?.length > 1 ? true : false}>
|
|
203
207
|
{({ rows, headers, getRowProps, getTableProps }) => (
|
|
204
208
|
<TableContainer>
|
|
205
209
|
<Table {...getTableProps()} aria-label={t('billList', 'Bill list')}>
|
|
@@ -227,7 +231,7 @@ const BillsTable: React.FC = () => {
|
|
|
227
231
|
</TableContainer>
|
|
228
232
|
)}
|
|
229
233
|
</DataTable>
|
|
230
|
-
{
|
|
234
|
+
{billList?.length === 0 && (
|
|
231
235
|
<div className={styles.filterEmptyState}>
|
|
232
236
|
<Layer level={0}>
|
|
233
237
|
<Tile className={styles.filterEmptyStateTile}>
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
4
|
import { usePaginatedBills } from '../billing.resource';
|
|
5
|
+
import { BillStatus } from '../types';
|
|
5
6
|
import BillsTable from './bills-table.component';
|
|
6
7
|
|
|
7
8
|
jest.mock('../billing.resource', () => ({
|
|
@@ -42,7 +43,7 @@ const mockBillsData = [
|
|
|
42
43
|
paymentStatus: 'PENDING',
|
|
43
44
|
},
|
|
44
45
|
],
|
|
45
|
-
status:
|
|
46
|
+
status: BillStatus.PENDING,
|
|
46
47
|
cashPointUuid: 'cash-point-1',
|
|
47
48
|
cashPointName: 'Main Cash Point',
|
|
48
49
|
cashPointLocation: 'Main Hospital',
|
|
@@ -78,7 +79,7 @@ const mockBillsData = [
|
|
|
78
79
|
paymentStatus: 'PENDING',
|
|
79
80
|
},
|
|
80
81
|
],
|
|
81
|
-
status:
|
|
82
|
+
status: BillStatus.PENDING,
|
|
82
83
|
cashPointUuid: 'cash-point-1',
|
|
83
84
|
cashPointName: 'Main Cash Point',
|
|
84
85
|
cashPointLocation: 'Main Hospital',
|
|
@@ -151,7 +152,7 @@ describe('BillsTable', () => {
|
|
|
151
152
|
|
|
152
153
|
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
|
153
154
|
expect(screen.getByText(/filter by/i)).toBeInTheDocument();
|
|
154
|
-
expect(screen.getByText(/pending
|
|
155
|
+
expect(screen.getByText(/pending payment/i)).toBeInTheDocument();
|
|
155
156
|
});
|
|
156
157
|
|
|
157
158
|
test('should display an error state if there is a problem loading bill data', () => {
|
|
@@ -171,7 +172,7 @@ describe('BillsTable', () => {
|
|
|
171
172
|
expect(screen.getByText(/error state/i)).toBeInTheDocument();
|
|
172
173
|
expect(screen.queryByRole('table')).not.toBeInTheDocument();
|
|
173
174
|
expect(screen.getByText(/filter by/i)).toBeInTheDocument();
|
|
174
|
-
expect(screen.getByText(/pending
|
|
175
|
+
expect(screen.getByText(/pending payment/i)).toBeInTheDocument();
|
|
175
176
|
});
|
|
176
177
|
|
|
177
178
|
test('should pass search term to backend API', async () => {
|
|
@@ -200,7 +201,7 @@ describe('BillsTable', () => {
|
|
|
200
201
|
await user.type(searchInput, 'John');
|
|
201
202
|
|
|
202
203
|
await waitFor(() => {
|
|
203
|
-
expect(mockBills).toHaveBeenCalledWith(10, '
|
|
204
|
+
expect(mockBills).toHaveBeenCalledWith(10, 'POSTED', 'John');
|
|
204
205
|
});
|
|
205
206
|
|
|
206
207
|
expect(mockGoTo).toHaveBeenCalledWith(1);
|
|
@@ -220,7 +221,7 @@ describe('BillsTable', () => {
|
|
|
220
221
|
|
|
221
222
|
// First call: initial render with PENDING filter (default)
|
|
222
223
|
mockBills.mockImplementationOnce(() => ({
|
|
223
|
-
bills: mockBillsData.map((bill) => ({ ...bill, status:
|
|
224
|
+
bills: mockBillsData.map((bill) => ({ ...bill, status: BillStatus.PENDING })),
|
|
224
225
|
isLoading: false,
|
|
225
226
|
isValidating: false,
|
|
226
227
|
error: null,
|
|
@@ -244,7 +245,7 @@ describe('BillsTable', () => {
|
|
|
244
245
|
|
|
245
246
|
render(<BillsTable />);
|
|
246
247
|
|
|
247
|
-
const filterDropdown = screen.getByText('Pending
|
|
248
|
+
const filterDropdown = screen.getByText('Pending payment');
|
|
248
249
|
await user.click(filterDropdown);
|
|
249
250
|
|
|
250
251
|
const paidBillsOption = screen.getAllByText('Paid bills')[0];
|
|
@@ -325,6 +326,82 @@ describe('BillsTable', () => {
|
|
|
325
326
|
});
|
|
326
327
|
});
|
|
327
328
|
|
|
329
|
+
test('should default to "Pending payment" filter showing POSTED bills', () => {
|
|
330
|
+
render(<BillsTable />);
|
|
331
|
+
|
|
332
|
+
expect(screen.getByText('Pending payment')).toBeInTheDocument();
|
|
333
|
+
expect(mockBills).toHaveBeenCalledWith(expect.any(Number), 'POSTED', undefined);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('should show "Pending confirmation" option in filter dropdown', async () => {
|
|
337
|
+
const user = userEvent.setup();
|
|
338
|
+
render(<BillsTable />);
|
|
339
|
+
|
|
340
|
+
const filterDropdown = screen.getByText('Pending payment');
|
|
341
|
+
await user.click(filterDropdown);
|
|
342
|
+
|
|
343
|
+
expect(screen.getByRole('option', { name: /pending confirmation/i })).toBeInTheDocument();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('should filter by PENDING status when "Pending confirmation" is selected', async () => {
|
|
347
|
+
const user = userEvent.setup();
|
|
348
|
+
const mockGoTo = jest.fn();
|
|
349
|
+
|
|
350
|
+
mockBills.mockImplementation((_pageSize, status) => ({
|
|
351
|
+
bills: status === 'PENDING' ? [mockBillsData[0]] : mockBillsData,
|
|
352
|
+
isLoading: false,
|
|
353
|
+
isValidating: false,
|
|
354
|
+
error: null,
|
|
355
|
+
mutate: jest.fn(),
|
|
356
|
+
currentPage: 1,
|
|
357
|
+
totalCount: status === 'PENDING' ? 1 : 2,
|
|
358
|
+
goTo: mockGoTo,
|
|
359
|
+
}));
|
|
360
|
+
|
|
361
|
+
render(<BillsTable />);
|
|
362
|
+
|
|
363
|
+
const filterDropdown = screen.getByText('Pending payment');
|
|
364
|
+
await user.click(filterDropdown);
|
|
365
|
+
|
|
366
|
+
await user.click(screen.getByRole('option', { name: /pending confirmation/i }));
|
|
367
|
+
|
|
368
|
+
await waitFor(() => {
|
|
369
|
+
expect(mockBills).toHaveBeenCalledWith(expect.any(Number), 'PENDING', undefined);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('should filter by POSTED status when "Pending payment" is selected', async () => {
|
|
374
|
+
const user = userEvent.setup();
|
|
375
|
+
const mockGoTo = jest.fn();
|
|
376
|
+
|
|
377
|
+
mockBills.mockImplementation((_pageSize, status) => ({
|
|
378
|
+
bills: status === 'POSTED' ? [mockBillsData[1]] : mockBillsData,
|
|
379
|
+
isLoading: false,
|
|
380
|
+
isValidating: false,
|
|
381
|
+
error: null,
|
|
382
|
+
mutate: jest.fn(),
|
|
383
|
+
currentPage: 1,
|
|
384
|
+
totalCount: 1,
|
|
385
|
+
goTo: mockGoTo,
|
|
386
|
+
}));
|
|
387
|
+
|
|
388
|
+
render(<BillsTable />);
|
|
389
|
+
|
|
390
|
+
// Navigate away from the default POSTED filter first, then select it again
|
|
391
|
+
const filterDropdown = screen.getByText('Pending payment');
|
|
392
|
+
await user.click(filterDropdown);
|
|
393
|
+
await user.click(screen.getByRole('option', { name: /all bills/i }));
|
|
394
|
+
|
|
395
|
+
mockBills.mockClear();
|
|
396
|
+
|
|
397
|
+
await user.click(screen.getByText('All bills'));
|
|
398
|
+
await user.click(screen.getByRole('option', { name: /pending payment/i }));
|
|
399
|
+
|
|
400
|
+
await waitFor(() => {
|
|
401
|
+
expect(mockBills).toHaveBeenCalledWith(expect.any(Number), 'POSTED', undefined);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
328
405
|
test('should keep data visible during subsequent loads', () => {
|
|
329
406
|
mockBills.mockImplementationOnce(() => ({
|
|
330
407
|
bills: mockBillsData,
|
package/src/index.ts
CHANGED
|
@@ -130,3 +130,8 @@ export const deleteLineItemConfirmationModal = getAsyncLifecycle(
|
|
|
130
130
|
() => import('./modal/delete-line-item-confirmation.modal'),
|
|
131
131
|
options,
|
|
132
132
|
);
|
|
133
|
+
|
|
134
|
+
export const finalizeBillConfirmationModal = getAsyncLifecycle(
|
|
135
|
+
() => import('./modal/finalize-bill-confirmation.modal'),
|
|
136
|
+
options,
|
|
137
|
+
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { Button, InlineLoading } from '@carbon/react';
|
|
3
|
-
import { Printer } from '@carbon/react/icons';
|
|
3
|
+
import { Add, 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';
|
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
ErrorState,
|
|
9
9
|
ExtensionSlot,
|
|
10
10
|
formatDate,
|
|
11
|
+
launchWorkspace2,
|
|
11
12
|
parseDate,
|
|
13
|
+
showModal,
|
|
12
14
|
showSnackbar,
|
|
13
15
|
useConfig,
|
|
14
16
|
usePatient,
|
|
@@ -20,6 +22,7 @@ import InvoiceTable from './invoice-table.component';
|
|
|
20
22
|
import Payments from './payments/payments.component';
|
|
21
23
|
import PrintReceipt from './printable-invoice/print-receipt.component';
|
|
22
24
|
import PrintableInvoice from './printable-invoice/printable-invoice.component';
|
|
25
|
+
import { BillStatus } from '../types';
|
|
23
26
|
import styles from './invoice.scss';
|
|
24
27
|
|
|
25
28
|
interface InvoiceDetailsProps {
|
|
@@ -52,6 +55,14 @@ const Invoice: React.FC = () => {
|
|
|
52
55
|
});
|
|
53
56
|
}, [bill, patient]);
|
|
54
57
|
|
|
58
|
+
const handleFinalizeBill = () => {
|
|
59
|
+
const dispose = showModal('finalize-bill-confirmation-modal', {
|
|
60
|
+
bill,
|
|
61
|
+
onMutate: mutate,
|
|
62
|
+
closeModal: () => dispose(),
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
55
66
|
const handlePrint = useReactToPrint({
|
|
56
67
|
contentRef: componentRef,
|
|
57
68
|
documentTitle: `Invoice ${bill?.receiptNumber} - ${patient?.name?.[0]?.given?.join(' ')} ${patient?.name?.[0].family}`,
|
|
@@ -112,27 +123,46 @@ const Invoice: React.FC = () => {
|
|
|
112
123
|
return (
|
|
113
124
|
<div className={styles.invoiceContainer}>
|
|
114
125
|
{patient && patientUuid && <ExtensionSlot name="patient-header-slot" state={{ patient, patientUuid }} />}
|
|
126
|
+
<div className={styles.actionsContainer}>
|
|
127
|
+
{isValidating && (
|
|
128
|
+
<span>
|
|
129
|
+
<InlineLoading status="active" />
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
{bill?.status === BillStatus.PENDING && (
|
|
133
|
+
<>
|
|
134
|
+
<Button
|
|
135
|
+
kind="ghost"
|
|
136
|
+
renderIcon={Add}
|
|
137
|
+
onClick={() =>
|
|
138
|
+
launchWorkspace2('billing-form-workspace', {
|
|
139
|
+
patientUuid,
|
|
140
|
+
billUuid: bill.uuid,
|
|
141
|
+
onMutate: mutate,
|
|
142
|
+
})
|
|
143
|
+
}>
|
|
144
|
+
{t('addItemsToBill', 'Add items to bill')}
|
|
145
|
+
</Button>
|
|
146
|
+
<Button kind="primary" onClick={handleFinalizeBill}>
|
|
147
|
+
{t('finalizeBill', 'Finalize bill')}
|
|
148
|
+
</Button>
|
|
149
|
+
</>
|
|
150
|
+
)}
|
|
151
|
+
<Button
|
|
152
|
+
disabled={isPrinting || isLoadingPatient || isLoadingBill}
|
|
153
|
+
onClick={handlePrint}
|
|
154
|
+
renderIcon={(props) => <Printer size={24} {...props} />}
|
|
155
|
+
iconDescription={t('printBill', 'Print bill')}>
|
|
156
|
+
{t('printBill', 'Print bill')}
|
|
157
|
+
</Button>
|
|
158
|
+
{bill && (bill.status === BillStatus.PAID || bill.tenderedAmount > 0) && <PrintReceipt billUuid={bill.uuid} />}
|
|
159
|
+
</div>
|
|
115
160
|
<div className={styles.detailsContainer}>
|
|
116
161
|
<section className={styles.details}>
|
|
117
162
|
{Object.entries(invoiceDetails).map(([key, val]) => (
|
|
118
163
|
<InvoiceDetails key={key} label={key} value={val} />
|
|
119
164
|
))}
|
|
120
165
|
</section>
|
|
121
|
-
<div className={styles.actionsContainer}>
|
|
122
|
-
{isValidating && (
|
|
123
|
-
<span>
|
|
124
|
-
<InlineLoading status="active" />
|
|
125
|
-
</span>
|
|
126
|
-
)}
|
|
127
|
-
<Button
|
|
128
|
-
disabled={isPrinting || isLoadingPatient || isLoadingBill}
|
|
129
|
-
onClick={handlePrint}
|
|
130
|
-
renderIcon={(props) => <Printer size={24} {...props} />}
|
|
131
|
-
iconDescription={t('printBill', 'Print bill')}>
|
|
132
|
-
{t('printBill', 'Print bill')}
|
|
133
|
-
</Button>
|
|
134
|
-
{(bill?.status === 'PAID' || bill?.tenderedAmount > 0) && <PrintReceipt billUuid={bill?.uuid} />}
|
|
135
|
-
</div>
|
|
136
166
|
</div>
|
|
137
167
|
|
|
138
168
|
<div className={styles.invoiceContent}>
|
package/src/invoice/invoice.scss
CHANGED
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
justify-content: center;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
.actionsContainer {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: flex-end;
|
|
24
|
+
gap: layout.$spacing-03;
|
|
25
|
+
padding: layout.$spacing-04 layout.$spacing-05;
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
.detailsContainer {
|
|
21
29
|
display: flex;
|
|
22
30
|
}
|
|
@@ -27,17 +35,10 @@
|
|
|
27
35
|
flex-flow: row wrap;
|
|
28
36
|
align-items: center;
|
|
29
37
|
justify-content: space-between;
|
|
30
|
-
margin: layout.$spacing-05;
|
|
38
|
+
margin: layout.$spacing-05 layout.$spacing-06;
|
|
31
39
|
row-gap: layout.$spacing-06;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
.actionsContainer {
|
|
35
|
-
display: flex;
|
|
36
|
-
align-items: center;
|
|
37
|
-
gap: layout.$spacing-03;
|
|
38
|
-
margin: layout.$spacing-05;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
42
|
.label {
|
|
42
43
|
@include type.type-style('body-compact-02');
|
|
43
44
|
color: colors.$gray-70;
|
|
@@ -2,8 +2,15 @@ import React from 'react';
|
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
4
|
import { useReactToPrint } from 'react-to-print';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getDefaultsFromConfigSchema,
|
|
7
|
+
launchWorkspace2,
|
|
8
|
+
showModal,
|
|
9
|
+
useConfig,
|
|
10
|
+
usePatient,
|
|
11
|
+
} from '@openmrs/esm-framework';
|
|
6
12
|
import { configSchema, type BillingConfig } from '../config-schema';
|
|
13
|
+
import { BillStatus } from '../types';
|
|
7
14
|
import { mockBill, mockPatient } from 'mocks/bills.mock';
|
|
8
15
|
import { useBill } from '../billing.resource';
|
|
9
16
|
import { usePaymentModes } from './payments/payment.resource';
|
|
@@ -15,6 +22,7 @@ const mockUseBill = jest.mocked(useBill);
|
|
|
15
22
|
const mockUsePatient = jest.mocked(usePatient);
|
|
16
23
|
const mockUsePaymentModes = jest.mocked(usePaymentModes);
|
|
17
24
|
const mockUseReactToPrint = jest.mocked(useReactToPrint);
|
|
25
|
+
const mockShowModal = jest.mocked(showModal);
|
|
18
26
|
|
|
19
27
|
jest.mock('../helpers/functions', () => ({
|
|
20
28
|
convertToCurrency: jest.fn((amount) => `USD ${amount}`),
|
|
@@ -62,7 +70,7 @@ describe('Invoice', () => {
|
|
|
62
70
|
const defaultBillData = {
|
|
63
71
|
...mockBill,
|
|
64
72
|
uuid: 'test-uuid',
|
|
65
|
-
status:
|
|
73
|
+
status: BillStatus.PENDING,
|
|
66
74
|
totalAmount: 1000,
|
|
67
75
|
tenderedAmount: 0,
|
|
68
76
|
receiptNumber: 'RCPT-001',
|
|
@@ -188,7 +196,7 @@ describe('Invoice', () => {
|
|
|
188
196
|
mockUseBill.mockReturnValue({
|
|
189
197
|
bill: {
|
|
190
198
|
...defaultBillData,
|
|
191
|
-
status:
|
|
199
|
+
status: BillStatus.PAID,
|
|
192
200
|
tenderedAmount: 1000,
|
|
193
201
|
},
|
|
194
202
|
isLoading: false,
|
|
@@ -207,7 +215,7 @@ describe('Invoice', () => {
|
|
|
207
215
|
mockUseBill.mockReturnValue({
|
|
208
216
|
bill: {
|
|
209
217
|
...defaultBillData,
|
|
210
|
-
status:
|
|
218
|
+
status: BillStatus.PENDING,
|
|
211
219
|
tenderedAmount: 500,
|
|
212
220
|
},
|
|
213
221
|
isLoading: false,
|
|
@@ -336,7 +344,7 @@ describe('Invoice', () => {
|
|
|
336
344
|
|
|
337
345
|
const updatedBill = {
|
|
338
346
|
...defaultBillData,
|
|
339
|
-
status:
|
|
347
|
+
status: BillStatus.PAID,
|
|
340
348
|
tenderedAmount: 1000,
|
|
341
349
|
};
|
|
342
350
|
|
|
@@ -456,7 +464,7 @@ describe('Invoice', () => {
|
|
|
456
464
|
mockUseBill.mockReturnValue({
|
|
457
465
|
bill: {
|
|
458
466
|
...defaultBillData,
|
|
459
|
-
status:
|
|
467
|
+
status: BillStatus.PENDING,
|
|
460
468
|
totalAmount: 1000,
|
|
461
469
|
tenderedAmount: 500, // Partial payment
|
|
462
470
|
},
|
|
@@ -485,7 +493,7 @@ describe('Invoice', () => {
|
|
|
485
493
|
mockUseBill.mockReturnValue({
|
|
486
494
|
bill: {
|
|
487
495
|
...defaultBillData,
|
|
488
|
-
status:
|
|
496
|
+
status: BillStatus.PENDING,
|
|
489
497
|
tenderedAmount: 0,
|
|
490
498
|
},
|
|
491
499
|
isLoading: false,
|
|
@@ -499,4 +507,117 @@ describe('Invoice', () => {
|
|
|
499
507
|
|
|
500
508
|
expect(screen.queryByTestId('mock-print-receipt')).not.toBeInTheDocument();
|
|
501
509
|
});
|
|
510
|
+
|
|
511
|
+
it('should show "Add items to bill" button for PENDING bills', async () => {
|
|
512
|
+
render(<Invoice />);
|
|
513
|
+
await waitForLoadingToFinish();
|
|
514
|
+
|
|
515
|
+
expect(screen.getByRole('button', { name: /add items to bill/i })).toBeInTheDocument();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('should not show "Add items to bill" button for PAID bills', async () => {
|
|
519
|
+
mockUseBill.mockReturnValue({
|
|
520
|
+
bill: {
|
|
521
|
+
...defaultBillData,
|
|
522
|
+
status: BillStatus.PAID,
|
|
523
|
+
tenderedAmount: 1000,
|
|
524
|
+
},
|
|
525
|
+
isLoading: false,
|
|
526
|
+
error: null,
|
|
527
|
+
isValidating: false,
|
|
528
|
+
mutate: jest.fn(),
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
render(<Invoice />);
|
|
532
|
+
await waitForLoadingToFinish();
|
|
533
|
+
|
|
534
|
+
expect(screen.queryByRole('button', { name: /add items to bill/i })).not.toBeInTheDocument();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('should show "Finalize bill" button for PENDING bills', async () => {
|
|
538
|
+
render(<Invoice />);
|
|
539
|
+
await waitForLoadingToFinish();
|
|
540
|
+
|
|
541
|
+
expect(screen.getByRole('button', { name: /finalize bill/i })).toBeInTheDocument();
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should not show "Finalize bill" button for POSTED bills', async () => {
|
|
545
|
+
mockUseBill.mockReturnValue({
|
|
546
|
+
bill: { ...defaultBillData, status: BillStatus.POSTED },
|
|
547
|
+
isLoading: false,
|
|
548
|
+
error: null,
|
|
549
|
+
isValidating: false,
|
|
550
|
+
mutate: jest.fn(),
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
render(<Invoice />);
|
|
554
|
+
await waitForLoadingToFinish();
|
|
555
|
+
|
|
556
|
+
expect(screen.queryByRole('button', { name: /finalize bill/i })).not.toBeInTheDocument();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('should not show "Finalize bill" button for PAID bills', async () => {
|
|
560
|
+
mockUseBill.mockReturnValue({
|
|
561
|
+
bill: { ...defaultBillData, status: BillStatus.PAID, tenderedAmount: 1000 },
|
|
562
|
+
isLoading: false,
|
|
563
|
+
error: null,
|
|
564
|
+
isValidating: false,
|
|
565
|
+
mutate: jest.fn(),
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
render(<Invoice />);
|
|
569
|
+
await waitForLoadingToFinish();
|
|
570
|
+
|
|
571
|
+
expect(screen.queryByRole('button', { name: /finalize bill/i })).not.toBeInTheDocument();
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('should open finalize confirmation modal when "Finalize bill" button is clicked', async () => {
|
|
575
|
+
const mockMutate = jest.fn();
|
|
576
|
+
const user = userEvent.setup();
|
|
577
|
+
|
|
578
|
+
mockUseBill.mockReturnValue({
|
|
579
|
+
bill: defaultBillData,
|
|
580
|
+
isLoading: false,
|
|
581
|
+
error: null,
|
|
582
|
+
isValidating: false,
|
|
583
|
+
mutate: mockMutate,
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
render(<Invoice />);
|
|
587
|
+
await waitForLoadingToFinish();
|
|
588
|
+
|
|
589
|
+
await user.click(screen.getByRole('button', { name: /finalize bill/i }));
|
|
590
|
+
|
|
591
|
+
expect(mockShowModal).toHaveBeenCalledWith('finalize-bill-confirmation-modal', {
|
|
592
|
+
bill: defaultBillData,
|
|
593
|
+
onMutate: mockMutate,
|
|
594
|
+
closeModal: expect.any(Function),
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('should launch workspace with billUuid when "Add items to bill" is clicked', async () => {
|
|
599
|
+
const mockMutate = jest.fn();
|
|
600
|
+
const mockLaunchWorkspace2 = jest.mocked(launchWorkspace2);
|
|
601
|
+
|
|
602
|
+
mockUseBill.mockReturnValue({
|
|
603
|
+
bill: defaultBillData,
|
|
604
|
+
isLoading: false,
|
|
605
|
+
error: null,
|
|
606
|
+
isValidating: false,
|
|
607
|
+
mutate: mockMutate,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const user = userEvent.setup();
|
|
611
|
+
render(<Invoice />);
|
|
612
|
+
await waitForLoadingToFinish();
|
|
613
|
+
|
|
614
|
+
const addItemsButton = screen.getByRole('button', { name: /add items to bill/i });
|
|
615
|
+
await user.click(addItemsButton);
|
|
616
|
+
|
|
617
|
+
expect(mockLaunchWorkspace2).toHaveBeenCalledWith('billing-form-workspace', {
|
|
618
|
+
patientUuid: 'patientUuid',
|
|
619
|
+
billUuid: 'test-uuid',
|
|
620
|
+
onMutate: mockMutate,
|
|
621
|
+
});
|
|
622
|
+
});
|
|
502
623
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useCallback } from 'react';
|
|
2
2
|
import { Layer, OverflowMenu, OverflowMenuItem } from '@carbon/react';
|
|
3
3
|
import { getCoreTranslation, isDesktop, showModal, useLayoutType } from '@openmrs/esm-framework';
|
|
4
|
-
import { type LineItem, type MappedBill } from '../types';
|
|
4
|
+
import { BillStatus, type LineItem, type MappedBill } from '../types';
|
|
5
5
|
import styles from './line-item-action-menu.scss';
|
|
6
6
|
|
|
7
7
|
type LineItemActionMenuProps = {
|
|
@@ -30,7 +30,7 @@ const LineItemActionMenu: React.FC<LineItemActionMenuProps> = ({ bill, item, onM
|
|
|
30
30
|
});
|
|
31
31
|
}, [item, onMutate]);
|
|
32
32
|
|
|
33
|
-
const isPending = bill?.status ===
|
|
33
|
+
const isPending = bill?.status === BillStatus.PENDING;
|
|
34
34
|
|
|
35
35
|
return (
|
|
36
36
|
<Layer>
|
|
@@ -11,7 +11,7 @@ import PaymentHistory from './payment-history/payment-history.component';
|
|
|
11
11
|
import { processBillPayment } from '../../billing.resource';
|
|
12
12
|
import { updateBillVisitAttribute } from './payment.resource';
|
|
13
13
|
import { convertToCurrency } from '../../helpers';
|
|
14
|
-
import type
|
|
14
|
+
import { BillStatus, type MappedBill } from '../../types';
|
|
15
15
|
import styles from './payments.scss';
|
|
16
16
|
|
|
17
17
|
type PaymentProps = {
|
|
@@ -113,7 +113,7 @@ const Payments: React.FC<PaymentProps> = ({ bill, mutate }) => {
|
|
|
113
113
|
</CardHeader>
|
|
114
114
|
<div>
|
|
115
115
|
<PaymentHistory bill={bill} />
|
|
116
|
-
<PaymentForm disablePayment={amountDue <= 0} />
|
|
116
|
+
<PaymentForm disablePayment={amountDue <= 0 || bill.status === BillStatus.PENDING} />
|
|
117
117
|
</div>
|
|
118
118
|
</div>
|
|
119
119
|
<div className={styles.divider} />
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type VisitReturnType,
|
|
10
10
|
} from '@openmrs/esm-framework';
|
|
11
11
|
import { useBillableServices } from '../../billable-services/billable-service.resource';
|
|
12
|
-
import { type MappedBill } from '../../types';
|
|
12
|
+
import { BillStatus, type MappedBill } from '../../types';
|
|
13
13
|
import { configSchema, type BillingConfig } from '../../config-schema';
|
|
14
14
|
import { usePaymentModes } from './payment.resource';
|
|
15
15
|
import Payments from './payments.component';
|
|
@@ -102,7 +102,7 @@ describe('Payments', () => {
|
|
|
102
102
|
},
|
|
103
103
|
],
|
|
104
104
|
receiptNumber: '12345',
|
|
105
|
-
status:
|
|
105
|
+
status: BillStatus.PAID,
|
|
106
106
|
identifier: 'invoice-123',
|
|
107
107
|
dateCreated: '2023-09-01T12:00:00Z',
|
|
108
108
|
lineItems: [],
|
|
@@ -224,6 +224,34 @@ describe('Payments', () => {
|
|
|
224
224
|
expect(screen.queryByPlaceholderText(/enter amount/i)).not.toBeInTheDocument();
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
+
it('should not show payment form when bill is in PENDING state', () => {
|
|
228
|
+
const pendingBill: MappedBill = {
|
|
229
|
+
...mockBill,
|
|
230
|
+
status: BillStatus.PENDING,
|
|
231
|
+
totalAmount: 100,
|
|
232
|
+
tenderedAmount: 0,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
render(<Payments bill={pendingBill} mutate={mockMutate} />);
|
|
236
|
+
|
|
237
|
+
expect(screen.queryByPlaceholderText(/enter amount/i)).not.toBeInTheDocument();
|
|
238
|
+
expect(screen.queryByText(/select payment method/i)).not.toBeInTheDocument();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should show payment form when bill is in POSTED state', () => {
|
|
242
|
+
const postedBill: MappedBill = {
|
|
243
|
+
...mockBill,
|
|
244
|
+
status: BillStatus.POSTED,
|
|
245
|
+
totalAmount: 100,
|
|
246
|
+
tenderedAmount: 0,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
render(<Payments bill={postedBill} mutate={mockMutate} />);
|
|
250
|
+
|
|
251
|
+
expect(screen.getByPlaceholderText(/enter amount/i)).toBeInTheDocument();
|
|
252
|
+
expect(screen.getByText(/select payment method/i)).toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
|
|
227
255
|
it('should return null when bill is not provided', () => {
|
|
228
256
|
const { container } = render(<Payments bill={null} mutate={mockMutate} />);
|
|
229
257
|
expect(container).toBeEmptyDOMElement();
|
|
@@ -232,6 +260,7 @@ describe('Payments', () => {
|
|
|
232
260
|
it('should show payment form when there is amount due', () => {
|
|
233
261
|
const billWithAmountDue: MappedBill = {
|
|
234
262
|
...mockBill,
|
|
263
|
+
status: BillStatus.POSTED,
|
|
235
264
|
totalAmount: 100,
|
|
236
265
|
tenderedAmount: 0,
|
|
237
266
|
lineItems: [],
|