@kenyaemr/esm-billing-app 5.4.1-pre.2103 → 5.4.1-pre.2108
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/.turbo/turbo-build.log +78 -78
- package/dist/574.js +1 -1
- package/dist/912.js +1 -1
- package/dist/912.js.map +1 -1
- package/dist/kenyaemr-esm-billing-app.js.buildmanifest.json +9 -9
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/claims/claims-management/table/claim-table.component.tsx +4 -1
- package/src/claims/dashboard/table/claims-table.component.tsx +35 -14
- package/src/hooks/usePaymentSchema.tsx +1 -0
- package/src/invoice/invoice-table.component.tsx +1 -1
- package/src/invoice/payments/payment-form/payment-form.component.tsx +107 -56
- package/src/invoice/payments/payment-form/payment-form.scss +5 -2
- package/src/invoice/payments/payments.component.tsx +6 -2
- package/src/invoice/payments/payments.test.tsx +42 -10
- package/src/invoice/payments/utils.ts +2 -0
- package/src/types/index.ts +24 -1
- package/src/utils.ts +6 -0
- package/translations/en.json +4 -1
package/dist/routes.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"},{"component":"requirePaymentModal","routeRegex":"^patient/.+/chart","online":true,"offline":false}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"}},{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","slot":"patient-chart-dashboard-slot","meta":{"name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot","path":"insurance-benefits","columns":1,"columnSpan":1},"featureFlag":"healthInformationExchange"},{"component":"benefitsPackage","name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"component":"benefitsEligibilyRequestForm","name":"benefits-eligibility-request-form"},{"component":"benefitsPreAuthForm","name":"benefits-pre-auth-form"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing","layoutMode":"anchored"}},{"name":"billing-check-in-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"name":"require-billing-modal","component":"requirePaymentModal"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"initiate-payment-modal","component":"initiatePaymentDialog"},{"name":"delete-billableservice-modal","component":"deleteBillableServiceModal"},{"name":"refund-bill-modal","component":"refundBillModal"},{"name":"delete-bill-modal","component":"deleteBillModal"},{"name":"lab-order-billable-item","component":"labOrder","slot":"top-of-lab-order-form-slot"},{"name":"procedure-order-billable-item","component":"procedureOrder","slot":"top-of-procedure-order-form-slot"},{"name":"imaging-order-billable-item","component":"imagingOrder","slot":"top-of-imaging-order-form-slot"},{"name":"price-info-order","component":"priceInfoOrder"},{"name":"drug-order-billable-item","component":"drugOrder","slot":"medication-info-slot"},{"name":"order-action-button","component":"orderActionButton","slots":["prescription-action-button-slot","imaging-orders-action","procedure-orders-action","tests-ordered-actions-slot"],"order":0},{"component":"billingOverviewLink","name":"billing-overview-link","order":0,"slot":"billing-dashboard-link-slot"},{"component":"paymentHistoryLink","name":"payment-history-link","slot":"billing-dashboard-link-slot"},{"component":"paymentPointsLink","name":"payment-points-link","slot":"billing-dashboard-link-slot"},{"component":"paymentModesLink","name":"payment-modes-link","slot":"billing-dashboard-link-slot"},{"component":"billManagerLink","name":"bill-manager-link","slot":"billing-dashboard-link-slot"},{"component":"chargeableItemsLink","name":"chargeable-items-link","slot":"billing-dashboard-link-slot"},{"component":"billableExemptionsLink","name":"billable-exemptions-link","slot":"billing-dashboard-link-slot"},{"component":"claimsManagementSideNavGroup","name":"claims-management-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"claims-management","title":"Claims management Overview","slot":"case-management-slot"},"featureFlag":"healthInformationExchange"},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","order":0,"slot":"claims-management-dashboard-link-slot"},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slot":"claims-management-dashboard-link-slot"},{"component":"claimsOverview","name":"claims-overview-dashboard-link","slot":"claims-management-overview-slot"},{"component":"waiveBillActionButton","name":"waive-bill-action-button","slot":"bill-actions-slot"},{"component":"deleteBillActionButton","name":"delete-bill-action-button","slot":"bill-actions-slot"},{"component":"refundLineItem","name":"refund-line-item","slot":"bill-actions-overflow-menu-slot"},{"name":"edit-line-item","component":"editLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"cancel-line-item","component":"cancelLineItem","slot":"bill-actions-overflow-menu-slot"}],"workspaces":[{"name":"create-bill-workspace","component":"createBillWorkspace","title":"Create Bill Workspace","type":"other-form"},{"name":"waive-bill-form","component":"waiveBillForm","title":"Waive Bill Form","type":"other-form"},{"name":"edit-bill-form","component":"editBillForm","title":"Edit Bill Form","type":"other-form"},{"name":"billable-service-form","component":"addServiceForm","title":"Create Charge Item Form","type":"other-form"},{"name":"commodity-form","component":"addCommodityForm","title":"Create Charge Item Form","type":"other-form"},{"name":"billing-form","component":"billingForm","title":"Billing Form","type":"other-form","width":"extra-wide"},{"name":"payment-mode-workspace","component":"paymentModeWorkspace","title":"Payment Mode Workspace","type":"other-form"},{"name":"cancel-bill-workspace","component":"cancelBillWorkspace","title":"Cancel Bill Workspace","type":"other-form"},{"name":"bill-deposit-workspace","component":"billDepositWorkspace","title":"Bill Deposit Workspace","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"manage-claim-request-modal","component":"manageClaimRequestModal"},{"name":"paid-bill-receipt-print-preview-modal","component":"paidBillReceiptPrintPreviewModal"},{"name":"clock-in-modal","component":"clockIn"},{"name":"create-bill-item-modal","component":"createBillItemModal"}],"version":"5.4.1-pre.
|
|
1
|
+
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"},{"component":"requirePaymentModal","routeRegex":"^patient/.+/chart","online":true,"offline":false}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"}},{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","slot":"patient-chart-dashboard-slot","meta":{"name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot","path":"insurance-benefits","columns":1,"columnSpan":1},"featureFlag":"healthInformationExchange"},{"component":"benefitsPackage","name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"component":"benefitsEligibilyRequestForm","name":"benefits-eligibility-request-form"},{"component":"benefitsPreAuthForm","name":"benefits-pre-auth-form"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing","layoutMode":"anchored"}},{"name":"billing-check-in-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"name":"require-billing-modal","component":"requirePaymentModal"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"initiate-payment-modal","component":"initiatePaymentDialog"},{"name":"delete-billableservice-modal","component":"deleteBillableServiceModal"},{"name":"refund-bill-modal","component":"refundBillModal"},{"name":"delete-bill-modal","component":"deleteBillModal"},{"name":"lab-order-billable-item","component":"labOrder","slot":"top-of-lab-order-form-slot"},{"name":"procedure-order-billable-item","component":"procedureOrder","slot":"top-of-procedure-order-form-slot"},{"name":"imaging-order-billable-item","component":"imagingOrder","slot":"top-of-imaging-order-form-slot"},{"name":"price-info-order","component":"priceInfoOrder"},{"name":"drug-order-billable-item","component":"drugOrder","slot":"medication-info-slot"},{"name":"order-action-button","component":"orderActionButton","slots":["prescription-action-button-slot","imaging-orders-action","procedure-orders-action","tests-ordered-actions-slot"],"order":0},{"component":"billingOverviewLink","name":"billing-overview-link","order":0,"slot":"billing-dashboard-link-slot"},{"component":"paymentHistoryLink","name":"payment-history-link","slot":"billing-dashboard-link-slot"},{"component":"paymentPointsLink","name":"payment-points-link","slot":"billing-dashboard-link-slot"},{"component":"paymentModesLink","name":"payment-modes-link","slot":"billing-dashboard-link-slot"},{"component":"billManagerLink","name":"bill-manager-link","slot":"billing-dashboard-link-slot"},{"component":"chargeableItemsLink","name":"chargeable-items-link","slot":"billing-dashboard-link-slot"},{"component":"billableExemptionsLink","name":"billable-exemptions-link","slot":"billing-dashboard-link-slot"},{"component":"claimsManagementSideNavGroup","name":"claims-management-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"claims-management","title":"Claims management Overview","slot":"case-management-slot"},"featureFlag":"healthInformationExchange"},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","order":0,"slot":"claims-management-dashboard-link-slot"},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slot":"claims-management-dashboard-link-slot"},{"component":"claimsOverview","name":"claims-overview-dashboard-link","slot":"claims-management-overview-slot"},{"component":"waiveBillActionButton","name":"waive-bill-action-button","slot":"bill-actions-slot"},{"component":"deleteBillActionButton","name":"delete-bill-action-button","slot":"bill-actions-slot"},{"component":"refundLineItem","name":"refund-line-item","slot":"bill-actions-overflow-menu-slot"},{"name":"edit-line-item","component":"editLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"cancel-line-item","component":"cancelLineItem","slot":"bill-actions-overflow-menu-slot"}],"workspaces":[{"name":"create-bill-workspace","component":"createBillWorkspace","title":"Create Bill Workspace","type":"other-form"},{"name":"waive-bill-form","component":"waiveBillForm","title":"Waive Bill Form","type":"other-form"},{"name":"edit-bill-form","component":"editBillForm","title":"Edit Bill Form","type":"other-form"},{"name":"billable-service-form","component":"addServiceForm","title":"Create Charge Item Form","type":"other-form"},{"name":"commodity-form","component":"addCommodityForm","title":"Create Charge Item Form","type":"other-form"},{"name":"billing-form","component":"billingForm","title":"Billing Form","type":"other-form","width":"extra-wide"},{"name":"payment-mode-workspace","component":"paymentModeWorkspace","title":"Payment Mode Workspace","type":"other-form"},{"name":"cancel-bill-workspace","component":"cancelBillWorkspace","title":"Cancel Bill Workspace","type":"other-form"},{"name":"bill-deposit-workspace","component":"billDepositWorkspace","title":"Bill Deposit Workspace","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"manage-claim-request-modal","component":"manageClaimRequestModal"},{"name":"paid-bill-receipt-print-preview-modal","component":"paidBillReceiptPrintPreviewModal"},{"name":"clock-in-modal","component":"clockIn"},{"name":"create-bill-item-modal","component":"createBillItemModal"}],"version":"5.4.1-pre.2108"}
|
package/package.json
CHANGED
|
@@ -92,6 +92,9 @@ const ClaimsTable: React.FC<TableProps> = ({ title, emptyStateText, emptyStateHe
|
|
|
92
92
|
const layout = useLayoutType();
|
|
93
93
|
const size = layout === 'tablet' ? 'lg' : 'md';
|
|
94
94
|
const filteredClaimIds = filteredClaims.map((claim) => claim.responseUUID);
|
|
95
|
+
const responseUUIDs = filteredClaimIds
|
|
96
|
+
.map((claimId) => claims.find((c) => c.responseUUID === claimId)?.responseUUID)
|
|
97
|
+
.filter((uuid) => uuid);
|
|
95
98
|
|
|
96
99
|
const getHeaders = (): Header[] => {
|
|
97
100
|
let baseHeaders = [
|
|
@@ -208,7 +211,7 @@ const ClaimsTable: React.FC<TableProps> = ({ title, emptyStateText, emptyStateHe
|
|
|
208
211
|
filters={filters}
|
|
209
212
|
onFilterChanged={setFilters}
|
|
210
213
|
statusOptions={status}
|
|
211
|
-
filteredClaimIds={
|
|
214
|
+
filteredClaimIds={responseUUIDs}
|
|
212
215
|
/>
|
|
213
216
|
</div>
|
|
214
217
|
<DataTable rows={results} headers={headers} isSortable useZebraStyles>
|
|
@@ -34,7 +34,6 @@ type ClaimsTableProps = {
|
|
|
34
34
|
|
|
35
35
|
const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, isLoadingBill, onSelectItem }) => {
|
|
36
36
|
const { t } = useTranslation();
|
|
37
|
-
const { lineItems } = bill;
|
|
38
37
|
const layout = useLayoutType();
|
|
39
38
|
const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
|
|
40
39
|
const [selectedLineItems, setSelectedLineItems] = useState<LineItem[]>([]);
|
|
@@ -43,20 +42,33 @@ const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, is
|
|
|
43
42
|
const [currentPage, setCurrentPage] = useState(1);
|
|
44
43
|
const [pageSize, setPageSize] = useState(10);
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
// Filter line items that are paid and paid through Insurance
|
|
46
|
+
const insurancePaidLineItems = useMemo(() => {
|
|
47
|
+
return (bill.lineItems || []).filter((lineItem) => {
|
|
48
|
+
// Check if payment status is PAID
|
|
49
|
+
const isPaid = lineItem.paymentStatus === 'PAID';
|
|
50
|
+
|
|
51
|
+
// Check if there's an Insurance payment for this line item
|
|
52
|
+
const hasInsurancePayment = bill.payments?.some(
|
|
53
|
+
(payment) => payment.billLineItem?.uuid === lineItem.uuid && payment.instanceType?.name === 'Insurance',
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return isPaid && hasInsurancePayment;
|
|
57
|
+
});
|
|
58
|
+
}, [bill.lineItems, bill.payments]);
|
|
47
59
|
|
|
48
60
|
const filteredLineItems = useMemo(() => {
|
|
49
61
|
if (!debouncedSearchTerm) {
|
|
50
|
-
return
|
|
62
|
+
return insurancePaidLineItems;
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
return fuzzy
|
|
54
|
-
.filter(debouncedSearchTerm,
|
|
55
|
-
extract: (lineItem: LineItem) => `${lineItem.item}`,
|
|
66
|
+
.filter(debouncedSearchTerm, insurancePaidLineItems, {
|
|
67
|
+
extract: (lineItem: LineItem) => `${lineItem.item || lineItem.billableService}`,
|
|
56
68
|
})
|
|
57
69
|
.sort((r1, r2) => r1.score - r2.score)
|
|
58
70
|
.map((result) => result.original);
|
|
59
|
-
}, [debouncedSearchTerm,
|
|
71
|
+
}, [debouncedSearchTerm, insurancePaidLineItems]);
|
|
60
72
|
|
|
61
73
|
const paginatedLineItems = useMemo(() => {
|
|
62
74
|
const startIndex = (currentPage - 1) * pageSize;
|
|
@@ -69,11 +81,20 @@ const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, is
|
|
|
69
81
|
{ header: t('serialNo', 'Serial No'), key: 'serialno' },
|
|
70
82
|
{ header: t('billItem', 'Bill Item'), key: 'inventoryname' },
|
|
71
83
|
{ header: t('status', 'Status'), key: 'status' },
|
|
84
|
+
{ header: t('paymentMethod', 'Payment Method'), key: 'paymentMethod' },
|
|
72
85
|
{ header: t('totalAmount', 'Total amount'), key: 'total' },
|
|
73
86
|
{ header: t('billCreationDate', 'Bill creation date'), key: 'dateofbillcreation' },
|
|
74
87
|
];
|
|
75
88
|
|
|
76
|
-
const processBillItem = (item) =>
|
|
89
|
+
const processBillItem = (item) => {
|
|
90
|
+
const itemName = item?.item || item?.billableService;
|
|
91
|
+
return itemName?.split(':')[1]?.trim() || itemName;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const getPaymentMethod = (lineItemUuid: string) => {
|
|
95
|
+
const payment = bill.payments?.find((p) => p.billLineItem?.uuid === lineItemUuid);
|
|
96
|
+
return payment?.instanceType?.name || 'Unknown';
|
|
97
|
+
};
|
|
77
98
|
|
|
78
99
|
const tableRows: Array<typeof DataTableRow> = useMemo(
|
|
79
100
|
() =>
|
|
@@ -84,11 +105,12 @@ const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, is
|
|
|
84
105
|
inventoryname: processBillItem(item),
|
|
85
106
|
serialno: bill.receiptNumber,
|
|
86
107
|
status: item.paymentStatus,
|
|
108
|
+
paymentMethod: getPaymentMethod(item.uuid),
|
|
87
109
|
total: item.price * item.quantity,
|
|
88
110
|
dateofbillcreation: formatDate(new Date(bill.dateCreated), { mode: 'standard' }),
|
|
89
111
|
};
|
|
90
112
|
}) ?? [],
|
|
91
|
-
[bill.dateCreated, bill.receiptNumber, paginatedLineItems],
|
|
113
|
+
[bill.dateCreated, bill.receiptNumber, bill.payments, paginatedLineItems],
|
|
92
114
|
);
|
|
93
115
|
|
|
94
116
|
if (isLoadingBill) {
|
|
@@ -126,10 +148,10 @@ const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, is
|
|
|
126
148
|
className={styles.tableContainer}
|
|
127
149
|
description={
|
|
128
150
|
<span className={styles.tableDescription}>
|
|
129
|
-
<span>{t('
|
|
151
|
+
<span>{t('insurancePaidItems', 'Items paid through Insurance')}</span>
|
|
130
152
|
</span>
|
|
131
153
|
}
|
|
132
|
-
title={t('
|
|
154
|
+
title={t('claimableItems', 'Claimable Items')}>
|
|
133
155
|
<div className={styles.toolbarWrapper}>
|
|
134
156
|
<TableToolbar {...getToolbarProps()} className={styles.tableToolbar} size={responsiveSize}>
|
|
135
157
|
<TableToolbarContent className={styles.headerContainer}>
|
|
@@ -143,7 +165,7 @@ const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, is
|
|
|
143
165
|
</TableToolbarContent>
|
|
144
166
|
</TableToolbar>
|
|
145
167
|
</div>
|
|
146
|
-
<Table {...getTableProps()} aria-label="claim line items" className={styles.table}>
|
|
168
|
+
<Table {...getTableProps()} aria-label="insurance claim line items" className={styles.table}>
|
|
147
169
|
<TableHead>
|
|
148
170
|
<TableRow>
|
|
149
171
|
{isSelectable ? <TableHeader /> : null}
|
|
@@ -184,14 +206,13 @@ const ClaimsTable: React.FC<ClaimsTableProps> = ({ bill, isSelectable = true, is
|
|
|
184
206
|
<Layer>
|
|
185
207
|
<Tile className={styles.filterEmptyStateTile}>
|
|
186
208
|
<p className={styles.filterEmptyStateContent}>
|
|
187
|
-
{t('
|
|
209
|
+
{t('noInsurancePaidItems', 'No items paid through insurance found')}
|
|
188
210
|
</p>
|
|
189
|
-
<p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
|
|
190
211
|
</Tile>
|
|
191
212
|
</Layer>
|
|
192
213
|
</div>
|
|
193
214
|
)}
|
|
194
|
-
{
|
|
215
|
+
{filteredLineItems.length > pageSize && (
|
|
195
216
|
<Pagination
|
|
196
217
|
forwardText="Next page"
|
|
197
218
|
backwardText="Previous page"
|
|
@@ -23,6 +23,7 @@ export function usePaymentSchema(bill: MappedBill) {
|
|
|
23
23
|
return amountDue >= 0 && value > 0;
|
|
24
24
|
}, 'Amount paid should not be greater than amount due'),
|
|
25
25
|
referenceCode: z.string(),
|
|
26
|
+
lineItemUuid: z.string().uuid().nonempty({ message: 'Line item selection is required' }),
|
|
26
27
|
})
|
|
27
28
|
.refine(
|
|
28
29
|
(data) => {
|
|
@@ -151,7 +151,7 @@ const InvoiceTable: React.FC<InvoiceTableProps> = ({ bill, isSelectable = true,
|
|
|
151
151
|
{...getRowProps({
|
|
152
152
|
row,
|
|
153
153
|
})}>
|
|
154
|
-
{
|
|
154
|
+
{isSelectable && (
|
|
155
155
|
<TableSelectRow
|
|
156
156
|
aria-label="Select row"
|
|
157
157
|
{...getSelectionProps({ row })}
|
|
@@ -6,23 +6,33 @@ import { Button, Dropdown, NumberInputSkeleton, TextInput, NumberInput } from '@
|
|
|
6
6
|
import { ErrorState } from '@openmrs/esm-patient-common-lib';
|
|
7
7
|
import styles from './payment-form.scss';
|
|
8
8
|
import { usePaymentModes } from '../../../billing.resource';
|
|
9
|
-
import { PaymentFormValue, PaymentMethod } from '../../../types';
|
|
9
|
+
import { type LineItem, PaymentFormValue, PaymentMethod } from '../../../types';
|
|
10
|
+
import { extractBillableName } from '../../../utils';
|
|
10
11
|
|
|
11
12
|
type PaymentFormProps = {
|
|
12
13
|
disablePayment: boolean;
|
|
13
14
|
amountDue: number;
|
|
14
|
-
append: (obj: { method: PaymentMethod; amount: number; referenceCode: string }) => void;
|
|
15
|
+
append: (obj: { method: PaymentMethod; amount: number; referenceCode: string; lineItemUuid: string }) => void;
|
|
15
16
|
fields: FieldArrayWithId<PaymentFormValue, 'payment', 'id'>[];
|
|
16
17
|
remove: UseFieldArrayRemove;
|
|
18
|
+
selectedLineItems: Array<LineItem>;
|
|
17
19
|
};
|
|
18
20
|
|
|
19
|
-
const PaymentForm: React.FC<PaymentFormProps> = ({
|
|
21
|
+
const PaymentForm: React.FC<PaymentFormProps> = ({
|
|
22
|
+
disablePayment,
|
|
23
|
+
amountDue,
|
|
24
|
+
append,
|
|
25
|
+
remove,
|
|
26
|
+
fields,
|
|
27
|
+
selectedLineItems,
|
|
28
|
+
}) => {
|
|
20
29
|
const { t } = useTranslation();
|
|
21
30
|
const {
|
|
22
31
|
control,
|
|
23
32
|
formState: { errors },
|
|
24
33
|
setFocus,
|
|
25
34
|
getValues,
|
|
35
|
+
watch,
|
|
26
36
|
} = useFormContext<PaymentFormValue>();
|
|
27
37
|
const { paymentModes, isLoading, error } = usePaymentModes();
|
|
28
38
|
|
|
@@ -31,9 +41,26 @@ const PaymentForm: React.FC<PaymentFormProps> = ({ disablePayment, amountDue, ap
|
|
|
31
41
|
const attributes = formValues?.payment?.[index]?.method?.attributeTypes ?? [];
|
|
32
42
|
return attributes.some((attribute) => attribute.required) || attributes?.length > 0;
|
|
33
43
|
};
|
|
44
|
+
const paymentValues = watch('payment');
|
|
34
45
|
|
|
46
|
+
const getUsedItemUuids = (currentIndex: number) => {
|
|
47
|
+
return paymentValues
|
|
48
|
+
.map((payment, index) => (index !== currentIndex ? payment?.lineItemUuid : null))
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
};
|
|
51
|
+
const getAvailableLineItems = (currentIndex: number) => {
|
|
52
|
+
const usedUuids = getUsedItemUuids(currentIndex);
|
|
53
|
+
return selectedLineItems.filter((item) => item.paymentStatus === 'PENDING' && !usedUuids.includes(item.uuid));
|
|
54
|
+
};
|
|
35
55
|
const handleAppendPaymentMode = useCallback(() => {
|
|
36
|
-
|
|
56
|
+
const availableItems = getAvailableLineItems(fields.length);
|
|
57
|
+
const initialItemUuid = availableItems.length === 1 ? availableItems[0].uuid : '';
|
|
58
|
+
append({
|
|
59
|
+
method: null,
|
|
60
|
+
amount: null,
|
|
61
|
+
referenceCode: '',
|
|
62
|
+
lineItemUuid: initialItemUuid,
|
|
63
|
+
});
|
|
37
64
|
setFocus(`payment.${fields.length}.method`);
|
|
38
65
|
}, [append, fields.length, setFocus]);
|
|
39
66
|
|
|
@@ -53,67 +80,91 @@ const PaymentForm: React.FC<PaymentFormProps> = ({ disablePayment, amountDue, ap
|
|
|
53
80
|
|
|
54
81
|
return (
|
|
55
82
|
<div className={styles.container}>
|
|
56
|
-
{fields.map((field, index) =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
render={({ field }) => (
|
|
82
|
-
<NumberInput
|
|
83
|
-
{...field}
|
|
84
|
-
id="paymentAmount"
|
|
85
|
-
onChange={(e) => field.onChange(Number(e.target.value))}
|
|
86
|
-
invalid={!!errors?.payment?.[index]?.amount}
|
|
87
|
-
invalidText={errors?.payment?.[index]?.amount?.message}
|
|
88
|
-
label={t('amount', 'Amount')}
|
|
89
|
-
placeholder={t('enterAmount', 'Enter amount')}
|
|
90
|
-
/>
|
|
91
|
-
)}
|
|
92
|
-
/>
|
|
93
|
-
{shouldShowReferenceCode(index) && (
|
|
83
|
+
{fields.map((field, index) => {
|
|
84
|
+
const availableLineItems = getAvailableLineItems(index);
|
|
85
|
+
return (
|
|
86
|
+
<div key={field.id} className={styles.paymentMethodContainer}>
|
|
87
|
+
<Controller
|
|
88
|
+
control={control}
|
|
89
|
+
name={`payment.${index}.lineItemUuid`}
|
|
90
|
+
render={({ field }) => (
|
|
91
|
+
<Dropdown
|
|
92
|
+
{...field}
|
|
93
|
+
id="paymentLineItem"
|
|
94
|
+
onChange={({ selectedItem }) => {
|
|
95
|
+
field.onChange(selectedItem?.uuid);
|
|
96
|
+
setFocus(`payment.${index}.method`);
|
|
97
|
+
}}
|
|
98
|
+
titleText={t('selectLineItemToPay', 'Select line item to pay')}
|
|
99
|
+
label={t('selectLineItemToPay', 'Select line item to pay')}
|
|
100
|
+
items={availableLineItems}
|
|
101
|
+
itemToString={(item) => extractBillableName(item)}
|
|
102
|
+
invalid={!!errors?.payment?.[index]?.lineItemUuid}
|
|
103
|
+
invalidText={errors?.payment?.[index]?.lineItemUuid?.message}
|
|
104
|
+
className={styles.lineItemDropdown}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
/>
|
|
94
108
|
<Controller
|
|
95
|
-
name={`payment.${index}.referenceCode`}
|
|
96
109
|
control={control}
|
|
110
|
+
name={`payment.${index}.method`}
|
|
97
111
|
render={({ field }) => (
|
|
98
|
-
<
|
|
112
|
+
<Dropdown
|
|
99
113
|
{...field}
|
|
100
|
-
id="
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
id="paymentMethod"
|
|
115
|
+
onChange={({ selectedItem }) => {
|
|
116
|
+
setFocus(`payment.${index}.amount`);
|
|
117
|
+
field.onChange(selectedItem);
|
|
118
|
+
}}
|
|
119
|
+
titleText={t('paymentMethod', 'Payment method')}
|
|
120
|
+
label={t('selectPaymentMethod', 'Select payment method')}
|
|
121
|
+
items={paymentModes}
|
|
122
|
+
itemToString={(item) => (item ? item.name : '')}
|
|
123
|
+
invalid={!!errors?.payment?.[index]?.method}
|
|
124
|
+
invalidText={errors?.payment?.[index]?.method?.message}
|
|
106
125
|
/>
|
|
107
126
|
)}
|
|
108
127
|
/>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
128
|
+
<Controller
|
|
129
|
+
control={control}
|
|
130
|
+
name={`payment.${index}.amount`}
|
|
131
|
+
render={({ field }) => (
|
|
132
|
+
<NumberInput
|
|
133
|
+
{...field}
|
|
134
|
+
id="paymentAmount"
|
|
135
|
+
onChange={(e) => field.onChange(Number(e.target.value))}
|
|
136
|
+
invalid={!!errors?.payment?.[index]?.amount}
|
|
137
|
+
invalidText={errors?.payment?.[index]?.amount?.message}
|
|
138
|
+
label={t('amount', 'Amount')}
|
|
139
|
+
placeholder={t('enterAmount', 'Enter amount')}
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
142
|
+
/>
|
|
143
|
+
{shouldShowReferenceCode(index) && (
|
|
144
|
+
<Controller
|
|
145
|
+
name={`payment.${index}.referenceCode`}
|
|
146
|
+
control={control}
|
|
147
|
+
render={({ field }) => (
|
|
148
|
+
<TextInput
|
|
149
|
+
{...field}
|
|
150
|
+
id="paymentReferenceCode"
|
|
151
|
+
labelText={t('referenceNumber', 'Reference number')}
|
|
152
|
+
placeholder={t('enterReferenceNumber', 'Enter ref. number')}
|
|
153
|
+
type="text"
|
|
154
|
+
invalid={!!errors?.payment?.[index]?.referenceCode}
|
|
155
|
+
invalidText={errors?.payment?.[index]?.referenceCode?.message}
|
|
156
|
+
/>
|
|
157
|
+
)}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
<div className={styles.removeButtonContainer}>
|
|
161
|
+
<TrashCan onClick={() => handleRemovePaymentMode(index)} className={styles.removeButton} size={20} />
|
|
162
|
+
</div>
|
|
112
163
|
</div>
|
|
113
|
-
|
|
114
|
-
)
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
115
166
|
<Button
|
|
116
|
-
disabled={disablePayment}
|
|
167
|
+
disabled={disablePayment || !selectedLineItems.length}
|
|
117
168
|
size="md"
|
|
118
169
|
onClick={handleAppendPaymentMode}
|
|
119
170
|
className={styles.paymentButtons}
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
|
|
20
20
|
.paymentMethodContainer {
|
|
21
21
|
display: grid;
|
|
22
|
-
grid-template-columns: repeat(
|
|
22
|
+
grid-template-columns: repeat(5, minmax(auto, 1fr));
|
|
23
23
|
align-items: flex-start;
|
|
24
|
-
column-gap:
|
|
24
|
+
column-gap: layout.$spacing-02;
|
|
25
25
|
margin: 0.625rem 0;
|
|
26
26
|
width: 100%;
|
|
27
27
|
}
|
|
@@ -52,3 +52,6 @@
|
|
|
52
52
|
.removeButton {
|
|
53
53
|
color: colors.$red-60;
|
|
54
54
|
}
|
|
55
|
+
.lineItemDropdown {
|
|
56
|
+
min-width: 10rem;
|
|
57
|
+
}
|
|
@@ -67,7 +67,6 @@ const Payments: React.FC<PaymentProps> = ({ bill, selectedLineItems }) => {
|
|
|
67
67
|
globalActiveSheet,
|
|
68
68
|
);
|
|
69
69
|
remove();
|
|
70
|
-
|
|
71
70
|
processBillPayment(paymentPayload, bill.uuid).then(
|
|
72
71
|
(resp) => {
|
|
73
72
|
showSnackbar({
|
|
@@ -142,7 +141,12 @@ const Payments: React.FC<PaymentProps> = ({ bill, selectedLineItems }) => {
|
|
|
142
141
|
className={styles.paymentError}
|
|
143
142
|
/>
|
|
144
143
|
)}
|
|
145
|
-
<PaymentForm
|
|
144
|
+
<PaymentForm
|
|
145
|
+
{...formArrayMethods}
|
|
146
|
+
selectedLineItems={selectedLineItems}
|
|
147
|
+
disablePayment={amountDue <= 0}
|
|
148
|
+
amountDue={amountDue}
|
|
149
|
+
/>
|
|
146
150
|
</div>
|
|
147
151
|
</div>
|
|
148
152
|
<div className={styles.divider} />
|
|
@@ -6,6 +6,7 @@ import { mockBill, mockedActiveSheet, mockLineItems, mockPaymentModes } from '..
|
|
|
6
6
|
import { processBillPayment, usePaymentModes } from '../../billing.resource';
|
|
7
7
|
import { useClockInStatus } from '../../payment-points/use-clock-in-status';
|
|
8
8
|
import Payments from './payments.component';
|
|
9
|
+
|
|
9
10
|
const mockProcessBillPayment = processBillPayment as jest.MockedFunction<typeof processBillPayment>;
|
|
10
11
|
const mockUsePaymentModes = usePaymentModes as jest.MockedFunction<typeof usePaymentModes>;
|
|
11
12
|
const mockShowSnackbar = showSnackbar as jest.MockedFunction<typeof showSnackbar>;
|
|
@@ -31,6 +32,7 @@ describe('Payment', () => {
|
|
|
31
32
|
},
|
|
32
33
|
},
|
|
33
34
|
};
|
|
35
|
+
|
|
34
36
|
mockProcessBillPayment.mockRejectedValueOnce(mockFieldErrorResponse);
|
|
35
37
|
mockUsePaymentModes.mockReturnValue({
|
|
36
38
|
paymentModes: mockPaymentModes,
|
|
@@ -49,10 +51,19 @@ describe('Payment', () => {
|
|
|
49
51
|
});
|
|
50
52
|
|
|
51
53
|
render(<Payments bill={mockBill as any} selectedLineItems={mockLineItems} />);
|
|
54
|
+
|
|
52
55
|
const addPaymentMethod = screen.getByRole('button', { name: /Add payment option/i });
|
|
53
56
|
await user.click(addPaymentMethod);
|
|
54
|
-
|
|
55
|
-
const
|
|
57
|
+
|
|
58
|
+
const lineItemDropdown = await screen.findByRole('combobox', { name: /Select line item to pay/i });
|
|
59
|
+
await user.click(lineItemDropdown);
|
|
60
|
+
|
|
61
|
+
const lineItemOptions = await screen.findAllByRole('option');
|
|
62
|
+
await user.click(lineItemOptions[0]);
|
|
63
|
+
|
|
64
|
+
const paymentMethodDropdown = await screen.findByRole('combobox', { name: /Payment method/i });
|
|
65
|
+
await user.click(paymentMethodDropdown);
|
|
66
|
+
const cashOption = await screen.findByRole('option', { name: /Cash/i });
|
|
56
67
|
await user.click(cashOption);
|
|
57
68
|
|
|
58
69
|
const amountInput = screen.getByRole('spinbutton', { name: /Amount/i });
|
|
@@ -71,6 +82,8 @@ describe('Payment', () => {
|
|
|
71
82
|
billableService: 'c15d25b9-12bb-441d-9241-cae541dd4575',
|
|
72
83
|
display: 'BillLineItem',
|
|
73
84
|
item: 'c15d25b9-12bb-441d-9241-cae541dd4575',
|
|
85
|
+
itemOrServiceConceptUuid: 'c42525b9-12bb-441d-9241-cae541dd4575',
|
|
86
|
+
serviceTypeUuid: '915d25b9-12bb-441d-9241-cae541dd4575',
|
|
74
87
|
lineItemOrder: 0,
|
|
75
88
|
order: null,
|
|
76
89
|
paymentStatus: 'PAID',
|
|
@@ -87,6 +100,8 @@ describe('Payment', () => {
|
|
|
87
100
|
billableService: '04be5832-5440-44d0-83d2-5c0dfd0ac7de',
|
|
88
101
|
display: 'BillLineItem',
|
|
89
102
|
item: '04be5832-5440-44d0-83d2-5c0dfd0ac7de',
|
|
103
|
+
itemOrServiceConceptUuid: 'c42525b9-12bb-441d-9241-cae541dd4575',
|
|
104
|
+
serviceTypeUuid: '915d25b9-12bb-441d-9241-cae541dd4575',
|
|
90
105
|
lineItemOrder: 1,
|
|
91
106
|
order: null,
|
|
92
107
|
paymentStatus: 'PAID',
|
|
@@ -103,6 +118,8 @@ describe('Payment', () => {
|
|
|
103
118
|
billableService: '3f5d0684-a280-477e-a67b-2a956a1f6dca',
|
|
104
119
|
display: 'BillLineItem',
|
|
105
120
|
item: '3f5d0684-a280-477e-a67b-2a956a1f6dca',
|
|
121
|
+
itemOrServiceConceptUuid: 'c42525b9-12bb-441d-9241-cae541dd4575',
|
|
122
|
+
serviceTypeUuid: '915d25b9-12bb-441d-9241-cae541dd4575',
|
|
106
123
|
lineItemOrder: 2,
|
|
107
124
|
order: null,
|
|
108
125
|
paymentStatus: 'PAID',
|
|
@@ -133,7 +150,13 @@ describe('Payment', () => {
|
|
|
133
150
|
],
|
|
134
151
|
patient: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
|
|
135
152
|
payments: [
|
|
136
|
-
{
|
|
153
|
+
{
|
|
154
|
+
amount: 100,
|
|
155
|
+
amountTendered: 100,
|
|
156
|
+
attributes: [],
|
|
157
|
+
instanceType: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74',
|
|
158
|
+
billLineItem: '314c25fd-2c90-4a7f-9f98-c99cd3f153e8',
|
|
159
|
+
},
|
|
137
160
|
],
|
|
138
161
|
status: 'PENDING',
|
|
139
162
|
},
|
|
@@ -158,18 +181,27 @@ describe('Payment', () => {
|
|
|
158
181
|
error: null,
|
|
159
182
|
mutate: jest.fn(),
|
|
160
183
|
});
|
|
184
|
+
|
|
161
185
|
render(<Payments bill={mockBill as any} selectedLineItems={mockLineItems} />);
|
|
186
|
+
|
|
162
187
|
const addPaymentMethod = screen.getByRole('button', { name: /Add payment option/i });
|
|
163
188
|
await user.click(addPaymentMethod);
|
|
164
189
|
|
|
165
|
-
|
|
166
|
-
expect(
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
const lineItemDropdown = await screen.findByRole('combobox', { name: /Select line item to pay/i });
|
|
191
|
+
expect(lineItemDropdown).toHaveFocus();
|
|
192
|
+
|
|
193
|
+
await user.click(lineItemDropdown);
|
|
194
|
+
const lineItemOptions = await screen.findAllByRole('option');
|
|
195
|
+
await user.click(lineItemOptions[0]);
|
|
196
|
+
|
|
197
|
+
const paymentMethodField = await screen.findByRole('combobox', { name: /Payment method/i });
|
|
198
|
+
expect(paymentMethodField).toHaveFocus();
|
|
199
|
+
|
|
200
|
+
await user.click(paymentMethodField);
|
|
201
|
+
const cashOption = await screen.findByRole('option', { name: /Cash/i });
|
|
169
202
|
await user.click(cashOption);
|
|
170
|
-
|
|
171
|
-
expect(screen.getByRole('spinbutton', { name: /Amount/i })).toHaveFocus();
|
|
203
|
+
|
|
172
204
|
const amountInput = screen.getByRole('spinbutton', { name: /Amount/i });
|
|
173
|
-
|
|
205
|
+
expect(amountInput).toHaveFocus();
|
|
174
206
|
});
|
|
175
207
|
});
|
|
@@ -126,6 +126,7 @@ export const createPaymentPayload = (
|
|
|
126
126
|
value: attribute.value,
|
|
127
127
|
})),
|
|
128
128
|
instanceType: payment.instanceType.uuid,
|
|
129
|
+
billLineItem: payment.billLineItem?.uuid,
|
|
129
130
|
}));
|
|
130
131
|
|
|
131
132
|
// Transform new payments
|
|
@@ -137,6 +138,7 @@ export const createPaymentPayload = (
|
|
|
137
138
|
value: formValue.referenceCode,
|
|
138
139
|
})),
|
|
139
140
|
instanceType: formValue.method?.uuid,
|
|
141
|
+
billLineItem: formValue.lineItemUuid,
|
|
140
142
|
}));
|
|
141
143
|
|
|
142
144
|
// Combine and calculate payments
|
package/src/types/index.ts
CHANGED
|
@@ -285,12 +285,35 @@ export interface Payment {
|
|
|
285
285
|
attributes: Attribute[];
|
|
286
286
|
amount: number;
|
|
287
287
|
amountTendered: number;
|
|
288
|
+
billLineItem: BillLineItem;
|
|
288
289
|
dateCreated: number;
|
|
289
290
|
voided: boolean;
|
|
290
291
|
resourceVersion: string;
|
|
291
292
|
}
|
|
293
|
+
export interface BillLineItem {
|
|
294
|
+
uuid: string;
|
|
295
|
+
display: string;
|
|
296
|
+
voided: boolean;
|
|
297
|
+
voidReason: any;
|
|
298
|
+
item: string;
|
|
299
|
+
billableService: string;
|
|
300
|
+
quantity: number;
|
|
301
|
+
price: number;
|
|
302
|
+
priceName: string;
|
|
303
|
+
priceUuid: string;
|
|
304
|
+
lineItemOrder: number;
|
|
305
|
+
paymentStatus: string;
|
|
306
|
+
itemOrServiceConceptUuid: string;
|
|
307
|
+
serviceTypeUuid: string;
|
|
308
|
+
resourceVersion: string;
|
|
309
|
+
}
|
|
292
310
|
|
|
293
|
-
export type FormPayment = {
|
|
311
|
+
export type FormPayment = {
|
|
312
|
+
method: PaymentMethod;
|
|
313
|
+
amount: string | number;
|
|
314
|
+
referenceCode?: number | string;
|
|
315
|
+
lineItemUuid: string;
|
|
316
|
+
};
|
|
294
317
|
|
|
295
318
|
export type PaymentFormValue = {
|
|
296
319
|
payment: Array<FormPayment>;
|
package/src/utils.ts
CHANGED
|
@@ -166,3 +166,9 @@ export const computeWaivedAmount = (bill: MappedBill) => {
|
|
|
166
166
|
.filter((payment) => payment.instanceType.name.toLowerCase() === 'waiver')
|
|
167
167
|
.reduce((curr: number, prev) => curr + Number(prev.amountTendered), 0);
|
|
168
168
|
};
|
|
169
|
+
|
|
170
|
+
export const extractBillableName = (billableService: string | { billableService: string }) => {
|
|
171
|
+
const parts =
|
|
172
|
+
typeof billableService === 'string' ? billableService.split(':') : billableService.billableService.split(':');
|
|
173
|
+
return parts.length > 1 ? parts[1] : '';
|
|
174
|
+
};
|