@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,91 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
5
|
+
|
|
6
|
+
.filterEmptyState {
|
|
7
|
+
align-items: center;
|
|
8
|
+
background-color: white;
|
|
9
|
+
display: flex;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
padding: layout.$spacing-09 !important;
|
|
12
|
+
text-align: center;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.filterEmptyStateTile {
|
|
16
|
+
margin: auto;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.filterEmptyStateContent {
|
|
20
|
+
@include type.type-style('heading-compact-02');
|
|
21
|
+
color: $text-02;
|
|
22
|
+
margin-bottom: 0.5rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.filterEmptyStateHelper {
|
|
26
|
+
@include type.type-style('body-compact-01');
|
|
27
|
+
color: $text-02;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.headerContainer {
|
|
31
|
+
background-color: colors.$gray-10;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.invoiceContainer {
|
|
35
|
+
margin: layout.$spacing-09 layout.$spacing-05 0;
|
|
36
|
+
border: 1px solid $ui-03;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.searchbox {
|
|
40
|
+
input:focus {
|
|
41
|
+
outline: 2px solid colors.$orange-40 !important;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.table {
|
|
46
|
+
td {
|
|
47
|
+
border-bottom: none !important;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.tableDescription {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: flex-start;
|
|
54
|
+
margin-top: layout.$spacing-02;
|
|
55
|
+
column-gap: layout.$spacing-01;
|
|
56
|
+
|
|
57
|
+
::after {
|
|
58
|
+
content: '';
|
|
59
|
+
display: block;
|
|
60
|
+
width: 2rem;
|
|
61
|
+
padding-top: 0.188rem;
|
|
62
|
+
border-bottom: 0.375rem solid var(--brand-03);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
& > span {
|
|
66
|
+
@include type.type-style('body-01');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.tableToolbar {
|
|
71
|
+
width: 20%;
|
|
72
|
+
min-width: 12.5rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.toolbarWrapper {
|
|
76
|
+
position: relative;
|
|
77
|
+
display: flex;
|
|
78
|
+
justify-content: flex-end;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
:global(.omrs-breakpoint-lt-desktop) {
|
|
82
|
+
.toolbarWrapper {
|
|
83
|
+
height: layout.$spacing-09;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
:global(.omrs-breakpoint-gt-tablet) {
|
|
88
|
+
.toolbarWrapper {
|
|
89
|
+
height: layout.$spacing-07;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Button, InlineLoading } from '@carbon/react';
|
|
3
|
+
import { Printer } from '@carbon/react/icons';
|
|
4
|
+
import { useParams } from 'react-router-dom';
|
|
5
|
+
import { useReactToPrint } from 'react-to-print';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { ExtensionSlot, usePatient } from '@openmrs/esm-framework';
|
|
8
|
+
import { ErrorState } from '@openmrs/esm-patient-common-lib';
|
|
9
|
+
import { convertToCurrency } from '../helpers';
|
|
10
|
+
import { type LineItem } from '../types';
|
|
11
|
+
import { useBill } from '../billing.resource';
|
|
12
|
+
import InvoiceTable from './invoice-table.component';
|
|
13
|
+
import Payments from './payments/payments.component';
|
|
14
|
+
import PrintReceipt from './printable-invoice/print-receipt.component';
|
|
15
|
+
import PrintableInvoice from './printable-invoice/printable-invoice.component';
|
|
16
|
+
import styles from './invoice.scss';
|
|
17
|
+
|
|
18
|
+
interface InvoiceDetailsProps {
|
|
19
|
+
label: string;
|
|
20
|
+
value: string | number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const Invoice: React.FC = () => {
|
|
24
|
+
const { t } = useTranslation();
|
|
25
|
+
const { billUuid, patientUuid } = useParams();
|
|
26
|
+
const { patient, isLoading: isLoadingPatient } = usePatient(patientUuid);
|
|
27
|
+
const { bill, isLoading: isLoadingBill, error } = useBill(billUuid);
|
|
28
|
+
const [isPrinting, setIsPrinting] = useState(false);
|
|
29
|
+
const [selectedLineItems, setSelectedLineItems] = useState([]);
|
|
30
|
+
const componentRef = useRef<HTMLDivElement>(null);
|
|
31
|
+
const onBeforeGetContentResolve = useRef<(() => void) | null>(null);
|
|
32
|
+
const handleSelectItem = (lineItems: Array<LineItem>) => {
|
|
33
|
+
setSelectedLineItems(lineItems);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleAfterPrint = useCallback(() => {
|
|
37
|
+
onBeforeGetContentResolve.current = null;
|
|
38
|
+
setIsPrinting(false);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const reactToPrintContent = useCallback(() => componentRef.current, []);
|
|
42
|
+
|
|
43
|
+
const handleOnBeforeGetContent = useCallback(() => {
|
|
44
|
+
return new Promise<void>((resolve) => {
|
|
45
|
+
if (patient && bill) {
|
|
46
|
+
setIsPrinting(true);
|
|
47
|
+
onBeforeGetContentResolve.current = resolve;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}, [bill, patient]);
|
|
51
|
+
|
|
52
|
+
const handlePrint = useReactToPrint({
|
|
53
|
+
content: reactToPrintContent,
|
|
54
|
+
documentTitle: `Invoice ${bill?.receiptNumber} - ${patient?.name?.[0]?.given?.join(' ')} ${
|
|
55
|
+
patient?.name?.[0].family
|
|
56
|
+
}`,
|
|
57
|
+
onBeforeGetContent: handleOnBeforeGetContent,
|
|
58
|
+
onAfterPrint: handleAfterPrint,
|
|
59
|
+
removeAfterPrint: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (isPrinting && onBeforeGetContentResolve.current) {
|
|
64
|
+
onBeforeGetContentResolve.current();
|
|
65
|
+
}
|
|
66
|
+
}, [isPrinting]);
|
|
67
|
+
|
|
68
|
+
const invoiceDetails = {
|
|
69
|
+
'Total Amount': convertToCurrency(bill?.totalAmount),
|
|
70
|
+
'Amount Tendered': convertToCurrency(bill?.tenderedAmount),
|
|
71
|
+
'Invoice Number': bill.receiptNumber,
|
|
72
|
+
'Date And Time': bill?.dateCreated,
|
|
73
|
+
'Invoice Status': bill?.status,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (isLoadingPatient && isLoadingBill) {
|
|
77
|
+
return (
|
|
78
|
+
<div className={styles.invoiceContainer}>
|
|
79
|
+
<InlineLoading
|
|
80
|
+
className={styles.loader}
|
|
81
|
+
status="active"
|
|
82
|
+
iconDescription="Loading"
|
|
83
|
+
description="Loading patient header..."
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (error) {
|
|
90
|
+
return (
|
|
91
|
+
<div className={styles.errorContainer}>
|
|
92
|
+
<ErrorState headerTitle={t('invoiceError', 'Invoice error')} error={error} />
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className={styles.invoiceContainer}>
|
|
99
|
+
{patient && patientUuid && <ExtensionSlot name="patient-header-slot" state={{ patient, patientUuid }} />}
|
|
100
|
+
<div className={styles.detailsContainer}>
|
|
101
|
+
<section className={styles.details}>
|
|
102
|
+
{Object.entries(invoiceDetails).map(([key, val]) => (
|
|
103
|
+
<InvoiceDetails key={key} label={key} value={val} />
|
|
104
|
+
))}
|
|
105
|
+
</section>
|
|
106
|
+
<div>
|
|
107
|
+
<Button
|
|
108
|
+
disabled={isPrinting}
|
|
109
|
+
onClick={handlePrint}
|
|
110
|
+
renderIcon={(props) => <Printer size={24} {...props} />}
|
|
111
|
+
iconDescription="Print bill"
|
|
112
|
+
size="md">
|
|
113
|
+
{t('printBill', 'Print bill')}
|
|
114
|
+
</Button>
|
|
115
|
+
{bill.status === 'PAID' ? <PrintReceipt billId={bill?.id} /> : null}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<InvoiceTable bill={bill} isLoadingBill={isLoadingBill} onSelectItem={handleSelectItem} />
|
|
120
|
+
<Payments bill={bill} selectedLineItems={selectedLineItems} />
|
|
121
|
+
|
|
122
|
+
<div className={styles.printContainer} ref={componentRef}>
|
|
123
|
+
{isPrinting && <PrintableInvoice bill={bill} patient={patient} isLoading={isLoadingPatient} />}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function InvoiceDetails({ label, value }: InvoiceDetailsProps) {
|
|
130
|
+
return (
|
|
131
|
+
<div>
|
|
132
|
+
<h1 className={styles.label}>{label}</h1>
|
|
133
|
+
<span className={styles.value}>{value}</span>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default Invoice;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.invoiceContainer {
|
|
6
|
+
background-color: colors.$gray-10;
|
|
7
|
+
height: calc(100vh - 3rem);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.errorContainer {
|
|
11
|
+
margin: layout.$spacing-05;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.loader {
|
|
15
|
+
display: flex;
|
|
16
|
+
min-height: layout.$spacing-09;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.detailsContainer {
|
|
21
|
+
display: flex;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.details {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex: 3;
|
|
27
|
+
flex-flow: row wrap;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: space-between;
|
|
30
|
+
margin: layout.$spacing-05;
|
|
31
|
+
row-gap: 1.5rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.label {
|
|
35
|
+
@include type.type-style('body-compact-02');
|
|
36
|
+
color: colors.$gray-70;
|
|
37
|
+
margin: layout.$spacing-01;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.value {
|
|
41
|
+
@include type.type-style('heading-03');
|
|
42
|
+
display: inline-block;
|
|
43
|
+
margin-top: layout.$spacing-04;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.backButton {
|
|
47
|
+
margin: layout.$spacing-04;
|
|
48
|
+
|
|
49
|
+
button {
|
|
50
|
+
display: flex;
|
|
51
|
+
padding-left: 0 !important;
|
|
52
|
+
|
|
53
|
+
svg {
|
|
54
|
+
order: 1;
|
|
55
|
+
margin-right: layout.$spacing-03;
|
|
56
|
+
margin-left: 0 !important;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
span {
|
|
60
|
+
order: 2;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.invoicePaymentsContainer {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
margin: layout.$spacing-05;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.paymentSection {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: row;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.billDetail {
|
|
77
|
+
font-weight: bold;
|
|
78
|
+
color: colors.$cool-gray-90;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@media screen {
|
|
82
|
+
.printContainer {
|
|
83
|
+
background-color: colors.$white;
|
|
84
|
+
display: none;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@media print {
|
|
89
|
+
html,
|
|
90
|
+
body {
|
|
91
|
+
background-color: colors.$white !important;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -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
|
+
}
|