@openmrs/esm-billing-app 1.0.2-pre.84 → 1.0.2-pre.849
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/.eslintrc +16 -2
- package/README.md +54 -9
- package/__mocks__/bills.mock.ts +12 -0
- package/__mocks__/react-i18next.js +6 -5
- package/dist/1119.js +1 -1
- package/dist/1146.js +1 -2
- package/dist/1146.js.map +1 -1
- package/dist/1197.js +1 -1
- package/dist/1856.js +1 -0
- package/dist/1856.js.map +1 -0
- package/dist/2146.js +1 -1
- package/dist/2177.js +2 -0
- package/dist/2177.js.LICENSE.txt +9 -0
- package/dist/2177.js.map +1 -0
- package/dist/2524.js +1 -0
- package/dist/2524.js.map +1 -0
- package/dist/2690.js +1 -1
- package/dist/3041.js +1 -0
- package/dist/3041.js.map +1 -0
- package/dist/3099.js +1 -1
- package/dist/3584.js +1 -1
- package/dist/3717.js +2 -0
- package/dist/3717.js.map +1 -0
- package/dist/4055.js +1 -1
- package/dist/4132.js +1 -1
- package/dist/4225.js +1 -0
- package/dist/4225.js.map +1 -0
- package/dist/4300.js +1 -1
- package/dist/4335.js +1 -1
- package/dist/4344.js +1 -0
- package/dist/4344.js.map +1 -0
- package/dist/4618.js +1 -1
- package/dist/4652.js +1 -1
- package/dist/4724.js +1 -0
- package/dist/4724.js.map +1 -0
- package/dist/4739.js +1 -1
- package/dist/4739.js.map +1 -1
- package/dist/4944.js +1 -1
- package/dist/5173.js +1 -1
- package/dist/5241.js +1 -1
- package/dist/5422.js +1 -0
- package/dist/5422.js.map +1 -0
- package/dist/5442.js +1 -1
- package/dist/5661.js +1 -1
- package/dist/6022.js +1 -1
- package/dist/6295.js +2 -0
- package/dist/{6525.js.LICENSE.txt → 6295.js.LICENSE.txt} +16 -4
- package/dist/6295.js.map +1 -0
- package/dist/6468.js +1 -1
- package/dist/6540.js +1 -1
- package/dist/6540.js.map +1 -1
- package/dist/6606.js +1 -0
- package/dist/6606.js.map +1 -0
- package/dist/6679.js +1 -1
- package/dist/6840.js +1 -1
- package/dist/6859.js +1 -1
- package/dist/7097.js +1 -1
- package/dist/7159.js +1 -1
- package/dist/723.js +1 -1
- package/dist/7617.js +1 -1
- package/dist/795.js +1 -1
- package/dist/8163.js +1 -1
- package/dist/8349.js +1 -1
- package/dist/8618.js +1 -1
- package/dist/890.js +1 -1
- package/dist/9214.js +1 -1
- package/dist/9538.js +1 -1
- package/dist/9569.js +1 -1
- package/dist/961.js +1 -1
- package/dist/961.js.map +1 -1
- package/dist/986.js +1 -1
- package/dist/9879.js +1 -1
- package/dist/9895.js +1 -1
- package/dist/9900.js +1 -1
- package/dist/9913.js +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-billing-app.js +1 -1
- package/dist/openmrs-esm-billing-app.js.buildmanifest.json +388 -282
- package/dist/openmrs-esm-billing-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/e2e/README.md +19 -18
- package/e2e/core/test.ts +1 -1
- package/e2e/fixtures/api.ts +1 -1
- package/e2e/specs/sample-test.spec.ts +0 -1
- package/e2e/support/github/Dockerfile +1 -1
- package/package.json +13 -10
- package/src/bill-history/bill-history.component.tsx +20 -28
- package/src/bill-history/bill-history.scss +4 -94
- package/src/bill-history/bill-history.test.tsx +37 -78
- package/src/bill-item-actions/bill-item-actions.scss +21 -5
- package/src/bill-item-actions/edit-bill-item.modal.tsx +225 -0
- package/src/bill-item-actions/edit-bill-item.test.tsx +214 -40
- package/src/billable-services/bill-waiver/bill-selection.component.tsx +5 -5
- package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +28 -32
- package/src/billable-services/bill-waiver/patient-bills.component.tsx +7 -7
- package/src/billable-services/bill-waiver/utils.ts +13 -3
- package/src/billable-services/billable-service.resource.ts +28 -12
- package/src/billable-services/billable-services-home.component.tsx +4 -4
- package/src/billable-services/billable-services.component.tsx +149 -148
- package/src/billable-services/billable-services.scss +3 -0
- package/src/billable-services/billable-services.test.tsx +6 -49
- package/src/billable-services/cash-point/add-cash-point.modal.tsx +168 -0
- package/src/billable-services/cash-point/cash-point-configuration.component.tsx +19 -193
- package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
- package/src/billable-services/create-edit/add-billable-service.component.tsx +356 -300
- package/src/billable-services/create-edit/add-billable-service.scss +6 -65
- package/src/billable-services/create-edit/add-billable-service.test.tsx +167 -81
- package/src/billable-services/create-edit/edit-billable-service.modal.tsx +51 -0
- package/src/billable-services/dashboard/service-metrics.component.tsx +11 -3
- package/src/billable-services/payment-modes/add-payment-mode.modal.tsx +121 -0
- package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +72 -0
- package/src/billable-services/payment-modes/payment-modes-config.component.tsx +125 -0
- package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -4
- package/src/billing-dashboard/billing-dashboard.scss +1 -1
- package/src/billing-form/billing-checkin-form.component.tsx +21 -17
- package/src/billing-form/billing-checkin-form.test.tsx +99 -26
- package/src/billing-form/billing-form.component.tsx +222 -292
- package/src/billing-form/billing-form.scss +143 -0
- package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +1 -1
- package/src/billing.resource.ts +69 -74
- package/src/bills-table/bills-table.component.tsx +3 -3
- package/src/bills-table/bills-table.test.tsx +98 -54
- package/src/config-schema.ts +52 -24
- package/src/dashboard.meta.ts +4 -2
- package/src/helpers/functions.ts +5 -4
- package/src/index.ts +17 -6
- package/src/invoice/invoice-table.component.tsx +36 -70
- package/src/invoice/invoice-table.scss +8 -5
- package/src/invoice/invoice-table.test.tsx +273 -62
- package/src/invoice/invoice.component.tsx +39 -32
- package/src/invoice/invoice.scss +11 -4
- package/src/invoice/invoice.test.tsx +324 -120
- package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +9 -9
- package/src/invoice/payments/payment-form/payment-form.component.tsx +43 -34
- package/src/invoice/payments/payment-form/payment-form.scss +5 -6
- package/src/invoice/payments/payment-form/payment-form.test.tsx +216 -66
- package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
- package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
- package/src/invoice/payments/payments.component.tsx +55 -67
- package/src/invoice/payments/payments.scss +4 -3
- package/src/invoice/payments/payments.test.tsx +282 -0
- package/src/invoice/payments/utils.ts +15 -27
- package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
- package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
- package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
- package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
- package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
- package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
- package/src/invoice/printable-invoice/printable-invoice.component.tsx +20 -34
- package/src/left-panel-link.test.tsx +1 -4
- package/src/metrics-cards/metrics-cards.component.tsx +12 -2
- package/src/metrics-cards/metrics-cards.scss +4 -0
- package/src/metrics-cards/metrics-cards.test.tsx +18 -5
- package/src/modal/require-payment-modal.test.tsx +27 -22
- package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +18 -19
- package/src/routes.json +25 -7
- package/src/types/index.ts +80 -18
- package/translations/am.json +125 -74
- package/translations/ar.json +126 -75
- package/translations/ar_SY.json +126 -75
- package/translations/bn.json +128 -77
- package/translations/de.json +126 -75
- package/translations/en.json +126 -75
- package/translations/en_US.json +126 -75
- package/translations/es.json +125 -74
- package/translations/es_MX.json +126 -75
- package/translations/fr.json +131 -80
- package/translations/he.json +125 -74
- package/translations/hi.json +126 -75
- package/translations/hi_IN.json +126 -75
- package/translations/id.json +126 -75
- package/translations/it.json +152 -101
- package/translations/ka.json +126 -75
- package/translations/km.json +125 -74
- package/translations/ku.json +126 -75
- package/translations/ky.json +126 -75
- package/translations/lg.json +126 -75
- package/translations/ne.json +126 -75
- package/translations/pl.json +126 -75
- package/translations/pt.json +126 -75
- package/translations/pt_BR.json +126 -75
- package/translations/qu.json +126 -75
- package/translations/ro_RO.json +216 -165
- package/translations/ru_RU.json +126 -75
- package/translations/si.json +126 -75
- package/translations/sw.json +126 -75
- package/translations/sw_KE.json +126 -75
- package/translations/tr.json +126 -75
- package/translations/tr_TR.json +126 -75
- package/translations/uk.json +126 -75
- package/translations/uz.json +126 -75
- package/translations/uz@Latn.json +126 -75
- package/translations/uz_UZ.json +126 -75
- package/translations/vi.json +126 -75
- package/translations/zh.json +126 -75
- package/translations/zh_CN.json +158 -107
- package/dist/1146.js.LICENSE.txt +0 -21
- package/dist/2352.js +0 -1
- package/dist/2352.js.map +0 -1
- package/dist/246.js +0 -1
- package/dist/246.js.map +0 -1
- package/dist/6525.js +0 -2
- package/dist/6525.js.map +0 -1
- package/dist/8556.js +0 -2
- package/dist/8556.js.map +0 -1
- package/dist/8638.js +0 -1
- package/dist/8638.js.map +0 -1
- package/dist/9968.js +0 -1
- package/dist/9968.js.map +0 -1
- package/src/bill-item-actions/edit-bill-item.component.tsx +0 -221
- package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
- package/src/invoice/payments/payments.component.test.tsx +0 -121
- /package/dist/{8556.js.LICENSE.txt → 3717.js.LICENSE.txt} +0 -0
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
|
|
5
|
+
import { configSchema, type BillingConfig } from '../config-schema';
|
|
4
6
|
import { useBills } from '../billing.resource';
|
|
5
7
|
import BillHistory from './bill-history.component';
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
jest.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
|
|
10
|
+
const mockUseBills = jest.mocked(useBills);
|
|
11
|
+
|
|
12
|
+
jest.mock('../billing.resource', () => ({
|
|
13
|
+
useBills: jest.fn(() => ({
|
|
14
|
+
bills: mockBillData,
|
|
15
|
+
isLoading: false,
|
|
16
|
+
isValidating: false,
|
|
17
|
+
error: null,
|
|
18
|
+
})),
|
|
12
19
|
}));
|
|
13
20
|
|
|
14
|
-
// Mock window.i18next
|
|
15
21
|
window.i18next = {
|
|
16
22
|
language: 'en-US',
|
|
17
23
|
} as any;
|
|
@@ -20,9 +26,7 @@ const testProps = {
|
|
|
20
26
|
patientUuid: 'some-uuid',
|
|
21
27
|
};
|
|
22
28
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const mockBillsData = [
|
|
29
|
+
const mockBillData = [
|
|
26
30
|
{ uuid: '1', patientName: 'John Doe', identifier: '12345678', billingService: 'Checkup', totalAmount: 500 },
|
|
27
31
|
{ uuid: '2', patientName: 'John Doe', identifier: '12345678', billingService: 'Consulatation', totalAmount: 600 },
|
|
28
32
|
{ uuid: '3', patientName: 'John Doe', identifier: '12345678', billingService: 'Child services', totalAmount: 700 },
|
|
@@ -37,70 +41,19 @@ const mockBillsData = [
|
|
|
37
41
|
{ uuid: '12', patientName: 'John Doe', identifier: '12345678', billingService: 'MCH', totalAmount: 1300 },
|
|
38
42
|
];
|
|
39
43
|
|
|
40
|
-
// Mock the invoice table component
|
|
41
|
-
jest.mock('../invoice/invoice-table.component', () => jest.fn(() => <div>Invoice table</div>));
|
|
42
|
-
|
|
43
|
-
// Mock the billing resource
|
|
44
|
-
jest.mock('../billing.resource', () => ({
|
|
45
|
-
useBills: jest.fn(() => ({
|
|
46
|
-
bills: mockBillsData,
|
|
47
|
-
isLoading: false,
|
|
48
|
-
isValidating: false,
|
|
49
|
-
error: null,
|
|
50
|
-
})),
|
|
51
|
-
}));
|
|
52
|
-
|
|
53
|
-
// Mock esm-patient-common-lib
|
|
54
|
-
jest.mock('@openmrs/esm-patient-common-lib', () => ({
|
|
55
|
-
CardHeader: jest.fn(({ children }) => <div>{children}</div>),
|
|
56
|
-
EmptyDataIllustration: jest.fn(() => <div>Empty state illustration</div>),
|
|
57
|
-
ErrorState: jest.fn(({ error }) => <div>Error: {error?.message}</div>),
|
|
58
|
-
launchPatientWorkspace: jest.fn(),
|
|
59
|
-
usePaginationInfo: jest.fn(() => ({
|
|
60
|
-
pageSizes: [10, 20, 30],
|
|
61
|
-
currentPage: 1,
|
|
62
|
-
})),
|
|
63
|
-
}));
|
|
64
|
-
|
|
65
|
-
// Mock esm-framework
|
|
66
|
-
jest.mock('@openmrs/esm-framework', () => ({
|
|
67
|
-
useLayoutType: jest.fn(() => 'small-desktop'),
|
|
68
|
-
isDesktop: jest.fn(() => true),
|
|
69
|
-
usePagination: jest.fn().mockImplementation((data) => ({
|
|
70
|
-
currentPage: 1,
|
|
71
|
-
goTo: jest.fn(),
|
|
72
|
-
results: data,
|
|
73
|
-
paginated: true,
|
|
74
|
-
})),
|
|
75
|
-
showToast: jest.fn(),
|
|
76
|
-
showNotification: jest.fn(),
|
|
77
|
-
createErrorHandler: jest.fn(),
|
|
78
|
-
createGlobalStore: jest.fn(),
|
|
79
|
-
getGlobalStore: jest.fn(() => ({
|
|
80
|
-
subscribe: jest.fn(),
|
|
81
|
-
getState: jest.fn(),
|
|
82
|
-
setState: jest.fn(),
|
|
83
|
-
})),
|
|
84
|
-
useConfig: jest.fn(() => ({
|
|
85
|
-
pageSize: 10,
|
|
86
|
-
defaultCurrency: 'USD',
|
|
87
|
-
})),
|
|
88
|
-
useSession: jest.fn(() => ({
|
|
89
|
-
sessionLocation: { uuid: 'some-uuid', display: 'Location' },
|
|
90
|
-
})),
|
|
91
|
-
formatDate: jest.fn((date) => date?.toString() ?? ''),
|
|
92
|
-
formatDatetime: jest.fn((date) => date?.toString() ?? ''),
|
|
93
|
-
parseDate: jest.fn((dateString) => new Date(dateString)),
|
|
94
|
-
ExtensionSlot: jest.fn(({ children }) => <>{children}</>),
|
|
95
|
-
}));
|
|
96
|
-
|
|
97
44
|
describe('BillHistory', () => {
|
|
98
|
-
|
|
99
|
-
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
mockUseConfig.mockReturnValue({ ...getDefaultsFromConfigSchema(configSchema), defaultCurrency: 'USD' });
|
|
100
47
|
});
|
|
101
48
|
|
|
102
49
|
test('should render loading datatable skeleton', () => {
|
|
103
|
-
|
|
50
|
+
mockUseBills.mockReturnValueOnce({
|
|
51
|
+
isLoading: true,
|
|
52
|
+
isValidating: false,
|
|
53
|
+
error: null,
|
|
54
|
+
bills: [],
|
|
55
|
+
mutate: jest.fn(),
|
|
56
|
+
});
|
|
104
57
|
render(<BillHistory {...testProps} />);
|
|
105
58
|
const loadingSkeleton = screen.getByRole('table');
|
|
106
59
|
expect(loadingSkeleton).toBeInTheDocument();
|
|
@@ -108,7 +61,7 @@ describe('BillHistory', () => {
|
|
|
108
61
|
});
|
|
109
62
|
|
|
110
63
|
test('should render error state when API call fails', () => {
|
|
111
|
-
|
|
64
|
+
mockUseBills.mockReturnValueOnce({
|
|
112
65
|
isLoading: false,
|
|
113
66
|
isValidating: false,
|
|
114
67
|
error: new Error('some error'),
|
|
@@ -116,31 +69,31 @@ describe('BillHistory', () => {
|
|
|
116
69
|
mutate: jest.fn(),
|
|
117
70
|
});
|
|
118
71
|
render(<BillHistory {...testProps} />);
|
|
119
|
-
const errorState = screen.getByText(
|
|
72
|
+
const errorState = screen.getByText(/Error/);
|
|
120
73
|
expect(errorState).toBeInTheDocument();
|
|
121
74
|
});
|
|
122
75
|
|
|
123
76
|
test('should render bills table', async () => {
|
|
124
77
|
const user = userEvent.setup();
|
|
125
|
-
|
|
78
|
+
mockUseBills.mockReturnValueOnce({
|
|
126
79
|
isLoading: false,
|
|
127
80
|
isValidating: false,
|
|
128
81
|
error: null,
|
|
129
|
-
bills:
|
|
82
|
+
bills: mockBillData as any,
|
|
130
83
|
mutate: jest.fn(),
|
|
131
84
|
});
|
|
132
85
|
render(<BillHistory {...testProps} />);
|
|
133
86
|
|
|
134
87
|
// Verify headers
|
|
135
|
-
expect(screen.getByText('
|
|
136
|
-
expect(screen.getByText('
|
|
88
|
+
expect(screen.getByText('Visit time')).toBeInTheDocument();
|
|
89
|
+
expect(screen.getByText('Identifier')).toBeInTheDocument();
|
|
137
90
|
|
|
138
91
|
const tableRowGroup = screen.getAllByRole('rowgroup');
|
|
139
92
|
expect(tableRowGroup).toHaveLength(2);
|
|
140
93
|
|
|
141
94
|
// Page navigation should work as expected
|
|
142
|
-
const nextPageButton = screen.getByRole('button', { name: /
|
|
143
|
-
const prevPageButton = screen.getByRole('button', { name: /
|
|
95
|
+
const nextPageButton = screen.getByRole('button', { name: /Next page/ });
|
|
96
|
+
const prevPageButton = screen.getByRole('button', { name: /Previous page/ });
|
|
144
97
|
|
|
145
98
|
expect(nextPageButton).toBeInTheDocument();
|
|
146
99
|
expect(prevPageButton).toBeInTheDocument();
|
|
@@ -156,7 +109,13 @@ describe('BillHistory', () => {
|
|
|
156
109
|
});
|
|
157
110
|
|
|
158
111
|
test('should render empty state view when there are no bills', () => {
|
|
159
|
-
|
|
112
|
+
mockUseBills.mockReturnValueOnce({
|
|
113
|
+
isLoading: false,
|
|
114
|
+
isValidating: false,
|
|
115
|
+
error: null,
|
|
116
|
+
bills: [],
|
|
117
|
+
mutate: jest.fn(),
|
|
118
|
+
});
|
|
160
119
|
render(<BillHistory {...testProps} />);
|
|
161
120
|
const emptyState = screen.getByText(/There are no bills to display./);
|
|
162
121
|
expect(emptyState).toBeInTheDocument();
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
1
2
|
@use '@carbon/layout';
|
|
2
3
|
@use '@carbon/type';
|
|
3
|
-
@use '@carbon/colors';
|
|
4
4
|
|
|
5
|
-
.section {
|
|
6
|
-
margin: layout.$spacing-03;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
5
|
.sectionTitle {
|
|
10
6
|
@include type.type-style('heading-compact-02');
|
|
11
7
|
color: colors.$gray-70;
|
|
@@ -16,6 +12,16 @@
|
|
|
16
12
|
padding-bottom: layout.$spacing-05;
|
|
17
13
|
}
|
|
18
14
|
|
|
15
|
+
.billInfo {
|
|
16
|
+
@include type.type-style('body-compact-01');
|
|
17
|
+
color: colors.$gray-70;
|
|
18
|
+
|
|
19
|
+
.separator {
|
|
20
|
+
margin: 0 layout.$spacing-02;
|
|
21
|
+
color: colors.$gray-50;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
.label {
|
|
20
26
|
@include type.type-style('heading-compact-01');
|
|
21
27
|
margin-bottom: layout.$spacing-05;
|
|
@@ -24,3 +30,13 @@
|
|
|
24
30
|
.controlField {
|
|
25
31
|
margin-bottom: layout.$spacing-05;
|
|
26
32
|
}
|
|
33
|
+
|
|
34
|
+
.loader {
|
|
35
|
+
&:global(.cds--inline-loading) {
|
|
36
|
+
min-height: layout.$spacing-05;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:global(.cds--inline-loading__text) {
|
|
40
|
+
font-size: unset;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Form,
|
|
5
|
+
InlineLoading,
|
|
6
|
+
ModalBody,
|
|
7
|
+
ModalFooter,
|
|
8
|
+
ModalHeader,
|
|
9
|
+
NumberInput,
|
|
10
|
+
Stack,
|
|
11
|
+
TextInput,
|
|
12
|
+
} from '@carbon/react';
|
|
13
|
+
import { useTranslation } from 'react-i18next';
|
|
14
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
15
|
+
import { mutate } from 'swr';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
18
|
+
import { getCoreTranslation, showSnackbar, useConfig } from '@openmrs/esm-framework';
|
|
19
|
+
import { apiBasePath } from '../constants';
|
|
20
|
+
import { getBillableServiceUuid } from '../invoice/payments/utils';
|
|
21
|
+
import { type LineItem, type MappedBill } from '../types';
|
|
22
|
+
import { updateBillItems } from '../billing.resource';
|
|
23
|
+
import { useBillableServices } from '../billable-services/billable-service.resource';
|
|
24
|
+
import { type BillingConfig } from '../config-schema';
|
|
25
|
+
import { convertToCurrency } from '../helpers';
|
|
26
|
+
import styles from './bill-item-actions.scss';
|
|
27
|
+
|
|
28
|
+
interface EditBillLineItemModalProps {
|
|
29
|
+
bill: MappedBill;
|
|
30
|
+
closeModal: () => void;
|
|
31
|
+
item: LineItem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const EditBillLineItemModal: React.FC<EditBillLineItemModalProps> = ({ bill, closeModal, item }) => {
|
|
35
|
+
const { t } = useTranslation();
|
|
36
|
+
const { defaultCurrency } = useConfig<BillingConfig>();
|
|
37
|
+
const { billableServices } = useBillableServices();
|
|
38
|
+
const [total, setTotal] = useState(0);
|
|
39
|
+
|
|
40
|
+
const schema = useMemo(
|
|
41
|
+
() =>
|
|
42
|
+
z.object({
|
|
43
|
+
// NOTE: Frontend-only validation - quantities <1 or >100 can still be submitted via API.
|
|
44
|
+
// Backend (BillServiceImpl.java:100) has empty validate() method.
|
|
45
|
+
// TODO: Add server-side validation to enforce data integrity
|
|
46
|
+
quantity: z.coerce
|
|
47
|
+
.number({
|
|
48
|
+
required_error: t('quantityRequired', 'Quantity is required'),
|
|
49
|
+
invalid_type_error: t('quantityMustBeNumber', 'Quantity must be a valid number'),
|
|
50
|
+
})
|
|
51
|
+
.int(t('quantityMustBeInteger', 'Quantity must be a whole number'))
|
|
52
|
+
.min(1, t('quantityMustBeAtLeastOne', 'Quantity must be at least 1'))
|
|
53
|
+
.max(100, t('quantityCannotExceed100', 'Quantity cannot exceed 100')),
|
|
54
|
+
price: z.coerce
|
|
55
|
+
.number({
|
|
56
|
+
required_error: t('priceIsRequired', 'Price is required'),
|
|
57
|
+
invalid_type_error: t('priceMustBeNumber', 'Price must be a valid number'),
|
|
58
|
+
})
|
|
59
|
+
.positive(t('priceMustBePositive', 'Price must be greater than 0')),
|
|
60
|
+
}),
|
|
61
|
+
[t],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
type BillLineItemForm = z.infer<typeof schema>;
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
control,
|
|
68
|
+
handleSubmit,
|
|
69
|
+
formState: { isSubmitting, errors },
|
|
70
|
+
watch,
|
|
71
|
+
} = useForm<BillLineItemForm>({
|
|
72
|
+
defaultValues: {
|
|
73
|
+
quantity: item.quantity,
|
|
74
|
+
price: item.price,
|
|
75
|
+
},
|
|
76
|
+
resolver: zodResolver(schema),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const quantity = watch('quantity');
|
|
80
|
+
const price = watch('price');
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const quantityNum = typeof quantity === 'number' ? quantity : parseFloat(quantity) || 0;
|
|
84
|
+
const priceNum = typeof price === 'number' ? price : parseFloat(price) || 0;
|
|
85
|
+
const newTotal = quantityNum * priceNum;
|
|
86
|
+
setTotal(isNaN(newTotal) ? 0 : newTotal);
|
|
87
|
+
}, [quantity, price]);
|
|
88
|
+
|
|
89
|
+
const onSubmit = async (data: BillLineItemForm) => {
|
|
90
|
+
const url = `${apiBasePath}bill`;
|
|
91
|
+
|
|
92
|
+
const newItem = {
|
|
93
|
+
...item,
|
|
94
|
+
quantity: data.quantity,
|
|
95
|
+
price: data.price,
|
|
96
|
+
billableService: getBillableServiceUuid(billableServices, item.billableService),
|
|
97
|
+
item: item?.item,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const previousLineitems = bill?.lineItems
|
|
101
|
+
.filter((currItem) => currItem.uuid !== item?.uuid)
|
|
102
|
+
.map((currItem) => ({
|
|
103
|
+
...currItem,
|
|
104
|
+
billableService: getBillableServiceUuid(billableServices, currItem.billableService),
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
const updatedLineItems = previousLineitems.concat(newItem);
|
|
108
|
+
|
|
109
|
+
const payload = {
|
|
110
|
+
cashPoint: bill.cashPointUuid,
|
|
111
|
+
cashier: bill.cashier.uuid,
|
|
112
|
+
lineItems: updatedLineItems,
|
|
113
|
+
patient: bill.patientUuid,
|
|
114
|
+
status: bill.status,
|
|
115
|
+
uuid: bill.uuid,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await updateBillItems(payload);
|
|
120
|
+
mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
|
|
121
|
+
showSnackbar({
|
|
122
|
+
title: t('lineItemUpdated', 'Line item updated'),
|
|
123
|
+
subtitle: t('lineItemUpdateSuccess', 'The bill line item has been updated successfully'),
|
|
124
|
+
kind: 'success',
|
|
125
|
+
});
|
|
126
|
+
closeModal();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
showSnackbar({
|
|
129
|
+
title: t('lineItemUpdateFailed', 'Failed to update line item'),
|
|
130
|
+
subtitle:
|
|
131
|
+
error?.message || t('lineItemUpdateErrorDefault', 'Unable to update the bill line item. Please try again.'),
|
|
132
|
+
kind: 'error',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (Object.keys(bill)?.length === 0) {
|
|
138
|
+
return <ModalHeader closeModal={closeModal} title={t('billLineItemEmpty', 'This bill has no line items')} />;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<>
|
|
143
|
+
<ModalHeader closeModal={closeModal} title={t('editBillLineItem', 'Edit bill line item')} />
|
|
144
|
+
<Form onSubmit={handleSubmit(onSubmit)}>
|
|
145
|
+
<ModalBody>
|
|
146
|
+
<Stack gap={5}>
|
|
147
|
+
<div className={styles.modalBody}>
|
|
148
|
+
<div className={styles.billInfo}>
|
|
149
|
+
{bill?.patientName}
|
|
150
|
+
<span className={styles.separator}>·</span>
|
|
151
|
+
{bill?.cashPointName}
|
|
152
|
+
<span className={styles.separator}>·</span>
|
|
153
|
+
{bill?.receiptNumber}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<section>
|
|
157
|
+
<p className={styles.label}>
|
|
158
|
+
{t('item', 'Item')}: {item?.billableService ? item?.billableService : item?.item}
|
|
159
|
+
</p>
|
|
160
|
+
<p className={styles.label}>
|
|
161
|
+
{t('currentPrice', 'Current price')}: {convertToCurrency(item?.price, defaultCurrency)}
|
|
162
|
+
</p>
|
|
163
|
+
<p className={styles.label}>
|
|
164
|
+
{t('serviceStatus', 'Service status')}: {item?.paymentStatus}
|
|
165
|
+
</p>
|
|
166
|
+
<Controller
|
|
167
|
+
name="quantity"
|
|
168
|
+
control={control}
|
|
169
|
+
render={({ field: { onChange, value } }) => (
|
|
170
|
+
<NumberInput
|
|
171
|
+
disableWheel
|
|
172
|
+
className={styles.controlField}
|
|
173
|
+
hideSteppers
|
|
174
|
+
id="quantityInput"
|
|
175
|
+
invalid={!!errors.quantity}
|
|
176
|
+
invalidText={errors.quantity?.message}
|
|
177
|
+
label={t('quantity', 'Quantity')}
|
|
178
|
+
onChange={(_event, state: { value: number | string; direction: string }) => {
|
|
179
|
+
onChange(state.value);
|
|
180
|
+
}}
|
|
181
|
+
value={value}
|
|
182
|
+
/>
|
|
183
|
+
)}
|
|
184
|
+
/>
|
|
185
|
+
|
|
186
|
+
<Controller
|
|
187
|
+
name="price"
|
|
188
|
+
control={control}
|
|
189
|
+
render={({ field: { value } }) => (
|
|
190
|
+
<TextInput
|
|
191
|
+
className={styles.controlField}
|
|
192
|
+
helperText={t('unitPriceHelperText', 'This is the unit price for this item')}
|
|
193
|
+
id="priceInput"
|
|
194
|
+
labelText={t('unitPrice', 'Unit price')}
|
|
195
|
+
readOnly
|
|
196
|
+
value={value}
|
|
197
|
+
/>
|
|
198
|
+
)}
|
|
199
|
+
/>
|
|
200
|
+
<p className={styles.label} aria-live="polite">
|
|
201
|
+
{t('total', 'Total')}: {convertToCurrency(total, defaultCurrency)}
|
|
202
|
+
</p>
|
|
203
|
+
</section>
|
|
204
|
+
</Stack>
|
|
205
|
+
</ModalBody>
|
|
206
|
+
<ModalFooter>
|
|
207
|
+
<Button kind="secondary" onClick={closeModal}>
|
|
208
|
+
{getCoreTranslation('cancel')}
|
|
209
|
+
</Button>
|
|
210
|
+
<Button type="submit" disabled={isSubmitting}>
|
|
211
|
+
{isSubmitting ? (
|
|
212
|
+
<div className={styles.inline}>
|
|
213
|
+
<InlineLoading className={styles.loader} description={`${t('submitting', 'Submitting')}...`} />
|
|
214
|
+
</div>
|
|
215
|
+
) : (
|
|
216
|
+
getCoreTranslation('save')
|
|
217
|
+
)}
|
|
218
|
+
</Button>
|
|
219
|
+
</ModalFooter>
|
|
220
|
+
</Form>
|
|
221
|
+
</>
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export default EditBillLineItemModal;
|