@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,152 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import AddBillableService from './add-billable-service.component';
|
|
5
|
+
import {
|
|
6
|
+
useBillableServices,
|
|
7
|
+
usePaymentModes,
|
|
8
|
+
useServiceTypes,
|
|
9
|
+
createBillableSerice,
|
|
10
|
+
} from '../billable-service.resource';
|
|
11
|
+
import { FetchResponse, navigate, showSnackbar } from '@openmrs/esm-framework';
|
|
12
|
+
|
|
13
|
+
const mockUseBillableServices = useBillableServices as jest.MockedFunction<typeof useBillableServices>;
|
|
14
|
+
const mockUsePaymentModes = usePaymentModes as jest.MockedFunction<typeof usePaymentModes>;
|
|
15
|
+
const mockUseServiceTypes = useServiceTypes as jest.MockedFunction<typeof useServiceTypes>;
|
|
16
|
+
const mockCreateBillableSerice = createBillableSerice as jest.MockedFunction<typeof createBillableSerice>;
|
|
17
|
+
const mockNavigate = navigate as jest.MockedFunction<typeof navigate>;
|
|
18
|
+
const mockShowSnackbar = showSnackbar as jest.MockedFunction<typeof showSnackbar>;
|
|
19
|
+
|
|
20
|
+
jest.mock('../billable-service.resource', () => ({
|
|
21
|
+
useBillableServices: jest.fn(),
|
|
22
|
+
usePaymentModes: jest.fn(),
|
|
23
|
+
useServiceTypes: jest.fn(),
|
|
24
|
+
createBillableSerice: jest.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const mockPaymentModes = [
|
|
28
|
+
{ uuid: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74', name: 'Cash', description: 'Cash Payment', retired: false },
|
|
29
|
+
{
|
|
30
|
+
uuid: 'beac329b-f1dc-4a33-9e7c-d95821a137a6',
|
|
31
|
+
name: 'Insurance',
|
|
32
|
+
description: 'Insurance method of payment',
|
|
33
|
+
retired: false,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
uuid: '28989582-e8c3-46b0-96d0-c249cb06d5c6',
|
|
37
|
+
name: 'MPESA',
|
|
38
|
+
description: 'Mobile money method of payment',
|
|
39
|
+
retired: false,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const mockServiceTypes = [
|
|
44
|
+
{ uuid: 'c9604249-db0a-4d03-b074-fc6bc2fa13e6', display: 'Lab service' },
|
|
45
|
+
{ uuid: 'b75e466f-a6f5-4d5e-849a-84424d3c85cd', display: 'Pharmacy service' },
|
|
46
|
+
{ uuid: 'ce914b2d-44f6-4b6c-933f-c57a3938e35b', display: 'Peer educator service' },
|
|
47
|
+
{ uuid: 'c23d3224-2218-4007-8f22-e1f3d5a8e58a', display: 'Nutrition service' },
|
|
48
|
+
{ uuid: '65487ff4-63b3-452a-8985-6a1f4a0cc08d', display: 'TB service' },
|
|
49
|
+
{ uuid: '9db142d5-5cc4-4c05-9f83-06ed294caa67', display: 'Family planning service' },
|
|
50
|
+
{ uuid: 'a487a743-62ce-4f93-a66b-c5154ee8987d', display: 'Adherence counselling service' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
xdescribe('AddBillableService', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
jest.resetAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('should render billable services form and generate correct payload', async () => {
|
|
59
|
+
const user = userEvent.setup();
|
|
60
|
+
mockUseBillableServices.mockReturnValue({
|
|
61
|
+
billableServices: [],
|
|
62
|
+
isLoading: false,
|
|
63
|
+
error: null,
|
|
64
|
+
mutate: jest.fn(),
|
|
65
|
+
isValidating: false,
|
|
66
|
+
});
|
|
67
|
+
mockUsePaymentModes.mockReturnValue({ paymentModes: mockPaymentModes, error: null, isLoading: false });
|
|
68
|
+
mockUseServiceTypes.mockReturnValue({ serviceTypes: mockServiceTypes, error: false, isLoading: false });
|
|
69
|
+
render(<AddBillableService />);
|
|
70
|
+
|
|
71
|
+
const formTtile = screen.getByRole('heading', { name: /Add Billable Services/i });
|
|
72
|
+
expect(formTtile).toBeInTheDocument();
|
|
73
|
+
|
|
74
|
+
const serviceNameTextInp = screen.getByRole('textbox', { name: /Service Name/i });
|
|
75
|
+
expect(serviceNameTextInp).toBeInTheDocument();
|
|
76
|
+
|
|
77
|
+
const serviceShortNameTextInp = screen.getByRole('textbox', { name: /Short Name/i });
|
|
78
|
+
expect(serviceShortNameTextInp).toBeInTheDocument();
|
|
79
|
+
|
|
80
|
+
await user.type(serviceNameTextInp, 'Test Service Name');
|
|
81
|
+
await user.type(serviceShortNameTextInp, 'Test Short Name');
|
|
82
|
+
|
|
83
|
+
expect(serviceNameTextInp).toHaveValue('Test Service Name');
|
|
84
|
+
expect(serviceShortNameTextInp).toHaveValue('Test Short Name');
|
|
85
|
+
|
|
86
|
+
const serviceTypeComboBox = screen.getByRole('combobox', { name: /Service Type/i });
|
|
87
|
+
expect(serviceTypeComboBox).toBeInTheDocument();
|
|
88
|
+
await user.click(serviceTypeComboBox);
|
|
89
|
+
const serviceTypeOptions = screen.getByRole('option', { name: /Lab service/i });
|
|
90
|
+
expect(serviceTypeOptions).toBeInTheDocument();
|
|
91
|
+
await user.click(serviceTypeOptions);
|
|
92
|
+
|
|
93
|
+
const addPaymentMethodBtn = screen.getByRole('button', { name: /Add payment option/i });
|
|
94
|
+
expect(addPaymentMethodBtn).toBeInTheDocument();
|
|
95
|
+
|
|
96
|
+
await user.click(addPaymentMethodBtn);
|
|
97
|
+
|
|
98
|
+
const paymentMethodComboBox = screen.getByRole('combobox', { name: /Payment Mode/i });
|
|
99
|
+
expect(paymentMethodComboBox).toBeInTheDocument();
|
|
100
|
+
await user.click(paymentMethodComboBox);
|
|
101
|
+
const paymentMethodOptions = screen.getByRole('option', { name: /Cash/i });
|
|
102
|
+
expect(paymentMethodOptions).toBeInTheDocument();
|
|
103
|
+
await user.click(paymentMethodOptions);
|
|
104
|
+
|
|
105
|
+
const priceTextInp = screen.getByRole('textbox', { name: /Price/i });
|
|
106
|
+
expect(priceTextInp).toBeInTheDocument();
|
|
107
|
+
await user.type(priceTextInp, '1000');
|
|
108
|
+
|
|
109
|
+
mockCreateBillableSerice.mockReturnValue(Promise.resolve({} as FetchResponse<any>));
|
|
110
|
+
const saveBtn = screen.getByRole('button', { name: /Save/i });
|
|
111
|
+
expect(saveBtn).toBeInTheDocument();
|
|
112
|
+
await user.click(saveBtn);
|
|
113
|
+
|
|
114
|
+
expect(mockCreateBillableSerice).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect(mockCreateBillableSerice).toHaveBeenCalledWith({
|
|
116
|
+
name: 'Test Service Name',
|
|
117
|
+
shortName: 'Test Short Name',
|
|
118
|
+
serviceType: undefined,
|
|
119
|
+
servicePrices: [
|
|
120
|
+
{
|
|
121
|
+
paymentMode: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74',
|
|
122
|
+
price: '01000',
|
|
123
|
+
name: 'Cash',
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
serviceStatus: 'ENABLED',
|
|
127
|
+
});
|
|
128
|
+
expect(mockNavigate).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(mockNavigate).toHaveBeenCalledWith({ to: '/openmrs/spa/billable-services' });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("should navigate back to billable services dashboard when 'Cancel' button is clicked", async () => {
|
|
133
|
+
const user = userEvent.setup();
|
|
134
|
+
mockUseBillableServices.mockReturnValue({
|
|
135
|
+
billableServices: [],
|
|
136
|
+
isLoading: false,
|
|
137
|
+
error: null,
|
|
138
|
+
mutate: jest.fn(),
|
|
139
|
+
isValidating: false,
|
|
140
|
+
});
|
|
141
|
+
mockUsePaymentModes.mockReturnValue({ paymentModes: mockPaymentModes, error: null, isLoading: false });
|
|
142
|
+
mockUseServiceTypes.mockReturnValue({ serviceTypes: mockServiceTypes, error: false, isLoading: false });
|
|
143
|
+
render(<AddBillableService />);
|
|
144
|
+
|
|
145
|
+
const cancelBtn = screen.getByRole('button', { name: /Cancel/i });
|
|
146
|
+
expect(cancelBtn).toBeInTheDocument();
|
|
147
|
+
await user.click(cancelBtn);
|
|
148
|
+
|
|
149
|
+
expect(mockNavigate).toHaveBeenCalledTimes(1);
|
|
150
|
+
expect(mockNavigate).toHaveBeenCalledWith({ to: '/openmrs/spa/billable-services' });
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import BillableServices from '../billable-services.component';
|
|
3
|
+
import styles from './dashboard.scss';
|
|
4
|
+
import { ExtensionSlot } from '@openmrs/esm-framework';
|
|
5
|
+
|
|
6
|
+
export default function BillableServicesDashboard() {
|
|
7
|
+
return (
|
|
8
|
+
<main className={styles.container}>
|
|
9
|
+
<ExtensionSlot name="billing-home-tiles-slot" />
|
|
10
|
+
<main className={styles.servicesTableContainer}>
|
|
11
|
+
<BillableServices />
|
|
12
|
+
</main>
|
|
13
|
+
</main>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
height: calc(100vh - 3rem);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.servicesTableContainer {
|
|
10
|
+
margin: 2rem 1rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.illo {
|
|
14
|
+
margin-top: layout.$spacing-05;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.content {
|
|
18
|
+
@include type.type-style('heading-compact-01');
|
|
19
|
+
color: colors.$gray-70;
|
|
20
|
+
margin-top: layout.$spacing-05;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.tile {
|
|
24
|
+
border: 1px solid colors.$gray-20;
|
|
25
|
+
padding: 1.5rem 0;
|
|
26
|
+
text-align: center;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen, render } from '@testing-library/react';
|
|
3
|
+
import BillableServicesDashboard from './dashboard.component';
|
|
4
|
+
|
|
5
|
+
test('renders an empty state when there are no services', () => {
|
|
6
|
+
renderBillingDashboard();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
function renderBillingDashboard() {
|
|
10
|
+
render(<BillableServicesDashboard />);
|
|
11
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { InlineLoading } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { ErrorState } from '@openmrs/esm-patient-common-lib';
|
|
5
|
+
import { useBillableServices } from '../billable-service.resource';
|
|
6
|
+
import Card from '../../metrics-cards/card.component';
|
|
7
|
+
import styles from '../../metrics-cards/metrics-cards.scss';
|
|
8
|
+
import { ExtensionSlot } from '@openmrs/esm-framework';
|
|
9
|
+
|
|
10
|
+
export default function ServiceMetrics() {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const { isLoading, error } = useBillableServices();
|
|
13
|
+
|
|
14
|
+
const cards = useMemo(
|
|
15
|
+
() => [
|
|
16
|
+
{ title: 'Cash Revenue', count: '--' },
|
|
17
|
+
{ title: 'Insurance Revenue', count: '--' },
|
|
18
|
+
{ title: 'Pending Claims', count: '--' },
|
|
19
|
+
],
|
|
20
|
+
[],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (isLoading) {
|
|
24
|
+
return (
|
|
25
|
+
<section className={styles.container}>
|
|
26
|
+
<InlineLoading status="active" iconDescription="Loading" description="Loading service metrics..." />
|
|
27
|
+
</section>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (error) {
|
|
32
|
+
return <ErrorState headerTitle={t('serviceMetrics', 'Service Metrics')} error={error} />;
|
|
33
|
+
}
|
|
34
|
+
return (
|
|
35
|
+
<section className={styles.container}>
|
|
36
|
+
{cards.map((card) => (
|
|
37
|
+
<Card key={card.title} title={card.title} count={card.count} />
|
|
38
|
+
))}
|
|
39
|
+
</section>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import BillableServicesCardLink from './billable-services-admin-card-link.component';
|
|
4
|
+
|
|
5
|
+
describe('BillableServicesCardLink', () => {
|
|
6
|
+
test('should render billable services admin link', () => {
|
|
7
|
+
renderBillableServicesCardLink();
|
|
8
|
+
const manageBillableServicesText = screen.getByText('Manage billable services');
|
|
9
|
+
expect(manageBillableServicesText).toHaveClass('heading');
|
|
10
|
+
|
|
11
|
+
const billiableText = screen.getByText('Billable Services');
|
|
12
|
+
expect(billiableText).toHaveClass('content');
|
|
13
|
+
|
|
14
|
+
const billiableServiceLink = screen.getByRole('link', { name: /Billable Services/i });
|
|
15
|
+
expect(billiableServiceLink).toHaveAttribute('href', '/spa/billable-services');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function renderBillableServicesCardLink() {
|
|
20
|
+
render(<BillableServicesCardLink />);
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Layer, ClickableTile } from '@carbon/react';
|
|
4
|
+
import { ArrowRight } from '@carbon/react/icons';
|
|
5
|
+
|
|
6
|
+
const BillableServicesCardLink: React.FC = () => {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const header = t('manageBillableServices', 'Manage billable services');
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<Layer>
|
|
12
|
+
<ClickableTile href={`${window.spaBase}/billable-services`} target="_blank" rel="noopener noreferrer">
|
|
13
|
+
<div>
|
|
14
|
+
<div className="heading">{header}</div>
|
|
15
|
+
<div className="content">{t('billableServices', 'Billable Services')}</div>
|
|
16
|
+
</div>
|
|
17
|
+
<div className="iconWrapper">
|
|
18
|
+
<ArrowRight size={16} />
|
|
19
|
+
</div>
|
|
20
|
+
</ClickableTile>
|
|
21
|
+
</Layer>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default BillableServicesCardLink;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import BillingHeader from '../billing-header/billing-header.component';
|
|
4
|
+
import MetricsCards from '../metrics-cards/metrics-cards.component';
|
|
5
|
+
import BillsTable from '../bills-table/bills-table.component';
|
|
6
|
+
import styles from './billing-dashboard.scss';
|
|
7
|
+
|
|
8
|
+
export function BillingDashboard() {
|
|
9
|
+
const { t } = useTranslation();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<main className={styles.container}>
|
|
13
|
+
<BillingHeader title={t('home', 'Home')} />
|
|
14
|
+
<MetricsCards />
|
|
15
|
+
<section className={styles.billsTableContainer}>
|
|
16
|
+
<BillsTable />
|
|
17
|
+
</section>
|
|
18
|
+
</main>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
height: calc(100vh - 3rem);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.billsTableContainer {
|
|
10
|
+
margin: 2rem 1rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.illo {
|
|
14
|
+
margin-top: layout.$spacing-05;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.content {
|
|
18
|
+
@include type.type-style('heading-compact-01');
|
|
19
|
+
color: colors.$gray-70;
|
|
20
|
+
margin-top: layout.$spacing-05;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.tile {
|
|
24
|
+
border: 1px solid colors.$gray-20;
|
|
25
|
+
padding: 1.5rem 0;
|
|
26
|
+
text-align: center;
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen, render } from '@testing-library/react';
|
|
3
|
+
import { BillingDashboard } from './billing-dashboard.component';
|
|
4
|
+
|
|
5
|
+
test('renders an empty state when there are no billing records', () => {
|
|
6
|
+
renderBillingDashboard();
|
|
7
|
+
|
|
8
|
+
expect(screen.getByTitle(/billing module illustration/i)).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function renderBillingDashboard() {
|
|
12
|
+
render(<BillingDashboard />);
|
|
13
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { Dropdown, InlineLoading, InlineNotification } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { showSnackbar, useConfig } from '@openmrs/esm-framework';
|
|
5
|
+
import { useCashPoint, useBillableItems, createPatientBill } from './billing-form.resource';
|
|
6
|
+
import VisitAttributesForm from './visit-attributes/visit-attributes-form.component';
|
|
7
|
+
import styles from './billing-checkin-form.scss';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PRICE = 500.00001;
|
|
10
|
+
const PENDING_PAYMENT_STATUS = 'PENDING';
|
|
11
|
+
|
|
12
|
+
type BillingCheckInFormProps = {
|
|
13
|
+
patientUuid: string;
|
|
14
|
+
setExtraVisitInfo: (state) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, setExtraVisitInfo }) => {
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
const { cashPoints, isLoading: isLoadingCashPoints, error: cashError } = useCashPoint();
|
|
20
|
+
const { lineItems, isLoading: isLoadingLineItems, error: lineError } = useBillableItems();
|
|
21
|
+
const [attributes, setAttributes] = useState([]);
|
|
22
|
+
const [paymentMethod, setPaymentMethod] = useState<any>();
|
|
23
|
+
let lineList = [];
|
|
24
|
+
|
|
25
|
+
const handleCreateExtraVisitInfo = useCallback((createBillPayload) => {
|
|
26
|
+
createPatientBill(createBillPayload).then(
|
|
27
|
+
(res) => {
|
|
28
|
+
showSnackbar({ title: 'Patient Bill', subtitle: 'Patient has been billed successfully', kind: 'success' });
|
|
29
|
+
},
|
|
30
|
+
(error) => {
|
|
31
|
+
showSnackbar({
|
|
32
|
+
title: 'Patient Bill Error',
|
|
33
|
+
subtitle: 'An error has occurred while creating patient bill',
|
|
34
|
+
kind: 'error',
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const handleBillingService = ({ selectedItem }) => {
|
|
41
|
+
const cashPointUuid = cashPoints?.[0]?.uuid ?? '';
|
|
42
|
+
const itemUuid = selectedItem?.uuid ?? '';
|
|
43
|
+
|
|
44
|
+
// should default to first price if check returns empty. todo - update backend to return default price
|
|
45
|
+
const priceForPaymentMode =
|
|
46
|
+
selectedItem.servicePrices.find((p) => p.paymentMode?.uuid === paymentMethod) || selectedItem?.servicePrices[0];
|
|
47
|
+
|
|
48
|
+
const createBillPayload = {
|
|
49
|
+
lineItems: [
|
|
50
|
+
{
|
|
51
|
+
billableService: itemUuid,
|
|
52
|
+
quantity: 1,
|
|
53
|
+
price: priceForPaymentMode ? priceForPaymentMode.price : '0.000',
|
|
54
|
+
priceName: 'Default',
|
|
55
|
+
priceUuid: priceForPaymentMode ? priceForPaymentMode.uuid : '',
|
|
56
|
+
lineItemOrder: 0,
|
|
57
|
+
paymentStatus: PENDING_PAYMENT_STATUS,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
cashPoint: cashPointUuid,
|
|
61
|
+
patient: patientUuid,
|
|
62
|
+
status: PENDING_PAYMENT_STATUS,
|
|
63
|
+
payments: [],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
setExtraVisitInfo({
|
|
67
|
+
createBillPayload,
|
|
68
|
+
handleCreateExtraVisitInfo: () => handleCreateExtraVisitInfo(createBillPayload),
|
|
69
|
+
attributes,
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (isLoadingLineItems || isLoadingCashPoints) {
|
|
74
|
+
return (
|
|
75
|
+
<InlineLoading
|
|
76
|
+
status="active"
|
|
77
|
+
iconDescription={t('loading', 'Loading')}
|
|
78
|
+
description={t('loadingBillingServices', 'Loading billing services...')}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (paymentMethod) {
|
|
84
|
+
lineList = [];
|
|
85
|
+
lineList = lineItems.filter((e) =>
|
|
86
|
+
e.servicePrices.some((p) => p.paymentMode && p.paymentMode.uuid === paymentMethod),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const setServicePrice = (prices) => {
|
|
91
|
+
const matchingPrice = prices.find((p) => p.paymentMode?.uuid === paymentMethod);
|
|
92
|
+
return matchingPrice ? `(${matchingPrice.name}:${matchingPrice.price})` : '';
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (cashError || lineError) {
|
|
96
|
+
return (
|
|
97
|
+
<InlineNotification
|
|
98
|
+
kind="error"
|
|
99
|
+
lowContrast
|
|
100
|
+
title={t('billErrorService', 'Bill service error')}
|
|
101
|
+
subtitle={t('errorLoadingBillServices', 'Error loading bill services')}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<section className={styles.sectionContainer}>
|
|
108
|
+
<VisitAttributesForm setAttributes={setAttributes} setPaymentMethod={setPaymentMethod} />
|
|
109
|
+
{
|
|
110
|
+
<>
|
|
111
|
+
<div className={styles.sectionTitle}>{t('billing', 'Billing')}</div>
|
|
112
|
+
<div className={styles.sectionField}></div>
|
|
113
|
+
<Dropdown
|
|
114
|
+
label={t('selectBillableService', 'Select a billable service...')}
|
|
115
|
+
onChange={handleBillingService}
|
|
116
|
+
id="billable-items"
|
|
117
|
+
items={lineList}
|
|
118
|
+
itemToString={(item) => (item ? `${item.name} ${setServicePrice(item.servicePrices)}` : '')}
|
|
119
|
+
titleText={t('billableService', 'Billable service')}
|
|
120
|
+
/>
|
|
121
|
+
</>
|
|
122
|
+
}
|
|
123
|
+
</section>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default React.memo(BillingCheckInForm);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
|
|
5
|
+
.sectionContainer {
|
|
6
|
+
margin: 0 layout.$spacing-03;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.sectionTitle {
|
|
10
|
+
@include type.type-style('heading-compact-02');
|
|
11
|
+
color: colors.$gray-70;
|
|
12
|
+
margin: 0 0 layout.$spacing-03 0;
|
|
13
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { screen, render } from '@testing-library/react';
|
|
4
|
+
import { useBillableItems, useCashPoint, createPatientBill, usePaymentMethods } from './billing-form.resource';
|
|
5
|
+
import BillingCheckInForm from './billing-checkin-form.component';
|
|
6
|
+
|
|
7
|
+
const mockCashPoints = [
|
|
8
|
+
{
|
|
9
|
+
uuid: '54065383-b4d4-42d2-af4d-d250a1fd2590',
|
|
10
|
+
name: 'Cashier 2',
|
|
11
|
+
description: '',
|
|
12
|
+
retired: false,
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const mockBillableItems = [
|
|
17
|
+
{
|
|
18
|
+
uuid: 'b37dddd6-4490-4bf7-b694-43bf19d04059',
|
|
19
|
+
conceptUuid: '1926AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
20
|
+
conceptName: 'Consultation billable item',
|
|
21
|
+
hasExpiration: false,
|
|
22
|
+
preferredVendorUuid: '359006e7-2669-4204-aee8-27462514b10a',
|
|
23
|
+
preferredVendorName: 'Consolt',
|
|
24
|
+
categoryUuid: '6469ff7e-f8c7-42d6-bff3-ac9605ec99df',
|
|
25
|
+
categoryName: 'Non Drug',
|
|
26
|
+
commonName: 'Consultation',
|
|
27
|
+
acronym: 'CONSULT',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
uuid: 'b47dddd6-4490-4bf7-b694-43bf19d04059',
|
|
31
|
+
conceptUuid: '1926AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
32
|
+
conceptName: 'Lab Testing billable item',
|
|
33
|
+
hasExpiration: false,
|
|
34
|
+
preferredVendorUuid: '359006e7-2669-4204-aee8-27462514b10a',
|
|
35
|
+
preferredVendorName: 'Consolt',
|
|
36
|
+
categoryUuid: '6469ff7e-f8c7-42d6-bff3-ac9605ec99df',
|
|
37
|
+
categoryName: 'Non Drug',
|
|
38
|
+
commonName: 'Lab Testing',
|
|
39
|
+
acronym: 'CONSULT',
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const mockUseCashPoint = useCashPoint as jest.MockedFunction<typeof useCashPoint>;
|
|
44
|
+
const mockUseBillableItems = useBillableItems as jest.MockedFunction<typeof useBillableItems>;
|
|
45
|
+
const mockCreatePatientBill = createPatientBill as jest.MockedFunction<typeof createPatientBill>;
|
|
46
|
+
const mockusePaymentMethods = usePaymentMethods as jest.MockedFunction<typeof usePaymentMethods>;
|
|
47
|
+
|
|
48
|
+
jest.mock('./billing-form.resource', () => ({
|
|
49
|
+
useBillableItems: jest.fn(),
|
|
50
|
+
useCashPoint: jest.fn(),
|
|
51
|
+
createPatientBill: jest.fn(),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const testProps = { patientUuid: 'some-patient-uuid', setExtraVisitInfo: jest.fn() };
|
|
55
|
+
|
|
56
|
+
xdescribe('BillingCheckInForm', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
jest.resetAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should show the loading spinner while retrieving data', () => {
|
|
62
|
+
mockUseBillableItems.mockReturnValueOnce({ lineItems: [], isLoading: true, error: null });
|
|
63
|
+
mockUseCashPoint.mockReturnValueOnce({ cashPoints: [], isLoading: true, error: null });
|
|
64
|
+
renderBillingCheckinForm();
|
|
65
|
+
|
|
66
|
+
expect(screen.getByText(/Loading billing services.../)).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should show error state when an error occurs while fetching data', () => {
|
|
70
|
+
const error = new Error('Internal server error');
|
|
71
|
+
mockUseBillableItems.mockReturnValueOnce({ lineItems: [], isLoading: false, error });
|
|
72
|
+
mockUseCashPoint.mockReturnValueOnce({ cashPoints: [], isLoading: false, error });
|
|
73
|
+
renderBillingCheckinForm();
|
|
74
|
+
|
|
75
|
+
expect(screen.getByText('Bill service error')).toBeInTheDocument();
|
|
76
|
+
expect(screen.getByText('Error loading bill services')).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('should render the form correctly and generate the required payload', async () => {
|
|
80
|
+
const user = userEvent.setup();
|
|
81
|
+
mockUseCashPoint.mockReturnValue({ cashPoints: [], isLoading: false, error: null });
|
|
82
|
+
mockUseBillableItems.mockReturnValue({ lineItems: mockBillableItems, isLoading: false, error: null });
|
|
83
|
+
renderBillingCheckinForm();
|
|
84
|
+
|
|
85
|
+
const paymentTypeSelect = screen.getByRole('group', { name: 'Payment Details' });
|
|
86
|
+
expect(paymentTypeSelect).toBeInTheDocument();
|
|
87
|
+
|
|
88
|
+
const paymentTypeRadio = screen.getByRole('radio', { name: 'Paying' });
|
|
89
|
+
expect(paymentTypeRadio).toBeInTheDocument();
|
|
90
|
+
await user.click(paymentTypeRadio);
|
|
91
|
+
|
|
92
|
+
const billiableSelect = screen.getByRole('combobox', { name: 'Billable service' });
|
|
93
|
+
expect(billiableSelect).toBeInTheDocument();
|
|
94
|
+
await user.click(screen.getByRole('combobox', { name: 'Billable service' }));
|
|
95
|
+
|
|
96
|
+
await user.click(screen.getByText('Lab Testing'));
|
|
97
|
+
|
|
98
|
+
expect(testProps.setExtraVisitInfo).toHaveBeenCalled();
|
|
99
|
+
expect(testProps.setExtraVisitInfo).toHaveBeenCalledWith({
|
|
100
|
+
createBillPayload: {
|
|
101
|
+
lineItems: [
|
|
102
|
+
{
|
|
103
|
+
item: 'b47dddd6-4490-4bf7-b694-43bf19d04059',
|
|
104
|
+
quantity: 1,
|
|
105
|
+
price: 500.00001,
|
|
106
|
+
priceName: 'Default',
|
|
107
|
+
priceUuid: '',
|
|
108
|
+
lineItemOrder: 0,
|
|
109
|
+
paymentStatus: 'PENDING',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
cashPoint: '',
|
|
113
|
+
patient: 'some-patient-uuid',
|
|
114
|
+
status: 'PENDING',
|
|
115
|
+
payments: [],
|
|
116
|
+
},
|
|
117
|
+
handleCreateExtraVisitInfo: expect.anything(),
|
|
118
|
+
attributes: [
|
|
119
|
+
{
|
|
120
|
+
attributeType: 'caf2124f-00a9-4620-a250-efd8535afd6d',
|
|
121
|
+
value: '1c30ee58-82d4-4ea4-a8c1-4bf2f9dfc8cf',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
attributeType: '919b51c9-8e2e-468f-8354-181bf3e55786',
|
|
125
|
+
value: true,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function renderBillingCheckinForm() {
|
|
133
|
+
return render(<BillingCheckInForm {...testProps} />);
|
|
134
|
+
}
|