@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
|
@@ -6,37 +6,12 @@
|
|
|
6
6
|
.form {
|
|
7
7
|
display: flex;
|
|
8
8
|
flex-direction: column;
|
|
9
|
-
justify-content: space-between;
|
|
10
9
|
height: 100%;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
.section {
|
|
14
|
-
margin: layout.$spacing-03;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.sectionTitle {
|
|
18
|
-
@include type.type-style('heading-compact-02');
|
|
19
|
-
color: $text-02;
|
|
20
|
-
margin-bottom: layout.$spacing-04;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.modalBody {
|
|
24
|
-
padding-bottom: layout.$spacing-05;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.container {
|
|
28
10
|
margin: layout.$spacing-05;
|
|
29
11
|
}
|
|
30
12
|
|
|
31
|
-
.paymentContainer {
|
|
32
|
-
margin: layout.$layout-01;
|
|
33
|
-
padding: layout.$layout-01;
|
|
34
|
-
width: 70%;
|
|
35
|
-
border-right: 1px solid colors.$cool-gray-40;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
13
|
.paymentButtons {
|
|
39
|
-
margin: layout.$
|
|
14
|
+
margin: layout.$spacing-05 0;
|
|
40
15
|
}
|
|
41
16
|
|
|
42
17
|
.paymentMethodContainer {
|
|
@@ -48,22 +23,6 @@
|
|
|
48
23
|
width: 100%;
|
|
49
24
|
}
|
|
50
25
|
|
|
51
|
-
.paymentTotals {
|
|
52
|
-
margin-top: layout.$spacing-01;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.processPayments {
|
|
56
|
-
display: flex;
|
|
57
|
-
justify-content: flex-end;
|
|
58
|
-
margin: layout.$spacing-05;
|
|
59
|
-
column-gap: layout.$spacing-04;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.errorPaymentContainer {
|
|
63
|
-
margin: layout.$spacing-04;
|
|
64
|
-
min-height: layout.$spacing-09;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
26
|
.removeButtonContainer {
|
|
68
27
|
display: flex;
|
|
69
28
|
align-self: center;
|
|
@@ -99,39 +58,21 @@
|
|
|
99
58
|
|
|
100
59
|
.conceptLabel {
|
|
101
60
|
@include type.type-style('label-02');
|
|
102
|
-
margin: layout.$spacing-05;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.errorContainer {
|
|
106
|
-
margin: layout.$spacing-05;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
.serviceError {
|
|
110
|
-
:global(.cds--search-input):focus {
|
|
111
|
-
outline: 2.5px solid $danger;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
:global(.cds--search-magnifier) {
|
|
115
|
-
svg {
|
|
116
|
-
fill: $danger;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
61
|
+
margin-bottom: layout.$spacing-05;
|
|
119
62
|
}
|
|
120
63
|
|
|
121
64
|
.errorMessage {
|
|
122
65
|
@include type.type-style('label-02');
|
|
123
66
|
color: $danger;
|
|
124
|
-
margin-
|
|
67
|
+
margin-bottom: layout.$spacing-05;
|
|
125
68
|
}
|
|
126
69
|
|
|
127
|
-
.
|
|
70
|
+
.loader {
|
|
128
71
|
&:global(.cds--inline-loading) {
|
|
129
72
|
min-height: layout.$spacing-05;
|
|
130
73
|
}
|
|
131
74
|
}
|
|
132
75
|
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
font-size: 0.875rem;
|
|
76
|
+
.serviceNameLabel {
|
|
77
|
+
@include type.type-style('body-compact-02');
|
|
136
78
|
}
|
|
137
|
-
|
|
@@ -1,26 +1,29 @@
|
|
|
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 { type FetchResponse
|
|
4
|
+
import { navigate, type FetchResponse } from '@openmrs/esm-framework';
|
|
5
5
|
import {
|
|
6
|
+
createBillableService,
|
|
6
7
|
useBillableServices,
|
|
8
|
+
useConceptsSearch,
|
|
7
9
|
usePaymentModes,
|
|
8
10
|
useServiceTypes,
|
|
9
|
-
createBillableSerice,
|
|
10
11
|
} from '../billable-service.resource';
|
|
11
12
|
import AddBillableService from './add-billable-service.component';
|
|
12
13
|
|
|
13
|
-
const mockUseBillableServices =
|
|
14
|
-
const mockUsePaymentModes =
|
|
15
|
-
const mockUseServiceTypes =
|
|
16
|
-
const
|
|
17
|
-
const
|
|
14
|
+
const mockUseBillableServices = jest.mocked(useBillableServices);
|
|
15
|
+
const mockUsePaymentModes = jest.mocked(usePaymentModes);
|
|
16
|
+
const mockUseServiceTypes = jest.mocked(useServiceTypes);
|
|
17
|
+
const mockCreateBillableService = jest.mocked(createBillableService);
|
|
18
|
+
const mockUseConceptsSearch = jest.mocked(useConceptsSearch);
|
|
18
19
|
|
|
19
20
|
jest.mock('../billable-service.resource', () => ({
|
|
20
21
|
useBillableServices: jest.fn(),
|
|
21
22
|
usePaymentModes: jest.fn(),
|
|
22
23
|
useServiceTypes: jest.fn(),
|
|
23
|
-
|
|
24
|
+
createBillableService: jest.fn(),
|
|
25
|
+
updateBillableService: jest.fn(),
|
|
26
|
+
useConceptsSearch: jest.fn(),
|
|
24
27
|
}));
|
|
25
28
|
|
|
26
29
|
const mockPaymentModes = [
|
|
@@ -49,106 +52,189 @@ const mockServiceTypes = [
|
|
|
49
52
|
{ uuid: 'a487a743-62ce-4f93-a66b-c5154ee8987d', display: 'Adherence counselling service' },
|
|
50
53
|
];
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
// Test helpers (canonical pattern)
|
|
56
|
+
const setupMocks = () => {
|
|
57
|
+
mockUseBillableServices.mockReturnValue({
|
|
58
|
+
billableServices: [],
|
|
59
|
+
isLoading: false,
|
|
60
|
+
error: null,
|
|
61
|
+
mutate: jest.fn(),
|
|
62
|
+
isValidating: false,
|
|
55
63
|
});
|
|
56
|
-
|
|
64
|
+
mockUsePaymentModes.mockReturnValue({ paymentModes: mockPaymentModes, error: null, isLoadingPaymentModes: false });
|
|
65
|
+
mockUseServiceTypes.mockReturnValue({ serviceTypes: mockServiceTypes, error: false, isLoadingServiceTypes: false });
|
|
66
|
+
mockUseConceptsSearch.mockReturnValue({ searchResults: [], isSearching: false, error: null });
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const renderAddBillableService = (props = {}) => {
|
|
70
|
+
const defaultProps = {
|
|
71
|
+
onClose: jest.fn(),
|
|
72
|
+
...props,
|
|
73
|
+
};
|
|
74
|
+
setupMocks();
|
|
75
|
+
return render(<AddBillableService {...defaultProps} />);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
interface FillOptions {
|
|
79
|
+
serviceName?: string;
|
|
80
|
+
shortName?: string;
|
|
81
|
+
skipPrice?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const fillRequiredFields = async (user, options: FillOptions = {}) => {
|
|
85
|
+
const { serviceName = 'Test Service Name', shortName = 'Test Short Name', skipPrice = false } = options;
|
|
86
|
+
|
|
87
|
+
if (serviceName) {
|
|
88
|
+
await user.type(screen.getByRole('textbox', { name: /Service name/i }), serviceName);
|
|
89
|
+
}
|
|
90
|
+
if (shortName) {
|
|
91
|
+
await user.type(screen.getByRole('textbox', { name: /Short name/i }), shortName);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await user.click(screen.getByRole('combobox', { name: /Service type/i }));
|
|
95
|
+
await user.click(screen.getByRole('option', { name: /Lab service/i }));
|
|
96
|
+
|
|
97
|
+
await user.click(screen.getByRole('combobox', { name: /Payment mode/i }));
|
|
98
|
+
await user.click(screen.getByRole('option', { name: /Cash/i }));
|
|
99
|
+
|
|
100
|
+
if (!skipPrice) {
|
|
101
|
+
const priceInput = screen.getByRole('spinbutton', { name: /Selling Price/i });
|
|
102
|
+
await user.type(priceInput, '100');
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const submitForm = async (user) => {
|
|
107
|
+
const saveBtn = screen.getByRole('button', { name: /save/i });
|
|
108
|
+
await user.click(saveBtn);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
describe('AddBillableService', () => {
|
|
57
112
|
test('should render billable services form and generate correct payload', async () => {
|
|
58
113
|
const user = userEvent.setup();
|
|
59
114
|
const mockOnClose = jest.fn();
|
|
60
|
-
|
|
61
|
-
billableServices: [],
|
|
62
|
-
isLoading: false,
|
|
63
|
-
error: null,
|
|
64
|
-
mutate: jest.fn(),
|
|
65
|
-
isValidating: false,
|
|
66
|
-
});
|
|
67
|
-
mockUsePaymentModes.mockReturnValue({ paymentModes: mockPaymentModes, error: null, isLoading: false });
|
|
68
|
-
mockUseServiceTypes.mockReturnValue({ serviceTypes: mockServiceTypes, error: false, isLoading: false });
|
|
69
|
-
|
|
70
|
-
render(<AddBillableService onClose={mockOnClose} />);
|
|
115
|
+
renderAddBillableService({ onClose: mockOnClose });
|
|
71
116
|
|
|
72
|
-
const formTitle = screen.getByRole('heading', { name: /Add
|
|
117
|
+
const formTitle = screen.getByRole('heading', { name: /Add billable service/i });
|
|
73
118
|
expect(formTitle).toBeInTheDocument();
|
|
74
119
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const serviceShortNameTextInp = screen.getByRole('textbox', { name: /Short Name/i });
|
|
79
|
-
expect(serviceShortNameTextInp).toBeInTheDocument();
|
|
80
|
-
|
|
81
|
-
await user.type(serviceNameTextInp, 'Test Service Name');
|
|
82
|
-
await user.type(serviceShortNameTextInp, 'Test Short Name');
|
|
83
|
-
|
|
84
|
-
expect(serviceNameTextInp).toHaveValue('Test Service Name');
|
|
85
|
-
expect(serviceShortNameTextInp).toHaveValue('Test Short Name');
|
|
86
|
-
|
|
87
|
-
const serviceTypeComboBox = screen.getByRole('combobox', { name: /Service Type/i });
|
|
88
|
-
expect(serviceTypeComboBox).toBeInTheDocument();
|
|
89
|
-
await user.click(serviceTypeComboBox);
|
|
90
|
-
const serviceTypeOptions = screen.getByRole('option', { name: /Lab service/i });
|
|
91
|
-
expect(serviceTypeOptions).toBeInTheDocument();
|
|
92
|
-
await user.click(serviceTypeOptions);
|
|
93
|
-
|
|
94
|
-
const addPaymentMethodBtn = screen.getByRole('button', { name: /Add payment option/i });
|
|
95
|
-
expect(addPaymentMethodBtn).toBeInTheDocument();
|
|
96
|
-
|
|
97
|
-
await user.click(addPaymentMethodBtn);
|
|
120
|
+
await fillRequiredFields(user);
|
|
121
|
+
mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
|
|
98
122
|
|
|
99
|
-
|
|
100
|
-
expect(paymentMethodComboBox).toBeInTheDocument();
|
|
101
|
-
await user.click(paymentMethodComboBox);
|
|
102
|
-
const paymentMethodOptions = screen.getByRole('option', { name: /Cash/i });
|
|
103
|
-
expect(paymentMethodOptions).toBeInTheDocument();
|
|
104
|
-
await user.click(paymentMethodOptions);
|
|
123
|
+
await submitForm(user);
|
|
105
124
|
|
|
106
|
-
|
|
107
|
-
expect(
|
|
108
|
-
await user.type(priceTextInp, '1000');
|
|
109
|
-
|
|
110
|
-
mockCreateBillableSerice.mockReturnValue(Promise.resolve({} as FetchResponse<any>));
|
|
111
|
-
const saveBtn = screen.getByRole('button', { name: /Save/i });
|
|
112
|
-
expect(saveBtn).toBeInTheDocument();
|
|
113
|
-
await user.click(saveBtn);
|
|
114
|
-
|
|
115
|
-
expect(mockCreateBillableSerice).toHaveBeenCalledTimes(1);
|
|
116
|
-
expect(mockCreateBillableSerice).toHaveBeenCalledWith({
|
|
125
|
+
expect(mockCreateBillableService).toHaveBeenCalledTimes(1);
|
|
126
|
+
expect(mockCreateBillableService).toHaveBeenCalledWith({
|
|
117
127
|
name: 'Test Service Name',
|
|
118
128
|
shortName: 'Test Short Name',
|
|
119
|
-
serviceType:
|
|
129
|
+
serviceType: 'c9604249-db0a-4d03-b074-fc6bc2fa13e6',
|
|
120
130
|
servicePrices: [
|
|
121
131
|
{
|
|
122
132
|
paymentMode: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74',
|
|
123
|
-
price:
|
|
133
|
+
price: 100,
|
|
124
134
|
name: 'Cash',
|
|
125
135
|
},
|
|
126
136
|
],
|
|
127
137
|
serviceStatus: 'ENABLED',
|
|
138
|
+
concept: undefined,
|
|
128
139
|
});
|
|
129
|
-
expect(
|
|
130
|
-
expect(
|
|
140
|
+
expect(navigate).toHaveBeenCalledTimes(1);
|
|
141
|
+
expect(navigate).toHaveBeenCalledWith({ to: '/openmrs/spa/billable-services' });
|
|
131
142
|
});
|
|
132
143
|
|
|
133
144
|
test("should navigate back to billable services dashboard when 'Cancel' button is clicked", async () => {
|
|
134
145
|
const user = userEvent.setup();
|
|
135
146
|
const mockOnClose = jest.fn();
|
|
136
|
-
|
|
137
|
-
billableServices: [],
|
|
138
|
-
isLoading: false,
|
|
139
|
-
error: null,
|
|
140
|
-
mutate: jest.fn(),
|
|
141
|
-
isValidating: false,
|
|
142
|
-
});
|
|
143
|
-
mockUsePaymentModes.mockReturnValue({ paymentModes: mockPaymentModes, error: null, isLoading: false });
|
|
144
|
-
mockUseServiceTypes.mockReturnValue({ serviceTypes: mockServiceTypes, error: false, isLoading: false });
|
|
147
|
+
renderAddBillableService({ onClose: mockOnClose });
|
|
145
148
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const cancelBtn = screen.getByRole('button', { name: /Cancel/i });
|
|
149
|
-
expect(cancelBtn).toBeInTheDocument();
|
|
149
|
+
const cancelBtn = screen.getByRole('button', { name: /cancel/i });
|
|
150
150
|
await user.click(cancelBtn);
|
|
151
151
|
|
|
152
152
|
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
|
153
153
|
});
|
|
154
|
+
|
|
155
|
+
describe('Form Validation', () => {
|
|
156
|
+
test('should show "Price must be greater than 0" error for zero price', async () => {
|
|
157
|
+
const user = userEvent.setup();
|
|
158
|
+
renderAddBillableService();
|
|
159
|
+
|
|
160
|
+
await fillRequiredFields(user, { skipPrice: true });
|
|
161
|
+
|
|
162
|
+
const priceInput = screen.getByRole('spinbutton', { name: /selling price/i });
|
|
163
|
+
await user.type(priceInput, '0');
|
|
164
|
+
|
|
165
|
+
await submitForm(user);
|
|
166
|
+
|
|
167
|
+
expect(screen.getByText('Price must be greater than 0')).toBeInTheDocument();
|
|
168
|
+
expect(mockCreateBillableService).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should show "Price must be greater than 0" error for negative price', async () => {
|
|
172
|
+
const user = userEvent.setup();
|
|
173
|
+
renderAddBillableService();
|
|
174
|
+
|
|
175
|
+
await fillRequiredFields(user, { skipPrice: true });
|
|
176
|
+
|
|
177
|
+
const priceInput = screen.getByRole('spinbutton', { name: /Selling Price/i });
|
|
178
|
+
await user.type(priceInput, '-10');
|
|
179
|
+
|
|
180
|
+
await submitForm(user);
|
|
181
|
+
|
|
182
|
+
expect(screen.getByText('Price must be greater than 0')).toBeInTheDocument();
|
|
183
|
+
expect(mockCreateBillableService).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('should show "Service name is required" error when service name is empty', async () => {
|
|
187
|
+
const user = userEvent.setup();
|
|
188
|
+
renderAddBillableService();
|
|
189
|
+
|
|
190
|
+
// Fill all fields except service name
|
|
191
|
+
await user.type(screen.getByRole('textbox', { name: /Short name/i }), 'Test Short Name');
|
|
192
|
+
|
|
193
|
+
await user.click(screen.getByRole('combobox', { name: /Service type/i }));
|
|
194
|
+
await user.click(screen.getByRole('option', { name: /Lab service/i }));
|
|
195
|
+
|
|
196
|
+
await user.click(screen.getByRole('combobox', { name: /Payment mode/i }));
|
|
197
|
+
await user.click(screen.getByRole('option', { name: /Cash/i }));
|
|
198
|
+
|
|
199
|
+
const priceInput = screen.getByRole('spinbutton', { name: /Selling Price/i });
|
|
200
|
+
await user.type(priceInput, '100');
|
|
201
|
+
|
|
202
|
+
await submitForm(user);
|
|
203
|
+
|
|
204
|
+
expect(screen.getByText('Service name is required')).toBeInTheDocument();
|
|
205
|
+
expect(mockCreateBillableService).not.toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('should accept valid decimal price values', async () => {
|
|
209
|
+
const user = userEvent.setup();
|
|
210
|
+
renderAddBillableService();
|
|
211
|
+
|
|
212
|
+
await fillRequiredFields(user, { skipPrice: true });
|
|
213
|
+
|
|
214
|
+
const priceInput = screen.getByRole('spinbutton', { name: /selling price/i });
|
|
215
|
+
await user.type(priceInput, '10.50');
|
|
216
|
+
|
|
217
|
+
mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
|
|
218
|
+
|
|
219
|
+
await submitForm(user);
|
|
220
|
+
|
|
221
|
+
expect(screen.queryByText('Price is required')).not.toBeInTheDocument();
|
|
222
|
+
expect(screen.queryByText('Price must be greater than 0')).not.toBeInTheDocument();
|
|
223
|
+
expect(mockCreateBillableService).toHaveBeenCalledTimes(1);
|
|
224
|
+
expect(mockCreateBillableService).toHaveBeenCalledWith({
|
|
225
|
+
name: 'Test Service Name',
|
|
226
|
+
shortName: 'Test Short Name',
|
|
227
|
+
serviceType: 'c9604249-db0a-4d03-b074-fc6bc2fa13e6',
|
|
228
|
+
servicePrices: [
|
|
229
|
+
{
|
|
230
|
+
paymentMode: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74',
|
|
231
|
+
price: 10.5,
|
|
232
|
+
name: 'Cash',
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
serviceStatus: 'ENABLED',
|
|
236
|
+
concept: undefined,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
154
240
|
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
|
|
4
|
+
import { getCoreTranslation } from '@openmrs/esm-framework';
|
|
5
|
+
import { type BillableService } from '../../types';
|
|
6
|
+
import AddBillableService from './add-billable-service.component';
|
|
7
|
+
|
|
8
|
+
interface EditBillableServiceModalProps {
|
|
9
|
+
closeModal: () => void;
|
|
10
|
+
onServiceUpdated: () => void;
|
|
11
|
+
serviceToEdit?: BillableService;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const EditBillableServiceModal: React.FC<EditBillableServiceModalProps> = ({
|
|
15
|
+
closeModal,
|
|
16
|
+
serviceToEdit,
|
|
17
|
+
onServiceUpdated,
|
|
18
|
+
}) => {
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<ModalHeader closeModal={closeModal} title={t('billableService', 'Billable service')} />
|
|
24
|
+
<ModalBody>
|
|
25
|
+
<AddBillableService
|
|
26
|
+
serviceToEdit={serviceToEdit}
|
|
27
|
+
isModal
|
|
28
|
+
onClose={closeModal}
|
|
29
|
+
onServiceUpdated={onServiceUpdated}
|
|
30
|
+
/>
|
|
31
|
+
</ModalBody>
|
|
32
|
+
<ModalFooter>
|
|
33
|
+
<Button kind="secondary" onClick={closeModal}>
|
|
34
|
+
{getCoreTranslation('cancel')}
|
|
35
|
+
</Button>
|
|
36
|
+
<Button
|
|
37
|
+
onClick={() => {
|
|
38
|
+
// Trigger form submission programmatically
|
|
39
|
+
const form = document.getElementById('billable-service-form') as HTMLFormElement;
|
|
40
|
+
if (form) {
|
|
41
|
+
form.requestSubmit();
|
|
42
|
+
}
|
|
43
|
+
}}>
|
|
44
|
+
{getCoreTranslation('save')}
|
|
45
|
+
</Button>
|
|
46
|
+
</ModalFooter>
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default EditBillableServiceModal;
|
|
@@ -2,10 +2,10 @@ import React, { useMemo } from 'react';
|
|
|
2
2
|
import { InlineLoading } from '@carbon/react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { ErrorState } from '@openmrs/esm-patient-common-lib';
|
|
5
|
+
import { getCoreTranslation } from '@openmrs/esm-framework';
|
|
5
6
|
import { useBillableServices } from '../billable-service.resource';
|
|
6
7
|
import Card from '../../metrics-cards/card.component';
|
|
7
8
|
import styles from '../../metrics-cards/metrics-cards.scss';
|
|
8
|
-
import { ExtensionSlot } from '@openmrs/esm-framework';
|
|
9
9
|
|
|
10
10
|
export default function ServiceMetrics() {
|
|
11
11
|
const { t } = useTranslation();
|
|
@@ -23,13 +23,21 @@ export default function ServiceMetrics() {
|
|
|
23
23
|
if (isLoading) {
|
|
24
24
|
return (
|
|
25
25
|
<section className={styles.container}>
|
|
26
|
-
<InlineLoading
|
|
26
|
+
<InlineLoading
|
|
27
|
+
status="active"
|
|
28
|
+
iconDescription={getCoreTranslation('loading')}
|
|
29
|
+
description={t('loadingServiceMetrics', 'Loading service metrics') + '...'}
|
|
30
|
+
/>
|
|
27
31
|
</section>
|
|
28
32
|
);
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
if (error) {
|
|
32
|
-
return
|
|
36
|
+
return (
|
|
37
|
+
<div className={styles.errorContainer}>
|
|
38
|
+
<ErrorState headerTitle={t('serviceMetrics', 'Service Metrics')} error={error} />
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
33
41
|
}
|
|
34
42
|
return (
|
|
35
43
|
<section className={styles.container}>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useForm, Controller } from 'react-hook-form';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
6
|
+
import { Button, Form, ModalBody, ModalFooter, ModalHeader, Stack, TextInput } from '@carbon/react';
|
|
7
|
+
import { showSnackbar, openmrsFetch, restBaseUrl, getCoreTranslation } from '@openmrs/esm-framework';
|
|
8
|
+
|
|
9
|
+
type PaymentModeFormValues = {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface AddPaymentModeModalProps {
|
|
15
|
+
closeModal: () => void;
|
|
16
|
+
onPaymentModeAdded: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const AddPaymentModeModal: React.FC<AddPaymentModeModalProps> = ({ closeModal, onPaymentModeAdded }) => {
|
|
20
|
+
const { t } = useTranslation();
|
|
21
|
+
|
|
22
|
+
const paymentModeSchema = z.object({
|
|
23
|
+
name: z.string().min(1, t('paymentModeNameRequired', 'Payment Mode Name is required')),
|
|
24
|
+
description: z.string().optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
control,
|
|
29
|
+
handleSubmit,
|
|
30
|
+
reset,
|
|
31
|
+
formState: { errors, isSubmitting },
|
|
32
|
+
} = useForm<PaymentModeFormValues>({
|
|
33
|
+
resolver: zodResolver(paymentModeSchema),
|
|
34
|
+
defaultValues: {
|
|
35
|
+
name: '',
|
|
36
|
+
description: '',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const onSubmit = async (data: PaymentModeFormValues) => {
|
|
41
|
+
try {
|
|
42
|
+
await openmrsFetch(`${restBaseUrl}/billing/paymentMode`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
},
|
|
47
|
+
body: {
|
|
48
|
+
name: data.name,
|
|
49
|
+
description: data.description || '',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
showSnackbar({
|
|
54
|
+
title: t('success', 'Success'),
|
|
55
|
+
subtitle: t('paymentModeSaved', 'Payment mode was successfully saved.'),
|
|
56
|
+
kind: 'success',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
closeModal();
|
|
60
|
+
reset({ name: '', description: '' });
|
|
61
|
+
onPaymentModeAdded();
|
|
62
|
+
} catch (err) {
|
|
63
|
+
showSnackbar({
|
|
64
|
+
title: getCoreTranslation('error'),
|
|
65
|
+
subtitle: err?.message || t('errorSavingPaymentMode', 'An error occurred while saving the payment mode.'),
|
|
66
|
+
kind: 'error',
|
|
67
|
+
isLowContrast: false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
<ModalHeader closeModal={closeModal} title={t('addPaymentMode', 'Add Payment Mode')} />
|
|
75
|
+
<Form onSubmit={handleSubmit(onSubmit)}>
|
|
76
|
+
<ModalBody>
|
|
77
|
+
<Stack gap={5}>
|
|
78
|
+
<Controller
|
|
79
|
+
name="name"
|
|
80
|
+
control={control}
|
|
81
|
+
render={({ field }) => (
|
|
82
|
+
<TextInput
|
|
83
|
+
id="payment-mode-name"
|
|
84
|
+
labelText={t('paymentModeNameLabel', 'Payment Mode Name')}
|
|
85
|
+
placeholder={t('paymentModeNamePlaceholder', 'For example, Cash, Credit Card')}
|
|
86
|
+
invalid={!!errors.name}
|
|
87
|
+
invalidText={errors.name?.message}
|
|
88
|
+
{...field}
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
/>
|
|
92
|
+
<Controller
|
|
93
|
+
name="description"
|
|
94
|
+
control={control}
|
|
95
|
+
render={({ field }) => (
|
|
96
|
+
<TextInput
|
|
97
|
+
id="payment-mode-description"
|
|
98
|
+
labelText={t('description', 'Description')}
|
|
99
|
+
placeholder={t('descriptionPlaceholder', 'For example, Used for all cash transactions')}
|
|
100
|
+
invalid={!!errors.description}
|
|
101
|
+
invalidText={errors.description?.message}
|
|
102
|
+
{...field}
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
105
|
+
/>
|
|
106
|
+
</Stack>
|
|
107
|
+
</ModalBody>
|
|
108
|
+
<ModalFooter>
|
|
109
|
+
<Button kind="secondary" onClick={closeModal}>
|
|
110
|
+
{getCoreTranslation('cancel')}
|
|
111
|
+
</Button>
|
|
112
|
+
<Button type="submit" disabled={isSubmitting}>
|
|
113
|
+
{isSubmitting ? t('saving', 'Saving') + '...' : getCoreTranslation('save')}
|
|
114
|
+
</Button>
|
|
115
|
+
</ModalFooter>
|
|
116
|
+
</Form>
|
|
117
|
+
</>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export default AddPaymentModeModal;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
|
|
4
|
+
import { showSnackbar, openmrsFetch, restBaseUrl, getCoreTranslation } from '@openmrs/esm-framework';
|
|
5
|
+
|
|
6
|
+
interface DeletePaymentModeModalProps {
|
|
7
|
+
closeModal: () => void;
|
|
8
|
+
paymentModeUuid: string;
|
|
9
|
+
paymentModeName: string;
|
|
10
|
+
onPaymentModeDeleted: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DeletePaymentModeModal: React.FC<DeletePaymentModeModalProps> = ({
|
|
14
|
+
closeModal,
|
|
15
|
+
paymentModeUuid,
|
|
16
|
+
paymentModeName,
|
|
17
|
+
onPaymentModeDeleted,
|
|
18
|
+
}) => {
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
21
|
+
|
|
22
|
+
const handleDelete = async () => {
|
|
23
|
+
setIsDeleting(true);
|
|
24
|
+
try {
|
|
25
|
+
await openmrsFetch(`${restBaseUrl}/billing/paymentMode/${paymentModeUuid}`, {
|
|
26
|
+
method: 'DELETE',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
showSnackbar({
|
|
30
|
+
title: t('success', 'Success'),
|
|
31
|
+
subtitle: t('paymentModeDeleted', 'Payment mode was successfully deleted.'),
|
|
32
|
+
kind: 'success',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
closeModal();
|
|
36
|
+
onPaymentModeDeleted();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
showSnackbar({
|
|
39
|
+
title: getCoreTranslation('error'),
|
|
40
|
+
subtitle: err?.message || t('errorDeletingPaymentMode', 'An error occurred while deleting the payment mode.'),
|
|
41
|
+
kind: 'error',
|
|
42
|
+
isLowContrast: false,
|
|
43
|
+
});
|
|
44
|
+
} finally {
|
|
45
|
+
setIsDeleting(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<ModalHeader closeModal={closeModal} title={t('deletePaymentMode', 'Delete Payment Mode')} />
|
|
52
|
+
<ModalBody>
|
|
53
|
+
<p>{t('confirmDeleteMessage', 'Are you sure you want to delete this payment mode? Proceed cautiously.')}</p>
|
|
54
|
+
{paymentModeName && (
|
|
55
|
+
<p>
|
|
56
|
+
<strong>{t('paymentModeName', 'Payment Mode Name: {{paymentModeName}}', { paymentModeName })}</strong>
|
|
57
|
+
</p>
|
|
58
|
+
)}
|
|
59
|
+
</ModalBody>
|
|
60
|
+
<ModalFooter>
|
|
61
|
+
<Button kind="secondary" onClick={closeModal}>
|
|
62
|
+
{getCoreTranslation('cancel')}
|
|
63
|
+
</Button>
|
|
64
|
+
<Button kind="danger" onClick={handleDelete} disabled={isDeleting}>
|
|
65
|
+
{isDeleting ? t('deleting', 'Deleting') + '...' : getCoreTranslation('delete')}
|
|
66
|
+
</Button>
|
|
67
|
+
</ModalFooter>
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default DeletePaymentModeModal;
|