@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/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.1799"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-billing-app",
3
- "version": "5.4.1-pre.1799",
3
+ "version": "5.4.1-pre.1818",
4
4
  "description": "Billing app for KenyaEMR",
5
5
  "browser": "dist/kenyaemr-esm-billing-app.js",
6
6
  "main": "src/index.ts",
@@ -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
  };
@@ -28,7 +28,6 @@ const DrugOrder: React.FC<DrugOrderProps> = ({ order }) => {
28
28
  if (isLoading || isLoadingInventory) {
29
29
  return null;
30
30
  }
31
-
32
31
  return (
33
32
  <div className={styles.drugOrderContainer}>
34
33
  {stockItem && stockItem.length > 0 ? (
@@ -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 styles from './test-order-action.scss';
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
- }, [isDispenseOrder, modalName, order, additionalProps, dispenseFormProps, handleModalClose]);
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 = hasPendingPayment
74
- ? t('unsettledBill', 'Unsettled bill')
75
- : isDispenseOrder
76
- ? actionText ?? t('dispense', 'Dispense')
77
- : actionText ?? t('pickLabRequest', 'Pick Lab Request');
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: isLoadingBill, hasPendingPayment } = useOrderPendingPaymentStatus(patientUuid, orderUuid);
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 (isLoadingBill || isLoadingQueue) {
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
- isLoadingBill,
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').mockReturnValueOnce({ isLoading: true, hasPendingPayment: false });
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').mockReturnValueOnce({ isLoading: false, hasPendingPayment: false });
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
+ };
@@ -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
@@ -302,6 +302,10 @@
302
302
  {
303
303
  "name": "clock-in-modal",
304
304
  "component": "clockIn"
305
+ },
306
+ {
307
+ "name": "create-bill-item-modal",
308
+ "component": "createBillItemModal"
305
309
  }
306
310
  ]
307
311
  }
@@ -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",