@openmrs/esm-billing-app 1.0.2-pre.80 → 1.0.2-pre.802

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.
Files changed (205) hide show
  1. package/.eslintrc +16 -2
  2. package/README.md +54 -9
  3. package/__mocks__/bills.mock.ts +12 -0
  4. package/__mocks__/react-i18next.js +6 -5
  5. package/dist/1119.js +1 -1
  6. package/dist/1146.js +1 -2
  7. package/dist/1146.js.map +1 -1
  8. package/dist/1197.js +1 -1
  9. package/dist/1856.js +1 -0
  10. package/dist/1856.js.map +1 -0
  11. package/dist/2146.js +1 -1
  12. package/dist/2177.js +2 -0
  13. package/dist/2177.js.LICENSE.txt +9 -0
  14. package/dist/2177.js.map +1 -0
  15. package/dist/2524.js +1 -0
  16. package/dist/2524.js.map +1 -0
  17. package/dist/2690.js +1 -1
  18. package/dist/3041.js +1 -0
  19. package/dist/3041.js.map +1 -0
  20. package/dist/3099.js +1 -1
  21. package/dist/3584.js +1 -1
  22. package/dist/4055.js +1 -1
  23. package/dist/4132.js +1 -1
  24. package/dist/4225.js +1 -0
  25. package/dist/4225.js.map +1 -0
  26. package/dist/4300.js +1 -1
  27. package/dist/4335.js +1 -1
  28. package/dist/4618.js +1 -1
  29. package/dist/4652.js +1 -1
  30. package/dist/4724.js +1 -0
  31. package/dist/4724.js.map +1 -0
  32. package/dist/4739.js +1 -1
  33. package/dist/4739.js.map +1 -1
  34. package/dist/4944.js +1 -1
  35. package/dist/5173.js +1 -1
  36. package/dist/5241.js +1 -1
  37. package/dist/5422.js +1 -0
  38. package/dist/5422.js.map +1 -0
  39. package/dist/5442.js +1 -1
  40. package/dist/5661.js +1 -1
  41. package/dist/6022.js +1 -1
  42. package/dist/6468.js +1 -1
  43. package/dist/6540.js +1 -1
  44. package/dist/6540.js.map +1 -1
  45. package/dist/6606.js +1 -0
  46. package/dist/6606.js.map +1 -0
  47. package/dist/6679.js +1 -1
  48. package/dist/6840.js +1 -1
  49. package/dist/6859.js +1 -1
  50. package/dist/7097.js +1 -1
  51. package/dist/7159.js +1 -1
  52. package/dist/723.js +1 -1
  53. package/dist/7452.js +2 -0
  54. package/dist/7452.js.map +1 -0
  55. package/dist/7617.js +1 -1
  56. package/dist/795.js +1 -1
  57. package/dist/8163.js +1 -1
  58. package/dist/8349.js +1 -1
  59. package/dist/8618.js +1 -1
  60. package/dist/890.js +1 -1
  61. package/dist/8930.js +2 -0
  62. package/dist/{6525.js.LICENSE.txt → 8930.js.LICENSE.txt} +16 -4
  63. package/dist/8930.js.map +1 -0
  64. package/dist/9214.js +1 -1
  65. package/dist/942.js +1 -0
  66. package/dist/942.js.map +1 -0
  67. package/dist/9538.js +1 -1
  68. package/dist/9569.js +1 -1
  69. package/dist/961.js +1 -1
  70. package/dist/961.js.map +1 -1
  71. package/dist/986.js +1 -1
  72. package/dist/9879.js +1 -1
  73. package/dist/9895.js +1 -1
  74. package/dist/9900.js +1 -1
  75. package/dist/9913.js +1 -1
  76. package/dist/main.js +1 -1
  77. package/dist/main.js.map +1 -1
  78. package/dist/openmrs-esm-billing-app.js +1 -1
  79. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +368 -262
  80. package/dist/openmrs-esm-billing-app.js.map +1 -1
  81. package/dist/routes.json +1 -1
  82. package/e2e/README.md +19 -18
  83. package/e2e/core/test.ts +1 -1
  84. package/e2e/fixtures/api.ts +1 -1
  85. package/e2e/specs/sample-test.spec.ts +0 -1
  86. package/e2e/support/github/Dockerfile +1 -1
  87. package/package.json +13 -10
  88. package/src/bill-history/bill-history.component.tsx +17 -25
  89. package/src/bill-history/bill-history.scss +4 -94
  90. package/src/bill-history/bill-history.test.tsx +37 -78
  91. package/src/bill-item-actions/bill-item-actions.scss +0 -4
  92. package/src/bill-item-actions/{edit-bill-item.component.tsx → edit-bill-item.modal.tsx} +100 -78
  93. package/src/bill-item-actions/edit-bill-item.test.tsx +116 -31
  94. package/src/billable-services/bill-waiver/bill-selection.component.tsx +2 -2
  95. package/src/billable-services/bill-waiver/patient-bills.component.tsx +3 -3
  96. package/src/billable-services/billable-service.resource.ts +17 -9
  97. package/src/billable-services/billable-services-home.component.tsx +1 -1
  98. package/src/billable-services/billable-services.component.tsx +142 -145
  99. package/src/billable-services/billable-services.scss +3 -0
  100. package/src/billable-services/billable-services.test.tsx +2 -45
  101. package/src/billable-services/cash-point/add-cash-point.modal.tsx +168 -0
  102. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +18 -192
  103. package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
  104. package/src/billable-services/create-edit/add-billable-service.component.tsx +349 -298
  105. package/src/billable-services/create-edit/add-billable-service.scss +5 -6
  106. package/src/billable-services/create-edit/add-billable-service.test.tsx +166 -80
  107. package/src/billable-services/create-edit/edit-billable-service.modal.tsx +51 -0
  108. package/src/billable-services/payment-modes/add-payment-mode.modal.tsx +121 -0
  109. package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +72 -0
  110. package/src/billable-services/payment-modes/payment-modes-config.component.tsx +125 -0
  111. package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -4
  112. package/src/billing-form/billing-checkin-form.component.tsx +2 -3
  113. package/src/billing-form/billing-checkin-form.test.tsx +97 -24
  114. package/src/billing-form/billing-form.component.tsx +216 -269
  115. package/src/billing-form/billing-form.scss +143 -0
  116. package/src/billing.resource.ts +16 -19
  117. package/src/bills-table/bills-table.test.tsx +98 -54
  118. package/src/config-schema.ts +52 -24
  119. package/src/dashboard.meta.ts +4 -2
  120. package/src/helpers/functions.ts +5 -4
  121. package/src/index.ts +17 -6
  122. package/src/invoice/invoice-table.component.tsx +34 -68
  123. package/src/invoice/invoice-table.scss +8 -5
  124. package/src/invoice/invoice-table.test.tsx +273 -62
  125. package/src/invoice/invoice.component.tsx +38 -29
  126. package/src/invoice/invoice.scss +11 -4
  127. package/src/invoice/invoice.test.tsx +324 -120
  128. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +9 -9
  129. package/src/invoice/payments/payment-form/payment-form.component.tsx +43 -34
  130. package/src/invoice/payments/payment-form/payment-form.scss +5 -6
  131. package/src/invoice/payments/payment-form/payment-form.test.tsx +216 -66
  132. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  133. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  134. package/src/invoice/payments/payments.component.tsx +53 -65
  135. package/src/invoice/payments/payments.scss +4 -3
  136. package/src/invoice/payments/payments.test.tsx +282 -0
  137. package/src/invoice/payments/utils.ts +5 -23
  138. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
  139. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  140. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  141. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  142. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
  143. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
  144. package/src/invoice/printable-invoice/printable-invoice.component.tsx +19 -33
  145. package/src/left-panel-link.test.tsx +1 -4
  146. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  147. package/src/modal/require-payment-modal.test.tsx +27 -22
  148. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +17 -18
  149. package/src/routes.json +22 -2
  150. package/src/types/index.ts +26 -17
  151. package/translations/am.json +70 -21
  152. package/translations/ar.json +70 -21
  153. package/translations/ar_SY.json +70 -21
  154. package/translations/bn.json +75 -26
  155. package/translations/de.json +70 -21
  156. package/translations/en.json +69 -21
  157. package/translations/en_US.json +70 -21
  158. package/translations/es.json +70 -21
  159. package/translations/es_MX.json +70 -21
  160. package/translations/fr.json +83 -34
  161. package/translations/he.json +70 -21
  162. package/translations/hi.json +70 -21
  163. package/translations/hi_IN.json +70 -21
  164. package/translations/id.json +70 -21
  165. package/translations/it.json +105 -56
  166. package/translations/ka.json +70 -21
  167. package/translations/km.json +70 -21
  168. package/translations/ku.json +70 -21
  169. package/translations/ky.json +70 -21
  170. package/translations/lg.json +70 -21
  171. package/translations/ne.json +70 -21
  172. package/translations/pl.json +70 -21
  173. package/translations/pt.json +70 -21
  174. package/translations/pt_BR.json +70 -21
  175. package/translations/qu.json +70 -21
  176. package/translations/ro_RO.json +214 -165
  177. package/translations/ru_RU.json +70 -21
  178. package/translations/si.json +70 -21
  179. package/translations/sw.json +70 -21
  180. package/translations/sw_KE.json +70 -21
  181. package/translations/tr.json +70 -21
  182. package/translations/tr_TR.json +70 -21
  183. package/translations/uk.json +70 -21
  184. package/translations/uz.json +70 -21
  185. package/translations/uz@Latn.json +70 -21
  186. package/translations/uz_UZ.json +70 -21
  187. package/translations/vi.json +70 -21
  188. package/translations/zh.json +70 -21
  189. package/translations/zh_CN.json +125 -76
  190. package/dist/1146.js.LICENSE.txt +0 -21
  191. package/dist/2352.js +0 -1
  192. package/dist/2352.js.map +0 -1
  193. package/dist/246.js +0 -1
  194. package/dist/246.js.map +0 -1
  195. package/dist/6525.js +0 -2
  196. package/dist/6525.js.map +0 -1
  197. package/dist/8556.js +0 -2
  198. package/dist/8556.js.map +0 -1
  199. package/dist/8638.js +0 -1
  200. package/dist/8638.js.map +0 -1
  201. package/dist/9968.js +0 -1
  202. package/dist/9968.js.map +0 -1
  203. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  204. package/src/invoice/payments/payments.component.test.tsx +0 -121
  205. /package/dist/{8556.js.LICENSE.txt → 7452.js.LICENSE.txt} +0 -0
@@ -6,12 +6,8 @@
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;
10
+ margin: layout.$spacing-05;
15
11
  }
16
12
 
17
13
  .sectionTitle {
@@ -99,7 +95,7 @@
99
95
 
100
96
  .conceptLabel {
101
97
  @include type.type-style('label-02');
102
- margin: layout.$spacing-05;
98
+ margin-bottom: layout.$spacing-05;
103
99
  }
104
100
 
105
101
  .errorContainer {
@@ -135,3 +131,6 @@
135
131
  font-size: 0.875rem;
136
132
  }
137
133
 
134
+ .serviceNameLabel {
135
+ @include type.type-style('body-compact-02');
136
+ }
@@ -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, navigate } from '@openmrs/esm-framework';
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 = useBillableServices as jest.MockedFunction<typeof useBillableServices>;
14
- const mockUsePaymentModes = usePaymentModes as jest.MockedFunction<typeof usePaymentModes>;
15
- const mockUseServiceTypes = useServiceTypes as jest.MockedFunction<typeof useServiceTypes>;
16
- const mockCreateBillableSerice = createBillableSerice as jest.MockedFunction<typeof createBillableSerice>;
17
- const mockNavigate = navigate as jest.MockedFunction<typeof navigate>;
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
- createBillableSerice: jest.fn(),
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
- xdescribe('AddBillableService', () => {
53
- beforeEach(() => {
54
- jest.resetAllMocks();
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('textbox', { 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
- mockUseBillableServices.mockReturnValue({
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
117
  const formTitle = screen.getByRole('heading', { name: /Add Billable Services/i });
73
118
  expect(formTitle).toBeInTheDocument();
74
119
 
75
- const serviceNameTextInp = screen.getByRole('textbox', { name: /Service Name/i });
76
- expect(serviceNameTextInp).toBeInTheDocument();
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
- const paymentMethodComboBox = screen.getByRole('combobox', { name: /Payment Mode/i });
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
- const priceTextInp = screen.getByRole('textbox', { name: /Price/i });
107
- expect(priceTextInp).toBeInTheDocument();
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: undefined,
129
+ serviceType: 'c9604249-db0a-4d03-b074-fc6bc2fa13e6',
120
130
  servicePrices: [
121
131
  {
122
132
  paymentMode: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74',
123
- price: '01000',
133
+ price: 100,
124
134
  name: 'Cash',
125
135
  },
126
136
  ],
127
137
  serviceStatus: 'ENABLED',
138
+ concept: undefined,
128
139
  });
129
- expect(mockNavigate).toHaveBeenCalledTimes(1);
130
- expect(mockNavigate).toHaveBeenCalledWith({ to: '/openmrs/spa/billable-services' });
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
- mockUseBillableServices.mockReturnValue({
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
- render(<AddBillableService onClose={mockOnClose} />);
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('textbox', { 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('textbox', { 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('textbox', { 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('textbox', { 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;
@@ -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('paymentModeName', 'Payment Mode Name')}
85
+ placeholder={t('paymentModeNamePlaceholder', 'e.g., 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', 'e.g., 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;