@openmrs/esm-billing-app 1.0.1-pre.100
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/.editorconfig +12 -0
- package/.eslintignore +2 -0
- package/.eslintrc +57 -0
- package/.husky/pre-commit +7 -0
- package/.husky/pre-push +6 -0
- package/.prettierignore +14 -0
- package/.turbo.json +18 -0
- package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
- package/LICENSE +401 -0
- package/README.md +7 -0
- package/__mocks__/bills.mock.ts +394 -0
- package/__mocks__/delivery-summary.mock.ts +89 -0
- package/__mocks__/encounter-observation.mock.ts +10651 -0
- package/__mocks__/encounter-observations.mock.ts +6189 -0
- package/__mocks__/hiv-summary.mock.ts +22 -0
- package/__mocks__/patient-summary.mock.ts +32 -0
- package/__mocks__/patient.mock.ts +59 -0
- package/__mocks__/program-summary.mock.ts +43 -0
- package/__mocks__/react-i18next.js +57 -0
- package/dist/146.js +1 -0
- package/dist/146.js.map +1 -0
- package/dist/294.js +2 -0
- package/dist/294.js.LICENSE.txt +9 -0
- package/dist/294.js.map +1 -0
- package/dist/319.js +1 -0
- package/dist/384.js +1 -0
- package/dist/384.js.map +1 -0
- package/dist/421.js +1 -0
- package/dist/421.js.map +1 -0
- package/dist/533.js +1 -0
- package/dist/533.js.map +1 -0
- package/dist/574.js +1 -0
- package/dist/591.js +2 -0
- package/dist/591.js.LICENSE.txt +9 -0
- package/dist/591.js.map +1 -0
- package/dist/614.js +2 -0
- package/dist/614.js.LICENSE.txt +37 -0
- package/dist/614.js.map +1 -0
- package/dist/753.js +1 -0
- package/dist/753.js.map +1 -0
- package/dist/757.js +1 -0
- package/dist/770.js +1 -0
- package/dist/770.js.map +1 -0
- package/dist/783.js +1 -0
- package/dist/783.js.map +1 -0
- package/dist/788.js +1 -0
- package/dist/800.js +2 -0
- package/dist/800.js.LICENSE.txt +3 -0
- package/dist/800.js.map +1 -0
- package/dist/807.js +1 -0
- package/dist/833.js +1 -0
- package/dist/935.js +2 -0
- package/dist/935.js.LICENSE.txt +19 -0
- package/dist/935.js.map +1 -0
- package/dist/992.js +1 -0
- package/dist/992.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +47 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-billing-app.js +1 -0
- package/dist/openmrs-esm-billing-app.js.buildmanifest.json +609 -0
- package/dist/openmrs-esm-billing-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/e2e/README.md +115 -0
- package/e2e/core/global-setup.ts +32 -0
- package/e2e/core/index.ts +1 -0
- package/e2e/core/test.ts +20 -0
- package/e2e/fixtures/api.ts +27 -0
- package/e2e/fixtures/index.ts +1 -0
- package/e2e/pages/home-page.ts +9 -0
- package/e2e/pages/index.ts +1 -0
- package/e2e/specs/sample-test.spec.ts +11 -0
- package/e2e/support/github/Dockerfile +34 -0
- package/e2e/support/github/docker-compose.yml +24 -0
- package/e2e/support/github/run-e2e-docker-env.sh +49 -0
- package/example.env +6 -0
- package/i18next-parser.config.js +89 -0
- package/jest.config.js +34 -0
- package/package.json +124 -0
- package/playwright.config.ts +32 -0
- package/prettier.config.js +8 -0
- package/src/bill-history/bill-history.component.tsx +199 -0
- package/src/bill-history/bill-history.scss +151 -0
- package/src/bill-history/bill-history.test.tsx +122 -0
- package/src/billable-services/bill-waiver/bill-selection.component.tsx +76 -0
- package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +110 -0
- package/src/billable-services/bill-waiver/bill-waiver-form.scss +34 -0
- package/src/billable-services/bill-waiver/bill-waiver.component.tsx +32 -0
- package/src/billable-services/bill-waiver/bill-waiver.scss +10 -0
- package/src/billable-services/bill-waiver/patient-bills.component.tsx +137 -0
- package/src/billable-services/bill-waiver/utils.ts +41 -0
- package/src/billable-services/billable-service.resource.ts +72 -0
- package/src/billable-services/billable-services-home.component.tsx +51 -0
- package/src/billable-services/billable-services.component.tsx +255 -0
- package/src/billable-services/billable-services.scss +218 -0
- package/src/billable-services/billable-services.test.tsx +16 -0
- package/src/billable-services/create-edit/add-billable-service.component.tsx +322 -0
- package/src/billable-services/create-edit/add-billable-service.scss +131 -0
- package/src/billable-services/create-edit/add-billable-service.test.tsx +152 -0
- package/src/billable-services/dashboard/dashboard.component.tsx +15 -0
- package/src/billable-services/dashboard/dashboard.scss +27 -0
- package/src/billable-services/dashboard/dashboard.test.tsx +11 -0
- package/src/billable-services/dashboard/service-metrics.component.tsx +41 -0
- package/src/billable-services-admin-card-link.component.test.tsx +21 -0
- package/src/billable-services-admin-card-link.component.tsx +25 -0
- package/src/billing-dashboard/billing-dashboard.component.tsx +20 -0
- package/src/billing-dashboard/billing-dashboard.scss +27 -0
- package/src/billing-dashboard/billing-dashboard.test.tsx +13 -0
- package/src/billing-form/billing-checkin-form.component.tsx +127 -0
- package/src/billing-form/billing-checkin-form.scss +13 -0
- package/src/billing-form/billing-checkin-form.test.tsx +134 -0
- package/src/billing-form/billing-form.component.tsx +347 -0
- package/src/billing-form/billing-form.resource.ts +32 -0
- package/src/billing-form/billing-form.scss +88 -0
- package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +173 -0
- package/src/billing-form/visit-attributes/visit-attributes-form.scss +22 -0
- package/src/billing-header/billing-header.component.tsx +43 -0
- package/src/billing-header/billing-header.scss +83 -0
- package/src/billing-header/billing-illustration.component.tsx +30 -0
- package/src/billing.resource.ts +148 -0
- package/src/bills-table/bills-table.component.tsx +280 -0
- package/src/bills-table/bills-table.scss +181 -0
- package/src/bills-table/bills-table.test.tsx +154 -0
- package/src/config-schema.ts +50 -0
- package/src/constants.ts +3 -0
- package/src/dashboard.meta.ts +7 -0
- package/src/declarations.d.ts +4 -0
- package/src/helpers/functions.ts +66 -0
- package/src/helpers/index.ts +1 -0
- package/src/index.ts +72 -0
- package/src/invoice/invoice-table.component.tsx +189 -0
- package/src/invoice/invoice-table.scss +91 -0
- package/src/invoice/invoice.component.tsx +144 -0
- package/src/invoice/invoice.scss +93 -0
- package/src/invoice/invoice.test.tsx +242 -0
- package/src/invoice/payments/invoice-breakdown/invoice-breakdown.component.tsx +17 -0
- package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +29 -0
- package/src/invoice/payments/payment-form/payment-form.component.tsx +105 -0
- package/src/invoice/payments/payment-form/payment-form.scss +54 -0
- package/src/invoice/payments/payment-history/payment-history.component.tsx +69 -0
- package/src/invoice/payments/payment.resource.ts +44 -0
- package/src/invoice/payments/payments.component.tsx +147 -0
- package/src/invoice/payments/payments.scss +46 -0
- package/src/invoice/payments/utils.ts +68 -0
- package/src/invoice/payments/visit-tags/visit-attribute.component.tsx +21 -0
- package/src/invoice/printable-invoice/print-receipt.component.tsx +29 -0
- package/src/invoice/printable-invoice/print-receipt.scss +14 -0
- package/src/invoice/printable-invoice/printable-footer.component.tsx +19 -0
- package/src/invoice/printable-invoice/printable-footer.scss +17 -0
- package/src/invoice/printable-invoice/printable-footer.test.tsx +30 -0
- package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +63 -0
- package/src/invoice/printable-invoice/printable-invoice-header.scss +61 -0
- package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +58 -0
- package/src/invoice/printable-invoice/printable-invoice.component.tsx +146 -0
- package/src/invoice/printable-invoice/printable-invoice.scss +50 -0
- package/src/left-panel-link.component.tsx +41 -0
- package/src/left-panel-link.test.tsx +38 -0
- package/src/metrics-cards/card.component.tsx +14 -0
- package/src/metrics-cards/card.scss +20 -0
- package/src/metrics-cards/metrics-cards.component.tsx +42 -0
- package/src/metrics-cards/metrics-cards.scss +12 -0
- package/src/metrics-cards/metrics-cards.test.tsx +44 -0
- package/src/metrics-cards/metrics.resource.ts +45 -0
- package/src/modal/require-payment-modal.component.tsx +85 -0
- package/src/modal/require-payment.scss +6 -0
- package/src/root.component.tsx +19 -0
- package/src/root.scss +30 -0
- package/src/routes.json +78 -0
- package/src/setup-tests.ts +13 -0
- package/src/types/index.ts +181 -0
- package/test-helpers.tsx +23 -0
- package/translations/am.json +117 -0
- package/translations/en.json +117 -0
- package/translations/es.json +117 -0
- package/translations/fr.json +117 -0
- package/translations/he.json +117 -0
- package/translations/km.json +117 -0
- package/tsconfig.json +16 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen, render } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { useReactToPrint } from 'react-to-print';
|
|
5
|
+
import { showSnackbar } from '@openmrs/esm-framework';
|
|
6
|
+
import { mockPayments, mockBill } from '../../__mocks__/bills.mock';
|
|
7
|
+
import { useBill, processBillPayment } from '../billing.resource';
|
|
8
|
+
import { usePaymentModes } from './payments/payment.resource';
|
|
9
|
+
import Invoice from './invoice.component';
|
|
10
|
+
|
|
11
|
+
const mockedBill = jest.mocked(useBill);
|
|
12
|
+
const mockedProcessBillPayment = jest.mocked(processBillPayment);
|
|
13
|
+
const mockedUsePaymentModes = jest.mocked(usePaymentModes);
|
|
14
|
+
const mockedUseReactToPrint = jest.mocked(useReactToPrint);
|
|
15
|
+
|
|
16
|
+
jest.mock('./payments/payment.resource', () => ({
|
|
17
|
+
usePaymentModes: jest.fn(),
|
|
18
|
+
updateBillVisitAttribute: jest.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
jest.mock('../billing.resource', () => ({
|
|
22
|
+
useBill: jest.fn(),
|
|
23
|
+
processBillPayment: jest.fn(),
|
|
24
|
+
useDefaultFacility: jest.fn().mockReturnValue({ uuid: '54065383-b4d4-42d2-af4d-d250a1fd2590', display: 'MTRH' }),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
jest.mock('react-router-dom', () => {
|
|
28
|
+
const originalModule = jest.requireActual('react-router-dom');
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
...originalModule,
|
|
32
|
+
useParams: jest.fn().mockReturnValue({ patientUuid: 'patientUuid', billUuid: 'billUuid' }),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
jest.mock('react-to-print', () => {
|
|
37
|
+
const originalModule = jest.requireActual('react-to-print');
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...originalModule,
|
|
41
|
+
useReactToPrint: jest.fn(),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
jest.mock('@openmrs/esm-framework', () => {
|
|
46
|
+
const originalModule = jest.requireActual('@openmrs/esm-framework');
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...originalModule,
|
|
50
|
+
usePatient: jest.fn().mockReturnValue({
|
|
51
|
+
patient: {
|
|
52
|
+
id: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
|
|
53
|
+
name: [{ given: ['John'], family: 'Doe' }],
|
|
54
|
+
},
|
|
55
|
+
patientUuid: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
|
|
56
|
+
isLoading: false,
|
|
57
|
+
error: null,
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
xdescribe('Invoice', () => {
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
mockedBill.mockReturnValue({
|
|
65
|
+
bill: mockBill,
|
|
66
|
+
isLoading: false,
|
|
67
|
+
error: null,
|
|
68
|
+
isValidating: false,
|
|
69
|
+
mutate: jest.fn(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
mockedUsePaymentModes.mockReturnValue({
|
|
73
|
+
paymentModes: [
|
|
74
|
+
{ uuid: 'uuid', name: 'Cash', description: 'Cash Method', retired: false },
|
|
75
|
+
{ uuid: 'uuid1', name: 'MPESA', description: 'MPESA Method', retired: false },
|
|
76
|
+
],
|
|
77
|
+
isLoading: false,
|
|
78
|
+
error: null,
|
|
79
|
+
mutate: jest.fn(),
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(() => jest.clearAllMocks());
|
|
84
|
+
|
|
85
|
+
test('should be able to search through the invoice table and settle a bill', async () => {
|
|
86
|
+
const user = userEvent.setup();
|
|
87
|
+
|
|
88
|
+
renderInvoice();
|
|
89
|
+
|
|
90
|
+
const expectedHeaders = [
|
|
91
|
+
/Total amount/i,
|
|
92
|
+
/Amount tendered/i,
|
|
93
|
+
/Date and time/i,
|
|
94
|
+
/Invoice status/i,
|
|
95
|
+
/Invoice number/i,
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
expectedHeaders.forEach((header) => {
|
|
99
|
+
expect(screen.getByRole('heading', { name: header })).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const printButton = screen.getByRole('button', { name: /Print bill/i });
|
|
103
|
+
expect(printButton).toBeInTheDocument();
|
|
104
|
+
|
|
105
|
+
// Should show the line items table with the correct headers
|
|
106
|
+
const expectedColumnHeaders = [/No/i, /Bill item/i, /Bill code/i, /Status/i, /Quantity/i, /Price/i, /Total/i];
|
|
107
|
+
|
|
108
|
+
expectedColumnHeaders.forEach((columnHeader) => {
|
|
109
|
+
expect(screen.getByRole('columnheader', { name: columnHeader })).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(screen.getByRole('heading', { name: /Line items/i })).toBeInTheDocument();
|
|
113
|
+
expect(screen.getByText(/Items to be billed/i)).toBeInTheDocument();
|
|
114
|
+
|
|
115
|
+
// Should be able to search the line items table
|
|
116
|
+
const searchInput = screen.getByRole('searchbox');
|
|
117
|
+
expect(searchInput).toBeInTheDocument();
|
|
118
|
+
await user.type(searchInput, 'Hemoglobin');
|
|
119
|
+
expect(screen.getByText('Hemoglobin')).toBeInTheDocument();
|
|
120
|
+
|
|
121
|
+
await user.type(searchInput, 'Some random text');
|
|
122
|
+
expect(screen.queryByText('Hemoglobin')).not.toBeInTheDocument();
|
|
123
|
+
expect(screen.getByText(/No matching items to display/i)).toBeInTheDocument();
|
|
124
|
+
await user.clear(searchInput);
|
|
125
|
+
|
|
126
|
+
const row = mockBill.lineItems[0].item + ' ' + mockBill.receiptNumber + ' ' + mockBill.status.toUpperCase();
|
|
127
|
+
|
|
128
|
+
expect(screen.getByRole('row', { name: new RegExp(row, 'i') })).toBeInTheDocument();
|
|
129
|
+
|
|
130
|
+
// should be able to handle payments
|
|
131
|
+
const paymentSection = await screen.findByRole('heading', { name: /Payments/i });
|
|
132
|
+
expect(paymentSection).toBeInTheDocument();
|
|
133
|
+
|
|
134
|
+
const addPaymentOptionButton = await screen.findByRole('button', { name: /Add payment option/i });
|
|
135
|
+
expect(addPaymentOptionButton).toBeInTheDocument();
|
|
136
|
+
await user.click(addPaymentOptionButton);
|
|
137
|
+
const paymentModeInput = screen.getByRole('combobox', { name: /Payment method/i });
|
|
138
|
+
expect(paymentModeInput).toBeInTheDocument();
|
|
139
|
+
await user.click(paymentModeInput);
|
|
140
|
+
|
|
141
|
+
// select cash payment mode
|
|
142
|
+
const cashPaymentMode = await screen.findByText('Cash');
|
|
143
|
+
expect(cashPaymentMode).toBeInTheDocument();
|
|
144
|
+
await user.click(cashPaymentMode);
|
|
145
|
+
|
|
146
|
+
// enter payment amount
|
|
147
|
+
const paymentAmountInput = screen.getByPlaceholderText('Enter amount');
|
|
148
|
+
expect(paymentAmountInput).toBeInTheDocument();
|
|
149
|
+
await user.type(paymentAmountInput, '100');
|
|
150
|
+
|
|
151
|
+
// enter payment reference number
|
|
152
|
+
const paymentReferenceNumberInput = screen.getByRole('textbox', { name: /Reference number/ });
|
|
153
|
+
expect(paymentReferenceNumberInput).toBeInTheDocument();
|
|
154
|
+
await user.type(paymentReferenceNumberInput, '123456');
|
|
155
|
+
|
|
156
|
+
expect(addPaymentOptionButton).toBeDisabled();
|
|
157
|
+
|
|
158
|
+
// should process payment
|
|
159
|
+
mockedProcessBillPayment.mockResolvedValueOnce(Promise.resolve({} as any));
|
|
160
|
+
const processPaymentButton = screen.getByRole('button', { name: /Process Payment/i });
|
|
161
|
+
expect(processPaymentButton).toBeInTheDocument();
|
|
162
|
+
await user.click(processPaymentButton);
|
|
163
|
+
|
|
164
|
+
expect(processBillPayment).toHaveBeenCalledTimes(1);
|
|
165
|
+
expect(processBillPayment).toHaveBeenCalledWith(
|
|
166
|
+
{
|
|
167
|
+
cashPoint: '54065383-b4d4-42d2-af4d-d250a1fd2590',
|
|
168
|
+
cashier: 'fe00dd43-4c39-4ce9-9832-bc3620c80c6c',
|
|
169
|
+
patient: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
|
|
170
|
+
payments: [{ amount: 100, amountTendered: 100, attributes: [], instanceType: 'uuid' }],
|
|
171
|
+
status: 'PAID',
|
|
172
|
+
},
|
|
173
|
+
'6eb8d678-514d-46ad-9554-51e48d96d567',
|
|
174
|
+
);
|
|
175
|
+
expect(showSnackbar).toHaveBeenCalled();
|
|
176
|
+
expect(showSnackbar).toHaveBeenCalledWith({
|
|
177
|
+
kind: 'success',
|
|
178
|
+
subtitle: 'Bill payment processing has been successful',
|
|
179
|
+
timeoutInMs: 3000,
|
|
180
|
+
title: 'Bill payment',
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('should show print preview when print button is clicked', async () => {
|
|
185
|
+
const user = userEvent.setup();
|
|
186
|
+
|
|
187
|
+
renderInvoice();
|
|
188
|
+
|
|
189
|
+
const printButton = screen.getByRole('button', { name: /Print bill/i });
|
|
190
|
+
expect(printButton).toBeInTheDocument();
|
|
191
|
+
await user.click(printButton);
|
|
192
|
+
expect(mockedUseReactToPrint).toHaveBeenCalledTimes(1);
|
|
193
|
+
expect(mockedUseReactToPrint).toHaveBeenCalledWith(
|
|
194
|
+
expect.objectContaining({
|
|
195
|
+
documentTitle: 'Invoice 0035-6 - John Doe',
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('should show payment history if bill is paid and disable adding more payments', async () => {
|
|
201
|
+
const user = userEvent.setup();
|
|
202
|
+
mockedBill.mockReturnValue({
|
|
203
|
+
bill: {
|
|
204
|
+
...mockBill,
|
|
205
|
+
status: 'PAID',
|
|
206
|
+
payments: mockPayments,
|
|
207
|
+
tenderedAmount: 100,
|
|
208
|
+
},
|
|
209
|
+
isLoading: false,
|
|
210
|
+
error: null,
|
|
211
|
+
isValidating: false,
|
|
212
|
+
mutate: jest.fn(),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
mockedUsePaymentModes.mockReturnValue({
|
|
216
|
+
paymentModes: [
|
|
217
|
+
{ uuid: 'uuid', name: 'Cash', description: 'Cash Method', retired: false },
|
|
218
|
+
{ uuid: 'uuid1', name: 'MPESA', description: 'MPESA Method', retired: false },
|
|
219
|
+
],
|
|
220
|
+
isLoading: false,
|
|
221
|
+
error: null,
|
|
222
|
+
mutate: jest.fn(),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
renderInvoice();
|
|
226
|
+
const paymentHistorySection = screen.getByRole('heading', { name: /Payments/i });
|
|
227
|
+
expect(paymentHistorySection).toBeInTheDocument();
|
|
228
|
+
|
|
229
|
+
const expectedColumnHeaders = [/Date of payment/, /Bill amount/, /Amount tendered/, /Payment method/];
|
|
230
|
+
expectedColumnHeaders.forEach((header) => {
|
|
231
|
+
expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const addPaymentOptionButton = await screen.findByRole('button', { name: /Add payment option/i });
|
|
235
|
+
expect(addPaymentOptionButton).toBeInTheDocument();
|
|
236
|
+
expect(addPaymentOptionButton).toBeDisabled();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
function renderInvoice() {
|
|
241
|
+
return render(<Invoice />);
|
|
242
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from './invoice-breakdown.scss';
|
|
3
|
+
|
|
4
|
+
type InvoiceBreakDownProps = {
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
hasBalance?: Boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const InvoiceBreakDown: React.FC<InvoiceBreakDownProps> = ({ label, value, hasBalance }) => {
|
|
11
|
+
return (
|
|
12
|
+
<div className={styles.invoiceBreakdown}>
|
|
13
|
+
<span className={hasBalance ? styles.extendedLabel : styles.label}>{label}: </span>
|
|
14
|
+
<span className={styles.value}>{value}</span>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.invoiceBreakdown {
|
|
6
|
+
display: grid;
|
|
7
|
+
grid-template-columns: 1fr 1fr;
|
|
8
|
+
align-items: flex-end;
|
|
9
|
+
margin: layout.$spacing-02 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.label {
|
|
13
|
+
@include type.type-style('heading-03');
|
|
14
|
+
color: colors.$gray-100;
|
|
15
|
+
text-align: end;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.value {
|
|
19
|
+
@extend .label;
|
|
20
|
+
font-weight: bold;
|
|
21
|
+
margin-left: layout.$spacing-03;
|
|
22
|
+
text-align: start;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.extendedLabel {
|
|
26
|
+
@extend .label;
|
|
27
|
+
font-weight: bold;
|
|
28
|
+
color: crimson;
|
|
29
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { TrashCan, Add } from '@carbon/react/icons';
|
|
5
|
+
import { Button, Dropdown, NumberInputSkeleton, TextInput, NumberInput } from '@carbon/react';
|
|
6
|
+
import { ErrorState } from '@openmrs/esm-patient-common-lib';
|
|
7
|
+
import { type PaymentFormValue } from '../payments.component';
|
|
8
|
+
import { usePaymentModes } from '../payment.resource';
|
|
9
|
+
import styles from './payment-form.scss';
|
|
10
|
+
|
|
11
|
+
type PaymentFormProps = { disablePayment: boolean; amountDue: number };
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PAYMENT = { method: '', amount: 0, referenceCode: '' };
|
|
14
|
+
|
|
15
|
+
const PaymentForm: React.FC<PaymentFormProps> = ({ disablePayment, amountDue }) => {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const {
|
|
18
|
+
control,
|
|
19
|
+
formState: { errors },
|
|
20
|
+
} = useFormContext<PaymentFormValue>();
|
|
21
|
+
const { paymentModes, isLoading, error } = usePaymentModes();
|
|
22
|
+
const { fields, remove, append } = useFieldArray({ name: 'payment', control: control });
|
|
23
|
+
|
|
24
|
+
const handleAppendPaymentMode = useCallback(() => append(DEFAULT_PAYMENT), [append]);
|
|
25
|
+
const handleRemovePaymentMode = useCallback((index) => remove(index), [remove]);
|
|
26
|
+
|
|
27
|
+
if (isLoading) {
|
|
28
|
+
return <NumberInputSkeleton />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (error) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={styles.errorPaymentContainer}>
|
|
34
|
+
<ErrorState headerTitle={t('errorLoadingPaymentModes', 'Payment modes error')} error={error} />
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className={styles.container}>
|
|
41
|
+
{fields.map((field, index) => (
|
|
42
|
+
<div key={field.id} className={styles.paymentMethodContainer}>
|
|
43
|
+
<Controller
|
|
44
|
+
control={control}
|
|
45
|
+
name={`payment.${index}.method`}
|
|
46
|
+
render={({ field }) => (
|
|
47
|
+
<Dropdown
|
|
48
|
+
id="paymentMethod"
|
|
49
|
+
onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)}
|
|
50
|
+
titleText={t('paymentMethod', 'Payment method')}
|
|
51
|
+
label={t('selectPaymentMethod', 'Select payment method')}
|
|
52
|
+
items={paymentModes}
|
|
53
|
+
itemToString={(item) => (item ? item.name : '')}
|
|
54
|
+
invalid={!!errors?.payment?.[index]?.method}
|
|
55
|
+
invalidText={errors?.payment?.[index]?.method?.message}
|
|
56
|
+
/>
|
|
57
|
+
)}
|
|
58
|
+
/>
|
|
59
|
+
<Controller
|
|
60
|
+
control={control}
|
|
61
|
+
name={`payment.${index}.amount`}
|
|
62
|
+
render={({ field }) => (
|
|
63
|
+
<NumberInput
|
|
64
|
+
id="paymentAmount"
|
|
65
|
+
{...field}
|
|
66
|
+
onChange={(e) => field.onChange(Number(e.target.value))}
|
|
67
|
+
invalid={!!errors?.payment?.[index]?.amount}
|
|
68
|
+
invalidText={errors?.payment?.[index]?.amount?.message}
|
|
69
|
+
label={t('amount', 'Amount')}
|
|
70
|
+
placeholder={t('enterAmount', 'Enter amount')}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
/>
|
|
74
|
+
<Controller
|
|
75
|
+
name={`payment.${index}.referenceCode`}
|
|
76
|
+
control={control}
|
|
77
|
+
render={({ field }) => (
|
|
78
|
+
<TextInput
|
|
79
|
+
id="paymentReferenceCode"
|
|
80
|
+
{...field}
|
|
81
|
+
labelText={t('referenceNumber', 'Reference number')}
|
|
82
|
+
placeholder={t('enterReferenceNumber', 'Enter ref. number')}
|
|
83
|
+
type="text"
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
/>
|
|
87
|
+
<div className={styles.removeButtonContainer}>
|
|
88
|
+
<TrashCan onClick={handleRemovePaymentMode} className={styles.removeButton} size={20} />
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
<Button
|
|
93
|
+
disabled={disablePayment}
|
|
94
|
+
size="md"
|
|
95
|
+
onClick={handleAppendPaymentMode}
|
|
96
|
+
className={styles.paymentButtons}
|
|
97
|
+
renderIcon={(props) => <Add size={24} {...props} />}
|
|
98
|
+
iconDescription="Add">
|
|
99
|
+
{t('addPaymentOptions', 'Add payment option')}
|
|
100
|
+
</Button>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export default PaymentForm;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
margin: 1rem;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.paymentContainer {
|
|
10
|
+
margin: layout.$layout-01;
|
|
11
|
+
padding: layout.$layout-01;
|
|
12
|
+
width: 70%;
|
|
13
|
+
border-right: 1px solid colors.$cool-gray-40;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.paymentButtons {
|
|
17
|
+
margin: layout.$layout-01 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.paymentMethodContainer {
|
|
21
|
+
display: grid;
|
|
22
|
+
grid-template-columns: repeat(4, minmax(auto, 1fr));
|
|
23
|
+
align-items: flex-start;
|
|
24
|
+
column-gap: 1rem;
|
|
25
|
+
margin: 0.625rem 0;
|
|
26
|
+
width: 100%;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.paymentTotals {
|
|
30
|
+
margin-top: layout.$spacing-01;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.processPayments {
|
|
34
|
+
display: flex;
|
|
35
|
+
justify-content: flex-end;
|
|
36
|
+
margin: layout.$spacing-05;
|
|
37
|
+
column-gap: layout.$spacing-04;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.errorPaymentContainer {
|
|
41
|
+
margin: layout.$spacing-04;
|
|
42
|
+
min-height: layout.$spacing-09;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.removeButtonContainer {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-self: center;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
margin-left: layout.$spacing-07;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.removeButton {
|
|
53
|
+
color: colors.$red-60;
|
|
54
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DataTable, Table, TableHead, TableRow, TableHeader, TableBody, TableCell } from '@carbon/react';
|
|
3
|
+
import { type MappedBill } from '../../../types';
|
|
4
|
+
import { formatDate, useConfig } from '@openmrs/esm-framework';
|
|
5
|
+
import { convertToCurrency } from '../../../helpers';
|
|
6
|
+
|
|
7
|
+
type PaymentHistoryProps = {
|
|
8
|
+
bill: MappedBill;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const PaymentHistory: React.FC<PaymentHistoryProps> = ({ bill }) => {
|
|
12
|
+
const { defaultCurrency } = useConfig();
|
|
13
|
+
const headers = [
|
|
14
|
+
{
|
|
15
|
+
key: 'dateCreated',
|
|
16
|
+
header: 'Date of payment',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
key: 'amount',
|
|
20
|
+
header: 'Bill amount',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'amountTendered',
|
|
24
|
+
header: 'Amount tendered',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: 'paymentMethod',
|
|
28
|
+
header: 'Payment method',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
const rows = bill?.payments?.map((payment) => ({
|
|
32
|
+
id: `${payment.uuid}`,
|
|
33
|
+
dateCreated: formatDate(new Date(payment.dateCreated)),
|
|
34
|
+
amountTendered: convertToCurrency(payment.amountTendered, defaultCurrency),
|
|
35
|
+
amount: convertToCurrency(payment.amount, defaultCurrency),
|
|
36
|
+
paymentMethod: payment.instanceType.name,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
if (Object.values(bill?.payments ?? {}).length === 0) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<DataTable size="sm" rows={rows} headers={headers}>
|
|
45
|
+
{({ rows, headers, getTableProps, getHeaderProps, getRowProps }) => (
|
|
46
|
+
<Table {...getTableProps()}>
|
|
47
|
+
<TableHead>
|
|
48
|
+
<TableRow>
|
|
49
|
+
{headers.map((header) => (
|
|
50
|
+
<TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
|
|
51
|
+
))}
|
|
52
|
+
</TableRow>
|
|
53
|
+
</TableHead>
|
|
54
|
+
<TableBody>
|
|
55
|
+
{rows.map((row) => (
|
|
56
|
+
<TableRow {...getRowProps({ row })}>
|
|
57
|
+
{row.cells.map((cell) => (
|
|
58
|
+
<TableCell key={cell.id}>{cell.value}</TableCell>
|
|
59
|
+
))}
|
|
60
|
+
</TableRow>
|
|
61
|
+
))}
|
|
62
|
+
</TableBody>
|
|
63
|
+
</Table>
|
|
64
|
+
)}
|
|
65
|
+
</DataTable>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default PaymentHistory;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import useSWR from 'swr';
|
|
2
|
+
import { type Visit, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
3
|
+
import { apiBasePath } from '../../constants';
|
|
4
|
+
|
|
5
|
+
type PaymentMethod = {
|
|
6
|
+
uuid: string;
|
|
7
|
+
description: string;
|
|
8
|
+
name: string;
|
|
9
|
+
retired: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const swrOption = {
|
|
13
|
+
errorRetryCount: 2,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const usePaymentModes = () => {
|
|
17
|
+
const url = `${apiBasePath}paymentMode`;
|
|
18
|
+
const { data, isLoading, error, mutate } = useSWR<{ data: { results: Array<PaymentMethod> } }>(
|
|
19
|
+
url,
|
|
20
|
+
openmrsFetch,
|
|
21
|
+
swrOption,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
paymentModes: data?.data?.results ?? [],
|
|
26
|
+
isLoading,
|
|
27
|
+
mutate,
|
|
28
|
+
error,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const updateBillVisitAttribute = async (visit: Visit) => {
|
|
33
|
+
const { uuid, attributes } = visit;
|
|
34
|
+
const pendingPaymentAtrributeUuid = attributes?.find(
|
|
35
|
+
(attribute) => attribute.attributeType.uuid === '919b51c9-8e2e-468f-8354-181bf3e55786',
|
|
36
|
+
)?.uuid;
|
|
37
|
+
return openmrsFetch(`${restBaseUrl}/visit/${uuid}/attribute/${pendingPaymentAtrributeUuid}`, {
|
|
38
|
+
body: { value: false },
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
},
|
|
42
|
+
method: 'POST',
|
|
43
|
+
});
|
|
44
|
+
};
|