@openmrs/esm-billing-app 1.1.1 → 1.1.2-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/cache/53e233a916ffe7d2-meta.json +1 -0
- package/.turbo/cache/53e233a916ffe7d2.tar.zst +0 -0
- package/.turbo/turbo-build.log +44 -0
- package/__mocks__/bills.mock.ts +6 -5
- package/dist/1119.js +1 -1
- package/dist/1197.js +1 -1
- package/dist/1435.js +1 -0
- package/dist/1435.js.map +1 -0
- package/dist/1807.js +1 -0
- package/dist/1807.js.map +1 -0
- package/dist/2146.js +1 -1
- package/dist/2177.js +1 -1
- package/dist/2177.js.map +1 -1
- package/dist/2690.js +1 -1
- package/dist/2704.js +1 -0
- package/dist/2704.js.map +1 -0
- package/dist/3002.js +1 -0
- package/dist/3002.js.map +1 -0
- package/dist/3041.js +1 -1
- package/dist/3041.js.map +1 -1
- package/dist/3099.js +1 -1
- package/dist/3184.js +1 -1
- package/dist/3184.js.map +1 -1
- package/dist/3584.js +1 -1
- package/dist/4055.js +1 -1
- package/dist/4132.js +1 -1
- package/dist/4225.js +1 -1
- package/dist/4225.js.map +1 -1
- package/dist/4300.js +1 -1
- package/dist/4335.js +1 -1
- package/dist/439.js +1 -1
- package/dist/4618.js +1 -1
- package/dist/4652.js +1 -1
- package/dist/4944.js +1 -1
- package/dist/5173.js +1 -1
- package/dist/5241.js +1 -1
- package/dist/5422.js +1 -1
- package/dist/5422.js.map +1 -1
- package/dist/5442.js +1 -1
- package/dist/5661.js +1 -1
- package/dist/6022.js +1 -1
- package/dist/6404.js +1 -0
- package/dist/6404.js.map +1 -0
- package/dist/6468.js +1 -1
- package/dist/6540.js +1 -1
- package/dist/6540.js.map +1 -1
- package/dist/6589.js +1 -1
- package/dist/6606.js +1 -1
- package/dist/6606.js.map +1 -1
- package/dist/6679.js +1 -1
- package/dist/6792.js +1 -0
- package/dist/6792.js.map +1 -0
- package/dist/6840.js +1 -1
- package/dist/6859.js +1 -1
- package/dist/7097.js +1 -1
- package/dist/7159.js +1 -1
- package/dist/723.js +1 -1
- package/dist/7255.js +1 -1
- package/dist/7255.js.map +1 -1
- package/dist/7617.js +1 -1
- package/dist/795.js +1 -1
- package/dist/8163.js +1 -1
- package/dist/8341.js +2 -0
- package/dist/{1907.js.LICENSE.txt → 8341.js.LICENSE.txt} +0 -15
- package/dist/8341.js.map +1 -0
- package/dist/8349.js +1 -1
- package/dist/8371.js +1 -1
- package/dist/8421.js +1 -0
- package/dist/8421.js.map +1 -0
- package/dist/8618.js +1 -1
- package/dist/890.js +1 -1
- package/dist/9214.js +1 -1
- package/dist/9538.js +1 -1
- package/dist/9569.js +1 -1
- package/dist/961.js +1 -1
- package/dist/961.js.map +1 -1
- package/dist/986.js +1 -1
- package/dist/9879.js +1 -1
- package/dist/9895.js +1 -1
- package/dist/9900.js +1 -1
- package/dist/9913.js +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.LICENSE.txt +0 -15
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-billing-app.js +1 -1
- package/dist/openmrs-esm-billing-app.js.buildmanifest.json +284 -259
- package/dist/openmrs-esm-billing-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/e2e/commands/patient-operations.ts +1 -1
- package/e2e/pages/billing-dashboard-page.ts +3 -1
- package/e2e/pages/billing-form-page.ts +5 -0
- package/e2e/pages/invoice-page.ts +10 -0
- package/e2e/specs/billing-dashboard.spec.ts +126 -3
- package/e2e/specs/billing-patient-chart.spec.ts +95 -9
- package/package.json +3 -6
- package/src/bill-history/bill-action-menu.component.tsx +41 -0
- package/src/bill-history/bill-action-menu.scss +3 -0
- package/src/bill-history/bill-history.component.tsx +15 -5
- package/src/bill-history/bill-history.scss +0 -1
- package/src/bill-history/bill-history.test.tsx +78 -1
- package/src/bill-item-actions/edit-bill-item.modal.tsx +1 -1
- package/src/bill-item-actions/edit-bill-item.test.tsx +40 -0
- package/src/billable-services/bill-waiver/bill-waiver.component.tsx +3 -1
- package/src/billing-dashboard/billing-dashboard.component.tsx +3 -16
- package/src/billing-form/billing-checkin-form.component.tsx +116 -57
- package/src/billing-form/billing-checkin-form.scss +26 -2
- package/src/billing-form/billing-checkin-form.test.tsx +51 -1
- package/src/billing-form/billing-form.resource.test.ts +87 -0
- package/src/billing-form/billing-form.resource.ts +33 -0
- package/src/billing-form/billing-form.scss +54 -7
- package/src/billing-form/billing-form.test.tsx +547 -0
- package/src/billing-form/billing-form.workspace.tsx +150 -45
- package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +25 -2
- package/src/billing-form/visit-attributes/visit-attributes-form.scss +29 -0
- package/src/billing-header/billing-header.component.tsx +1 -34
- package/src/billing-header/billing-header.scss +0 -50
- package/src/billing.resource.test.ts +11 -11
- package/src/billing.resource.ts +42 -12
- package/src/bills-table/bills-table.component.tsx +16 -12
- package/src/bills-table/bills-table.test.tsx +84 -7
- package/src/index.ts +5 -0
- package/src/invoice/invoice.component.tsx +46 -16
- package/src/invoice/invoice.scss +9 -8
- package/src/invoice/invoice.test.tsx +128 -7
- package/src/invoice/line-item-action-menu.component.tsx +2 -2
- package/src/invoice/payments/payments.component.tsx +2 -2
- package/src/invoice/payments/payments.test.tsx +31 -2
- package/src/metrics-cards/metrics.resource.ts +3 -4
- package/src/modal/finalize-bill-confirmation.modal.test.tsx +209 -0
- package/src/modal/finalize-bill-confirmation.modal.tsx +86 -0
- package/src/modal/require-payment.modal.tsx +2 -1
- package/src/routes.json +4 -0
- package/src/types/index.ts +10 -1
- package/tools/setup-tests.ts +7 -6
- package/translations/am.json +28 -0
- package/translations/ar.json +28 -0
- package/translations/ar_SY.json +28 -0
- package/translations/bn.json +28 -0
- package/translations/cs.json +28 -0
- package/translations/de.json +266 -238
- package/translations/en.json +29 -0
- package/translations/en_US.json +28 -0
- package/translations/es.json +28 -0
- package/translations/es_MX.json +28 -0
- package/translations/fr.json +28 -0
- package/translations/he.json +28 -0
- package/translations/hi.json +28 -0
- package/translations/hi_IN.json +28 -0
- package/translations/id.json +28 -0
- package/translations/it.json +28 -0
- package/translations/ka.json +28 -0
- package/translations/km.json +28 -0
- package/translations/ku.json +28 -0
- package/translations/ky.json +28 -0
- package/translations/lg.json +28 -0
- package/translations/ne.json +28 -0
- package/translations/pl.json +28 -0
- package/translations/pt.json +28 -0
- package/translations/pt_BR.json +28 -0
- package/translations/qu.json +28 -0
- package/translations/ro_RO.json +28 -0
- package/translations/ru_RU.json +28 -0
- package/translations/si.json +28 -0
- package/translations/sq.json +28 -0
- package/translations/sw.json +28 -0
- package/translations/sw_KE.json +28 -0
- package/translations/tr.json +28 -0
- package/translations/tr_TR.json +28 -0
- package/translations/uk.json +28 -0
- package/translations/uz.json +28 -0
- package/translations/uz@Latn.json +28 -0
- package/translations/uz_UZ.json +28 -0
- package/translations/vi.json +28 -0
- package/translations/zh.json +268 -240
- package/translations/zh_CN.json +30 -2
- package/translations/zh_TW.json +28 -0
- package/turbo.json +29 -0
- package/dist/1537.js +0 -1
- package/dist/1537.js.map +0 -1
- package/dist/1907.js +0 -2
- package/dist/1907.js.map +0 -1
- package/dist/1981.js +0 -1
- package/dist/1981.js.map +0 -1
- package/dist/2820.js +0 -1
- package/dist/2820.js.map +0 -1
- package/dist/8025.js +0 -1
- package/dist/8025.js.map +0 -1
- package/dist/9727.js +0 -2
- package/dist/9727.js.LICENSE.txt +0 -14
- package/dist/9727.js.map +0 -1
- package/dist/9756.js +0 -1
- package/dist/9756.js.map +0 -1
- package/src/hooks/selectedDateContext.ts +0 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import {
|
|
4
4
|
Button,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
EmptyCardIllustration,
|
|
26
26
|
ErrorState,
|
|
27
27
|
formatDate,
|
|
28
|
+
getCoreTranslation,
|
|
28
29
|
launchWorkspace2,
|
|
29
30
|
parseDate,
|
|
30
31
|
useConfig,
|
|
@@ -33,7 +34,9 @@ import {
|
|
|
33
34
|
} from '@openmrs/esm-framework';
|
|
34
35
|
import { useBills } from '../billing.resource';
|
|
35
36
|
import { convertToCurrency } from '../helpers';
|
|
37
|
+
import BillActionMenu from './bill-action-menu.component';
|
|
36
38
|
import InvoiceTable from '../invoice/invoice-table.component';
|
|
39
|
+
import { BillStatus } from '../types';
|
|
37
40
|
import styles from './bill-history.scss';
|
|
38
41
|
|
|
39
42
|
interface BillHistoryProps {
|
|
@@ -47,6 +50,7 @@ const BillHistory: React.FC<BillHistoryProps> = ({ patientUuid }) => {
|
|
|
47
50
|
const { pageSize, defaultCurrency } = useConfig();
|
|
48
51
|
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
|
|
49
52
|
const { pageSizes } = usePaginationInfo(pageSize, bills?.length, currentPage, results?.length);
|
|
53
|
+
const billsByUuid = useMemo(() => new Map(bills?.map((bill) => [bill.uuid, bill])), [bills]);
|
|
50
54
|
|
|
51
55
|
const headerData = [
|
|
52
56
|
{
|
|
@@ -116,7 +120,7 @@ const BillHistory: React.FC<BillHistoryProps> = ({ patientUuid }) => {
|
|
|
116
120
|
})
|
|
117
121
|
}
|
|
118
122
|
kind="ghost">
|
|
119
|
-
{t('
|
|
123
|
+
{t('createBill', 'Create bill')}
|
|
120
124
|
</Button>
|
|
121
125
|
</Tile>
|
|
122
126
|
</Layer>
|
|
@@ -140,7 +144,7 @@ const BillHistory: React.FC<BillHistoryProps> = ({ patientUuid }) => {
|
|
|
140
144
|
})
|
|
141
145
|
}
|
|
142
146
|
renderIcon={Add}>
|
|
143
|
-
{t('
|
|
147
|
+
{t('createBill', 'Create bill')}
|
|
144
148
|
</Button>
|
|
145
149
|
</CardHeader>
|
|
146
150
|
<div className={styles.billHistoryContainer}>
|
|
@@ -168,11 +172,12 @@ const BillHistory: React.FC<BillHistoryProps> = ({ patientUuid }) => {
|
|
|
168
172
|
{header.header}
|
|
169
173
|
</TableHeader>
|
|
170
174
|
))}
|
|
175
|
+
<TableHeader aria-label={getCoreTranslation('actions')} />
|
|
171
176
|
</TableRow>
|
|
172
177
|
</TableHead>
|
|
173
178
|
<TableBody>
|
|
174
179
|
{rows.map((row, i) => {
|
|
175
|
-
const currentBill =
|
|
180
|
+
const currentBill = billsByUuid.get(row.id);
|
|
176
181
|
|
|
177
182
|
return (
|
|
178
183
|
<React.Fragment key={row.id}>
|
|
@@ -182,9 +187,14 @@ const BillHistory: React.FC<BillHistoryProps> = ({ patientUuid }) => {
|
|
|
182
187
|
{cell.value}
|
|
183
188
|
</TableCell>
|
|
184
189
|
))}
|
|
190
|
+
<TableCell className="cds--table-column-menu">
|
|
191
|
+
{currentBill?.status === BillStatus.PENDING && (
|
|
192
|
+
<BillActionMenu bill={currentBill} patientUuid={patientUuid} onMutate={mutate} />
|
|
193
|
+
)}
|
|
194
|
+
</TableCell>
|
|
185
195
|
</TableExpandRow>
|
|
186
196
|
{row.isExpanded ? (
|
|
187
|
-
<TableExpandedRow colSpan={headers.length +
|
|
197
|
+
<TableExpandedRow colSpan={headers.length + 2}>
|
|
188
198
|
<div className={styles.container} key={i}>
|
|
189
199
|
<InvoiceTable bill={currentBill} onMutate={mutate} isLoadingBill={isValidating} />
|
|
190
200
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { render, screen } from '@testing-library/react';
|
|
4
|
-
import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
|
|
4
|
+
import { getDefaultsFromConfigSchema, launchWorkspace2, useConfig } from '@openmrs/esm-framework';
|
|
5
5
|
import { configSchema, type BillingConfig } from '../config-schema';
|
|
6
6
|
import { useBills } from '../billing.resource';
|
|
7
7
|
import BillHistory from './bill-history.component';
|
|
@@ -120,4 +120,81 @@ describe('BillHistory', () => {
|
|
|
120
120
|
const emptyState = screen.getByText(/There are no bills to display./);
|
|
121
121
|
expect(emptyState).toBeInTheDocument();
|
|
122
122
|
});
|
|
123
|
+
|
|
124
|
+
test('should show overflow menu with "Add items to bill" for PENDING bills', async () => {
|
|
125
|
+
const user = userEvent.setup();
|
|
126
|
+
const pendingBill = {
|
|
127
|
+
...mockBillData[0],
|
|
128
|
+
status: 'PENDING',
|
|
129
|
+
dateCreated: '2024-01-01',
|
|
130
|
+
receiptNumber: 'REC-001',
|
|
131
|
+
lineItems: [{ uuid: 'item-1', item: 'Test', quantity: 1, price: 100, paymentStatus: 'PENDING' }],
|
|
132
|
+
};
|
|
133
|
+
mockUseBills.mockReturnValueOnce({
|
|
134
|
+
isLoading: false,
|
|
135
|
+
isValidating: false,
|
|
136
|
+
error: null,
|
|
137
|
+
bills: [pendingBill] as any,
|
|
138
|
+
mutate: jest.fn(),
|
|
139
|
+
});
|
|
140
|
+
render(<BillHistory {...testProps} />);
|
|
141
|
+
|
|
142
|
+
const overflowMenu = screen.getByTestId('action-menu-1');
|
|
143
|
+
await user.click(overflowMenu);
|
|
144
|
+
|
|
145
|
+
expect(screen.getByText(/add items to bill/i)).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('should not show overflow menu for PAID bills', () => {
|
|
149
|
+
const paidBill = {
|
|
150
|
+
...mockBillData[0],
|
|
151
|
+
status: 'PAID',
|
|
152
|
+
dateCreated: '2024-01-01',
|
|
153
|
+
receiptNumber: 'REC-001',
|
|
154
|
+
lineItems: [{ uuid: 'item-1', item: 'Test', quantity: 1, price: 100, paymentStatus: 'PAID' }],
|
|
155
|
+
};
|
|
156
|
+
mockUseBills.mockReturnValueOnce({
|
|
157
|
+
isLoading: false,
|
|
158
|
+
isValidating: false,
|
|
159
|
+
error: null,
|
|
160
|
+
bills: [paidBill] as any,
|
|
161
|
+
mutate: jest.fn(),
|
|
162
|
+
});
|
|
163
|
+
render(<BillHistory {...testProps} />);
|
|
164
|
+
|
|
165
|
+
expect(screen.queryByTestId('action-menu-1')).not.toBeInTheDocument();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('should launch workspace with billUuid when "Add items to bill" is clicked', async () => {
|
|
169
|
+
const user = userEvent.setup();
|
|
170
|
+
const mockMutate = jest.fn();
|
|
171
|
+
const mockLaunchWorkspace2 = jest.mocked(launchWorkspace2);
|
|
172
|
+
const pendingBill = {
|
|
173
|
+
...mockBillData[0],
|
|
174
|
+
status: 'PENDING',
|
|
175
|
+
dateCreated: '2024-01-01',
|
|
176
|
+
receiptNumber: 'REC-001',
|
|
177
|
+
lineItems: [{ uuid: 'item-1', item: 'Test', quantity: 1, price: 100, paymentStatus: 'PENDING' }],
|
|
178
|
+
};
|
|
179
|
+
mockUseBills.mockReturnValueOnce({
|
|
180
|
+
isLoading: false,
|
|
181
|
+
isValidating: false,
|
|
182
|
+
error: null,
|
|
183
|
+
bills: [pendingBill] as any,
|
|
184
|
+
mutate: mockMutate,
|
|
185
|
+
});
|
|
186
|
+
render(<BillHistory {...testProps} />);
|
|
187
|
+
|
|
188
|
+
const overflowMenu = screen.getByTestId('action-menu-1');
|
|
189
|
+
await user.click(overflowMenu);
|
|
190
|
+
|
|
191
|
+
const addItemsMenuItem = screen.getByText(/add items to bill/i);
|
|
192
|
+
await user.click(addItemsMenuItem);
|
|
193
|
+
|
|
194
|
+
expect(mockLaunchWorkspace2).toHaveBeenCalledWith('billing-form-workspace', {
|
|
195
|
+
patientUuid: 'some-uuid',
|
|
196
|
+
billUuid: '1',
|
|
197
|
+
onMutate: mockMutate,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
123
200
|
});
|
|
@@ -55,7 +55,7 @@ const EditBillLineItemModal: React.FC<EditBillLineItemModalProps> = ({ bill, clo
|
|
|
55
55
|
required_error: t('priceIsRequired', 'Price is required'),
|
|
56
56
|
invalid_type_error: t('priceMustBeNumber', 'Price must be a valid number'),
|
|
57
57
|
})
|
|
58
|
-
.
|
|
58
|
+
.min(0, t('priceMustBeNonNegative', 'Price must be 0 or greater')),
|
|
59
59
|
}),
|
|
60
60
|
[t],
|
|
61
61
|
);
|
|
@@ -334,6 +334,46 @@ describe('EditBillItem', () => {
|
|
|
334
334
|
});
|
|
335
335
|
});
|
|
336
336
|
|
|
337
|
+
test('allows updating the quantity of a zero-price (free) service while keeping price at zero', async () => {
|
|
338
|
+
const user = userEvent.setup();
|
|
339
|
+
mockUpdateBillItems.mockResolvedValueOnce({} as FetchResponse<any>);
|
|
340
|
+
|
|
341
|
+
const freeServiceItem = { ...mockItem, price: 0 };
|
|
342
|
+
const billWithFreeItem: MappedBill = {
|
|
343
|
+
...mockBill,
|
|
344
|
+
lineItems: [{ ...mockBill.lineItems[0], price: 0 }],
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
render(
|
|
348
|
+
<EditBillLineItemModal
|
|
349
|
+
bill={billWithFreeItem}
|
|
350
|
+
item={freeServiceItem}
|
|
351
|
+
closeModal={mockCloseModal}
|
|
352
|
+
onMutate={mockOnMutate}
|
|
353
|
+
/>,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
expect(screen.getByLabelText(/unit price/i)).toHaveValue('0');
|
|
357
|
+
|
|
358
|
+
const quantityInput = screen.getByRole('spinbutton', { name: /quantity/i });
|
|
359
|
+
await user.clear(quantityInput);
|
|
360
|
+
await user.type(quantityInput, '5');
|
|
361
|
+
|
|
362
|
+
expect(screen.getByText(/total/i)).toHaveTextContent(/0/);
|
|
363
|
+
|
|
364
|
+
await user.click(screen.getByText(/save/i));
|
|
365
|
+
|
|
366
|
+
await waitFor(() => {
|
|
367
|
+
expect(mockUpdateBillItems).toHaveBeenCalled();
|
|
368
|
+
const payload = mockUpdateBillItems.mock.calls[0][0];
|
|
369
|
+
const updatedItem = payload.lineItems.find((li) => li.uuid === freeServiceItem.uuid);
|
|
370
|
+
expect(updatedItem?.quantity).toBe(5);
|
|
371
|
+
expect(updatedItem?.price).toBe(0);
|
|
372
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'success' }));
|
|
373
|
+
expect(mockCloseModal).toHaveBeenCalled();
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
337
377
|
test('shows validation error when quantity field is left empty', async () => {
|
|
338
378
|
const user = userEvent.setup();
|
|
339
379
|
render(
|
|
@@ -2,13 +2,15 @@ import React, { useState } from 'react';
|
|
|
2
2
|
import { ExtensionSlot, UserHasAccess } from '@openmrs/esm-framework';
|
|
3
3
|
import { useBills } from '../../billing.resource';
|
|
4
4
|
import PatientBills from './patient-bills.component';
|
|
5
|
+
import { BillStatus } from '../../types';
|
|
5
6
|
import styles from './bill-waiver.scss';
|
|
6
7
|
|
|
7
8
|
const BillWaiver: React.FC = () => {
|
|
8
9
|
const [patientUuid, setPatientUuid] = useState<string>('');
|
|
9
10
|
const { bills } = useBills(patientUuid);
|
|
10
11
|
|
|
11
|
-
const filterBills =
|
|
12
|
+
const filterBills =
|
|
13
|
+
bills.filter((bill) => bill?.status !== BillStatus.PAID && patientUuid === bill.patientUuid) ?? [];
|
|
12
14
|
|
|
13
15
|
return (
|
|
14
16
|
<UserHasAccess privilege="coreapps.systemAdministration">
|
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { useParams } from 'react-router-dom';
|
|
4
|
-
import dayjs from 'dayjs';
|
|
5
3
|
import BillingHeader from '../billing-header/billing-header.component';
|
|
6
4
|
import BillsTable from '../bills-table/bills-table.component';
|
|
7
|
-
import SelectedDateContext from '../hooks/selectedDateContext';
|
|
8
|
-
import { omrsDateFormat } from '../constants';
|
|
9
5
|
import styles from './billing-dashboard.scss';
|
|
10
6
|
|
|
11
7
|
export function BillingDashboard() {
|
|
12
8
|
const { t } = useTranslation();
|
|
13
|
-
const [selectedDate, setSelectedDate] = useState<string>(dayjs().startOf('day').format(omrsDateFormat));
|
|
14
|
-
|
|
15
|
-
const params = useParams();
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
if (params.date) {
|
|
19
|
-
setSelectedDate(dayjs(params.date).startOf('day').format(omrsDateFormat));
|
|
20
|
-
}
|
|
21
|
-
}, [params.date]);
|
|
22
9
|
|
|
23
10
|
return (
|
|
24
|
-
|
|
11
|
+
<>
|
|
25
12
|
<BillingHeader title={t('home', 'Home')} />
|
|
26
13
|
{/**
|
|
27
14
|
*
|
|
@@ -32,6 +19,6 @@ export function BillingDashboard() {
|
|
|
32
19
|
<section className={styles.billsTableContainer}>
|
|
33
20
|
<BillsTable />
|
|
34
21
|
</section>
|
|
35
|
-
|
|
22
|
+
</>
|
|
36
23
|
);
|
|
37
24
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import React, { useCallback, useState } from 'react';
|
|
2
|
-
import { Dropdown, InlineLoading, InlineNotification } from '@carbon/react';
|
|
1
|
+
import React, { useCallback, useState, useMemo, useEffect, useRef } from 'react';
|
|
3
2
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { Dropdown, InlineLoading, InlineNotification } from '@carbon/react';
|
|
4
|
+
import { showSnackbar, getCoreTranslation, useConfig } from '@openmrs/esm-framework';
|
|
5
|
+
import { useCashPoint, useBillableItems, createPatientBill, useLastVisitInfo } from './billing-form.resource';
|
|
6
6
|
import VisitAttributesForm from './visit-attributes/visit-attributes-form.component';
|
|
7
|
+
import { BillStatus } from '../types';
|
|
7
8
|
import styles from './billing-checkin-form.scss';
|
|
8
9
|
|
|
9
|
-
const PENDING_PAYMENT_STATUS = 'PENDING';
|
|
10
|
-
|
|
11
10
|
type BillingCheckInFormProps = {
|
|
12
11
|
patientUuid: string;
|
|
13
12
|
setExtraVisitInfo: (state) => void;
|
|
@@ -15,11 +14,53 @@ type BillingCheckInFormProps = {
|
|
|
15
14
|
|
|
16
15
|
const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, setExtraVisitInfo }) => {
|
|
17
16
|
const { t } = useTranslation();
|
|
17
|
+
const { categoryConcepts } = useConfig();
|
|
18
|
+
|
|
19
|
+
const { lastVisitInfo, isLoading: isLoadingLastVisitInfo, error: lastVisitError } = useLastVisitInfo(patientUuid);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (lastVisitError) {
|
|
23
|
+
showSnackbar({
|
|
24
|
+
title: t('lastVisitError', 'Last visit error'),
|
|
25
|
+
subtitle: t('errorLoadingLastVisit', 'An error occurred while loading the last visit'),
|
|
26
|
+
kind: 'error',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}, [lastVisitError, t]);
|
|
30
|
+
|
|
18
31
|
const { cashPoints, isLoading: isLoadingCashPoints, error: cashError } = useCashPoint();
|
|
19
32
|
const { lineItems, isLoading: isLoadingLineItems, error: lineError } = useBillableItems();
|
|
33
|
+
|
|
20
34
|
const [attributes, setAttributes] = useState([]);
|
|
21
|
-
const [paymentMethod, setPaymentMethod] = useState<
|
|
22
|
-
|
|
35
|
+
const [paymentMethod, setPaymentMethod] = useState<string | null>(null);
|
|
36
|
+
const [selectedBillableItem, setSelectedBillableItem] = useState<any | null>(null);
|
|
37
|
+
|
|
38
|
+
const attributesRef = useRef(attributes);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
attributesRef.current = attributes;
|
|
41
|
+
}, [attributes]);
|
|
42
|
+
|
|
43
|
+
const isNonPaying = useMemo(() => {
|
|
44
|
+
if (!categoryConcepts?.nonPayingDetails || !attributes?.length) return false;
|
|
45
|
+
return attributes.some((attr) => attr.value === categoryConcepts.nonPayingDetails);
|
|
46
|
+
}, [attributes, categoryConcepts]);
|
|
47
|
+
|
|
48
|
+
const lineList = useMemo(() => {
|
|
49
|
+
if (isNonPaying) return [];
|
|
50
|
+
if (!paymentMethod || !lineItems?.length) return [];
|
|
51
|
+
|
|
52
|
+
return lineItems.filter((e) => e.servicePrices.some((p) => p.paymentMode?.uuid === paymentMethod));
|
|
53
|
+
}, [lineItems, paymentMethod, isNonPaying]);
|
|
54
|
+
|
|
55
|
+
// reset bill and selection when payment changes or on switching to non-paying
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setExtraVisitInfo({
|
|
58
|
+
createBillPayload: null,
|
|
59
|
+
handleCreateExtraVisitInfo: null,
|
|
60
|
+
attributes: attributesRef.current,
|
|
61
|
+
});
|
|
62
|
+
setSelectedBillableItem(null);
|
|
63
|
+
}, [paymentMethod, isNonPaying, setExtraVisitInfo]);
|
|
23
64
|
|
|
24
65
|
const handleCreateExtraVisitInfo = useCallback(
|
|
25
66
|
async (createBillPayload) => {
|
|
@@ -41,38 +82,42 @@ const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, se
|
|
|
41
82
|
[t],
|
|
42
83
|
);
|
|
43
84
|
|
|
44
|
-
const handleBillingService = (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// should default to first price if check returns empty. todo - update backend to return default price
|
|
49
|
-
const priceForPaymentMode =
|
|
50
|
-
selectedItem.servicePrices.find((p) => p.paymentMode?.uuid === paymentMethod) || selectedItem?.servicePrices[0];
|
|
51
|
-
|
|
52
|
-
const createBillPayload = {
|
|
53
|
-
lineItems: [
|
|
54
|
-
{
|
|
55
|
-
billableService: itemUuid,
|
|
56
|
-
quantity: 1,
|
|
57
|
-
price: priceForPaymentMode ? priceForPaymentMode.price : '0.000',
|
|
58
|
-
priceName: 'Default',
|
|
59
|
-
priceUuid: priceForPaymentMode ? priceForPaymentMode.uuid : '',
|
|
60
|
-
lineItemOrder: 0,
|
|
61
|
-
paymentStatus: PENDING_PAYMENT_STATUS,
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
|
-
cashPoint: cashPointUuid,
|
|
65
|
-
patient: patientUuid,
|
|
66
|
-
status: PENDING_PAYMENT_STATUS,
|
|
67
|
-
payments: [],
|
|
68
|
-
};
|
|
85
|
+
const handleBillingService = useCallback(
|
|
86
|
+
({ selectedItem }: { selectedItem }) => {
|
|
87
|
+
setSelectedBillableItem(selectedItem);
|
|
69
88
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
const cashPointUuid = cashPoints?.[0]?.uuid ?? '';
|
|
90
|
+
const itemUuid = selectedItem?.uuid ?? '';
|
|
91
|
+
|
|
92
|
+
// should default to first price if check returns empty. todo - update backend to return default price
|
|
93
|
+
const priceForPaymentMode =
|
|
94
|
+
selectedItem.servicePrices.find((p) => p.paymentMode?.uuid === paymentMethod) || selectedItem?.servicePrices[0];
|
|
95
|
+
|
|
96
|
+
const createBillPayload = {
|
|
97
|
+
lineItems: [
|
|
98
|
+
{
|
|
99
|
+
billableService: itemUuid,
|
|
100
|
+
quantity: 1,
|
|
101
|
+
price: priceForPaymentMode ? priceForPaymentMode.price : '0.000',
|
|
102
|
+
priceName: 'Default',
|
|
103
|
+
priceUuid: priceForPaymentMode ? priceForPaymentMode.uuid : '',
|
|
104
|
+
lineItemOrder: 0,
|
|
105
|
+
paymentStatus: BillStatus.PENDING,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
cashPoint: cashPointUuid,
|
|
109
|
+
patient: patientUuid,
|
|
110
|
+
status: BillStatus.PENDING,
|
|
111
|
+
payments: [],
|
|
112
|
+
};
|
|
113
|
+
setExtraVisitInfo({
|
|
114
|
+
createBillPayload,
|
|
115
|
+
handleCreateExtraVisitInfo: () => handleCreateExtraVisitInfo(createBillPayload),
|
|
116
|
+
attributes,
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
[attributes, cashPoints, handleCreateExtraVisitInfo, paymentMethod, patientUuid, setExtraVisitInfo],
|
|
120
|
+
);
|
|
76
121
|
|
|
77
122
|
if (isLoadingLineItems || isLoadingCashPoints) {
|
|
78
123
|
return (
|
|
@@ -84,13 +129,6 @@ const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, se
|
|
|
84
129
|
);
|
|
85
130
|
}
|
|
86
131
|
|
|
87
|
-
if (paymentMethod) {
|
|
88
|
-
lineList = [];
|
|
89
|
-
lineList = lineItems.filter((e) =>
|
|
90
|
-
e.servicePrices.some((p) => p.paymentMode && p.paymentMode.uuid === paymentMethod),
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
132
|
const setServicePrice = (prices) => {
|
|
95
133
|
const matchingPrice = prices.find((p) => p.paymentMode?.uuid === paymentMethod);
|
|
96
134
|
return matchingPrice ? `(${matchingPrice.name}: ${matchingPrice.price})` : '';
|
|
@@ -109,17 +147,38 @@ const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, se
|
|
|
109
147
|
|
|
110
148
|
return (
|
|
111
149
|
<section className={styles.sectionContainer}>
|
|
112
|
-
<
|
|
113
|
-
{
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
150
|
+
<h1 className={styles.sectionLabel}>{t('billingDetails', 'Billing details')}</h1>
|
|
151
|
+
<div className={styles.sectionField}>
|
|
152
|
+
{!isLoadingLastVisitInfo && !lastVisitError && lastVisitInfo && (
|
|
153
|
+
<div className={styles.lastVisitBanner}>
|
|
154
|
+
<InlineNotification
|
|
155
|
+
hideCloseButton
|
|
156
|
+
kind="info"
|
|
157
|
+
title={t('lastVisitInfo', 'Last Visit Information')}
|
|
158
|
+
subtitle={t('lastVisitMsg', 'The last visit was a {{type}} {{count}} days ago at {{location}}', {
|
|
159
|
+
count: lastVisitInfo.diffDays,
|
|
160
|
+
type: lastVisitInfo.type,
|
|
161
|
+
location: lastVisitInfo.location,
|
|
162
|
+
})}
|
|
163
|
+
lowContrast
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
<VisitAttributesForm setAttributes={setAttributes} setPaymentMethod={setPaymentMethod} />
|
|
168
|
+
|
|
169
|
+
{lineList.length > 0 && (
|
|
170
|
+
<Dropdown
|
|
171
|
+
key={`billable-${paymentMethod}`}
|
|
172
|
+
id="billable-items"
|
|
173
|
+
items={lineList}
|
|
174
|
+
itemToString={(item) => (item ? `${item.name} ${setServicePrice(item.servicePrices)}` : '')}
|
|
175
|
+
label={t('selectBillableService', 'Select a billable service')}
|
|
176
|
+
onChange={handleBillingService}
|
|
177
|
+
selectedItem={selectedBillableItem}
|
|
178
|
+
titleText={t('billableService', 'Billable service')}
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
123
182
|
</section>
|
|
124
183
|
);
|
|
125
184
|
};
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
2
4
|
|
|
3
|
-
.
|
|
4
|
-
|
|
5
|
+
.sectionLabel {
|
|
6
|
+
@include type.type-style('heading-compact-02');
|
|
7
|
+
color: colors.$gray-70;
|
|
8
|
+
margin: 0 0 layout.$spacing-03 0;
|
|
9
|
+
min-width: 8rem;
|
|
5
10
|
}
|
|
6
11
|
|
|
12
|
+
.lastVisitBanner {
|
|
13
|
+
margin-bottom: layout.$spacing-05;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
:global(.omrs-breakpoint-lt-desktop) {
|
|
17
|
+
.sectionContainer {
|
|
18
|
+
display: flex;
|
|
19
|
+
|
|
20
|
+
.sectionLabel {
|
|
21
|
+
flex-basis: 30%;
|
|
22
|
+
min-width: 8rem;
|
|
23
|
+
text-align: left;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.sectionField {
|
|
27
|
+
flex-basis: 70%;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -3,13 +3,14 @@ import userEvent from '@testing-library/user-event';
|
|
|
3
3
|
import { screen, render } from '@testing-library/react';
|
|
4
4
|
import { useConfig } from '@openmrs/esm-framework';
|
|
5
5
|
import { type BillingConfig } from '../config-schema';
|
|
6
|
-
import { useBillableItems, useCashPoint, usePaymentMethods } from './billing-form.resource';
|
|
6
|
+
import { useBillableItems, useCashPoint, usePaymentMethods, useLastVisitInfo } from './billing-form.resource';
|
|
7
7
|
import BillingCheckInForm from './billing-checkin-form.component';
|
|
8
8
|
|
|
9
9
|
const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
|
|
10
10
|
const mockUseCashPoint = jest.mocked(useCashPoint);
|
|
11
11
|
const mockUseBillableItems = jest.mocked(useBillableItems);
|
|
12
12
|
const mockUsePaymentMethods = jest.mocked(usePaymentMethods);
|
|
13
|
+
const mockUseLastVisitInfo = jest.mocked(useLastVisitInfo);
|
|
13
14
|
|
|
14
15
|
const mockCashPoints = [
|
|
15
16
|
{
|
|
@@ -89,6 +90,7 @@ jest.mock('./billing-form.resource', () => ({
|
|
|
89
90
|
useCashPoint: jest.fn(),
|
|
90
91
|
createPatientBill: jest.fn(),
|
|
91
92
|
usePaymentMethods: jest.fn(),
|
|
93
|
+
useLastVisitInfo: jest.fn(),
|
|
92
94
|
}));
|
|
93
95
|
|
|
94
96
|
const testProps = { patientUuid: 'some-patient-uuid', setExtraVisitInfo: jest.fn() };
|
|
@@ -116,6 +118,7 @@ describe('BillingCheckInForm', () => {
|
|
|
116
118
|
},
|
|
117
119
|
} as BillingConfig);
|
|
118
120
|
mockUsePaymentMethods.mockReturnValue({ paymentModes: mockPaymentMethods, isLoading: false, error: null });
|
|
121
|
+
mockUseLastVisitInfo.mockReturnValue({ lastVisitInfo: null, isLoading: false, error: null });
|
|
119
122
|
});
|
|
120
123
|
|
|
121
124
|
test('should show the loading spinner while retrieving data', () => {
|
|
@@ -136,6 +139,53 @@ describe('BillingCheckInForm', () => {
|
|
|
136
139
|
expect(screen.getByText(/error loading bill services/i)).toBeInTheDocument();
|
|
137
140
|
});
|
|
138
141
|
|
|
142
|
+
test('should show the last visit banner when last visit info is available', () => {
|
|
143
|
+
mockUseBillableItems.mockReturnValue({ lineItems: [], isLoading: false, error: null });
|
|
144
|
+
mockUseCashPoint.mockReturnValue({ cashPoints: [], isLoading: false, error: null });
|
|
145
|
+
mockUseLastVisitInfo.mockReturnValue({
|
|
146
|
+
lastVisitInfo: { diffDays: 3, type: 'Outpatient', location: 'Main Clinic' },
|
|
147
|
+
isLoading: false,
|
|
148
|
+
error: null,
|
|
149
|
+
});
|
|
150
|
+
renderBillingCheckinForm();
|
|
151
|
+
|
|
152
|
+
expect(screen.getByText(/Last Visit Information/i)).toBeInTheDocument();
|
|
153
|
+
expect(screen.getByText(/3 days ago/i)).toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('should not show the last visit banner when there is no recent visit', () => {
|
|
157
|
+
mockUseBillableItems.mockReturnValue({ lineItems: [], isLoading: false, error: null });
|
|
158
|
+
mockUseCashPoint.mockReturnValue({ cashPoints: [], isLoading: false, error: null });
|
|
159
|
+
mockUseLastVisitInfo.mockReturnValue({ lastVisitInfo: null, isLoading: false, error: null });
|
|
160
|
+
renderBillingCheckinForm();
|
|
161
|
+
|
|
162
|
+
expect(screen.queryByText(/Last Visit Information/i)).not.toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('should show billable service dropdown when a paying method is selected', async () => {
|
|
166
|
+
const user = userEvent.setup();
|
|
167
|
+
mockUseCashPoint.mockReturnValue({ cashPoints: mockCashPoints, isLoading: false, error: null });
|
|
168
|
+
mockUseBillableItems.mockReturnValue({ lineItems: mockBillableItems, isLoading: false, error: null });
|
|
169
|
+
renderBillingCheckinForm();
|
|
170
|
+
|
|
171
|
+
await user.click(screen.getByRole('radio', { name: 'Paying' }));
|
|
172
|
+
const paymentMethodDropdown = await screen.findByRole('combobox', { name: /payment method/i });
|
|
173
|
+
await user.click(paymentMethodDropdown);
|
|
174
|
+
await user.click(await screen.findByText('Insurance'));
|
|
175
|
+
|
|
176
|
+
expect(screen.getByRole('combobox', { name: /billable service/i })).toBeInTheDocument();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('should hide billable service dropdown when switched to non-paying', async () => {
|
|
180
|
+
const user = userEvent.setup();
|
|
181
|
+
mockUseCashPoint.mockReturnValue({ cashPoints: mockCashPoints, isLoading: false, error: null });
|
|
182
|
+
mockUseBillableItems.mockReturnValue({ lineItems: mockBillableItems, isLoading: false, error: null });
|
|
183
|
+
renderBillingCheckinForm();
|
|
184
|
+
|
|
185
|
+
await user.click(screen.getByRole('radio', { name: /non paying/i }));
|
|
186
|
+
expect(screen.queryByRole('combobox', { name: /billable service/i })).not.toBeInTheDocument();
|
|
187
|
+
});
|
|
188
|
+
|
|
139
189
|
test('should render the form correctly and generate the required payload', async () => {
|
|
140
190
|
const user = userEvent.setup();
|
|
141
191
|
mockUseCashPoint.mockReturnValue({ cashPoints: mockCashPoints, isLoading: false, error: null });
|