@kenyaemr/esm-billing-app 5.4.1-pre.1799 → 5.4.1-pre.1818
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 +71 -64
- package/dist/300.js +1 -1
- package/dist/{475.js → 548.js} +1 -1
- package/dist/548.js.map +1 -0
- package/dist/kenyaemr-esm-billing-app.js +1 -1
- package/dist/kenyaemr-esm-billing-app.js.buildmanifest.json +26 -26
- package/dist/kenyaemr-esm-billing-app.js.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/billable-services/bill-manager/modals/create-bill-item-modal.component.tsx +208 -0
- package/src/billable-services/bill-manager/modals/create-bill-item-modal.scss +26 -0
- package/src/billable-services/billable-service.resource.tsx +1 -1
- package/src/billable-services/billiable-item/drug-order/drug-order.component.tsx +0 -1
- package/src/billable-services/billiable-item/test-order/test-order-action.component.tsx +56 -13
- package/src/billable-services/billiable-item/test-order/test-order-action.resource.tsx +14 -3
- package/src/billable-services/billiable-item/test-order/test-order-action.test.tsx +51 -2
- package/src/billable-services/billiable-item/useBillableItem.tsx +16 -1
- package/src/billing.resource.ts +1 -0
- package/src/index.ts +3 -1
- package/src/routes.json +4 -0
- package/translations/en.json +2 -0
- package/dist/475.js.map +0 -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":"billing-test-order-action","component":"testOrderAction","slot":"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":"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"}],"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":"retry-claim-request-modal","component":"retryClaimRequestModal"},{"name":"paid-bill-receipt-print-preview-modal","component":"paidBillReceiptPrintPreviewModal"},{"name":"clock-in-modal","component":"clockIn"}],"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":"billing-test-order-action","component":"testOrderAction","slot":"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":"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"}],"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":"retry-claim-request-modal","component":"retryClaimRequestModal"},{"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.1818"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
ModalBody,
|
|
6
|
+
ModalFooter,
|
|
7
|
+
ModalHeader,
|
|
8
|
+
Column,
|
|
9
|
+
Dropdown,
|
|
10
|
+
Stack,
|
|
11
|
+
Table,
|
|
12
|
+
TableBody,
|
|
13
|
+
TableCell,
|
|
14
|
+
TableHead,
|
|
15
|
+
TableHeader,
|
|
16
|
+
TableRow,
|
|
17
|
+
} from '@carbon/react';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
20
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
21
|
+
import { useBillableServices } from '../../billable-service.resource';
|
|
22
|
+
import { showSnackbar, useConfig } from '@openmrs/esm-framework';
|
|
23
|
+
import { BillingConfig } from '../../../config-schema';
|
|
24
|
+
import { billingFormSchema, processBillItems } from '../../../billing.resource';
|
|
25
|
+
import styles from './create-bill-item-modal.scss';
|
|
26
|
+
import { useStockItemQuantity } from '../../billiable-item/useBillableItem';
|
|
27
|
+
|
|
28
|
+
interface CreateBillItemModalProps {
|
|
29
|
+
closeModal: () => void;
|
|
30
|
+
medicationRequestBundle: {
|
|
31
|
+
request: fhir.MedicationRequest;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type FormType = z.infer<typeof billingFormSchema>;
|
|
36
|
+
|
|
37
|
+
const CreateBillItemModal: React.FC<CreateBillItemModalProps> = ({ closeModal, medicationRequestBundle }) => {
|
|
38
|
+
const { t } = useTranslation();
|
|
39
|
+
const { billableServices, error, isLoading } = useBillableServices();
|
|
40
|
+
const { cashPointUuid, cashierUuid } = useConfig<BillingConfig>();
|
|
41
|
+
|
|
42
|
+
const patientUuid = medicationRequestBundle?.request?.subject?.reference?.split('/')[1];
|
|
43
|
+
const orderUuid = medicationRequestBundle?.request?.id;
|
|
44
|
+
const quantityToDispense = medicationRequestBundle?.request?.dispenseRequest?.quantity?.value;
|
|
45
|
+
const drugUuid = medicationRequestBundle?.request?.medicationReference?.reference?.split('/')[1];
|
|
46
|
+
const { stockItemUuid } = useStockItemQuantity(drugUuid);
|
|
47
|
+
|
|
48
|
+
const billableItem =
|
|
49
|
+
billableServices?.filter((service) => {
|
|
50
|
+
const stockItem = service?.stockItem.split(':')[0];
|
|
51
|
+
return stockItem === stockItemUuid;
|
|
52
|
+
}) || [];
|
|
53
|
+
const defaultPaymentStatus = 'PENDING';
|
|
54
|
+
|
|
55
|
+
const form = useForm<FormType>({
|
|
56
|
+
mode: 'onChange',
|
|
57
|
+
resolver: zodResolver(billingFormSchema),
|
|
58
|
+
defaultValues: {
|
|
59
|
+
cashPoint: cashPointUuid,
|
|
60
|
+
cashier: cashierUuid,
|
|
61
|
+
patient: patientUuid,
|
|
62
|
+
status: 'PENDING',
|
|
63
|
+
lineItems: billableItem.map((service) => ({
|
|
64
|
+
billableService: service.uuid,
|
|
65
|
+
lineItemOrder: 0,
|
|
66
|
+
quantity: quantityToDispense || 1,
|
|
67
|
+
price: 0,
|
|
68
|
+
paymentStatus: defaultPaymentStatus,
|
|
69
|
+
priceUuid: '',
|
|
70
|
+
priceName: '',
|
|
71
|
+
order: orderUuid,
|
|
72
|
+
})),
|
|
73
|
+
payments: [],
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const handleSubmit = async (data: FormType) => {
|
|
78
|
+
const validatedLineItems = data.lineItems.map((item, index) => ({
|
|
79
|
+
...item,
|
|
80
|
+
billableService: billableItem[index]?.uuid,
|
|
81
|
+
order: orderUuid,
|
|
82
|
+
paymentStatus: defaultPaymentStatus,
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
const validatedData = {
|
|
86
|
+
...data,
|
|
87
|
+
lineItems: validatedLineItems,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await processBillItems(validatedData);
|
|
92
|
+
showSnackbar({
|
|
93
|
+
title: t('billItems', 'Save Bill'),
|
|
94
|
+
subtitle: 'Bill processing has been successful',
|
|
95
|
+
kind: 'success',
|
|
96
|
+
timeoutInMs: 3000,
|
|
97
|
+
});
|
|
98
|
+
// TODO: mutate the bill
|
|
99
|
+
closeModal();
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('Bill processing error:', error);
|
|
102
|
+
showSnackbar({
|
|
103
|
+
title: 'Bill processing error',
|
|
104
|
+
kind: 'error',
|
|
105
|
+
subtitle: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const calculateTotal = (index: number) => {
|
|
111
|
+
const price = form.watch(`lineItems.${index}.price`) || 0;
|
|
112
|
+
const quantity = form.watch(`lineItems.${index}.quantity`) || 1;
|
|
113
|
+
return price * quantity;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div>
|
|
118
|
+
<form onSubmit={form.handleSubmit(handleSubmit)}>
|
|
119
|
+
<ModalHeader closeModal={closeModal} title={t('billing', 'Billing')} />
|
|
120
|
+
<ModalBody>
|
|
121
|
+
<Stack gap={4} className={styles.grid}>
|
|
122
|
+
<Column className={styles.billingItem}>
|
|
123
|
+
<Table aria-label="billing items table">
|
|
124
|
+
<TableHead>
|
|
125
|
+
<TableRow>
|
|
126
|
+
<TableHeader>Item</TableHeader>
|
|
127
|
+
<TableHeader>Quantity</TableHeader>
|
|
128
|
+
<TableHeader>PaymentMethod</TableHeader>
|
|
129
|
+
<TableHeader>Price</TableHeader>
|
|
130
|
+
<TableHeader>Total</TableHeader>
|
|
131
|
+
</TableRow>
|
|
132
|
+
</TableHead>
|
|
133
|
+
<TableBody>
|
|
134
|
+
{billableItem.map((service, index) => (
|
|
135
|
+
<TableRow key={service.uuid}>
|
|
136
|
+
<TableCell>{service?.name || 'Service Not Found'}</TableCell>
|
|
137
|
+
<TableCell>
|
|
138
|
+
<Controller
|
|
139
|
+
control={form.control}
|
|
140
|
+
name={`lineItems.${index}.quantity`}
|
|
141
|
+
defaultValue={quantityToDispense}
|
|
142
|
+
render={({ field }) => (
|
|
143
|
+
<input
|
|
144
|
+
{...field}
|
|
145
|
+
type="number"
|
|
146
|
+
className="form-control"
|
|
147
|
+
min={1}
|
|
148
|
+
max={quantityToDispense}
|
|
149
|
+
onChange={(e) => {
|
|
150
|
+
const value = parseInt(e.target.value, 10);
|
|
151
|
+
field.onChange(value);
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
/>
|
|
156
|
+
</TableCell>
|
|
157
|
+
<TableCell>
|
|
158
|
+
<Controller
|
|
159
|
+
control={form.control}
|
|
160
|
+
name={`lineItems.${index}.priceUuid`}
|
|
161
|
+
render={({ field }) => (
|
|
162
|
+
<Dropdown
|
|
163
|
+
id={`priceUuid-${index}`}
|
|
164
|
+
invalid={!!form.formState.errors.lineItems?.[index]?.priceUuid}
|
|
165
|
+
invalidText={form.formState.errors.lineItems?.[index]?.priceUuid?.message}
|
|
166
|
+
onChange={(e) => {
|
|
167
|
+
const selectedPrice = service?.servicePrices.find((p) => p.uuid === e.selectedItem);
|
|
168
|
+
if (selectedPrice) {
|
|
169
|
+
field.onChange(e.selectedItem);
|
|
170
|
+
form.setValue(`lineItems.${index}.price`, selectedPrice.price);
|
|
171
|
+
form.setValue(`lineItems.${index}.priceName`, selectedPrice.name);
|
|
172
|
+
form.setValue(`lineItems.${index}.billableService`, service.uuid);
|
|
173
|
+
form.setValue(`lineItems.${index}.order`, orderUuid);
|
|
174
|
+
form.setValue(`lineItems.${index}.paymentStatus`, defaultPaymentStatus);
|
|
175
|
+
}
|
|
176
|
+
}}
|
|
177
|
+
selectedItem={field.value}
|
|
178
|
+
label="Choose method"
|
|
179
|
+
items={service?.servicePrices.map((r) => r.uuid) ?? []}
|
|
180
|
+
itemToString={(item) => service?.servicePrices.find((r) => r.uuid === item)?.name ?? ''}
|
|
181
|
+
direction="top"
|
|
182
|
+
/>
|
|
183
|
+
)}
|
|
184
|
+
/>
|
|
185
|
+
</TableCell>
|
|
186
|
+
<TableCell>{form.watch(`lineItems.${index}.price`) ?? ' '}</TableCell>
|
|
187
|
+
<TableCell>{calculateTotal(index)}</TableCell>
|
|
188
|
+
</TableRow>
|
|
189
|
+
))}
|
|
190
|
+
</TableBody>
|
|
191
|
+
</Table>
|
|
192
|
+
</Column>
|
|
193
|
+
</Stack>
|
|
194
|
+
</ModalBody>
|
|
195
|
+
<ModalFooter>
|
|
196
|
+
<Button className={styles.button} kind="secondary" onClick={closeModal}>
|
|
197
|
+
{t('discard', 'Discard')}
|
|
198
|
+
</Button>
|
|
199
|
+
<Button className={styles.button} kind="primary" type="submit" disabled={form.formState.isSubmitting}>
|
|
200
|
+
{t('saveAndClose', 'Save & Close')}
|
|
201
|
+
</Button>
|
|
202
|
+
</ModalFooter>
|
|
203
|
+
</form>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export default CreateBillItemModal;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@use '@carbon/type';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
|
|
5
|
+
.heading {
|
|
6
|
+
@include type.type-style('heading-compact-01');
|
|
7
|
+
margin: layout.$spacing-05 0 layout.$spacing-05;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.grid {
|
|
11
|
+
margin: layout.$spacing-05 layout.$spacing-05;
|
|
12
|
+
padding-top: layout.$spacing-05;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.billingItem {
|
|
16
|
+
margin-top: layout.$spacing-10;
|
|
17
|
+
overflow-x: auto;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.button {
|
|
21
|
+
height: layout.$spacing-10;
|
|
22
|
+
display: flex;
|
|
23
|
+
align-content: flex-start;
|
|
24
|
+
align-items: baseline;
|
|
25
|
+
min-width: 50%;
|
|
26
|
+
}
|
|
@@ -7,7 +7,7 @@ type ResponseObject = {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
export const useBillableServices = () => {
|
|
10
|
-
const url = `/ws/rest/v1/cashier/billableService?v=custom:(uuid,name,shortName,serviceStatus,serviceType:(uuid,display),servicePrices:(uuid,name,paymentMode,price),concept:(uuid,display))`;
|
|
10
|
+
const url = `/ws/rest/v1/cashier/billableService?v=custom:(uuid,name,shortName,stockItem:(uuid, display),serviceStatus,serviceType:(uuid,display),servicePrices:(uuid,name,paymentMode,price),concept:(uuid,display))`;
|
|
11
11
|
const { data, isLoading, isValidating, error, mutate } = useSWR<{ data: ResponseObject }>(url, openmrsFetch, {});
|
|
12
12
|
return { billableServices: data?.data.results ?? [], isLoading, isValidating, error, mutate };
|
|
13
13
|
};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { InlineLoading, Button } from '@carbon/react';
|
|
2
|
-
import { Order } from '@openmrs/esm-patient-common-lib';
|
|
3
2
|
import React, { useCallback } from 'react';
|
|
4
3
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import { useTestOrderBillStatus } from './test-order-action.resource';
|
|
6
|
-
import { launchWorkspace, showModal } from '@openmrs/esm-framework';
|
|
7
4
|
import { mutate } from 'swr';
|
|
8
|
-
import
|
|
5
|
+
import { useOrderBill, useTestOrderBillStatus } from './test-order-action.resource';
|
|
6
|
+
import { launchWorkspace, showModal } from '@openmrs/esm-framework';
|
|
7
|
+
import { Order } from '@openmrs/esm-patient-common-lib';
|
|
9
8
|
import { createMedicationDispenseProps } from './dispense.resource';
|
|
9
|
+
import { useStockItemQuantity } from '../useBillableItem';
|
|
10
|
+
import { useBillableServices } from '../../billable-service.resource';
|
|
11
|
+
import styles from './test-order-action.scss';
|
|
10
12
|
|
|
11
13
|
type TestOrderProps = {
|
|
12
14
|
order?: Order;
|
|
@@ -32,9 +34,16 @@ const TestOrderAction: React.FC<TestOrderProps> = (props) => {
|
|
|
32
34
|
const dispenseFormProps = isDispenseOrder ? createMedicationDispenseProps(props) : null;
|
|
33
35
|
const orderUuid = order?.uuid ?? medicationRequestBundle?.request?.id;
|
|
34
36
|
const patientUuid = order?.patient?.uuid ?? medicationRequestBundle?.request?.subject?.reference?.split('/')[1];
|
|
35
|
-
|
|
36
37
|
const { isLoading, hasPendingPayment } = useTestOrderBillStatus(orderUuid, patientUuid);
|
|
37
|
-
|
|
38
|
+
const drugUuid = medicationRequestBundle?.request?.medicationReference?.reference?.split('/')[1];
|
|
39
|
+
const { billableServices } = useBillableServices();
|
|
40
|
+
const { stockItemQuantity, stockItemUuid } = useStockItemQuantity(drugUuid);
|
|
41
|
+
const billableItem =
|
|
42
|
+
billableServices?.filter((service) => {
|
|
43
|
+
const stockItem = service?.stockItem.split(':')[0];
|
|
44
|
+
return stockItem === stockItemUuid;
|
|
45
|
+
}) || [];
|
|
46
|
+
const { itemHasBill } = useOrderBill(patientUuid, orderUuid);
|
|
38
47
|
// Handle modal close and revalidation
|
|
39
48
|
const handleModalClose = useCallback(() => {
|
|
40
49
|
mutate((key) => typeof key === 'string' && key.startsWith(additionalProps?.mutateUrl as string), undefined, {
|
|
@@ -43,6 +52,17 @@ const TestOrderAction: React.FC<TestOrderProps> = (props) => {
|
|
|
43
52
|
}, [additionalProps?.mutateUrl]);
|
|
44
53
|
|
|
45
54
|
const launchModal = useCallback(() => {
|
|
55
|
+
if (stockItemQuantity > 0 && itemHasBill.length < 1 && billableItem.length > 0) {
|
|
56
|
+
const disposeBill = showModal(modalName ?? 'create-bill-item-modal', {
|
|
57
|
+
closeModal: () => {
|
|
58
|
+
handleModalClose();
|
|
59
|
+
disposeBill();
|
|
60
|
+
},
|
|
61
|
+
medicationRequestBundle,
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
if (isDispenseOrder) {
|
|
47
67
|
launchWorkspace('dispense-workspace', dispenseFormProps);
|
|
48
68
|
return;
|
|
@@ -56,7 +76,18 @@ const TestOrderAction: React.FC<TestOrderProps> = (props) => {
|
|
|
56
76
|
order,
|
|
57
77
|
...(additionalProps && { additionalProps }),
|
|
58
78
|
});
|
|
59
|
-
}, [
|
|
79
|
+
}, [
|
|
80
|
+
isDispenseOrder,
|
|
81
|
+
modalName,
|
|
82
|
+
order,
|
|
83
|
+
additionalProps,
|
|
84
|
+
dispenseFormProps,
|
|
85
|
+
handleModalClose,
|
|
86
|
+
medicationRequestBundle,
|
|
87
|
+
stockItemQuantity,
|
|
88
|
+
billableItem.length,
|
|
89
|
+
itemHasBill.length,
|
|
90
|
+
]);
|
|
60
91
|
|
|
61
92
|
if (isLoading) {
|
|
62
93
|
return (
|
|
@@ -70,18 +101,30 @@ const TestOrderAction: React.FC<TestOrderProps> = (props) => {
|
|
|
70
101
|
return null;
|
|
71
102
|
}
|
|
72
103
|
|
|
73
|
-
const buttonText =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
104
|
+
const buttonText = (() => {
|
|
105
|
+
if (hasPendingPayment) {
|
|
106
|
+
return t('unsettledBill', 'Unsettled bill');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (stockItemQuantity < 1) {
|
|
110
|
+
return t('outOfStock', 'Out of Stock');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (stockItemQuantity > 0 && itemHasBill.length === 0 && billableItem.length > 0) {
|
|
114
|
+
return t('bill', 'Bill');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return isDispenseOrder
|
|
118
|
+
? actionText ?? t('dispense', 'Dispense')
|
|
119
|
+
: actionText ?? t('pickLabRequest', 'Pick Lab Request');
|
|
120
|
+
})();
|
|
78
121
|
|
|
79
122
|
return (
|
|
80
123
|
<Button
|
|
81
124
|
kind="primary"
|
|
82
125
|
className={!isDispenseOrder ? styles.actionButton : ''}
|
|
83
126
|
size={!isDispenseOrder ? 'md' : ''}
|
|
84
|
-
disabled={hasPendingPayment}
|
|
127
|
+
disabled={hasPendingPayment || stockItemQuantity < 1}
|
|
85
128
|
onClick={launchModal}>
|
|
86
129
|
{buttonText}
|
|
87
130
|
</Button>
|
|
@@ -9,7 +9,7 @@ export const useTestOrderBillStatus = (orderUuid: string, patientUuid: string) =
|
|
|
9
9
|
const config = useConfig<BillingConfig>();
|
|
10
10
|
const { currentVisit } = useVisit(patientUuid);
|
|
11
11
|
const { isEmergencyPatient, isLoading: isLoadingQueue } = usePatientQueue(patientUuid);
|
|
12
|
-
const { isLoading
|
|
12
|
+
const { isLoading, hasPendingPayment } = useOrderPendingPaymentStatus(patientUuid, orderUuid);
|
|
13
13
|
|
|
14
14
|
// We want to check if the payment method is in the excluded list this includes insurances, where patient do not need to pay immediately
|
|
15
15
|
const isExcludedPaymentMethod = checkPaymentMethodExclusion(
|
|
@@ -18,7 +18,7 @@ export const useTestOrderBillStatus = (orderUuid: string, patientUuid: string) =
|
|
|
18
18
|
);
|
|
19
19
|
|
|
20
20
|
return useMemo(() => {
|
|
21
|
-
if (
|
|
21
|
+
if (isLoading || isLoadingQueue) {
|
|
22
22
|
return { hasPendingPayment: false, isLoading: true };
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -33,7 +33,7 @@ export const useTestOrderBillStatus = (orderUuid: string, patientUuid: string) =
|
|
|
33
33
|
return { hasPendingPayment, isLoading: false };
|
|
34
34
|
}, [
|
|
35
35
|
isLoadingQueue,
|
|
36
|
-
|
|
36
|
+
isLoading,
|
|
37
37
|
currentVisit?.visitType?.uuid,
|
|
38
38
|
config?.inPatientVisitTypeUuid,
|
|
39
39
|
isExcludedPaymentMethod,
|
|
@@ -74,3 +74,14 @@ export const useOrderPendingPaymentStatus = (patientUuid: string, orderUuid: str
|
|
|
74
74
|
|
|
75
75
|
return useMemo(() => ({ hasPendingPayment, isLoading, error }), [hasPendingPayment, isLoading, error]);
|
|
76
76
|
};
|
|
77
|
+
|
|
78
|
+
export const useOrderBill = (patientUuid: string, orderUuid: string) => {
|
|
79
|
+
const { patientBills, isLoading, error } = usePatientBills(patientUuid);
|
|
80
|
+
const itemHasBill = useMemo(() => {
|
|
81
|
+
return patientBills
|
|
82
|
+
?.map((bill) => bill.lineItems)
|
|
83
|
+
.flat()
|
|
84
|
+
.filter((lineItem) => lineItem.order && lineItem.order.uuid === orderUuid);
|
|
85
|
+
}, [patientBills, orderUuid]);
|
|
86
|
+
return { itemHasBill };
|
|
87
|
+
};
|
|
@@ -6,8 +6,12 @@ import * as resource from './test-order-action.resource';
|
|
|
6
6
|
import userEvent from '@testing-library/user-event';
|
|
7
7
|
import { launchWorkspace, showModal } from '@openmrs/esm-framework';
|
|
8
8
|
import { createMedicationDispenseProps } from './dispense.resource';
|
|
9
|
+
import { useStockItemQuantity } from '../useBillableItem';
|
|
9
10
|
|
|
10
11
|
jest.mock('./test-order-action.resource');
|
|
12
|
+
jest.mock('../useBillableItem', () => ({
|
|
13
|
+
useStockItemQuantity: jest.fn(),
|
|
14
|
+
}));
|
|
11
15
|
|
|
12
16
|
const mockTestProps = {
|
|
13
17
|
order: { uuid: '123', patient: { uuid: '456' } } as Order,
|
|
@@ -75,13 +79,31 @@ describe('TestOrderAction', () => {
|
|
|
75
79
|
});
|
|
76
80
|
|
|
77
81
|
test('should render loading when isLoading is true', () => {
|
|
78
|
-
jest.spyOn(resource, 'useTestOrderBillStatus').
|
|
82
|
+
jest.spyOn(resource, 'useTestOrderBillStatus').mockReturnValue({ isLoading: true, hasPendingPayment: false });
|
|
83
|
+
jest.spyOn(resource, 'useOrderBill').mockReturnValue({
|
|
84
|
+
itemHasBill: [],
|
|
85
|
+
});
|
|
86
|
+
(useStockItemQuantity as jest.Mock).mockReturnValue({
|
|
87
|
+
stockItemQuantity: 5,
|
|
88
|
+
stockItemUuid: 'some-uuid',
|
|
89
|
+
isLoading: false,
|
|
90
|
+
error: undefined,
|
|
91
|
+
});
|
|
79
92
|
render(<TestOrderAction {...testProps} />);
|
|
80
93
|
expect(screen.getByText('Verifying bill status...')).toBeInTheDocument();
|
|
81
94
|
});
|
|
82
95
|
|
|
83
96
|
test("should display `Unsettled bill for test` when there's a pending payment", () => {
|
|
84
97
|
jest.spyOn(resource, 'useTestOrderBillStatus').mockReturnValueOnce({ isLoading: false, hasPendingPayment: true });
|
|
98
|
+
jest.spyOn(resource, 'useOrderBill').mockReturnValueOnce({
|
|
99
|
+
itemHasBill: [],
|
|
100
|
+
});
|
|
101
|
+
(useStockItemQuantity as jest.Mock).mockReturnValueOnce({
|
|
102
|
+
stockItemQuantity: 5,
|
|
103
|
+
stockItemUuid: 'some-uuid',
|
|
104
|
+
isLoading: false,
|
|
105
|
+
error: undefined,
|
|
106
|
+
});
|
|
85
107
|
render(<TestOrderAction {...testProps} />);
|
|
86
108
|
expect(screen.getByText('Unsettled bill')).toBeInTheDocument();
|
|
87
109
|
});
|
|
@@ -89,6 +111,15 @@ describe('TestOrderAction', () => {
|
|
|
89
111
|
test("should display `Pick Lab Request` when there's no pending payment", async () => {
|
|
90
112
|
const user = userEvent.setup();
|
|
91
113
|
jest.spyOn(resource, 'useTestOrderBillStatus').mockReturnValueOnce({ isLoading: false, hasPendingPayment: false });
|
|
114
|
+
jest.spyOn(resource, 'useOrderBill').mockReturnValueOnce({
|
|
115
|
+
itemHasBill: [],
|
|
116
|
+
});
|
|
117
|
+
(useStockItemQuantity as jest.Mock).mockReturnValueOnce({
|
|
118
|
+
stockItemQuantity: 5,
|
|
119
|
+
stockItemUuid: 'some-uuid',
|
|
120
|
+
isLoading: false,
|
|
121
|
+
error: undefined,
|
|
122
|
+
});
|
|
92
123
|
render(<TestOrderAction {...testProps} />);
|
|
93
124
|
const pickLabRequestMenuItem = screen.getByText('Pick Lab Request');
|
|
94
125
|
await user.click(pickLabRequestMenuItem);
|
|
@@ -101,7 +132,16 @@ describe('TestOrderAction', () => {
|
|
|
101
132
|
});
|
|
102
133
|
|
|
103
134
|
test('should not render the dispense form if closeable is false', () => {
|
|
104
|
-
jest.spyOn(resource, 'useTestOrderBillStatus').
|
|
135
|
+
jest.spyOn(resource, 'useTestOrderBillStatus').mockReturnValue({ isLoading: false, hasPendingPayment: false });
|
|
136
|
+
jest.spyOn(resource, 'useOrderBill').mockReturnValue({
|
|
137
|
+
itemHasBill: [],
|
|
138
|
+
});
|
|
139
|
+
(useStockItemQuantity as jest.Mock).mockReturnValue({
|
|
140
|
+
stockItemQuantity: 5,
|
|
141
|
+
stockItemUuid: 'some-uuid',
|
|
142
|
+
isLoading: false,
|
|
143
|
+
error: undefined,
|
|
144
|
+
});
|
|
105
145
|
render(<TestOrderAction {...testProps} closeable={false} />);
|
|
106
146
|
expect(screen.queryByText('Dispense')).not.toBeInTheDocument();
|
|
107
147
|
});
|
|
@@ -109,6 +149,15 @@ describe('TestOrderAction', () => {
|
|
|
109
149
|
test('should launch the dispense form when dispense order is part of props', async () => {
|
|
110
150
|
const user = userEvent.setup();
|
|
111
151
|
jest.spyOn(resource, 'useTestOrderBillStatus').mockReturnValueOnce({ isLoading: false, hasPendingPayment: false });
|
|
152
|
+
jest.spyOn(resource, 'useOrderBill').mockReturnValueOnce({
|
|
153
|
+
itemHasBill: [],
|
|
154
|
+
});
|
|
155
|
+
(useStockItemQuantity as jest.Mock).mockReturnValueOnce({
|
|
156
|
+
stockItemQuantity: 5,
|
|
157
|
+
stockItemUuid: 'some-uuid',
|
|
158
|
+
isLoading: false,
|
|
159
|
+
error: undefined,
|
|
160
|
+
});
|
|
112
161
|
render(<TestOrderAction {...mockTestProps} />);
|
|
113
162
|
const dispenseButton = screen.getByRole('button', { name: 'Dispense' });
|
|
114
163
|
expect(dispenseButton).toBeInTheDocument();
|
|
@@ -39,10 +39,25 @@ export const useSockItemInventory = (stockItemId: string) => {
|
|
|
39
39
|
const { data, error, isLoading } = useSWR<{
|
|
40
40
|
data: { results: Array<{ quantityUoM: string; quantity: number; partyName: string }> };
|
|
41
41
|
}>(url, openmrsFetch);
|
|
42
|
-
|
|
43
42
|
return {
|
|
44
43
|
stockItem: (data?.data?.results as Array<any>) ?? [],
|
|
45
44
|
isLoading: isLoading,
|
|
46
45
|
error,
|
|
47
46
|
};
|
|
48
47
|
};
|
|
48
|
+
|
|
49
|
+
export const useStockItemQuantity = (drugUuid: string) => {
|
|
50
|
+
const url = `/ws/rest/v1/stockmanagement/stockiteminventory?v=default&limit=10&totalCount=true&drugUuid=${drugUuid}`;
|
|
51
|
+
const { data, error, isLoading } = useSWR<{
|
|
52
|
+
data: {
|
|
53
|
+
results: Array<{ quantityUoM: string; quantity: number; partyName: string; stockItemUuid: string }>;
|
|
54
|
+
total: number;
|
|
55
|
+
};
|
|
56
|
+
}>(url, openmrsFetch);
|
|
57
|
+
return {
|
|
58
|
+
stockItemQuantity: data?.data?.total ?? 0,
|
|
59
|
+
stockItemUuid: data?.data?.results[0]?.stockItemUuid ?? '',
|
|
60
|
+
isLoading: isLoading,
|
|
61
|
+
error,
|
|
62
|
+
};
|
|
63
|
+
};
|
package/src/billing.resource.ts
CHANGED
|
@@ -275,6 +275,7 @@ export const billingFormSchema = z.object({
|
|
|
275
275
|
priceName: z.string().optional().default('Default'),
|
|
276
276
|
priceUuid: z.string().uuid(),
|
|
277
277
|
lineItemOrder: z.number().optional().default(0),
|
|
278
|
+
order: z.string().optional().default(''),
|
|
278
279
|
paymentStatus: z.enum(['PENDING']),
|
|
279
280
|
}),
|
|
280
281
|
)
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineConfigSchema, getSyncLifecycle, registerFeatureFlag } from '@openmrs/esm-framework';
|
|
1
|
+
import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle, registerFeatureFlag } from '@openmrs/esm-framework';
|
|
2
2
|
import { createDashboardGroup, createDashboardLink } from '@openmrs/esm-patient-common-lib';
|
|
3
3
|
import BenefitsPackage from './benefits-package/benefits-package.component';
|
|
4
4
|
import Benefits from './benefits-package/benefits/benefits.component';
|
|
@@ -12,6 +12,7 @@ import WaiveBillActionButton from './billable-services/bill-manager/bill-actions
|
|
|
12
12
|
import { DeleteBillModal } from './billable-services/bill-manager/modals/delete-bill.modal';
|
|
13
13
|
import { RefundBillModal } from './billable-services/bill-manager/modals/refund-bill.modal';
|
|
14
14
|
import { DeleteBillableServiceModal } from './billable-services/bill-manager/modals/serviceItemCard.component';
|
|
15
|
+
import CreateBillItemModal from './billable-services/bill-manager/modals/create-bill-item-modal.component';
|
|
15
16
|
import CancelBillWorkspace from './billable-services/bill-manager/workspaces/cancel-bill/cancel-bill.workspace';
|
|
16
17
|
import { EditBillForm } from './billable-services/bill-manager/workspaces/edit-bill/edit-bill-form.workspace';
|
|
17
18
|
import { WaiveBillForm } from './billable-services/bill-manager/workspaces/waive-bill/waive-bill-form.workspace';
|
|
@@ -156,6 +157,7 @@ export const root = getSyncLifecycle(rootComponent, options);
|
|
|
156
157
|
export const billingPatientSummary = getSyncLifecycle(BillHistory, options);
|
|
157
158
|
export const billingCheckInForm = getSyncLifecycle(BillingCheckInForm, options);
|
|
158
159
|
export const deleteBillableServiceModal = getSyncLifecycle(DeleteBillableServiceModal, options);
|
|
160
|
+
export const createBillItemModal = getSyncLifecycle(CreateBillItemModal, options);
|
|
159
161
|
|
|
160
162
|
export const billingForm = getSyncLifecycle(BillingForm, options);
|
|
161
163
|
export const requirePaymentModal = getSyncLifecycle(RequirePaymentModal, options);
|
package/src/routes.json
CHANGED
package/translations/en.json
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"attributeRetired": "Attribute retired",
|
|
23
23
|
"attributeRetiredReason": "Attribute retired reason",
|
|
24
24
|
"benefits": "Benefits",
|
|
25
|
+
"bill": "Bill",
|
|
25
26
|
"billableExemptionAdministration": "Exemption Administration",
|
|
26
27
|
"billableServicesError": "Billable services error",
|
|
27
28
|
"billAmount": "Bill Amount",
|
|
@@ -176,6 +177,7 @@
|
|
|
176
177
|
"noTransactionHistory": "No transaction history",
|
|
177
178
|
"noTransactionHistorySubtitle": "No transaction history loaded for the selected filters",
|
|
178
179
|
"notSearchedState": "Please search for a patient in the input above",
|
|
180
|
+
"outOfStock": "Out of Stock",
|
|
179
181
|
"overflowMenu": "Overflow menu",
|
|
180
182
|
"overPayment": "Over payment",
|
|
181
183
|
"overPaymentSubtitle": "Amount paid {{totalAmountTendered}} should not be greater than amount due {{selectedLineItemsAmountDue}} for selected line items",
|