@openmrs/esm-billing-app 1.0.1-pre.14
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 +392 -0
- package/__mocks__/delivery-summary.mock.ts +87 -0
- package/__mocks__/encounter-observation.mock.ts +10649 -0
- package/__mocks__/encounter-observations.mock.ts +6187 -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/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/450.js +1 -0
- package/dist/450.js.map +1 -0
- package/dist/476.js +1 -0
- package/dist/476.js.map +1 -0
- package/dist/574.js +1 -0
- package/dist/757.js +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/96.js +2 -0
- package/dist/96.js.LICENSE.txt +47 -0
- package/dist/96.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 +462 -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 +26 -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 +123 -0
- package/playwright.config.ts +32 -0
- package/prettier.config.js +8 -0
- package/src/bill-history/bill-history.component.tsx +187 -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 +72 -0
- package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +108 -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 +135 -0
- package/src/billable-services/bill-waiver/utils.ts +41 -0
- package/src/billable-services/billable-service.resource.ts +71 -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 +42 -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 +131 -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 +25 -0
- package/src/billing-form/billing-form.resource.ts +31 -0
- package/src/billing-form/billing-form.scss +5 -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 +120 -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 +3 -0
- package/src/dashboard.meta.ts +6 -0
- package/src/declarations.d.ts +4 -0
- package/src/helpers/functions.ts +63 -0
- package/src/helpers/index.ts +1 -0
- package/src/index.ts +56 -0
- package/src/invoice/invoice-table.component.tsx +185 -0
- package/src/invoice/invoice-table.scss +91 -0
- package/src/invoice/invoice.component.tsx +138 -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 +68 -0
- package/src/invoice/payments/payment.resource.ts +43 -0
- package/src/invoice/payments/payments.component.tsx +140 -0
- package/src/invoice/payments/payments.scss +46 -0
- package/src/invoice/payments/utils.ts +30 -0
- package/src/invoice/payments/visit-tags/visit-attribute.component.tsx +21 -0
- package/src/invoice/printable-invoice/print-receipt.component.tsx +28 -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 +11 -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 +41 -0
- package/src/metrics-cards/metrics.resource.ts +45 -0
- package/src/modal/require-payment-modal.component.tsx +81 -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 +79 -0
- package/src/setup-tests.ts +13 -0
- package/src/types/index.ts +167 -0
- package/test-helpers.tsx +23 -0
- package/translations/am.json +107 -0
- package/translations/en.json +107 -0
- package/translations/es.json +107 -0
- package/translations/fr.json +107 -0
- package/translations/he.json +107 -0
- package/translations/km.json +107 -0
- package/tsconfig.json +16 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
margin: 2rem 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.emptyStateContainer,
|
|
10
|
+
.loaderContainer {
|
|
11
|
+
@extend .container;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.billListContainer {
|
|
15
|
+
background-color: $ui-02;
|
|
16
|
+
border: 1px solid $ui-03;
|
|
17
|
+
width: 100%;
|
|
18
|
+
margin: 0 auto;
|
|
19
|
+
max-width: 95vw;
|
|
20
|
+
padding-bottom: 0;
|
|
21
|
+
|
|
22
|
+
:has(.filterEmptyState) {
|
|
23
|
+
border-bottom: none;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.filterContainer {
|
|
28
|
+
flex: 1;
|
|
29
|
+
|
|
30
|
+
:global(.cds--dropdown__wrapper--inline) {
|
|
31
|
+
gap: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
:global(.cds--list-box__menu-icon) {
|
|
35
|
+
height: 1rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
:global(.cds--list-box__menu) {
|
|
39
|
+
min-width: max-content;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:global(.cds--list-box) {
|
|
43
|
+
margin-left: layout.$spacing-03;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.menu {
|
|
48
|
+
margin-left: layout.$spacing-03;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.headerContainer {
|
|
52
|
+
display: flex;
|
|
53
|
+
justify-content: space-between;
|
|
54
|
+
align-items: center;
|
|
55
|
+
padding: layout.$spacing-04 layout.$spacing-05;
|
|
56
|
+
background-color: $ui-02;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.backgroundDataFetchingIndicator {
|
|
60
|
+
align-items: center;
|
|
61
|
+
display: flex;
|
|
62
|
+
flex: 1;
|
|
63
|
+
justify-content: space-between;
|
|
64
|
+
|
|
65
|
+
&:global(.cds--inline-loading) {
|
|
66
|
+
max-height: 1rem;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.tableContainer section {
|
|
71
|
+
position: relative;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.tableContainer a {
|
|
75
|
+
text-decoration: none;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.pagination {
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
|
|
81
|
+
&:global(.cds--pagination) {
|
|
82
|
+
border-top: none;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.hiddenRow {
|
|
87
|
+
display: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.emptyRow {
|
|
91
|
+
padding: 0 1rem;
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.visitSummaryContainer {
|
|
97
|
+
width: 100%;
|
|
98
|
+
max-width: 768px;
|
|
99
|
+
margin: 1rem auto;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.expandedActiveVisitRow > td > div {
|
|
103
|
+
max-height: max-content !important;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.expandedActiveVisitRow td {
|
|
107
|
+
padding: 0 2rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.expandedActiveVisitRow th[colspan] td[colspan] > div:first-child {
|
|
111
|
+
padding: 0 1rem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.action {
|
|
115
|
+
margin-bottom: layout.$spacing-03;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.illo {
|
|
119
|
+
margin-top: layout.$spacing-05;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.content {
|
|
123
|
+
@include type.type-style('heading-compact-01');
|
|
124
|
+
color: $text-02;
|
|
125
|
+
margin-top: layout.$spacing-05;
|
|
126
|
+
margin-bottom: layout.$spacing-03;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.desktopHeading,
|
|
130
|
+
.tabletHeading {
|
|
131
|
+
text-align: left;
|
|
132
|
+
text-transform: capitalize;
|
|
133
|
+
flex: 1;
|
|
134
|
+
|
|
135
|
+
h4 {
|
|
136
|
+
@include type.type-style('heading-compact-02');
|
|
137
|
+
color: $text-02;
|
|
138
|
+
|
|
139
|
+
&:after {
|
|
140
|
+
content: '';
|
|
141
|
+
display: block;
|
|
142
|
+
width: 2rem;
|
|
143
|
+
padding-top: 3px;
|
|
144
|
+
border-bottom: 0.375rem solid;
|
|
145
|
+
@include brand-03(border-bottom-color);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.tile {
|
|
151
|
+
text-align: center;
|
|
152
|
+
border: 1px solid $ui-03;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.menuitem {
|
|
156
|
+
max-width: none;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.filterEmptyState {
|
|
160
|
+
display: flex;
|
|
161
|
+
justify-content: center;
|
|
162
|
+
align-items: center;
|
|
163
|
+
padding: layout.$spacing-05;
|
|
164
|
+
margin: layout.$spacing-09;
|
|
165
|
+
text-align: center;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.filterEmptyStateTile {
|
|
169
|
+
margin: auto;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.filterEmptyStateContent {
|
|
173
|
+
@include type.type-style('heading-compact-02');
|
|
174
|
+
color: $text-02;
|
|
175
|
+
margin-bottom: 0.5rem;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.filterEmptyStateHelper {
|
|
179
|
+
@include type.type-style('body-compact-01');
|
|
180
|
+
color: $text-02;
|
|
181
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import { useBills } from '../billing.resource';
|
|
5
|
+
import BillsTable from './bills-table.component';
|
|
6
|
+
|
|
7
|
+
const mockbills = useBills as jest.Mock;
|
|
8
|
+
|
|
9
|
+
const mockBillsData = [
|
|
10
|
+
{ uuid: '1', patientName: 'John Doe', identifier: '12345678', visitType: 'Checkup', patientUuid: 'uuid1' },
|
|
11
|
+
{ uuid: '2', patientName: 'Mary Smith', identifier: '98765432', visitType: 'Wake up', patientUuid: 'uuid2' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
jest.mock('../billing.resource', () => ({
|
|
15
|
+
...jest.requireActual('../billing.resource'),
|
|
16
|
+
useBills: jest.fn(() => ({
|
|
17
|
+
bills: mockBillsData,
|
|
18
|
+
isLoading: false,
|
|
19
|
+
isValidating: false,
|
|
20
|
+
error: null,
|
|
21
|
+
})),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
jest.mock('@openmrs/esm-framework', () => ({
|
|
25
|
+
...jest.requireActual('@openmrs/esm-framework'),
|
|
26
|
+
ErrorState: jest.fn(() => (
|
|
27
|
+
<div>
|
|
28
|
+
Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site
|
|
29
|
+
administrator and quote the error code above.
|
|
30
|
+
</div>
|
|
31
|
+
)),
|
|
32
|
+
useConfig: jest.fn(() => ({ bills: { pageSizes: [10, 20, 30, 40, 50], pageSize: 10 } })),
|
|
33
|
+
usePagination: jest.fn().mockImplementation((data) => ({
|
|
34
|
+
currentPage: 1,
|
|
35
|
+
goTo: () => {},
|
|
36
|
+
results: data,
|
|
37
|
+
paginated: false,
|
|
38
|
+
})),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
describe('BillsTable', () => {
|
|
42
|
+
let user;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
user = userEvent.setup();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
xit('renders data table with pending bills', () => {
|
|
49
|
+
render(<BillsTable />);
|
|
50
|
+
|
|
51
|
+
expect(screen.getByText('Visit time')).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByText('Identifier')).toBeInTheDocument();
|
|
53
|
+
const expectedColumnHeaders = [/Visit time/, /Identifier/, /Name/, /Billing service/];
|
|
54
|
+
expectedColumnHeaders.forEach((header) => {
|
|
55
|
+
expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const patientNameLink = screen.getByText('John Doe');
|
|
59
|
+
expect(patientNameLink).toBeInTheDocument();
|
|
60
|
+
expect(patientNameLink.tagName).toBe('A');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('displays empty state when there are no bills', () => {
|
|
64
|
+
mockbills.mockImplementationOnce(() => ({
|
|
65
|
+
bills: [],
|
|
66
|
+
isLoading: false,
|
|
67
|
+
isValidating: false,
|
|
68
|
+
error: null,
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
render(<BillsTable />);
|
|
72
|
+
|
|
73
|
+
expect(screen.getByText(/there are no bills to display/i)).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should not display the table when the data is loading', () => {
|
|
77
|
+
mockbills.mockImplementationOnce(() => ({
|
|
78
|
+
bills: undefined,
|
|
79
|
+
isLoading: true,
|
|
80
|
+
isValidating: false,
|
|
81
|
+
error: null,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
render(<BillsTable />);
|
|
85
|
+
|
|
86
|
+
const expectedColumnHeaders = [/Visit time/, /Identifier/, /Name/, /Billing service/, /Department/];
|
|
87
|
+
expectedColumnHeaders.forEach((header) => {
|
|
88
|
+
expect(screen.queryByRole('columnheader', { name: new RegExp(header, 'i') })).not.toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should display the error state when there is error', () => {
|
|
93
|
+
mockbills.mockImplementationOnce(() => ({
|
|
94
|
+
activeVisits: undefined,
|
|
95
|
+
isLoading: false,
|
|
96
|
+
isValidating: false,
|
|
97
|
+
error: 'Error in fetching data',
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
render(<BillsTable />);
|
|
101
|
+
|
|
102
|
+
expect(screen.getByText(/sorry, there was a problem displaying this information/i)).toBeInTheDocument();
|
|
103
|
+
expect(screen.queryByRole('table')).not.toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('should filter bills by search term and bill payment status', async () => {
|
|
107
|
+
render(<BillsTable />);
|
|
108
|
+
|
|
109
|
+
const searchInput = screen.getByRole('searchbox');
|
|
110
|
+
await user.type(searchInput, 'John Doe');
|
|
111
|
+
|
|
112
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
113
|
+
expect(screen.queryByText('Mary Smith')).not.toBeInTheDocument();
|
|
114
|
+
|
|
115
|
+
await user.clear(searchInput);
|
|
116
|
+
await user.type(searchInput, 'Mary Smith');
|
|
117
|
+
|
|
118
|
+
expect(screen.getByText('Mary Smith')).toBeInTheDocument();
|
|
119
|
+
expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
|
|
120
|
+
|
|
121
|
+
// Should filter the table when bill payment status combobox is changed
|
|
122
|
+
const billCategorySelect = screen.getByRole('combobox');
|
|
123
|
+
expect(billCategorySelect).toBeInTheDocument();
|
|
124
|
+
await user.click(billCategorySelect, { name: 'All bills' });
|
|
125
|
+
expect(mockbills).toHaveBeenCalledWith('', '');
|
|
126
|
+
|
|
127
|
+
await user.click(screen.getByText('Pending bills'));
|
|
128
|
+
expect(screen.getByText('Pending bills')).toBeInTheDocument();
|
|
129
|
+
expect(mockbills).toHaveBeenCalledWith('', 'PENDING');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('should show the loading spinner while retrieving data', () => {
|
|
133
|
+
mockbills.mockImplementationOnce(() => ({
|
|
134
|
+
bills: undefined,
|
|
135
|
+
isLoading: true,
|
|
136
|
+
isValidating: false,
|
|
137
|
+
error: null,
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
render(<BillsTable />);
|
|
141
|
+
|
|
142
|
+
const dataTableSkeleton = screen.getByRole('table');
|
|
143
|
+
expect(dataTableSkeleton).toBeInTheDocument();
|
|
144
|
+
expect(dataTableSkeleton).toHaveClass('cds--skeleton cds--data-table cds--data-table--zebra');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('should render patient name as a link', async () => {
|
|
148
|
+
render(<BillsTable />);
|
|
149
|
+
|
|
150
|
+
const patientNameLink = screen.getByRole('link', { name: 'John Doe' });
|
|
151
|
+
expect(patientNameLink).toBeInTheDocument();
|
|
152
|
+
expect(patientNameLink).toHaveAttribute('href', '/openmrs/spa/home/billing/patient/uuid1/1');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type Payment, type LineItem } from '../types';
|
|
2
|
+
|
|
3
|
+
// amount already paid
|
|
4
|
+
export function calculateTotalAmountTendered(payments: Array<Payment>) {
|
|
5
|
+
return Array.isArray(payments)
|
|
6
|
+
? payments.reduce((totalAmount, item) => {
|
|
7
|
+
// Ensure that "amount" property is present and numeric
|
|
8
|
+
if (typeof item.amount === 'number' && item.voided !== true) {
|
|
9
|
+
return totalAmount + item.amount;
|
|
10
|
+
}
|
|
11
|
+
return totalAmount;
|
|
12
|
+
}, 0)
|
|
13
|
+
: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// balance
|
|
17
|
+
export function calculateTotalBalance(lineItems: Array<LineItem>, payments: Array<Payment>) {
|
|
18
|
+
return Math.min(this.calculateTotalAmount(lineItems) - this.calculateTotalAmountTendered(payments));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// total bill
|
|
22
|
+
export function calculateTotalAmount(lineItems: Array<LineItem>) {
|
|
23
|
+
return Array.isArray(lineItems)
|
|
24
|
+
? lineItems.reduce((totalAmount, item) => {
|
|
25
|
+
// Ensure that "price" and "quantity" properties are present and numeric
|
|
26
|
+
if (typeof item.price === 'number' && typeof item.quantity === 'number' && item.voided !== true) {
|
|
27
|
+
return totalAmount + item.price * item.quantity;
|
|
28
|
+
}
|
|
29
|
+
return totalAmount;
|
|
30
|
+
}, 0)
|
|
31
|
+
: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const convertToCurrency = (amountToConvert: number) => {
|
|
35
|
+
const formatter = new Intl.NumberFormat('en-KE', {
|
|
36
|
+
style: 'currency',
|
|
37
|
+
currency: 'KES',
|
|
38
|
+
minimumFractionDigits: 2,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let formattedAmount = formatter.format(Math.abs(amountToConvert));
|
|
42
|
+
|
|
43
|
+
if (amountToConvert < 0) {
|
|
44
|
+
formattedAmount = `(${formattedAmount})`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return formattedAmount;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const getGender = (gender: string, t) => {
|
|
51
|
+
switch (gender) {
|
|
52
|
+
case 'male':
|
|
53
|
+
return t('male', 'Male');
|
|
54
|
+
case 'female':
|
|
55
|
+
return t('female', 'Female');
|
|
56
|
+
case 'other':
|
|
57
|
+
return t('other', 'Other');
|
|
58
|
+
case 'unknown':
|
|
59
|
+
return t('unknown', 'Unknown');
|
|
60
|
+
default:
|
|
61
|
+
return gender;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './functions';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { configSchema } from './config-schema';
|
|
2
|
+
import { createDashboardLink } from '@openmrs/esm-patient-common-lib';
|
|
3
|
+
import { createLeftPanelLink } from './left-panel-link.component';
|
|
4
|
+
import { dashboardMeta } from './dashboard.meta';
|
|
5
|
+
import { defineConfigSchema, getSyncLifecycle } from '@openmrs/esm-framework';
|
|
6
|
+
import BillableServiceHome from './billable-services/billable-services-home.component';
|
|
7
|
+
import BillableServicesCardLink from './billable-services-admin-card-link.component';
|
|
8
|
+
import BillHistory from './bill-history/bill-history.component';
|
|
9
|
+
import BillingCheckInForm from './billing-form/billing-checkin-form.component';
|
|
10
|
+
import BillingForm from './billing-form/billing-form.component';
|
|
11
|
+
import RequirePaymentModal from './modal/require-payment-modal.component';
|
|
12
|
+
import RootComponent from './root.component';
|
|
13
|
+
import VisitAttributeTags from './invoice/payments/visit-tags/visit-attribute.component';
|
|
14
|
+
import BillableServicesDashboard from './billable-services/dashboard/dashboard.component';
|
|
15
|
+
import ServiceMetrics from './billable-services/dashboard/service-metrics.component';
|
|
16
|
+
|
|
17
|
+
const moduleName = '@openmrs/esm-billing-app';
|
|
18
|
+
|
|
19
|
+
const options = {
|
|
20
|
+
featureName: 'billing',
|
|
21
|
+
moduleName,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// t('billing', 'Billing')
|
|
25
|
+
export const billingDashboardLink = getSyncLifecycle(
|
|
26
|
+
createLeftPanelLink({
|
|
27
|
+
name: 'billing',
|
|
28
|
+
title: 'Billing',
|
|
29
|
+
}),
|
|
30
|
+
options,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
|
|
34
|
+
|
|
35
|
+
export function startupApp() {
|
|
36
|
+
defineConfigSchema(moduleName, configSchema);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const billingSummaryDashboardLink = getSyncLifecycle(
|
|
40
|
+
createDashboardLink({ ...dashboardMeta, moduleName }),
|
|
41
|
+
options,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export const billingServicesTiles = getSyncLifecycle(ServiceMetrics, {
|
|
45
|
+
featureName: 'billing-home-tiles',
|
|
46
|
+
moduleName,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const billableServicesCardLink = getSyncLifecycle(BillableServicesCardLink, options);
|
|
50
|
+
export const billableServicesHome = getSyncLifecycle(BillableServiceHome, options);
|
|
51
|
+
export const billingCheckInForm = getSyncLifecycle(BillingCheckInForm, options);
|
|
52
|
+
export const billingForm = getSyncLifecycle(BillingForm, options);
|
|
53
|
+
export const billingPatientSummary = getSyncLifecycle(BillHistory, options);
|
|
54
|
+
export const requirePaymentModal = getSyncLifecycle(RequirePaymentModal, options);
|
|
55
|
+
export const root = getSyncLifecycle(RootComponent, options);
|
|
56
|
+
export const visitAttributeTags = getSyncLifecycle(VisitAttributeTags, options);
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import fuzzy from 'fuzzy';
|
|
4
|
+
import {
|
|
5
|
+
DataTable,
|
|
6
|
+
DataTableSkeleton,
|
|
7
|
+
Layer,
|
|
8
|
+
Table,
|
|
9
|
+
TableBody,
|
|
10
|
+
TableCell,
|
|
11
|
+
TableContainer,
|
|
12
|
+
TableHead,
|
|
13
|
+
TableHeader,
|
|
14
|
+
TableRow,
|
|
15
|
+
TableToolbar,
|
|
16
|
+
TableToolbarContent,
|
|
17
|
+
TableToolbarSearch,
|
|
18
|
+
TableSelectRow,
|
|
19
|
+
Tile,
|
|
20
|
+
type DataTableHeader,
|
|
21
|
+
type DataTableRow,
|
|
22
|
+
} from '@carbon/react';
|
|
23
|
+
import { isDesktop, useDebounce, useLayoutType } from '@openmrs/esm-framework';
|
|
24
|
+
import { type LineItem, type MappedBill } from '../types';
|
|
25
|
+
import styles from './invoice-table.scss';
|
|
26
|
+
|
|
27
|
+
type InvoiceTableProps = {
|
|
28
|
+
bill: MappedBill;
|
|
29
|
+
isSelectable?: boolean;
|
|
30
|
+
isLoadingBill?: boolean;
|
|
31
|
+
onSelectItem?: (selectedLineItems: LineItem[]) => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true, isLoadingBill, onSelectItem }) => {
|
|
35
|
+
const { t } = useTranslation();
|
|
36
|
+
const { lineItems } = bill;
|
|
37
|
+
const layout = useLayoutType();
|
|
38
|
+
const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
|
|
39
|
+
const [selectedLineItems, setSelectedLineItems] = useState([]);
|
|
40
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
41
|
+
const debouncedSearchTerm = useDebounce(searchTerm);
|
|
42
|
+
|
|
43
|
+
const filteredLineItems = useMemo(() => {
|
|
44
|
+
if (!debouncedSearchTerm) {
|
|
45
|
+
return lineItems;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return debouncedSearchTerm
|
|
49
|
+
? fuzzy
|
|
50
|
+
.filter(debouncedSearchTerm, lineItems, {
|
|
51
|
+
extract: (lineItem: LineItem) => `${lineItem.item}`,
|
|
52
|
+
})
|
|
53
|
+
.sort((r1, r2) => r1.score - r2.score)
|
|
54
|
+
.map((result) => result.original)
|
|
55
|
+
: lineItems;
|
|
56
|
+
}, [debouncedSearchTerm, lineItems]);
|
|
57
|
+
|
|
58
|
+
const tableHeaders: Array<typeof DataTableHeader> = [
|
|
59
|
+
{ header: 'No', key: 'no' },
|
|
60
|
+
{ header: 'Bill item', key: 'billItem' },
|
|
61
|
+
{ header: 'Bill code', key: 'billCode' },
|
|
62
|
+
{ header: 'Status', key: 'status' },
|
|
63
|
+
{ header: 'Quantity', key: 'quantity' },
|
|
64
|
+
{ header: 'Price', key: 'price' },
|
|
65
|
+
{ header: 'Total', key: 'total' },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const tableRows: Array<typeof DataTableRow> = useMemo(
|
|
69
|
+
() =>
|
|
70
|
+
filteredLineItems?.map((item, index) => {
|
|
71
|
+
return {
|
|
72
|
+
no: `${index + 1}`,
|
|
73
|
+
id: `${item.uuid}`,
|
|
74
|
+
billItem: item.item || item.billableService,
|
|
75
|
+
billCode: bill.receiptNumber,
|
|
76
|
+
status: bill.status,
|
|
77
|
+
quantity: item.quantity,
|
|
78
|
+
price: item.price,
|
|
79
|
+
total: item.price * item.quantity,
|
|
80
|
+
};
|
|
81
|
+
}) ?? [],
|
|
82
|
+
[bill.receiptNumber, bill.status, filteredLineItems],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (isLoadingBill) {
|
|
86
|
+
return (
|
|
87
|
+
<div className={styles.loaderContainer}>
|
|
88
|
+
<DataTableSkeleton
|
|
89
|
+
columnCount={tableHeaders.length}
|
|
90
|
+
showHeader={false}
|
|
91
|
+
showToolbar={false}
|
|
92
|
+
size={responsiveSize}
|
|
93
|
+
zebra
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const handleRowSelection = (row: typeof DataTableRow, checked: boolean) => {
|
|
100
|
+
const matchingRow = filteredLineItems.find((item) => item.uuid === row.id);
|
|
101
|
+
let newSelectedLineItems;
|
|
102
|
+
|
|
103
|
+
if (checked) {
|
|
104
|
+
newSelectedLineItems = [...selectedLineItems, matchingRow];
|
|
105
|
+
} else {
|
|
106
|
+
newSelectedLineItems = selectedLineItems.filter((item) => item.uuid !== row.id);
|
|
107
|
+
}
|
|
108
|
+
setSelectedLineItems(newSelectedLineItems);
|
|
109
|
+
onSelectItem(newSelectedLineItems);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className={styles.invoiceContainer}>
|
|
114
|
+
<DataTable headers={tableHeaders} isSortable rows={tableRows} size={responsiveSize} useZebraStyles>
|
|
115
|
+
{({ rows, headers, getRowProps, getSelectionProps, getTableProps, getToolbarProps }) => (
|
|
116
|
+
<TableContainer
|
|
117
|
+
description={
|
|
118
|
+
<span className={styles.tableDescription}>
|
|
119
|
+
<span>{t('itemsToBeBilled', 'Items to be billed')}</span>
|
|
120
|
+
</span>
|
|
121
|
+
}
|
|
122
|
+
title={t('lineItems', 'Line items')}>
|
|
123
|
+
<div className={styles.toolbarWrapper}>
|
|
124
|
+
<TableToolbar {...getToolbarProps()} className={styles.tableToolbar} size={responsiveSize}>
|
|
125
|
+
<TableToolbarContent className={styles.headerContainer}>
|
|
126
|
+
<TableToolbarSearch
|
|
127
|
+
className={styles.searchbox}
|
|
128
|
+
expanded
|
|
129
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
|
130
|
+
placeholder={t('searchThisTable', 'Search this table')}
|
|
131
|
+
size={responsiveSize}
|
|
132
|
+
/>
|
|
133
|
+
</TableToolbarContent>
|
|
134
|
+
</TableToolbar>
|
|
135
|
+
</div>
|
|
136
|
+
<Table {...getTableProps()} aria-label="Invoice line items" className={styles.table}>
|
|
137
|
+
<TableHead>
|
|
138
|
+
<TableRow>
|
|
139
|
+
{rows.length > 1 && isSelectable ? <TableHeader /> : null}
|
|
140
|
+
{headers.map((header) => (
|
|
141
|
+
<TableHeader key={header.key}>{header.header}</TableHeader>
|
|
142
|
+
))}
|
|
143
|
+
</TableRow>
|
|
144
|
+
</TableHead>
|
|
145
|
+
<TableBody>
|
|
146
|
+
{rows.map((row) => (
|
|
147
|
+
<TableRow
|
|
148
|
+
key={row.id}
|
|
149
|
+
{...getRowProps({
|
|
150
|
+
row,
|
|
151
|
+
})}>
|
|
152
|
+
{rows.length > 1 && isSelectable && (
|
|
153
|
+
<TableSelectRow
|
|
154
|
+
aria-label="Select row"
|
|
155
|
+
{...getSelectionProps({ row })}
|
|
156
|
+
onChange={(checked: boolean) => handleRowSelection(row, checked)}
|
|
157
|
+
/>
|
|
158
|
+
)}
|
|
159
|
+
{row.cells.map((cell) => (
|
|
160
|
+
<TableCell key={cell.id}>{cell.value}</TableCell>
|
|
161
|
+
))}
|
|
162
|
+
</TableRow>
|
|
163
|
+
))}
|
|
164
|
+
</TableBody>
|
|
165
|
+
</Table>
|
|
166
|
+
</TableContainer>
|
|
167
|
+
)}
|
|
168
|
+
</DataTable>
|
|
169
|
+
{filteredLineItems?.length === 0 && (
|
|
170
|
+
<div className={styles.filterEmptyState}>
|
|
171
|
+
<Layer>
|
|
172
|
+
<Tile className={styles.filterEmptyStateTile}>
|
|
173
|
+
<p className={styles.filterEmptyStateContent}>
|
|
174
|
+
{t('noMatchingItemsToDisplay', 'No matching items to display')}
|
|
175
|
+
</p>
|
|
176
|
+
<p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
|
|
177
|
+
</Tile>
|
|
178
|
+
</Layer>
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export default InvoiceTable;
|