@openmrs/esm-billing-app 1.0.1-pre.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +12 -0
- package/.eslintignore +2 -0
- package/.eslintrc +57 -0
- package/.husky/pre-commit +7 -0
- package/.husky/pre-push +6 -0
- package/.prettierignore +14 -0
- package/.turbo.json +18 -0
- package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
- package/LICENSE +401 -0
- package/README.md +7 -0
- package/__mocks__/bills.mock.ts +394 -0
- package/__mocks__/delivery-summary.mock.ts +89 -0
- package/__mocks__/encounter-observation.mock.ts +10651 -0
- package/__mocks__/encounter-observations.mock.ts +6189 -0
- package/__mocks__/hiv-summary.mock.ts +22 -0
- package/__mocks__/patient-summary.mock.ts +32 -0
- package/__mocks__/patient.mock.ts +59 -0
- package/__mocks__/program-summary.mock.ts +43 -0
- package/__mocks__/react-i18next.js +57 -0
- package/dist/146.js +1 -0
- package/dist/146.js.map +1 -0
- package/dist/294.js +2 -0
- package/dist/294.js.LICENSE.txt +9 -0
- package/dist/294.js.map +1 -0
- package/dist/319.js +1 -0
- package/dist/384.js +1 -0
- package/dist/384.js.map +1 -0
- package/dist/421.js +1 -0
- package/dist/421.js.map +1 -0
- package/dist/533.js +1 -0
- package/dist/533.js.map +1 -0
- package/dist/574.js +1 -0
- package/dist/591.js +2 -0
- package/dist/591.js.LICENSE.txt +9 -0
- package/dist/591.js.map +1 -0
- package/dist/614.js +2 -0
- package/dist/614.js.LICENSE.txt +37 -0
- package/dist/614.js.map +1 -0
- package/dist/753.js +1 -0
- package/dist/753.js.map +1 -0
- package/dist/757.js +1 -0
- package/dist/770.js +1 -0
- package/dist/770.js.map +1 -0
- package/dist/783.js +1 -0
- package/dist/783.js.map +1 -0
- package/dist/788.js +1 -0
- package/dist/800.js +2 -0
- package/dist/800.js.LICENSE.txt +3 -0
- package/dist/800.js.map +1 -0
- package/dist/807.js +1 -0
- package/dist/833.js +1 -0
- package/dist/935.js +2 -0
- package/dist/935.js.LICENSE.txt +19 -0
- package/dist/935.js.map +1 -0
- package/dist/992.js +1 -0
- package/dist/992.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +47 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-billing-app.js +1 -0
- package/dist/openmrs-esm-billing-app.js.buildmanifest.json +609 -0
- package/dist/openmrs-esm-billing-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/e2e/README.md +115 -0
- package/e2e/core/global-setup.ts +32 -0
- package/e2e/core/index.ts +1 -0
- package/e2e/core/test.ts +20 -0
- package/e2e/fixtures/api.ts +27 -0
- package/e2e/fixtures/index.ts +1 -0
- package/e2e/pages/home-page.ts +9 -0
- package/e2e/pages/index.ts +1 -0
- package/e2e/specs/sample-test.spec.ts +11 -0
- package/e2e/support/github/Dockerfile +34 -0
- package/e2e/support/github/docker-compose.yml +24 -0
- package/e2e/support/github/run-e2e-docker-env.sh +49 -0
- package/example.env +6 -0
- package/i18next-parser.config.js +89 -0
- package/jest.config.js +34 -0
- package/package.json +124 -0
- package/playwright.config.ts +32 -0
- package/prettier.config.js +8 -0
- package/src/bill-history/bill-history.component.tsx +199 -0
- package/src/bill-history/bill-history.scss +151 -0
- package/src/bill-history/bill-history.test.tsx +122 -0
- package/src/billable-services/bill-waiver/bill-selection.component.tsx +76 -0
- package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +110 -0
- package/src/billable-services/bill-waiver/bill-waiver-form.scss +34 -0
- package/src/billable-services/bill-waiver/bill-waiver.component.tsx +32 -0
- package/src/billable-services/bill-waiver/bill-waiver.scss +10 -0
- package/src/billable-services/bill-waiver/patient-bills.component.tsx +137 -0
- package/src/billable-services/bill-waiver/utils.ts +41 -0
- package/src/billable-services/billable-service.resource.ts +72 -0
- package/src/billable-services/billable-services-home.component.tsx +51 -0
- package/src/billable-services/billable-services.component.tsx +255 -0
- package/src/billable-services/billable-services.scss +218 -0
- package/src/billable-services/billable-services.test.tsx +16 -0
- package/src/billable-services/create-edit/add-billable-service.component.tsx +322 -0
- package/src/billable-services/create-edit/add-billable-service.scss +131 -0
- package/src/billable-services/create-edit/add-billable-service.test.tsx +152 -0
- package/src/billable-services/dashboard/dashboard.component.tsx +15 -0
- package/src/billable-services/dashboard/dashboard.scss +27 -0
- package/src/billable-services/dashboard/dashboard.test.tsx +11 -0
- package/src/billable-services/dashboard/service-metrics.component.tsx +41 -0
- package/src/billable-services-admin-card-link.component.test.tsx +21 -0
- package/src/billable-services-admin-card-link.component.tsx +25 -0
- package/src/billing-dashboard/billing-dashboard.component.tsx +20 -0
- package/src/billing-dashboard/billing-dashboard.scss +27 -0
- package/src/billing-dashboard/billing-dashboard.test.tsx +13 -0
- package/src/billing-form/billing-checkin-form.component.tsx +127 -0
- package/src/billing-form/billing-checkin-form.scss +13 -0
- package/src/billing-form/billing-checkin-form.test.tsx +134 -0
- package/src/billing-form/billing-form.component.tsx +347 -0
- package/src/billing-form/billing-form.resource.ts +32 -0
- package/src/billing-form/billing-form.scss +88 -0
- package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +173 -0
- package/src/billing-form/visit-attributes/visit-attributes-form.scss +22 -0
- package/src/billing-header/billing-header.component.tsx +43 -0
- package/src/billing-header/billing-header.scss +83 -0
- package/src/billing-header/billing-illustration.component.tsx +30 -0
- package/src/billing.resource.ts +148 -0
- package/src/bills-table/bills-table.component.tsx +280 -0
- package/src/bills-table/bills-table.scss +181 -0
- package/src/bills-table/bills-table.test.tsx +154 -0
- package/src/config-schema.ts +50 -0
- package/src/constants.ts +3 -0
- package/src/dashboard.meta.ts +7 -0
- package/src/declarations.d.ts +4 -0
- package/src/helpers/functions.ts +66 -0
- package/src/helpers/index.ts +1 -0
- package/src/index.ts +72 -0
- package/src/invoice/invoice-table.component.tsx +189 -0
- package/src/invoice/invoice-table.scss +91 -0
- package/src/invoice/invoice.component.tsx +144 -0
- package/src/invoice/invoice.scss +93 -0
- package/src/invoice/invoice.test.tsx +242 -0
- package/src/invoice/payments/invoice-breakdown/invoice-breakdown.component.tsx +17 -0
- package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +29 -0
- package/src/invoice/payments/payment-form/payment-form.component.tsx +105 -0
- package/src/invoice/payments/payment-form/payment-form.scss +54 -0
- package/src/invoice/payments/payment-history/payment-history.component.tsx +69 -0
- package/src/invoice/payments/payment.resource.ts +44 -0
- package/src/invoice/payments/payments.component.tsx +147 -0
- package/src/invoice/payments/payments.scss +46 -0
- package/src/invoice/payments/utils.ts +68 -0
- package/src/invoice/payments/visit-tags/visit-attribute.component.tsx +21 -0
- package/src/invoice/printable-invoice/print-receipt.component.tsx +29 -0
- package/src/invoice/printable-invoice/print-receipt.scss +14 -0
- package/src/invoice/printable-invoice/printable-footer.component.tsx +19 -0
- package/src/invoice/printable-invoice/printable-footer.scss +17 -0
- package/src/invoice/printable-invoice/printable-footer.test.tsx +30 -0
- package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +63 -0
- package/src/invoice/printable-invoice/printable-invoice-header.scss +61 -0
- package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +58 -0
- package/src/invoice/printable-invoice/printable-invoice.component.tsx +146 -0
- package/src/invoice/printable-invoice/printable-invoice.scss +50 -0
- package/src/left-panel-link.component.tsx +41 -0
- package/src/left-panel-link.test.tsx +38 -0
- package/src/metrics-cards/card.component.tsx +14 -0
- package/src/metrics-cards/card.scss +20 -0
- package/src/metrics-cards/metrics-cards.component.tsx +42 -0
- package/src/metrics-cards/metrics-cards.scss +12 -0
- package/src/metrics-cards/metrics-cards.test.tsx +44 -0
- package/src/metrics-cards/metrics.resource.ts +45 -0
- package/src/modal/require-payment-modal.component.tsx +85 -0
- package/src/modal/require-payment.scss +6 -0
- package/src/root.component.tsx +19 -0
- package/src/root.scss +30 -0
- package/src/routes.json +78 -0
- package/src/setup-tests.ts +13 -0
- package/src/types/index.ts +181 -0
- package/test-helpers.tsx +23 -0
- package/translations/am.json +117 -0
- package/translations/en.json +117 -0
- package/translations/es.json +117 -0
- package/translations/fr.json +117 -0
- package/translations/he.json +117 -0
- package/translations/km.json +117 -0
- package/tsconfig.json +16 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
6
|
+
import { navigate, showSnackbar, useConfig, useVisit } from '@openmrs/esm-framework';
|
|
7
|
+
import { Button } from '@carbon/react';
|
|
8
|
+
import { CardHeader } from '@openmrs/esm-patient-common-lib';
|
|
9
|
+
import { type LineItem, type MappedBill } from '../../types';
|
|
10
|
+
import { convertToCurrency } from '../../helpers';
|
|
11
|
+
import { createPaymentPayload } from './utils';
|
|
12
|
+
import { processBillPayment } from '../../billing.resource';
|
|
13
|
+
import { InvoiceBreakDown } from './invoice-breakdown/invoice-breakdown.component';
|
|
14
|
+
import PaymentHistory from './payment-history/payment-history.component';
|
|
15
|
+
import PaymentForm from './payment-form/payment-form.component';
|
|
16
|
+
import { updateBillVisitAttribute } from './payment.resource';
|
|
17
|
+
import styles from './payments.scss';
|
|
18
|
+
import { useBillableServices } from '../../billable-services/billable-service.resource';
|
|
19
|
+
|
|
20
|
+
type PaymentProps = {
|
|
21
|
+
bill: MappedBill;
|
|
22
|
+
selectedLineItems: Array<LineItem>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type Payment = { method: string; amount: string | number; referenceCode?: number | string };
|
|
26
|
+
|
|
27
|
+
export type PaymentFormValue = {
|
|
28
|
+
payment: Array<Payment>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const Payments: React.FC<PaymentProps> = ({ bill, selectedLineItems }) => {
|
|
32
|
+
const { t } = useTranslation();
|
|
33
|
+
const { defaultCurrency } = useConfig();
|
|
34
|
+
const { billableServices, isLoading, isValidating, error, mutate } = useBillableServices();
|
|
35
|
+
const paymentSchema = z.object({
|
|
36
|
+
method: z.string().refine((value) => !!value, 'Payment method is required'),
|
|
37
|
+
amount: z
|
|
38
|
+
.number()
|
|
39
|
+
.lte(bill?.totalAmount - bill?.tenderedAmount, { message: 'Amount paid should not be greater than amount due' }),
|
|
40
|
+
referenceCode: z.union([z.number(), z.string()]).optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const paymentFormSchema = z.object({ payment: z.array(paymentSchema) });
|
|
44
|
+
const { currentVisit } = useVisit(bill?.patientUuid);
|
|
45
|
+
const methods = useForm<PaymentFormValue>({
|
|
46
|
+
mode: 'all',
|
|
47
|
+
defaultValues: {},
|
|
48
|
+
resolver: zodResolver(paymentFormSchema),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const formValues = useWatch({
|
|
52
|
+
name: 'payment',
|
|
53
|
+
control: methods.control,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const hasMoreThanOneLineItem = bill?.lineItems?.length > 1;
|
|
57
|
+
const computedTotal = hasMoreThanOneLineItem ? computeTotalPrice(selectedLineItems) : bill?.totalAmount ?? 0;
|
|
58
|
+
const totalAmountTendered = formValues?.reduce((curr: number, prev) => curr + Number(prev.amount) ?? 0, 0) ?? 0;
|
|
59
|
+
const amountDue = Number(computedTotal) - (Number(bill?.tenderedAmount) + Number(totalAmountTendered));
|
|
60
|
+
const newAmountDue = Number(bill?.totalAmount - bill?.tenderedAmount);
|
|
61
|
+
|
|
62
|
+
const handleNavigateToBillingDashboard = () =>
|
|
63
|
+
navigate({
|
|
64
|
+
to: window.getOpenmrsSpaBase() + 'home/billing',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const handleProcessPayment = () => {
|
|
68
|
+
const paymentPayload = createPaymentPayload(bill, bill.patientUuid, formValues, amountDue, billableServices, selectedLineItems);
|
|
69
|
+
processBillPayment(paymentPayload, bill.uuid).then(
|
|
70
|
+
(res) => {
|
|
71
|
+
showSnackbar({
|
|
72
|
+
title: t('billPayment', 'Bill payment'),
|
|
73
|
+
subtitle: 'Bill payment processing has been successful',
|
|
74
|
+
kind: 'success',
|
|
75
|
+
timeoutInMs: 3000,
|
|
76
|
+
});
|
|
77
|
+
if (currentVisit) {
|
|
78
|
+
updateBillVisitAttribute(currentVisit);
|
|
79
|
+
}
|
|
80
|
+
window.location.reload();
|
|
81
|
+
},
|
|
82
|
+
(error) => {
|
|
83
|
+
showSnackbar({ title: 'Bill payment error', kind: 'error', subtitle: error?.message });
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const amountDueDisplay = (amount: number) => (amount < 0 ? 'Client balance' : 'Amount Due');
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<FormProvider {...methods}>
|
|
92
|
+
<div className={styles.wrapper}>
|
|
93
|
+
<div className={styles.paymentContainer}>
|
|
94
|
+
<CardHeader title={t('payments', 'Payments')}>
|
|
95
|
+
<span></span>
|
|
96
|
+
</CardHeader>
|
|
97
|
+
<div>
|
|
98
|
+
{bill && <PaymentHistory bill={bill} />}
|
|
99
|
+
<PaymentForm disablePayment={computedTotal <= 0} amountDue={amountDue} />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div className={styles.divider} />
|
|
103
|
+
<div className={styles.paymentTotals}>
|
|
104
|
+
<InvoiceBreakDown
|
|
105
|
+
label={t('totalAmount', 'Total Amount')}
|
|
106
|
+
value={convertToCurrency(bill?.totalAmount, defaultCurrency)}
|
|
107
|
+
/>
|
|
108
|
+
<InvoiceBreakDown
|
|
109
|
+
label={t('totalTendered', 'Total Tendered')}
|
|
110
|
+
value={convertToCurrency(bill?.tenderedAmount, defaultCurrency)}
|
|
111
|
+
/>
|
|
112
|
+
<InvoiceBreakDown label={t('discount', 'Discount')} value={'--'} />
|
|
113
|
+
<InvoiceBreakDown
|
|
114
|
+
hasBalance={amountDue < 0 ?? false}
|
|
115
|
+
label={amountDueDisplay(amountDue)}
|
|
116
|
+
value={convertToCurrency(bill?.totalAmount - bill?.tenderedAmount, defaultCurrency)}
|
|
117
|
+
/>
|
|
118
|
+
<div className={styles.processPayments}>
|
|
119
|
+
<Button onClick={handleNavigateToBillingDashboard} kind="secondary">
|
|
120
|
+
{t('discard', 'Discard')}
|
|
121
|
+
</Button>
|
|
122
|
+
<Button onClick={() => handleProcessPayment()} disabled={!formValues?.length || !methods.formState.isValid}>
|
|
123
|
+
{t('processPayment', 'Process Payment')}
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</FormProvider>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const computeTotalPrice = (items) => {
|
|
133
|
+
if (items && !items.length) {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let totalPrice = 0;
|
|
138
|
+
|
|
139
|
+
items?.forEach((item) => {
|
|
140
|
+
const { price, quantity } = item;
|
|
141
|
+
totalPrice += price * quantity;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return totalPrice;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export default Payments;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.wrapper {
|
|
6
|
+
display: flex;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.divider {
|
|
10
|
+
background: colors.$gray-20;
|
|
11
|
+
height: 12rem;
|
|
12
|
+
margin: 2rem;
|
|
13
|
+
width: 1px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.paymentContainer {
|
|
17
|
+
margin: layout.$layout-01 0;
|
|
18
|
+
padding: layout.$layout-01;
|
|
19
|
+
width: 70%;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.paymentButtons {
|
|
23
|
+
margin: layout.$layout-01 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.paymentMethodContainer {
|
|
27
|
+
display: grid;
|
|
28
|
+
grid-template-columns: 1fr 1fr 1fr 1fr;
|
|
29
|
+
align-items: flex-end;
|
|
30
|
+
column-gap: 1rem;
|
|
31
|
+
margin: 0.625rem 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.paymentTotals {
|
|
35
|
+
margin: layout.$spacing-05 0;
|
|
36
|
+
padding: layout.$spacing-07 layout.$spacing-05;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.processPayments {
|
|
40
|
+
display: flex;
|
|
41
|
+
justify-content: flex-end;
|
|
42
|
+
margin: layout.$spacing-05;
|
|
43
|
+
padding-top: layout.$spacing-05;
|
|
44
|
+
column-gap: layout.$spacing-04;
|
|
45
|
+
border-top: 1px solid colors.$cool-gray-40;
|
|
46
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type OpenmrsResource } from '@openmrs/esm-framework';
|
|
2
|
+
import { type LineItem, type MappedBill } from '../../types';
|
|
3
|
+
import { type Payment } from './payments.component';
|
|
4
|
+
|
|
5
|
+
const hasLineItem = (lineItems: Array<LineItem>, item: LineItem) => {
|
|
6
|
+
if (lineItems?.length === 0) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const foundItem = lineItems.find((lineItem) => lineItem.uuid === item.uuid);
|
|
10
|
+
return Boolean(foundItem);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const createPaymentPayload = (
|
|
14
|
+
bill: MappedBill,
|
|
15
|
+
patientUuid: string,
|
|
16
|
+
formValues: Array<Payment>,
|
|
17
|
+
amountDue: number,
|
|
18
|
+
billableServices: Array<any>,
|
|
19
|
+
selectedLineItems: Array<LineItem>,
|
|
20
|
+
) => {
|
|
21
|
+
const { cashier } = bill;
|
|
22
|
+
const totalAmount = bill?.totalAmount;
|
|
23
|
+
const totalPaymentStatus = amountDue <= 0 ? 'PAID' : 'PENDING';
|
|
24
|
+
const previousPayments = bill.payments.map((payment) => ({
|
|
25
|
+
amount: payment.amount,
|
|
26
|
+
amountTendered: payment.amountTendered,
|
|
27
|
+
attributes: [],
|
|
28
|
+
instanceType: payment.instanceType.uuid,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const newPayments = formValues.map((formValue) => ({
|
|
32
|
+
amount: parseFloat(totalAmount.toFixed(2)),
|
|
33
|
+
amountTendered: parseFloat(Number(formValue.amount).toFixed(2)),
|
|
34
|
+
attributes: [],
|
|
35
|
+
instanceType: formValue.method,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
const updatedPayments = newPayments.concat(previousPayments);
|
|
39
|
+
const totalAmountRendered = updatedPayments.reduce((acc, payment) => acc + payment.amountTendered, 0);
|
|
40
|
+
const updatedLineItems = bill?.lineItems.map((lineItem) => ({
|
|
41
|
+
...lineItem,
|
|
42
|
+
billableService: getBillableServiceUuid(billableServices, lineItem.billableService),
|
|
43
|
+
item: lineItem?.item,
|
|
44
|
+
paymentStatus: hasLineItem(selectedLineItems ?? [], lineItem)
|
|
45
|
+
? totalAmountRendered >= lineItem.price * lineItem.quantity
|
|
46
|
+
? 'PAID'
|
|
47
|
+
: 'PENDING'
|
|
48
|
+
: lineItem.paymentStatus,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const allItemsBillPaymentStatus =
|
|
52
|
+
updatedLineItems.filter((item) => item.paymentStatus === 'PENDING').length === 0 ? 'PAID' : 'PENDING';
|
|
53
|
+
|
|
54
|
+
const processedPayment = {
|
|
55
|
+
cashPoint: bill.cashPointUuid,
|
|
56
|
+
cashier: cashier.uuid,
|
|
57
|
+
lineItems: updatedLineItems,
|
|
58
|
+
payments: [...updatedPayments],
|
|
59
|
+
patient: patientUuid,
|
|
60
|
+
status: selectedLineItems?.length > 0 ? allItemsBillPaymentStatus : totalPaymentStatus,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return processedPayment;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const getBillableServiceUuid = (billableServices: Array<any>, serviceName: string) => {
|
|
67
|
+
return billableServices.length ? billableServices.find((service) => service.name === serviceName).uuid : null;
|
|
68
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Tag } from '@carbon/react';
|
|
3
|
+
import { usePatientPaymentInfo } from '../../../billing.resource';
|
|
4
|
+
|
|
5
|
+
type VisitAttributeTagsProps = { patientUuid: string };
|
|
6
|
+
|
|
7
|
+
const VisitAttributeTags: React.FC<VisitAttributeTagsProps> = ({ patientUuid }) => {
|
|
8
|
+
const patientBillingInfo = usePatientPaymentInfo(patientUuid);
|
|
9
|
+
return (
|
|
10
|
+
<div>
|
|
11
|
+
{patientBillingInfo?.map((tag) => (
|
|
12
|
+
<React.Fragment key={tag.name}>
|
|
13
|
+
<Tag type="gray">{tag.name}</Tag>
|
|
14
|
+
<Tag type="cool-gray">{tag.value}</Tag>
|
|
15
|
+
</React.Fragment>
|
|
16
|
+
))}
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default VisitAttributeTags;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button } from '@carbon/react';
|
|
3
|
+
import { Printer } from '@carbon/react/icons';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { ConfigurableLink } from '@openmrs/esm-framework';
|
|
6
|
+
import styles from './print-receipt.scss';
|
|
7
|
+
import { apiBasePath } from '../../constants';
|
|
8
|
+
|
|
9
|
+
interface PrintReceiptProps {
|
|
10
|
+
billId: number;
|
|
11
|
+
}
|
|
12
|
+
const PrintReceipt: React.FC<PrintReceiptProps> = ({ billId }) => {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
return (
|
|
15
|
+
<Button
|
|
16
|
+
kind="secondary"
|
|
17
|
+
className={styles.button}
|
|
18
|
+
size="md"
|
|
19
|
+
renderIcon={(props) => <Printer size={24} {...props} />}>
|
|
20
|
+
<ConfigurableLink
|
|
21
|
+
className={styles.configurableLink}
|
|
22
|
+
to={`\${openmrsBase}${apiBasePath}receipt?billId=${billId}`}>
|
|
23
|
+
{t('printReceipt', 'Print receipt')}
|
|
24
|
+
</ConfigurableLink>{' '}
|
|
25
|
+
</Button>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default PrintReceipt;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useDefaultFacility } from '../../billing.resource';
|
|
3
|
+
import styles from './printable-footer.scss';
|
|
4
|
+
|
|
5
|
+
const PrintableFooter = () => {
|
|
6
|
+
const { data, isLoading } = useDefaultFacility();
|
|
7
|
+
|
|
8
|
+
if (isLoading) {
|
|
9
|
+
return <div>--</div>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className={styles.container}>
|
|
14
|
+
<p className={styles.itemFooter}>{data?.display}</p>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default PrintableFooter;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
position: fixed;
|
|
9
|
+
width: 100%;
|
|
10
|
+
bottom: 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.itemFooter {
|
|
14
|
+
padding: 1rem;
|
|
15
|
+
@include type.type-style('body-compact-02');
|
|
16
|
+
color: colors.$cool-gray-90;
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen, render } from '@testing-library/react';
|
|
3
|
+
import { useDefaultFacility } from '../../billing.resource';
|
|
4
|
+
import PrintableFooter from './printable-footer.component';
|
|
5
|
+
|
|
6
|
+
const mockUseDefaultFacility = useDefaultFacility as jest.MockedFunction<typeof useDefaultFacility>;
|
|
7
|
+
|
|
8
|
+
jest.mock('../../billing.resource', () => ({
|
|
9
|
+
useDefaultFacility: jest.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe('PrintableFooter', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should render PrintableFooter component', () => {
|
|
18
|
+
mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: false });
|
|
19
|
+
render(<PrintableFooter />);
|
|
20
|
+
const footer = screen.getByText('MTRH');
|
|
21
|
+
expect(footer).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('should show placeholder text when facility isLoading', () => {
|
|
25
|
+
mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: true });
|
|
26
|
+
render(<PrintableFooter />);
|
|
27
|
+
const footer = screen.getByText('--');
|
|
28
|
+
expect(footer).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type PatientDetails } from '../../types';
|
|
3
|
+
import { useConfig } from '@openmrs/esm-framework';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { useDefaultFacility } from '../../billing.resource';
|
|
6
|
+
import styles from './printable-invoice-header.scss';
|
|
7
|
+
|
|
8
|
+
interface PrintableInvoiceHeaderProps {
|
|
9
|
+
patientDetails: PatientDetails;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PrintableInvoiceHeader: React.FC<PrintableInvoiceHeaderProps> = ({ patientDetails }) => {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const { logo } = useConfig();
|
|
15
|
+
const { data } = useDefaultFacility();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className={styles.container}>
|
|
19
|
+
<div className={styles.printableHeader}>
|
|
20
|
+
<p className={styles.heading}>{t('invoice', 'Invoice')}</p>
|
|
21
|
+
{logo?.src ? (
|
|
22
|
+
<img className={styles.img} src={logo.src} alt={logo.alt} />
|
|
23
|
+
) : logo?.name ? (
|
|
24
|
+
logo.name
|
|
25
|
+
) : (
|
|
26
|
+
// OpenMRS Logo
|
|
27
|
+
<svg
|
|
28
|
+
className={styles.img}
|
|
29
|
+
role="img"
|
|
30
|
+
width={110}
|
|
31
|
+
height={40}
|
|
32
|
+
viewBox="0 0 380 119"
|
|
33
|
+
xmlns="http://www.w3.org/2000/svg">
|
|
34
|
+
<path
|
|
35
|
+
fillRule="evenodd"
|
|
36
|
+
clipRule="evenodd"
|
|
37
|
+
d="M40.29 40.328a27.755 27.755 0 0 1 19.688-8.154c7.669 0 14.613 3.102 19.647 8.116l.02-18.54A42.835 42.835 0 0 0 59.978 17c-7.089 0-13.813 1.93-19.709 4.968l.021 18.36ZM79.645 79.671a27.744 27.744 0 0 1-19.684 8.154c-7.67 0-14.614-3.101-19.651-8.116l-.02 18.54A42.857 42.857 0 0 0 59.96 103a42.833 42.833 0 0 0 19.672-4.751l.013-18.578ZM40.328 79.696c-5.038-5.037-8.154-11.995-8.154-19.685 0-7.669 3.102-14.612 8.116-19.65l-18.54-.02A42.85 42.85 0 0 0 17 60.012a42.819 42.819 0 0 0 4.752 19.672l18.576.013ZM79.634 40.289a27.753 27.753 0 0 1 8.154 19.688 27.744 27.744 0 0 1-8.117 19.646l18.542.02a42.842 42.842 0 0 0 4.749-19.666c0-7.09-1.714-13.779-4.751-19.675l-18.577-.013ZM156.184 60.002c0-8.748-6.118-15.776-15.025-15.776-8.909 0-15.025 7.028-15.025 15.776 0 8.749 6.116 15.78 15.025 15.78 8.907 0 15.025-7.031 15.025-15.78Zm-34.881 0c0-11.482 8.318-19.958 19.856-19.958 11.536 0 19.855 8.477 19.855 19.959 0 11.484-8.319 19.964-19.855 19.964-11.538 0-19.856-8.48-19.856-19.965ZM179.514 75.54c5.507 0 9.05-4.14 9.05-9.482 0-5.341-3.543-9.483-9.05-9.483-5.505 0-9.046 4.142-9.046 9.483 0 5.342 3.541 9.482 9.046 9.482ZM166.22 53.306h4.248v3.704h.11c2.344-2.725 5.449-4.36 9.154-4.36 8.014 0 13.408 5.67 13.408 13.408 0 7.63-5.613 13.406-12.752 13.406-4.58 0-8.231-2.29-9.81-5.178h-.11V90.87h-4.248V53.306ZM217.773 63.768c-.163-4.305-3-7.193-7.686-7.193-4.685 0-7.79 2.888-8.335 7.193h16.021Zm3.653 10.412c-3.001 3.868-6.596 5.284-11.339 5.284-8.01 0-12.914-5.993-12.914-13.406 0-7.901 5.559-13.407 13.08-13.407 7.196 0 12.096 4.906 12.096 13.354v1.362h-20.597c.325 4.413 3.704 8.173 8.335 8.173 3.65 0 6.105-1.307 8.12-3.868l3.219 2.508ZM227.854 59.356c0-2.346-.216-4.36-.216-6.05h4.031c0 1.363.11 2.777.11 4.195h.11c1.144-2.505 4.306-4.85 8.5-4.85 6.705 0 9.699 4.252 9.699 10.41v15.748h-4.248v-15.31c0-4.253-1.856-6.924-5.833-6.924-5.503 0-7.903 3.979-7.903 9.811V78.81h-4.25V59.356ZM259.211 41.008h6.708L278.8 70.791h.107l12.982-29.782h6.549v37.99h-4.506V47.124h-.106L280.192 79h-2.738l-13.629-31.875h-.107V79h-4.507V41.01ZM312.392 57.752h4.023c4.992 0 11.487 0 11.487-6.282 0-5.47-4.776-6.276-9.177-6.276h-6.333v12.558Zm-4.506-16.744h9.711c7.352 0 15.132 1.072 15.132 10.462 0 5.527-3.594 9.125-9.495 10.037L334.018 79h-5.525l-10.304-17.063h-5.797V79h-4.506V41.01ZM358.123 47.712c-1.506-2.413-4.187-3.486-6.926-3.486-3.973 0-8.1 1.88-8.1 6.385 0 3.49 1.931 5.047 7.994 6.98 5.903 1.878 11.377 3.809 11.377 11.267 0 7.567-6.495 11.11-13.36 11.11-4.402 0-9.125-1.45-11.7-5.262l3.862-3.165c1.61 2.794 4.83 4.24 8.105 4.24 3.862 0 8.263-2.253 8.263-6.601 0-4.669-3.165-5.474-9.928-7.728-5.366-1.771-9.442-4.134-9.442-10.463 0-7.298 6.277-10.945 12.929-10.945 4.241 0 7.836 1.178 10.625 4.45l-3.699 3.218Z"
|
|
38
|
+
/>
|
|
39
|
+
</svg>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div className={styles.printableBody}>
|
|
44
|
+
<div className={styles.billDetails}>
|
|
45
|
+
<p className={styles.itemHeading}>{t('billedTo', 'Billed to')}</p>
|
|
46
|
+
<p className={styles.itemLabel}>{patientDetails?.name}</p>
|
|
47
|
+
<p className={styles.itemLabel}>{patientDetails?.county}</p>
|
|
48
|
+
<p className={styles.itemLabel}>
|
|
49
|
+
{patientDetails?.subCounty}
|
|
50
|
+
{patientDetails?.city ? `, ${patientDetails?.city}` : null}
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className={styles.facilityDetails}>
|
|
55
|
+
<p className={styles.facilityName}>{data?.display}</p>
|
|
56
|
+
<p className={styles.itemLabel}>Kenya</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default PrintableInvoiceHeader;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
padding: 0 1rem 2rem;
|
|
7
|
+
border-bottom: 1px solid #ebedf2;
|
|
8
|
+
margin-bottom: 2rem;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.printableBody {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: row;
|
|
14
|
+
justify-content: space-between;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.printableHeader {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: row;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.img {
|
|
23
|
+
display: flex;
|
|
24
|
+
margin-left: auto;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.billDetails {
|
|
28
|
+
display: flex;
|
|
29
|
+
width: 50%;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.facilityDetails {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-flow: column wrap;
|
|
36
|
+
text-align: right;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.heading {
|
|
40
|
+
font-size: 40px;
|
|
41
|
+
text-transform: uppercase;
|
|
42
|
+
margin-bottom: layout.$spacing-05;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.facilityName {
|
|
46
|
+
@include type.type-style('heading-compact-02');
|
|
47
|
+
font-weight: bold;
|
|
48
|
+
color: colors.$green-70;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.itemHeading {
|
|
52
|
+
@include type.type-style('body-compact-02');
|
|
53
|
+
margin-bottom: 0.25rem;
|
|
54
|
+
font-weight: bold;
|
|
55
|
+
color: colors.$cool-gray-90;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.itemLabel {
|
|
59
|
+
@include type.type-style('body-compact-02');
|
|
60
|
+
color: colors.$cool-gray-90;
|
|
61
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen, render } from '@testing-library/react';
|
|
3
|
+
import { useConfig } from '@openmrs/esm-framework';
|
|
4
|
+
import { useDefaultFacility } from '../../billing.resource';
|
|
5
|
+
import PrintableInvoiceHeader from './printable-invoice-header.component';
|
|
6
|
+
|
|
7
|
+
const mockUseDefaultFacility = useDefaultFacility as jest.MockedFunction<typeof useDefaultFacility>;
|
|
8
|
+
const mockUseConfig = useConfig as jest.MockedFunction<typeof useConfig>;
|
|
9
|
+
|
|
10
|
+
jest.mock('../../billing.resource', () => ({
|
|
11
|
+
useDefaultFacility: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
jest.mock('@openmrs/esm-framework', () => ({
|
|
15
|
+
useConfig: jest.fn(),
|
|
16
|
+
}));
|
|
17
|
+
const testProps = {
|
|
18
|
+
patientDetails: {
|
|
19
|
+
name: 'John Doe',
|
|
20
|
+
county: 'Nairobi',
|
|
21
|
+
subCounty: 'Westlands',
|
|
22
|
+
city: 'Nairobi',
|
|
23
|
+
age: '45',
|
|
24
|
+
gender: 'Male',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('PrintableInvoiceHeader', () => {
|
|
29
|
+
test('should render PrintableInvoiceHeader component', () => {
|
|
30
|
+
mockUseConfig.mockReturnValue({ logo: { src: 'logo.png', alt: 'logo' } });
|
|
31
|
+
mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: false });
|
|
32
|
+
render(<PrintableInvoiceHeader {...testProps} />);
|
|
33
|
+
const header = screen.getByText('Invoice');
|
|
34
|
+
expect(header).toBeInTheDocument();
|
|
35
|
+
|
|
36
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
37
|
+
expect(screen.getByText('Nairobi')).toBeInTheDocument();
|
|
38
|
+
expect(screen.getByText('Westlands, Nairobi')).toBeInTheDocument();
|
|
39
|
+
expect(screen.getByText('MTRH')).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText('Kenya')).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should display the logo when logo is provided', () => {
|
|
44
|
+
mockUseConfig.mockReturnValue({ logo: { src: 'logo.png', alt: 'logo' } });
|
|
45
|
+
mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: false });
|
|
46
|
+
render(<PrintableInvoiceHeader {...testProps} />);
|
|
47
|
+
const logo = screen.getByAltText('logo');
|
|
48
|
+
expect(logo).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should display the default logo when logo is not provided', () => {
|
|
52
|
+
mockUseConfig.mockReturnValue({ logo: {} });
|
|
53
|
+
mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: false });
|
|
54
|
+
render(<PrintableInvoiceHeader {...testProps} />);
|
|
55
|
+
const logo = screen.getByRole('img');
|
|
56
|
+
expect(logo).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
});
|