@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.
Files changed (214) 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/3717.js +2 -0
  23. package/dist/3717.js.map +1 -0
  24. package/dist/4055.js +1 -1
  25. package/dist/4132.js +1 -1
  26. package/dist/4225.js +1 -0
  27. package/dist/4225.js.map +1 -0
  28. package/dist/4300.js +1 -1
  29. package/dist/4335.js +1 -1
  30. package/dist/4344.js +1 -0
  31. package/dist/4344.js.map +1 -0
  32. package/dist/4618.js +1 -1
  33. package/dist/4652.js +1 -1
  34. package/dist/4724.js +1 -0
  35. package/dist/4724.js.map +1 -0
  36. package/dist/4739.js +1 -1
  37. package/dist/4739.js.map +1 -1
  38. package/dist/4944.js +1 -1
  39. package/dist/5173.js +1 -1
  40. package/dist/5241.js +1 -1
  41. package/dist/5422.js +1 -0
  42. package/dist/5422.js.map +1 -0
  43. package/dist/5442.js +1 -1
  44. package/dist/5661.js +1 -1
  45. package/dist/6022.js +1 -1
  46. package/dist/6295.js +2 -0
  47. package/dist/{6525.js.LICENSE.txt → 6295.js.LICENSE.txt} +16 -4
  48. package/dist/6295.js.map +1 -0
  49. package/dist/6468.js +1 -1
  50. package/dist/6540.js +1 -1
  51. package/dist/6540.js.map +1 -1
  52. package/dist/6606.js +1 -0
  53. package/dist/6606.js.map +1 -0
  54. package/dist/6679.js +1 -1
  55. package/dist/6840.js +1 -1
  56. package/dist/6859.js +1 -1
  57. package/dist/7097.js +1 -1
  58. package/dist/7159.js +1 -1
  59. package/dist/723.js +1 -1
  60. package/dist/7617.js +1 -1
  61. package/dist/795.js +1 -1
  62. package/dist/8163.js +1 -1
  63. package/dist/8349.js +1 -1
  64. package/dist/8618.js +1 -1
  65. package/dist/890.js +1 -1
  66. package/dist/9214.js +1 -1
  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 +388 -282
  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 +20 -28
  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 +21 -5
  92. package/src/bill-item-actions/edit-bill-item.modal.tsx +225 -0
  93. package/src/bill-item-actions/edit-bill-item.test.tsx +214 -40
  94. package/src/billable-services/bill-waiver/bill-selection.component.tsx +5 -5
  95. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +28 -32
  96. package/src/billable-services/bill-waiver/patient-bills.component.tsx +7 -7
  97. package/src/billable-services/bill-waiver/utils.ts +13 -3
  98. package/src/billable-services/billable-service.resource.ts +28 -12
  99. package/src/billable-services/billable-services-home.component.tsx +4 -4
  100. package/src/billable-services/billable-services.component.tsx +149 -148
  101. package/src/billable-services/billable-services.scss +3 -0
  102. package/src/billable-services/billable-services.test.tsx +6 -49
  103. package/src/billable-services/cash-point/add-cash-point.modal.tsx +168 -0
  104. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +19 -193
  105. package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
  106. package/src/billable-services/create-edit/add-billable-service.component.tsx +356 -300
  107. package/src/billable-services/create-edit/add-billable-service.scss +6 -65
  108. package/src/billable-services/create-edit/add-billable-service.test.tsx +167 -81
  109. package/src/billable-services/create-edit/edit-billable-service.modal.tsx +51 -0
  110. package/src/billable-services/dashboard/service-metrics.component.tsx +11 -3
  111. package/src/billable-services/payment-modes/add-payment-mode.modal.tsx +121 -0
  112. package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +72 -0
  113. package/src/billable-services/payment-modes/payment-modes-config.component.tsx +125 -0
  114. package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -4
  115. package/src/billing-dashboard/billing-dashboard.scss +1 -1
  116. package/src/billing-form/billing-checkin-form.component.tsx +21 -17
  117. package/src/billing-form/billing-checkin-form.test.tsx +99 -26
  118. package/src/billing-form/billing-form.component.tsx +222 -292
  119. package/src/billing-form/billing-form.scss +143 -0
  120. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +1 -1
  121. package/src/billing.resource.ts +69 -74
  122. package/src/bills-table/bills-table.component.tsx +3 -3
  123. package/src/bills-table/bills-table.test.tsx +98 -54
  124. package/src/config-schema.ts +52 -24
  125. package/src/dashboard.meta.ts +4 -2
  126. package/src/helpers/functions.ts +5 -4
  127. package/src/index.ts +17 -6
  128. package/src/invoice/invoice-table.component.tsx +36 -70
  129. package/src/invoice/invoice-table.scss +8 -5
  130. package/src/invoice/invoice-table.test.tsx +273 -62
  131. package/src/invoice/invoice.component.tsx +39 -32
  132. package/src/invoice/invoice.scss +11 -4
  133. package/src/invoice/invoice.test.tsx +324 -120
  134. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +9 -9
  135. package/src/invoice/payments/payment-form/payment-form.component.tsx +43 -34
  136. package/src/invoice/payments/payment-form/payment-form.scss +5 -6
  137. package/src/invoice/payments/payment-form/payment-form.test.tsx +216 -66
  138. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  139. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  140. package/src/invoice/payments/payments.component.tsx +55 -67
  141. package/src/invoice/payments/payments.scss +4 -3
  142. package/src/invoice/payments/payments.test.tsx +282 -0
  143. package/src/invoice/payments/utils.ts +15 -27
  144. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
  145. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  146. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  147. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  148. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
  149. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
  150. package/src/invoice/printable-invoice/printable-invoice.component.tsx +20 -34
  151. package/src/left-panel-link.test.tsx +1 -4
  152. package/src/metrics-cards/metrics-cards.component.tsx +12 -2
  153. package/src/metrics-cards/metrics-cards.scss +4 -0
  154. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  155. package/src/modal/require-payment-modal.test.tsx +27 -22
  156. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +18 -19
  157. package/src/routes.json +25 -7
  158. package/src/types/index.ts +80 -18
  159. package/translations/am.json +125 -74
  160. package/translations/ar.json +126 -75
  161. package/translations/ar_SY.json +126 -75
  162. package/translations/bn.json +128 -77
  163. package/translations/de.json +126 -75
  164. package/translations/en.json +126 -75
  165. package/translations/en_US.json +126 -75
  166. package/translations/es.json +125 -74
  167. package/translations/es_MX.json +126 -75
  168. package/translations/fr.json +131 -80
  169. package/translations/he.json +125 -74
  170. package/translations/hi.json +126 -75
  171. package/translations/hi_IN.json +126 -75
  172. package/translations/id.json +126 -75
  173. package/translations/it.json +152 -101
  174. package/translations/ka.json +126 -75
  175. package/translations/km.json +125 -74
  176. package/translations/ku.json +126 -75
  177. package/translations/ky.json +126 -75
  178. package/translations/lg.json +126 -75
  179. package/translations/ne.json +126 -75
  180. package/translations/pl.json +126 -75
  181. package/translations/pt.json +126 -75
  182. package/translations/pt_BR.json +126 -75
  183. package/translations/qu.json +126 -75
  184. package/translations/ro_RO.json +216 -165
  185. package/translations/ru_RU.json +126 -75
  186. package/translations/si.json +126 -75
  187. package/translations/sw.json +126 -75
  188. package/translations/sw_KE.json +126 -75
  189. package/translations/tr.json +126 -75
  190. package/translations/tr_TR.json +126 -75
  191. package/translations/uk.json +126 -75
  192. package/translations/uz.json +126 -75
  193. package/translations/uz@Latn.json +126 -75
  194. package/translations/uz_UZ.json +126 -75
  195. package/translations/vi.json +126 -75
  196. package/translations/zh.json +126 -75
  197. package/translations/zh_CN.json +158 -107
  198. package/dist/1146.js.LICENSE.txt +0 -21
  199. package/dist/2352.js +0 -1
  200. package/dist/2352.js.map +0 -1
  201. package/dist/246.js +0 -1
  202. package/dist/246.js.map +0 -1
  203. package/dist/6525.js +0 -2
  204. package/dist/6525.js.map +0 -1
  205. package/dist/8556.js +0 -2
  206. package/dist/8556.js.map +0 -1
  207. package/dist/8638.js +0 -1
  208. package/dist/8638.js.map +0 -1
  209. package/dist/9968.js +0 -1
  210. package/dist/9968.js.map +0 -1
  211. package/src/bill-item-actions/edit-bill-item.component.tsx +0 -221
  212. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  213. package/src/invoice/payments/payments.component.test.tsx +0 -121
  214. /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.$layout-01 0;
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-top: 0.5rem;
67
+ margin-bottom: layout.$spacing-05;
125
68
  }
126
69
 
127
- .spinner {
70
+ .loader {
128
71
  &:global(.cds--inline-loading) {
129
72
  min-height: layout.$spacing-05;
130
73
  }
131
74
  }
132
75
 
133
- .errorMessage {
134
- color: red;
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, 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('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
- 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
- const formTitle = screen.getByRole('heading', { name: /Add Billable Services/i });
117
+ const formTitle = screen.getByRole('heading', { name: /Add billable service/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('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 status="active" iconDescription="Loading" description="Loading service metrics..." />
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 <ErrorState headerTitle={t('serviceMetrics', 'Service Metrics')} error={error} />;
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;