@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,42 @@
|
|
|
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
|
+
<ExtensionSlot name="billing-home-tiles-slot" />
|
|
40
|
+
</section>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -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,131 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { Dropdown, InlineLoading, InlineNotification } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { showSnackbar } 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
|
+
setBillingInfo: (state) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, setBillingInfo }) => {
|
|
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 shouldBillPatient =
|
|
26
|
+
attributes.find((item) => item.attributeType === 'caf2124f-00a9-4620-a250-efd8535afd6d')?.value ===
|
|
27
|
+
'1c30ee58-82d4-4ea4-a8c1-4bf2f9dfc8cf';
|
|
28
|
+
|
|
29
|
+
const handleCreateBill = useCallback(
|
|
30
|
+
(createBillPayload) => {
|
|
31
|
+
shouldBillPatient &&
|
|
32
|
+
createPatientBill(createBillPayload).then(
|
|
33
|
+
() => {
|
|
34
|
+
showSnackbar({ title: 'Patient Bill', subtitle: 'Patient has been billed successfully', kind: 'success' });
|
|
35
|
+
},
|
|
36
|
+
(error) => {
|
|
37
|
+
showSnackbar({
|
|
38
|
+
title: 'Patient Bill Error',
|
|
39
|
+
subtitle: 'An error has occurred while creating patient bill',
|
|
40
|
+
kind: 'error',
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
[shouldBillPatient],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const handleBillingService = ({ selectedItem }) => {
|
|
49
|
+
const cashPointUuid = cashPoints?.[0]?.uuid ?? '';
|
|
50
|
+
const itemUuid = selectedItem?.uuid ?? '';
|
|
51
|
+
|
|
52
|
+
// should default to first price if check returns empty. todo - update backend to return default price
|
|
53
|
+
const priceForPaymentMode =
|
|
54
|
+
selectedItem.servicePrices.find((p) => p.paymentMode?.uuid === paymentMethod) || selectedItem?.servicePrices[0];
|
|
55
|
+
|
|
56
|
+
const createBillPayload = {
|
|
57
|
+
lineItems: [
|
|
58
|
+
{
|
|
59
|
+
billableService: itemUuid,
|
|
60
|
+
quantity: 1,
|
|
61
|
+
price: priceForPaymentMode ? priceForPaymentMode.price : '0.000',
|
|
62
|
+
priceName: 'Default',
|
|
63
|
+
priceUuid: priceForPaymentMode ? priceForPaymentMode.uuid : '',
|
|
64
|
+
lineItemOrder: 0,
|
|
65
|
+
paymentStatus: PENDING_PAYMENT_STATUS,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
cashPoint: cashPointUuid,
|
|
69
|
+
patient: patientUuid,
|
|
70
|
+
status: PENDING_PAYMENT_STATUS,
|
|
71
|
+
payments: [],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
setBillingInfo({ createBillPayload, handleCreateBill: () => handleCreateBill(createBillPayload), attributes });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (isLoadingLineItems || isLoadingCashPoints) {
|
|
78
|
+
return (
|
|
79
|
+
<InlineLoading
|
|
80
|
+
status="active"
|
|
81
|
+
iconDescription={t('loading', 'Loading')}
|
|
82
|
+
description={t('loadingBillingServices', 'Loading billing services...')}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (paymentMethod) {
|
|
88
|
+
lineList = [];
|
|
89
|
+
lineList = lineItems.filter((e) =>
|
|
90
|
+
e.servicePrices.some((p) => p.paymentMode && p.paymentMode.uuid === paymentMethod),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const setServicePrice = (prices) => {
|
|
95
|
+
const matchingPrice = prices.find((p) => p.paymentMode?.uuid === paymentMethod);
|
|
96
|
+
return matchingPrice ? `(${matchingPrice.name}:${matchingPrice.price})` : '';
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (cashError || lineError) {
|
|
100
|
+
return (
|
|
101
|
+
<InlineNotification
|
|
102
|
+
kind="error"
|
|
103
|
+
lowContrast
|
|
104
|
+
title={t('billErrorService', 'Bill service error')}
|
|
105
|
+
subtitle={t('errorLoadingBillServices', 'Error loading bill services')}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<section className={styles.sectionContainer}>
|
|
112
|
+
<VisitAttributesForm setAttributes={setAttributes} setPaymentMethod={setPaymentMethod} />
|
|
113
|
+
{shouldBillPatient && (
|
|
114
|
+
<>
|
|
115
|
+
<div className={styles.sectionTitle}>{t('billing', 'Billing')}</div>
|
|
116
|
+
<div className={styles.sectionField}></div>
|
|
117
|
+
<Dropdown
|
|
118
|
+
label={t('selectBillableService', 'Select a billable service...')}
|
|
119
|
+
onChange={handleBillingService}
|
|
120
|
+
id="billable-items"
|
|
121
|
+
items={lineList}
|
|
122
|
+
itemToString={(item) => (item ? `${item.name} ${setServicePrice(item.servicePrices)}` : '')}
|
|
123
|
+
titleText={t('billableService', 'Billable service')}
|
|
124
|
+
/>
|
|
125
|
+
</>
|
|
126
|
+
)}
|
|
127
|
+
</section>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
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', setBillingInfo: 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.setBillingInfo).toHaveBeenCalled();
|
|
99
|
+
expect(testProps.setBillingInfo).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
|
+
handleCreateBill: 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
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { RadioButtonGroup, RadioButton } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import styles from './billing-form.scss';
|
|
5
|
+
|
|
6
|
+
type BillingFormProps = {
|
|
7
|
+
patientUuid: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const BillingForm: React.FC<BillingFormProps> = ({ patientUuid }) => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
return (
|
|
13
|
+
<div className={styles.billingFormContainer}>
|
|
14
|
+
<RadioButtonGroup
|
|
15
|
+
legendText={t('selectCategory', 'Select category')}
|
|
16
|
+
name="radio-button-group"
|
|
17
|
+
defaultSelected="radio-1">
|
|
18
|
+
<RadioButton labelText={t('drug', 'Drug')} value="radio-1" id="radio-1" />
|
|
19
|
+
<RadioButton labelText={t('nonDrug', 'Non drug')} value="radio-2" id="radio-2" />
|
|
20
|
+
</RadioButtonGroup>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default BillingForm;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import useSWR from 'swr';
|
|
2
|
+
import { type OpenmrsResource, openmrsFetch } from '@openmrs/esm-framework';
|
|
3
|
+
|
|
4
|
+
export const useBillableItems = () => {
|
|
5
|
+
const url = `/ws/rest/v1/cashier/billableService?v=custom:(uuid,name,shortName,serviceStatus,serviceType:(display),servicePrices:(uuid,name,price,paymentMode))`;
|
|
6
|
+
const { data, isLoading, error } = useSWR<{ data: { results: Array<OpenmrsResource> } }>(url, openmrsFetch);
|
|
7
|
+
return {
|
|
8
|
+
lineItems: data?.data?.results ?? [],
|
|
9
|
+
isLoading,
|
|
10
|
+
error,
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const useCashPoint = () => {
|
|
15
|
+
const url = `/ws/rest/v1/cashier/cashPoint`;
|
|
16
|
+
const { data, isLoading, error } = useSWR<{ data: { results: Array<OpenmrsResource> } }>(url, openmrsFetch);
|
|
17
|
+
|
|
18
|
+
return { isLoading, error, cashPoints: data?.data?.results ?? [] };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const createPatientBill = (payload) => {
|
|
22
|
+
const postUrl = `/ws/rest/v1/cashier/bill`;
|
|
23
|
+
return openmrsFetch(postUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const usePaymentMethods = () => {
|
|
27
|
+
const url = `/ws/rest/v1/cashier/paymentMode`;
|
|
28
|
+
const { data, isLoading, error } = useSWR<{ data: { results: Array<OpenmrsResource> } }>(url, openmrsFetch);
|
|
29
|
+
|
|
30
|
+
return { isLoading, error, paymentModes: data?.data?.results ?? [] };
|
|
31
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
4
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { TextInput, InlineLoading, ComboBox, RadioButtonGroup, RadioButton } from '@carbon/react';
|
|
7
|
+
import { usePaymentMethods } from '../billing-form.resource';
|
|
8
|
+
import styles from './visit-attributes-form.scss';
|
|
9
|
+
|
|
10
|
+
type VisitAttributesFormProps = {
|
|
11
|
+
setAttributes: (state) => void;
|
|
12
|
+
setPaymentMethod?: (value: any) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type VisitAttributesFormValue = {
|
|
16
|
+
paymentDetails: string;
|
|
17
|
+
paymentMethods: string;
|
|
18
|
+
insuranceScheme: string;
|
|
19
|
+
policyNumber: string;
|
|
20
|
+
patientCategory: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const visitAttributesFormSchema = z.object({
|
|
24
|
+
paymentDetails: z.string(),
|
|
25
|
+
paymentMethods: z.string(),
|
|
26
|
+
insuranceSchema: z.string(),
|
|
27
|
+
policyNumber: z.string(),
|
|
28
|
+
patientCategory: z.string(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const VisitAttributesForm: React.FC<VisitAttributesFormProps> = ({ setAttributes, setPaymentMethod }) => {
|
|
32
|
+
const { t } = useTranslation();
|
|
33
|
+
const { control, getValues, watch } = useForm<VisitAttributesFormValue>({
|
|
34
|
+
mode: 'all',
|
|
35
|
+
defaultValues: {},
|
|
36
|
+
resolver: zodResolver(visitAttributesFormSchema),
|
|
37
|
+
});
|
|
38
|
+
const [paymentDetails, paymentMethods, insuranceSchema, policyNumber, patientCategory] = watch([
|
|
39
|
+
'paymentDetails',
|
|
40
|
+
'paymentMethods',
|
|
41
|
+
'insuranceScheme',
|
|
42
|
+
'policyNumber',
|
|
43
|
+
'patientCategory',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentMethods();
|
|
47
|
+
React.useEffect(() => {
|
|
48
|
+
setAttributes(createVisitAttributesPayload());
|
|
49
|
+
}, [paymentDetails, paymentMethods, insuranceSchema, policyNumber, patientCategory]);
|
|
50
|
+
|
|
51
|
+
const createVisitAttributesPayload = () => {
|
|
52
|
+
const { patientCategory, paymentMethods, policyNumber, paymentDetails } = getValues();
|
|
53
|
+
setPaymentMethod(paymentMethods);
|
|
54
|
+
const formPayload = [
|
|
55
|
+
{ uuid: 'caf2124f-00a9-4620-a250-efd8535afd6d', value: paymentDetails },
|
|
56
|
+
{ uuid: 'c39b684c-250f-4781-a157-d6ad7353bc90', value: paymentMethods },
|
|
57
|
+
{ uuid: '0f4f3306-f01b-43c6-af5b-fdb60015cb02', value: policyNumber },
|
|
58
|
+
{ uuid: '2d0fa959-6780-41f1-85b1-402045935068', value: insuranceSchema },
|
|
59
|
+
{ uuid: '3b9dfac8-9e4d-11ee-8c90-0242ac120002', value: patientCategory },
|
|
60
|
+
{ uuid: '919b51c9-8e2e-468f-8354-181bf3e55786', value: true },
|
|
61
|
+
];
|
|
62
|
+
const visitAttributesPayload = formPayload.filter(
|
|
63
|
+
(item) => item.value !== undefined && item.value !== null && item.value !== '',
|
|
64
|
+
);
|
|
65
|
+
return Object.entries(visitAttributesPayload).map(([key, value]) => ({
|
|
66
|
+
attributeType: value.uuid,
|
|
67
|
+
value: value.value,
|
|
68
|
+
}));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (isLoadingPaymentModes) {
|
|
72
|
+
return (
|
|
73
|
+
<InlineLoading
|
|
74
|
+
status="active"
|
|
75
|
+
iconDescription={t('loadingDescription', 'Loading')}
|
|
76
|
+
description={t('loading', 'Loading data...')}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<section>
|
|
83
|
+
<div className={styles.sectionTitle}>{t('paymentDetails', 'Payment Details')}</div>
|
|
84
|
+
<Controller
|
|
85
|
+
name="paymentDetails"
|
|
86
|
+
control={control}
|
|
87
|
+
render={({ field }) => (
|
|
88
|
+
<RadioButtonGroup
|
|
89
|
+
onChange={(selected) => field.onChange(selected)}
|
|
90
|
+
orientation="vertical"
|
|
91
|
+
legendText={t('paymentDetails', 'Payment Details')}
|
|
92
|
+
name="payment-details-group">
|
|
93
|
+
<RadioButton labelText="Paying" value="1c30ee58-82d4-4ea4-a8c1-4bf2f9dfc8cf" id="radio-1" />
|
|
94
|
+
<RadioButton labelText="Non paying" value="a28d7929-050a-4249-a61a-551e9b8cc102" id="radio-2" />
|
|
95
|
+
</RadioButtonGroup>
|
|
96
|
+
)}
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
{paymentDetails === '1c30ee58-82d4-4ea4-a8c1-4bf2f9dfc8cf' && (
|
|
100
|
+
<Controller
|
|
101
|
+
control={control}
|
|
102
|
+
name="paymentMethods"
|
|
103
|
+
render={({ field }) => (
|
|
104
|
+
<ComboBox
|
|
105
|
+
className={styles.sectionField}
|
|
106
|
+
onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)}
|
|
107
|
+
id="paymentMethods"
|
|
108
|
+
items={paymentModes}
|
|
109
|
+
itemToString={(item) => (item ? item.name : '')}
|
|
110
|
+
titleText={t('paymentMethods', 'Payment methods')}
|
|
111
|
+
placeholder={t('selectPaymentMethod', 'Select payment method')}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{paymentMethods === 'beac329b-f1dc-4a33-9e7c-d95821a137a6' &&
|
|
118
|
+
paymentDetails === '1c30ee58-82d4-4ea4-a8c1-4bf2f9dfc8cf' && (
|
|
119
|
+
<>
|
|
120
|
+
<Controller
|
|
121
|
+
control={control}
|
|
122
|
+
name="insuranceScheme"
|
|
123
|
+
render={({ field }) => (
|
|
124
|
+
<TextInput
|
|
125
|
+
className={styles.sectionField}
|
|
126
|
+
onChange={(e) => field.onChange(e.target.value)}
|
|
127
|
+
id="insurance-scheme"
|
|
128
|
+
type="text"
|
|
129
|
+
labelText={t('insuranceScheme', 'Insurance scheme')}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
/>
|
|
133
|
+
<Controller
|
|
134
|
+
control={control}
|
|
135
|
+
name="policyNumber"
|
|
136
|
+
render={({ field }) => (
|
|
137
|
+
<TextInput
|
|
138
|
+
className={styles.sectionField}
|
|
139
|
+
onChange={(e) => field.onChange(e.target.value)}
|
|
140
|
+
{...field}
|
|
141
|
+
id="policy-number"
|
|
142
|
+
type="text"
|
|
143
|
+
labelText={t('policyNumber', 'Policy number')}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
/>
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{paymentDetails === 'a28d7929-050a-4249-a61a-551e9b8cc102' && (
|
|
151
|
+
<Controller
|
|
152
|
+
control={control}
|
|
153
|
+
name="patientCategory"
|
|
154
|
+
render={({ field }) => (
|
|
155
|
+
<ComboBox
|
|
156
|
+
className={styles.sectionField}
|
|
157
|
+
onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)}
|
|
158
|
+
id="patientCategory"
|
|
159
|
+
items={[
|
|
160
|
+
{ text: 'Child under 5', uuid: '2d61b762-6e32-4e2e-811f-ac72cbd3600a' },
|
|
161
|
+
{ text: 'Student', uuid: '159465AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
|
|
162
|
+
]}
|
|
163
|
+
itemToString={(item) => (item ? item.text : '')}
|
|
164
|
+
titleText={t('patientCategory', 'Patient category')}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
</section>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export default VisitAttributesForm;
|