@kenyaemr/esm-billing-app 5.4.1-pre.2075 → 5.4.1-pre.2083

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":"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":"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.2075"}
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":"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.2083"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-billing-app",
3
- "version": "5.4.1-pre.2075",
3
+ "version": "5.4.1-pre.2083",
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,271 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { useConfig, useLayoutType } from '@openmrs/esm-framework';
5
+ import { type Order, type OrderAction } from '@openmrs/esm-patient-common-lib';
6
+ import { processBillItems } from '../../../../billing.resource';
7
+ import CreateBillWorkspace from './create-bill.workspace';
8
+ import { useBillableItem } from '../../../billiable-item/useBillableItem';
9
+
10
+ jest.mock('react-i18next', () => ({
11
+ useTranslation: () => ({
12
+ t: (key, defaultText, options) => {
13
+ if (options) {
14
+ return defaultText.replace(/\{\{(\w+)\}\}/g, (_, key) => options[key] || '');
15
+ }
16
+ return defaultText || key;
17
+ },
18
+ }),
19
+ }));
20
+
21
+ jest.mock('@openmrs/esm-framework', () => ({
22
+ useConfig: jest.fn(),
23
+ useLayoutType: jest.fn(),
24
+ showSnackbar: jest.fn(),
25
+ ResponsiveWrapper: ({ children }) => <div>{children}</div>,
26
+ restBaseUrl: '/openmrs/ws/rest/v1',
27
+ }));
28
+
29
+ jest.mock('../../../billiable-item/useBillableItem', () => ({
30
+ useBillableItem: jest.fn(),
31
+ }));
32
+
33
+ jest.mock('../../../../billing.resource', () => ({
34
+ processBillItems: jest.fn(),
35
+ }));
36
+
37
+ jest.mock('swr', () => ({
38
+ mutate: jest.fn(),
39
+ }));
40
+
41
+ describe('CreateBillWorkspace', () => {
42
+ // Mock data
43
+ const mockOrder: Order = {
44
+ uuid: 'order-uuid',
45
+ orderNumber: 'ORD-123',
46
+ concept: {
47
+ uuid: 'concept-uuid',
48
+ display: 'Test Service',
49
+ },
50
+ action: 'NEW' as OrderAction,
51
+ asNeeded: false,
52
+ autoExpireDate: null,
53
+ careSetting: {
54
+ uuid: 'care-setting-uuid',
55
+ display: 'Outpatient',
56
+ },
57
+ dateActivated: '2024-01-01',
58
+ dateStopped: null,
59
+ drug: null,
60
+ dosingType: 'org.openmrs.FreeTextDosingInstructions',
61
+ dose: null,
62
+ doseUnits: null,
63
+ frequency: null,
64
+ instructions: null,
65
+ numRefills: 0,
66
+ orderType: {
67
+ uuid: 'order-type-uuid',
68
+ display: 'Test Order Type',
69
+ conceptClasses: [],
70
+ description: 'Test Order Type Description',
71
+ name: 'Test Order Type',
72
+ parent: 'parent-uuid',
73
+ retired: false,
74
+ },
75
+ orderer: {
76
+ uuid: 'orderer-uuid',
77
+ display: 'Test Orderer',
78
+ person: {
79
+ display: 'Test Person',
80
+ },
81
+ },
82
+ previousOrder: null,
83
+ quantity: 1,
84
+ quantityUnits: null,
85
+ route: null,
86
+ scheduledDate: null,
87
+ scheduleDate: null,
88
+ numberOfRepeats: '0',
89
+ urgency: 'ROUTINE',
90
+ accessionNumber: null,
91
+ auditInfo: {
92
+ creator: {
93
+ uuid: 'creator-uuid',
94
+ display: 'Test Creator',
95
+ },
96
+ dateCreated: '2024-01-01',
97
+ changedBy: null,
98
+ dateChanged: null,
99
+ },
100
+ fulfillerStatus: null,
101
+ fulfillerComment: null,
102
+ specimenSource: null,
103
+ laterality: null,
104
+ clinicalHistory: null,
105
+ commentToFulfiller: null,
106
+ display: 'Test Order',
107
+ type: 'order',
108
+ dispenseAsWritten: false,
109
+ dosingInstructions: null,
110
+ duration: null,
111
+ durationUnits: null,
112
+ brandName: null,
113
+ orderReason: null,
114
+ orderReasonNonCoded: null,
115
+ encounter: {
116
+ uuid: 'encounter-uuid',
117
+ display: 'Test Encounter',
118
+ },
119
+ patient: {
120
+ uuid: 'patient-uuid',
121
+ display: 'Test Patient',
122
+ },
123
+ };
124
+
125
+ const mockBillableItem = {
126
+ uuid: 'billable-uuid',
127
+ name: 'Test Service',
128
+ servicePrices: [
129
+ {
130
+ uuid: 'price-uuid-1',
131
+ price: 100,
132
+ paymentMode: { name: 'Cash' },
133
+ },
134
+ {
135
+ uuid: 'price-uuid-2',
136
+ price: 150,
137
+ paymentMode: { name: 'Insurance' },
138
+ },
139
+ ],
140
+ };
141
+
142
+ const defaultProps = {
143
+ patientUuid: 'patient-uuid',
144
+ order: mockOrder,
145
+ closeWorkspace: jest.fn(),
146
+ closeWorkspaceWithSavedChanges: jest.fn(),
147
+ promptBeforeClosing: jest.fn(),
148
+ setTitle: jest.fn(),
149
+ closeModal: jest.fn(),
150
+ };
151
+
152
+ // Reset mocks before each test
153
+ beforeEach(() => {
154
+ jest.clearAllMocks();
155
+
156
+ // Setup default mock implementations
157
+ (useConfig as jest.Mock).mockReturnValue({
158
+ cashPointUuid: 'cash-point-uuid',
159
+ cashierUuid: 'cashier-uuid',
160
+ });
161
+
162
+ (useLayoutType as jest.Mock).mockReturnValue('desktop');
163
+
164
+ (useBillableItem as jest.Mock).mockReturnValue({
165
+ billableItem: mockBillableItem,
166
+ isLoading: false,
167
+ });
168
+
169
+ (processBillItems as jest.Mock).mockResolvedValue({});
170
+ });
171
+
172
+ test('renders the form with correct initial state', async () => {
173
+ render(<CreateBillWorkspace {...defaultProps} />);
174
+
175
+ // Check that the title and description are rendered
176
+ expect(screen.getByText(/Create bill for order Test Service/)).toBeInTheDocument();
177
+ expect(screen.getByText(/Order Bill Creation ORD-123/)).toBeInTheDocument();
178
+
179
+ // Check form elements are rendered
180
+ const quantityInput = screen.getByRole('spinbutton', { name: /quantity/i });
181
+ expect(quantityInput).toHaveValue(1);
182
+
183
+ const priceDropdown = screen.getByRole('combobox', { name: /Unit Price/i });
184
+ expect(priceDropdown).toBeInTheDocument();
185
+ });
186
+
187
+ test('submits the form successfully', async () => {
188
+ const user = userEvent.setup();
189
+
190
+ render(<CreateBillWorkspace {...defaultProps} />);
191
+
192
+ // Get form elements
193
+ const quantityInput = screen.getByRole('spinbutton', { name: /quantity/i });
194
+ const priceDropdown = screen.getByRole('combobox', { name: /Unit Price/i });
195
+
196
+ // Set quantity
197
+ await user.clear(quantityInput);
198
+ await user.type(quantityInput, '2');
199
+
200
+ // Open and select from dropdown
201
+ await user.click(priceDropdown);
202
+
203
+ // Since Carbon dropdown items might be rendered in a portal or with complex structure,
204
+ // we need to find them by their text content regardless of their exact structure
205
+ const dropdownItem = await screen.findByText(/Cash - Ksh 100.00/);
206
+ await user.click(dropdownItem);
207
+
208
+ // Submit form
209
+ const submitButton = screen.getByRole('button', { name: /save & close/i });
210
+
211
+ // Wait for button to be enabled after form validation
212
+ await waitFor(() => {
213
+ expect(submitButton).not.toBeDisabled();
214
+ });
215
+
216
+ await user.click(submitButton);
217
+ });
218
+
219
+ test('handles loading state correctly', () => {
220
+ // Mock loading state
221
+ (useBillableItem as jest.Mock).mockReturnValue({
222
+ billableItem: null,
223
+ isLoading: true,
224
+ });
225
+
226
+ render(<CreateBillWorkspace {...defaultProps} />);
227
+
228
+ expect(screen.getByText(/Loading billable items/i)).toBeInTheDocument();
229
+ });
230
+
231
+ test('handles form errors correctly', async () => {
232
+ const user = userEvent.setup();
233
+
234
+ // Mock API error
235
+ const mockError = new Error('Failed to process bill');
236
+ (processBillItems as jest.Mock).mockRejectedValue(mockError);
237
+
238
+ render(<CreateBillWorkspace {...defaultProps} />);
239
+
240
+ // Fill form
241
+ const quantityInput = screen.getByRole('spinbutton', { name: /quantity/i });
242
+ const priceDropdown = screen.getByRole('combobox', { name: /Unit Price/i });
243
+
244
+ await user.clear(quantityInput);
245
+ await user.type(quantityInput, '2');
246
+
247
+ await user.click(priceDropdown);
248
+ const dropdownItem = await screen.findByText(/Cash - Ksh 100.00/);
249
+ await user.click(dropdownItem);
250
+
251
+ // Submit form
252
+ const submitButton = screen.getByRole('button', { name: /save & close/i });
253
+
254
+ await waitFor(() => {
255
+ expect(submitButton).not.toBeDisabled();
256
+ });
257
+
258
+ await user.click(submitButton);
259
+ });
260
+
261
+ test('cancel button closes workspace', async () => {
262
+ const user = userEvent.setup();
263
+
264
+ render(<CreateBillWorkspace {...defaultProps} />);
265
+
266
+ const cancelButton = screen.getByRole('button', { name: /cancel/i });
267
+ await user.click(cancelButton);
268
+
269
+ expect(defaultProps.closeWorkspace).toHaveBeenCalled();
270
+ });
271
+ });
@@ -1,49 +1,162 @@
1
1
  import React, { useEffect } from 'react';
2
- import { Dropdown, Button, ButtonSet, Form, InlineLoading, InlineNotification } from '@carbon/react';
3
- import { type Order } from '@openmrs/esm-patient-common-lib';
4
- import { useBillableItem } from '../../../billiable-item/useBillableItem';
5
- import styles from './create-bill.style.scss';
2
+ import { useTranslation } from 'react-i18next';
6
3
  import { Controller, useForm } from 'react-hook-form';
4
+ import { z } from 'zod';
5
+ import classNames from 'classnames';
6
+ import {
7
+ Dropdown,
8
+ Button,
9
+ ButtonSet,
10
+ InlineLoading,
11
+ InlineNotification,
12
+ NumberInput,
13
+ Stack,
14
+ Column,
15
+ } from '@carbon/react';
7
16
  import {
8
17
  type DefaultWorkspaceProps,
9
18
  ResponsiveWrapper,
10
19
  restBaseUrl,
11
20
  useConfig,
12
21
  useLayoutType,
22
+ showSnackbar,
13
23
  } from '@openmrs/esm-framework';
14
- import { useTranslation } from 'react-i18next';
15
- import classNames from 'classnames';
24
+ import { type Order } from '@openmrs/esm-patient-common-lib';
25
+ import { useBillableItem } from '../../../billiable-item/useBillableItem';
26
+ import styles from './create-bill.style.scss';
16
27
  import { convertToCurrency } from '../../../../helpers';
17
28
  import { BillingConfig } from '../../../../config-schema';
18
- import { z } from 'zod';
19
29
  import { processBillItems } from '../../../../billing.resource';
20
30
  import { mutate } from 'swr';
21
31
 
22
32
  type CreateBillWorkspaceProps = DefaultWorkspaceProps & {
23
33
  patientUuid: string;
24
34
  order: Order;
35
+ closeModal: () => void;
36
+ medicationRequestBundle?: {
37
+ request: fhir.MedicationRequest;
38
+ };
25
39
  };
26
40
 
27
41
  const createBillFormSchema = z.object({
28
42
  id: z.string().min(1),
29
43
  text: z.string().min(1),
30
44
  unitPrice: z.string().min(1),
45
+ quantity: z.number().min(1),
31
46
  });
32
47
 
33
48
  type CreateBillFormSchema = z.infer<typeof createBillFormSchema>;
34
49
 
50
+ interface BillFormProps {
51
+ billableItem: any;
52
+ quantityToDispense: number;
53
+ createBillForm: any;
54
+ errors: any;
55
+ calculateTotal: () => number;
56
+ comboBoxItems: Array<{
57
+ id: string;
58
+ text: string;
59
+ unitPrice: number;
60
+ }>;
61
+ }
62
+
63
+ interface MedicationBillFormProps extends Omit<BillFormProps, 'quantityToDispense'> {
64
+ medicationRequestBundle: {
65
+ request: fhir.MedicationRequest;
66
+ };
67
+ }
68
+
69
+ const BillForm: React.FC<BillFormProps> = ({
70
+ billableItem,
71
+ quantityToDispense,
72
+ createBillForm,
73
+ errors,
74
+ calculateTotal,
75
+ comboBoxItems,
76
+ }) => {
77
+ const { t } = useTranslation();
78
+
79
+ return (
80
+ <Stack gap={4}>
81
+ <Column>
82
+ <div className={styles.formField}>
83
+ <label className={styles.label}>{t('item', 'Item')}</label>
84
+ <div className={styles.value}>{billableItem?.name ?? 'Service Not Found'}</div>
85
+ </div>
86
+ </Column>
87
+ <Column>
88
+ <Controller
89
+ control={createBillForm.control}
90
+ name="quantity"
91
+ render={({ field }) => (
92
+ <NumberInput
93
+ {...field}
94
+ id="quantity"
95
+ label={t('quantity', 'Quantity')}
96
+ min={1}
97
+ max={quantityToDispense}
98
+ value={field.value}
99
+ onChange={(e, { value }) => field.onChange(value)}
100
+ />
101
+ )}
102
+ />
103
+ </Column>
104
+ <Column>
105
+ <Controller
106
+ control={createBillForm.control}
107
+ name="unitPrice"
108
+ render={({ field }) => (
109
+ <Dropdown
110
+ {...field}
111
+ onChange={({ selectedItem }) => {
112
+ field.onChange(selectedItem?.unitPrice?.toString() ?? '');
113
+ createBillForm.setValue('id', selectedItem?.id ?? '');
114
+ createBillForm.setValue('text', selectedItem?.text ?? '');
115
+ }}
116
+ id="unit-price"
117
+ itemToString={(item) => item?.text ?? ''}
118
+ items={comboBoxItems}
119
+ label={t('selectUnitPrice', 'Select Unit Price')}
120
+ titleText={t('unitPrice', 'Unit Price')}
121
+ type="default"
122
+ invalid={!!errors.unitPrice}
123
+ invalidText={errors.unitPrice?.message}
124
+ />
125
+ )}
126
+ />
127
+ </Column>
128
+ <Column>
129
+ <div className={styles.formField}>
130
+ <label className={styles.label}>{t('total', 'Total')}</label>
131
+ <div className={styles.value}>{convertToCurrency(calculateTotal())}</div>
132
+ </div>
133
+ </Column>
134
+ </Stack>
135
+ );
136
+ };
137
+
138
+ const MedicationBillForm: React.FC<MedicationBillFormProps> = ({ medicationRequestBundle, ...props }) => {
139
+ const quantityToDispense = medicationRequestBundle?.request?.dispenseRequest?.quantity?.value ?? 1;
140
+ return <BillForm {...props} quantityToDispense={quantityToDispense} />;
141
+ };
142
+
143
+ const StandardBillForm: React.FC<Omit<BillFormProps, 'quantityToDispense'>> = (props) => {
144
+ return <BillForm {...props} quantityToDispense={1} />;
145
+ };
146
+
35
147
  const CreateBillWorkspace: React.FC<CreateBillWorkspaceProps> = ({
36
148
  patientUuid,
37
149
  order,
38
150
  closeWorkspace,
39
151
  closeWorkspaceWithSavedChanges,
40
152
  promptBeforeClosing,
153
+ medicationRequestBundle,
41
154
  }) => {
42
155
  const { t } = useTranslation();
43
156
  const defaultPaymentStatus = 'PENDING';
44
157
  const isTablet = useLayoutType() === 'tablet';
45
158
  const { cashPointUuid, cashierUuid } = useConfig<BillingConfig>();
46
- const { billableItem, isLoading } = useBillableItem(order.concept.uuid);
159
+ const { billableItem, isLoading } = useBillableItem(order?.concept?.uuid ?? order?.drug?.concept?.uuid);
47
160
 
48
161
  const comboBoxItems =
49
162
  billableItem?.servicePrices?.map((item) => ({
@@ -57,14 +170,22 @@ const CreateBillWorkspace: React.FC<CreateBillWorkspaceProps> = ({
57
170
  id: '',
58
171
  text: '',
59
172
  unitPrice: '0',
173
+ quantity: medicationRequestBundle?.request?.dispenseRequest?.quantity?.value ?? 1,
60
174
  },
61
175
  });
62
176
 
63
177
  const {
64
178
  handleSubmit,
65
179
  formState: { isValid, isDirty, isSubmitting, errors },
180
+ watch,
66
181
  } = createBillForm;
67
182
 
183
+ const calculateTotal = () => {
184
+ const price = parseFloat(watch('unitPrice')) || 0;
185
+ const quantity = watch('quantity') || 1;
186
+ return price * quantity;
187
+ };
188
+
68
189
  const handleCreateBill = async (formData: CreateBillFormSchema) => {
69
190
  const unitPrice = parseFloat(formData.unitPrice);
70
191
  const createBillPayload = {
@@ -76,21 +197,37 @@ const CreateBillWorkspace: React.FC<CreateBillWorkspaceProps> = ({
76
197
  {
77
198
  billableService: billableItem?.uuid,
78
199
  lineItemOrder: 0,
79
- quantity: 1,
200
+ quantity: formData.quantity,
80
201
  price: unitPrice,
81
202
  paymentStatus: defaultPaymentStatus,
82
- priceUuid: '',
203
+ priceUuid: formData.id,
83
204
  priceName: formData.text,
84
205
  order: order.uuid,
85
206
  },
86
207
  ],
87
208
  payments: [],
88
209
  };
89
- await processBillItems(createBillPayload);
90
- mutate((key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/cashier/bill`), undefined, {
91
- revalidate: true,
92
- });
93
- closeWorkspaceWithSavedChanges();
210
+
211
+ try {
212
+ await processBillItems(createBillPayload);
213
+ showSnackbar({
214
+ title: t('billItems', 'Save Bill'),
215
+ subtitle: 'Bill processing has been successful',
216
+ kind: 'success',
217
+ timeoutInMs: 3000,
218
+ });
219
+ mutate((key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/cashier/bill`), undefined, {
220
+ revalidate: true,
221
+ });
222
+ closeWorkspaceWithSavedChanges();
223
+ } catch (error) {
224
+ console.error('Bill processing error:', error);
225
+ showSnackbar({
226
+ title: 'Bill processing error',
227
+ kind: 'error',
228
+ subtitle: error instanceof Error ? error.message : 'Unknown error occurred',
229
+ });
230
+ }
94
231
  };
95
232
 
96
233
  useEffect(() => {
@@ -99,8 +236,22 @@ const CreateBillWorkspace: React.FC<CreateBillWorkspaceProps> = ({
99
236
  }
100
237
  }, [isDirty, promptBeforeClosing]);
101
238
 
239
+ if (isLoading) {
240
+ return <InlineLoading description={t('loadingBillableItems', 'Loading billable items...')} />;
241
+ }
242
+
243
+ const commonFormProps = {
244
+ billableItem,
245
+ createBillForm,
246
+ errors,
247
+ calculateTotal,
248
+ comboBoxItems,
249
+ };
250
+
102
251
  return (
103
- <Form className={styles.form} onSubmit={handleSubmit(handleCreateBill)}>
252
+ <form
253
+ className={styles.form}
254
+ onSubmit={handleSubmit(handleCreateBill, (errors) => console.error('errors', errors))}>
104
255
  <div className={styles.formContainer}>
105
256
  <ResponsiveWrapper>
106
257
  <InlineNotification
@@ -109,34 +260,17 @@ const CreateBillWorkspace: React.FC<CreateBillWorkspaceProps> = ({
109
260
  lowContrast
110
261
  statusIconDescription="notification"
111
262
  subtitle={t('createBillForOrder', 'Create bill for order {{order}} by selecting the correct unit price', {
112
- order: order.concept.display,
263
+ order: order?.concept?.display ?? order?.drug?.display,
113
264
  })}
114
265
  title={t('orderBillCreation', 'Order Bill Creation {{orderNumber}}', { orderNumber: order.orderNumber })}
115
266
  />
116
267
  </ResponsiveWrapper>
117
268
  <ResponsiveWrapper>
118
- <Controller
119
- control={createBillForm.control}
120
- name="unitPrice"
121
- render={({ field }) => (
122
- <Dropdown
123
- {...field}
124
- onChange={({ selectedItem }) => {
125
- field.onChange(selectedItem?.unitPrice?.toString() || '');
126
- createBillForm.setValue('id', selectedItem?.id || '');
127
- createBillForm.setValue('text', selectedItem?.text || '');
128
- }}
129
- id="unit-price"
130
- itemToString={(item) => item?.text || ''}
131
- items={comboBoxItems}
132
- label={t('selectUnitPrice', 'Select Unit Price')}
133
- titleText={t('unitPrice', 'Unit Price')}
134
- type="default"
135
- invalid={!!errors.unitPrice}
136
- invalidText={errors.unitPrice?.message}
137
- />
138
- )}
139
- />
269
+ {medicationRequestBundle ? (
270
+ <MedicationBillForm {...commonFormProps} medicationRequestBundle={medicationRequestBundle} />
271
+ ) : (
272
+ <StandardBillForm {...commonFormProps} />
273
+ )}
140
274
  </ResponsiveWrapper>
141
275
  </div>
142
276
  <ButtonSet className={classNames({ [styles.tablet]: isTablet, [styles.desktop]: !isTablet })}>
@@ -151,7 +285,7 @@ const CreateBillWorkspace: React.FC<CreateBillWorkspaceProps> = ({
151
285
  )}
152
286
  </Button>
153
287
  </ButtonSet>
154
- </Form>
288
+ </form>
155
289
  );
156
290
  };
157
291
 
@@ -1,4 +1,4 @@
1
- import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
1
+ import { openmrsFetch, restBaseUrl, type OpenmrsResource } from '@openmrs/esm-framework';
2
2
  import useSWR from 'swr';
3
3
 
4
4
  export type ChargeAble = {
@@ -6,7 +6,7 @@ export type ChargeAble = {
6
6
  name: string;
7
7
  shortName: string;
8
8
  serviceStatus: 'ENABLED' | 'DISABLED';
9
- stockItem: string;
9
+ stockItem: OpenmrsResource;
10
10
  serviceType: {
11
11
  uuid: string;
12
12
  display: string;