@openmrs/esm-billing-app 1.0.2-pre.880 → 1.0.2-pre.889

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 (102) hide show
  1. package/dist/1119.js +1 -1
  2. package/dist/1197.js +1 -1
  3. package/dist/1537.js +1 -0
  4. package/dist/1537.js.map +1 -0
  5. package/dist/2146.js +1 -1
  6. package/dist/2690.js +1 -1
  7. package/dist/3099.js +1 -1
  8. package/dist/3584.js +1 -1
  9. package/dist/4055.js +1 -1
  10. package/dist/4132.js +1 -1
  11. package/dist/4300.js +1 -1
  12. package/dist/4335.js +1 -1
  13. package/dist/4618.js +1 -1
  14. package/dist/4652.js +1 -1
  15. package/dist/{2372.js → 4692.js} +1 -1
  16. package/dist/4692.js.map +1 -0
  17. package/dist/4944.js +1 -1
  18. package/dist/5173.js +1 -1
  19. package/dist/5241.js +1 -1
  20. package/dist/5442.js +1 -1
  21. package/dist/5661.js +1 -1
  22. package/dist/6022.js +1 -1
  23. package/dist/6468.js +1 -1
  24. package/dist/6679.js +1 -1
  25. package/dist/6840.js +1 -1
  26. package/dist/6859.js +1 -1
  27. package/dist/7097.js +1 -1
  28. package/dist/7159.js +1 -1
  29. package/dist/723.js +1 -1
  30. package/dist/7617.js +1 -1
  31. package/dist/795.js +1 -1
  32. package/dist/8163.js +1 -1
  33. package/dist/8349.js +1 -1
  34. package/dist/8618.js +1 -1
  35. package/dist/890.js +1 -1
  36. package/dist/9214.js +1 -1
  37. package/dist/9538.js +1 -1
  38. package/dist/9569.js +1 -1
  39. package/dist/986.js +1 -1
  40. package/dist/9879.js +1 -1
  41. package/dist/9895.js +1 -1
  42. package/dist/9900.js +1 -1
  43. package/dist/9913.js +1 -1
  44. package/dist/main.js +1 -1
  45. package/dist/main.js.map +1 -1
  46. package/dist/openmrs-esm-billing-app.js +1 -1
  47. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +169 -145
  48. package/dist/routes.json +1 -1
  49. package/package.json +2 -2
  50. package/src/billable-services/{create-edit/add-billable-service.scss → billable-service-form/billable-service-form.scss} +30 -1
  51. package/src/billable-services/{create-edit/add-billable-service.test.tsx → billable-service-form/billable-service-form.test.tsx} +178 -82
  52. package/src/billable-services/{create-edit/add-billable-service.component.tsx → billable-service-form/billable-service-form.workspace.tsx} +63 -47
  53. package/src/billable-services/billable-services-home.component.tsx +2 -8
  54. package/src/billable-services/billable-services-left-panel-menu.component.tsx +1 -1
  55. package/src/billable-services/billable-services-menu-item/item.component.tsx +5 -4
  56. package/src/billable-services/billable-services.component.tsx +14 -11
  57. package/src/billable-services/cash-point/add-cash-point.modal.tsx +47 -45
  58. package/src/billable-services-admin-card-link.component.test.tsx +2 -2
  59. package/src/billable-services-admin-card-link.component.tsx +1 -1
  60. package/src/index.ts +8 -4
  61. package/src/routes.json +7 -4
  62. package/translations/am.json +7 -2
  63. package/translations/ar.json +7 -2
  64. package/translations/ar_SY.json +7 -2
  65. package/translations/bn.json +7 -2
  66. package/translations/de.json +7 -2
  67. package/translations/en.json +7 -2
  68. package/translations/en_US.json +7 -2
  69. package/translations/es.json +7 -2
  70. package/translations/es_MX.json +7 -2
  71. package/translations/fr.json +7 -2
  72. package/translations/he.json +7 -2
  73. package/translations/hi.json +7 -2
  74. package/translations/hi_IN.json +7 -2
  75. package/translations/id.json +7 -2
  76. package/translations/it.json +7 -2
  77. package/translations/ka.json +7 -2
  78. package/translations/km.json +7 -2
  79. package/translations/ku.json +7 -2
  80. package/translations/ky.json +7 -2
  81. package/translations/lg.json +7 -2
  82. package/translations/ne.json +7 -2
  83. package/translations/pl.json +7 -2
  84. package/translations/pt.json +7 -2
  85. package/translations/pt_BR.json +7 -2
  86. package/translations/qu.json +7 -2
  87. package/translations/ro_RO.json +7 -2
  88. package/translations/ru_RU.json +7 -2
  89. package/translations/si.json +7 -2
  90. package/translations/sw.json +7 -2
  91. package/translations/sw_KE.json +7 -2
  92. package/translations/tr.json +7 -2
  93. package/translations/tr_TR.json +7 -2
  94. package/translations/uk.json +7 -2
  95. package/translations/uz.json +7 -2
  96. package/translations/uz@Latn.json +7 -2
  97. package/translations/uz_UZ.json +7 -2
  98. package/translations/vi.json +7 -2
  99. package/translations/zh.json +7 -2
  100. package/translations/zh_CN.json +7 -2
  101. package/dist/2372.js.map +0 -1
  102. package/src/billable-services/create-edit/edit-billable-service.modal.tsx +0 -51
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.24.0","fhir2":">=1.2"},"pages":[{"component":"billableServicesHome","route":"billable-services"}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"},"featureFlag":"billing"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing history"},"featureFlag":"billing"},{"name":"billable-services-app-menu-item","component":"billableServicesAppMenuItem","slot":"app-menu-item-slot","meta":{"name":"Billable Services"}},{"name":"billing-checkin-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm","featureFlag":"billing"},{"slot":"system-admin-page-card-link-slot","component":"billableServicesCardLink","name":"billable-services-admin-card-link"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"billable-services-left-panel-link","component":"billableServicesLeftPanelLink","slot":"billable-services-left-panel-slot","order":0},{"name":"bill-waiver-left-panel-link","component":"billWaiverLeftPanelLink","slot":"billable-services-left-panel-slot","order":1},{"name":"billing-settings-left-panel-menu","component":"billingSettingsLeftPanelMenu","slot":"billable-services-left-panel-slot","order":2}],"modals":[{"name":"add-cash-point-modal","component":"addCashPointModal"},{"name":"add-payment-mode-modal","component":"addPaymentModeModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"edit-bill-item-modal","component":"editBillLineItemModal"},{"name":"edit-bill-line-item-modal","component":"editBillLineItemModal"},{"name":"edit-billable-service-modal","component":"editBillableServiceModal"},{"name":"require-billing-modal","component":"requirePaymentModal"}],"workspaces":[{"name":"billing-form-workspace","title":"billingForm","component":"billingFormWorkspace","type":"form"}],"featureFlags":[{"flagName":"billing","label":"Billing module","description":"This feature introduces navigation links on the patient chart and home page to allow accessing the billing module features"}],"version":"1.0.2-pre.880"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.24.0","fhir2":">=1.2"},"pages":[{"component":"billableServicesHome","route":"billable-services"}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"},"featureFlag":"billing"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing history"},"featureFlag":"billing"},{"name":"billable-services-app-menu-item","component":"billableServicesAppMenuItem","slot":"app-menu-item-slot","meta":{"name":"Billable Services"}},{"name":"billing-checkin-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm","featureFlag":"billing"},{"slot":"system-admin-page-card-link-slot","component":"billableServicesCardLink","name":"billable-services-admin-card-link"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"billable-services-left-panel-link","component":"billableServicesLeftPanelLink","slot":"billable-services-left-panel-slot","order":0},{"name":"bill-waiver-left-panel-link","component":"billWaiverLeftPanelLink","slot":"billable-services-left-panel-slot","order":1},{"name":"billing-settings-left-panel-menu","component":"billingSettingsLeftPanelMenu","slot":"billable-services-left-panel-slot","order":2}],"modals":[{"name":"add-cash-point-modal","component":"addCashPointModal"},{"name":"add-payment-mode-modal","component":"addPaymentModeModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"edit-bill-item-modal","component":"editBillLineItemModal"},{"name":"edit-bill-line-item-modal","component":"editBillLineItemModal"},{"name":"require-billing-modal","component":"requirePaymentModal"}],"workspaces":[{"name":"billing-form-workspace","title":"billingForm","component":"billingFormWorkspace","type":"form"},{"name":"billable-service-form","title":"billableServiceForm","component":"billableServiceFormWorkspace","type":"form","width":"wider"}],"featureFlags":[{"flagName":"billing","label":"Billing module","description":"This feature introduces navigation links on the patient chart and home page to allow accessing the billing module features"}],"version":"1.0.2-pre.889"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-billing-app",
3
- "version": "1.0.2-pre.880",
3
+ "version": "1.0.2-pre.889",
4
4
  "description": "O3 frontend module for handling billing concerns in healthcare settings",
5
5
  "browser": "dist/openmrs-esm-billing-app.js",
6
6
  "main": "src/index.ts",
@@ -13,7 +13,7 @@
13
13
  "build": "webpack --mode production",
14
14
  "coverage": "yarn test --coverage",
15
15
  "debug": "npm run serve",
16
- "extract-translations": "i18next 'src/**/*.component.tsx' 'src/**/*.modal.tsx' 'src/index.ts' --config ./tools/i18next-parser.config.js",
16
+ "extract-translations": "i18next 'src/**/*.component.tsx' 'src/**/*.workspace.tsx' 'src/**/*.modal.tsx' 'src/index.ts' --config ./tools/i18next-parser.config.js",
17
17
  "lint": "eslint src --ext ts,tsx --max-warnings=0",
18
18
  "postinstall": "husky install",
19
19
  "prettier": "prettier --config prettier.config.js --write \"src/**/*.{ts,tsx,css,scss}\" \"e2e/**/*.ts\"",
@@ -6,8 +6,8 @@
6
6
  .form {
7
7
  display: flex;
8
8
  flex-direction: column;
9
+ justify-content: space-between;
9
10
  height: 100%;
10
- margin: layout.$spacing-05;
11
11
  }
12
12
 
13
13
  .paymentButtons {
@@ -74,3 +74,32 @@
74
74
  .serviceNameLabel {
75
75
  @include type.type-style('body-compact-02');
76
76
  }
77
+
78
+ .button {
79
+ height: layout.$spacing-10;
80
+ display: flex;
81
+ align-content: flex-start;
82
+ align-items: baseline;
83
+ min-width: 50%;
84
+
85
+ :global(.cds--inline-loading) {
86
+ min-height: layout.$spacing-05 !important;
87
+ }
88
+
89
+ :global(.cds--inline-loading__text) {
90
+ @include type.type-style('body-01');
91
+ }
92
+ }
93
+
94
+ .tablet {
95
+ padding: layout.$spacing-06 layout.$spacing-05;
96
+ background-color: $ui-02;
97
+ }
98
+
99
+ .desktop {
100
+ padding: 0;
101
+ }
102
+
103
+ .stack {
104
+ margin: layout.$spacing-05;
105
+ }
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import userEvent from '@testing-library/user-event';
3
- import { render, screen } from '@testing-library/react';
4
- import { navigate, type FetchResponse } from '@openmrs/esm-framework';
3
+ import { render, screen, fireEvent } from '@testing-library/react';
4
+ import { type FetchResponse } from '@openmrs/esm-framework';
5
5
  import {
6
6
  createBillableService,
7
7
  updateBillableService,
@@ -10,7 +10,10 @@ import {
10
10
  usePaymentModes,
11
11
  useServiceTypes,
12
12
  } from '../billable-service.resource';
13
- import AddBillableService, { transformServiceToFormData, normalizePrice } from './add-billable-service.component';
13
+ import BillableServiceFormWorkspace, {
14
+ transformServiceToFormData,
15
+ normalizePrice,
16
+ } from './billable-service-form.workspace';
14
17
  import type { BillableService } from '../../types';
15
18
 
16
19
  const mockUseBillableServices = jest.mocked(useBillableServices);
@@ -55,7 +58,7 @@ const mockServiceTypes = [
55
58
  { uuid: 'a487a743-62ce-4f93-a66b-c5154ee8987d', display: 'Adherence counselling service' },
56
59
  ];
57
60
 
58
- // Test helpers (canonical pattern)
61
+ // Test helpers
59
62
  const setupMocks = () => {
60
63
  mockUseBillableServices.mockReturnValue({
61
64
  billableServices: [],
@@ -69,13 +72,14 @@ const setupMocks = () => {
69
72
  mockUseConceptsSearch.mockReturnValue({ searchResults: [], isSearching: false, error: null });
70
73
  };
71
74
 
72
- const renderAddBillableService = (props = {}) => {
75
+ const renderBillableServicesForm = (props = {}) => {
73
76
  const defaultProps = {
74
- onClose: jest.fn(),
77
+ closeWorkspace: jest.fn(),
78
+ closeWorkspaceWithSavedChanges: jest.fn(),
75
79
  ...props,
76
80
  };
77
81
  setupMocks();
78
- return render(<AddBillableService {...defaultProps} />);
82
+ return render(<BillableServiceFormWorkspace {...defaultProps} />);
79
83
  };
80
84
 
81
85
  interface FillOptions {
@@ -106,24 +110,22 @@ const fillRequiredFields = async (user, options: FillOptions = {}) => {
106
110
  }
107
111
  };
108
112
 
109
- const submitForm = async (user) => {
110
- const saveBtn = screen.getByRole('button', { name: /save/i });
111
- await user.click(saveBtn);
113
+ const submitForm = async () => {
114
+ const user = userEvent.setup();
115
+ const saveButton = screen.getByRole('button', { name: /save/i });
116
+ await user.click(saveButton);
112
117
  };
113
118
 
114
- describe('AddBillableService', () => {
119
+ describe('BillableServiceFormWorkspace', () => {
115
120
  test('should render billable services form and generate correct payload', async () => {
116
121
  const user = userEvent.setup();
117
- const mockOnClose = jest.fn();
118
- renderAddBillableService({ onClose: mockOnClose });
119
-
120
- const formTitle = screen.getByRole('heading', { name: /Add billable service/i });
121
- expect(formTitle).toBeInTheDocument();
122
+ const mockCloseWorkspace = jest.fn();
123
+ renderBillableServicesForm({ closeWorkspace: mockCloseWorkspace });
122
124
 
123
125
  await fillRequiredFields(user);
124
126
  mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
125
127
 
126
- await submitForm(user);
128
+ await submitForm();
127
129
 
128
130
  expect(mockCreateBillableService).toHaveBeenCalledTimes(1);
129
131
  expect(mockCreateBillableService).toHaveBeenCalledWith({
@@ -140,25 +142,125 @@ describe('AddBillableService', () => {
140
142
  serviceStatus: 'ENABLED',
141
143
  concept: undefined,
142
144
  });
143
- expect(navigate).toHaveBeenCalledTimes(1);
144
- expect(navigate).toHaveBeenCalledWith({ to: '/openmrs/spa/billable-services' });
145
145
  });
146
146
 
147
- test("should navigate back to billable services dashboard when 'Cancel' button is clicked", async () => {
148
- const user = userEvent.setup();
149
- const mockOnClose = jest.fn();
150
- renderAddBillableService({ onClose: mockOnClose });
147
+ describe('Workspace Interactions', () => {
148
+ test('should call closeWorkspace when Cancel button is clicked', async () => {
149
+ const user = userEvent.setup();
150
+ const mockCloseWorkspace = jest.fn();
151
+ renderBillableServicesForm({ closeWorkspace: mockCloseWorkspace });
152
+
153
+ const cancelButton = screen.getByRole('button', { name: /cancel/i });
154
+ await user.click(cancelButton);
155
+
156
+ expect(mockCloseWorkspace).toHaveBeenCalledTimes(1);
157
+ });
158
+
159
+ test('should call closeWorkspaceWithSavedChanges after successful save', async () => {
160
+ const user = userEvent.setup();
161
+ const mockCloseWorkspaceWithSavedChanges = jest.fn();
162
+ renderBillableServicesForm({ closeWorkspaceWithSavedChanges: mockCloseWorkspaceWithSavedChanges });
163
+
164
+ await fillRequiredFields(user);
165
+ mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
166
+ await submitForm();
167
+
168
+ // Wait for async submission
169
+ await new Promise((resolve) => setTimeout(resolve, 100));
170
+
171
+ expect(mockCloseWorkspaceWithSavedChanges).toHaveBeenCalledTimes(1);
172
+ });
173
+
174
+ test('should disable buttons during submission', async () => {
175
+ const user = userEvent.setup();
176
+ let resolveCreate: (value: any) => void;
177
+ const createPromise = new Promise((resolve) => {
178
+ resolveCreate = resolve;
179
+ });
180
+ mockCreateBillableService.mockReturnValue(createPromise as any);
181
+
182
+ renderBillableServicesForm();
183
+
184
+ await fillRequiredFields(user);
185
+ const saveButton = screen.getByRole('button', { name: /save/i });
186
+ const cancelButton = screen.getByRole('button', { name: /cancel/i });
187
+
188
+ // Click save to trigger submission
189
+ await user.click(saveButton);
190
+
191
+ // Buttons should be disabled during submission
192
+ expect(saveButton).toBeDisabled();
193
+ expect(cancelButton).toBeDisabled();
194
+
195
+ // Resolve the promise to complete submission
196
+ resolveCreate!({} as FetchResponse<any>);
197
+ await new Promise((resolve) => setTimeout(resolve, 100));
198
+ });
199
+
200
+ test('should show loading indicator in save button during submission', async () => {
201
+ const user = userEvent.setup();
202
+ let resolveCreate: (value: any) => void;
203
+ const createPromise = new Promise((resolve) => {
204
+ resolveCreate = resolve;
205
+ });
206
+ mockCreateBillableService.mockReturnValue(createPromise as any);
207
+
208
+ renderBillableServicesForm();
209
+
210
+ await fillRequiredFields(user);
211
+ const saveButton = screen.getByRole('button', { name: /save/i });
212
+
213
+ await user.click(saveButton);
151
214
 
152
- const cancelBtn = screen.getByRole('button', { name: /cancel/i });
153
- await user.click(cancelBtn);
215
+ // Should show loading indicator
216
+ expect(await screen.findByText(/saving/i)).toBeInTheDocument();
154
217
 
155
- expect(mockOnClose).toHaveBeenCalledTimes(1);
218
+ // Resolve the promise
219
+ resolveCreate!({} as FetchResponse<any>);
220
+ await new Promise((resolve) => setTimeout(resolve, 100));
221
+ });
222
+
223
+ test('should call onWorkspaceClose callback after successful edit', async () => {
224
+ const mockOnWorkspaceClose = jest.fn();
225
+ const mockServiceToEdit: BillableService = {
226
+ uuid: 'test-uuid',
227
+ name: 'Test Service',
228
+ shortName: 'TS',
229
+ serviceStatus: 'ENABLED',
230
+ serviceType: {
231
+ uuid: 'type-uuid',
232
+ display: 'Lab service',
233
+ },
234
+ concept: null,
235
+ servicePrices: [
236
+ {
237
+ uuid: 'price-uuid',
238
+ name: 'Cash',
239
+ price: 100,
240
+ paymentMode: {
241
+ uuid: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74',
242
+ name: 'Cash',
243
+ },
244
+ },
245
+ ],
246
+ };
247
+
248
+ renderBillableServicesForm({ serviceToEdit: mockServiceToEdit, onWorkspaceClose: mockOnWorkspaceClose });
249
+
250
+ mockUpdateBillableService.mockResolvedValue({} as FetchResponse<any>);
251
+ await submitForm();
252
+
253
+ // Wait for async submission
254
+ await new Promise((resolve) => setTimeout(resolve, 100));
255
+
256
+ expect(mockOnWorkspaceClose).toHaveBeenCalledTimes(1);
257
+ });
156
258
  });
157
259
 
158
260
  describe('Form Validation', () => {
159
261
  test('should accept form submission without short name (short name is optional)', async () => {
160
262
  const user = userEvent.setup();
161
- renderAddBillableService();
263
+ renderBillableServicesForm();
162
264
 
163
265
  // Fill required fields but skip short name
164
266
  await user.type(screen.getByRole('textbox', { name: /Service name/i }), 'Lab Test');
@@ -174,7 +276,7 @@ describe('AddBillableService', () => {
174
276
 
175
277
  mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
176
278
 
177
- await submitForm(user);
279
+ await submitForm();
178
280
 
179
281
  expect(mockCreateBillableService).toHaveBeenCalledWith(
180
282
  expect.objectContaining({
@@ -186,7 +288,7 @@ describe('AddBillableService', () => {
186
288
 
187
289
  test('should enforce 255 character limit on service name input', async () => {
188
290
  const user = userEvent.setup();
189
- renderAddBillableService();
291
+ renderBillableServicesForm();
190
292
 
191
293
  const longName = 'A'.repeat(300); // Try to type 300 characters
192
294
  const input = screen.getByRole('textbox', { name: /Service name/i });
@@ -198,7 +300,7 @@ describe('AddBillableService', () => {
198
300
 
199
301
  test('should enforce 255 character limit on short name input', async () => {
200
302
  const user = userEvent.setup();
201
- renderAddBillableService();
303
+ renderBillableServicesForm();
202
304
 
203
305
  const longShortName = 'B'.repeat(300); // Try to type 300 characters
204
306
  const input = screen.getByRole('textbox', { name: /Short name/i });
@@ -210,14 +312,14 @@ describe('AddBillableService', () => {
210
312
 
211
313
  test('should show "Price must be greater than 0" error for zero price', async () => {
212
314
  const user = userEvent.setup();
213
- renderAddBillableService();
315
+ renderBillableServicesForm();
214
316
 
215
317
  await fillRequiredFields(user, { skipPrice: true });
216
318
 
217
319
  const priceInput = screen.getByRole('spinbutton', { name: /selling price/i });
218
320
  await user.type(priceInput, '0');
219
321
 
220
- await submitForm(user);
322
+ await submitForm();
221
323
 
222
324
  expect(screen.getByText('Price must be greater than 0')).toBeInTheDocument();
223
325
  expect(mockCreateBillableService).not.toHaveBeenCalled();
@@ -225,14 +327,14 @@ describe('AddBillableService', () => {
225
327
 
226
328
  test('should show "Price must be greater than 0" error for negative price', async () => {
227
329
  const user = userEvent.setup();
228
- renderAddBillableService();
330
+ renderBillableServicesForm();
229
331
 
230
332
  await fillRequiredFields(user, { skipPrice: true });
231
333
 
232
334
  const priceInput = screen.getByRole('spinbutton', { name: /Selling Price/i });
233
335
  await user.type(priceInput, '-10');
234
336
 
235
- await submitForm(user);
337
+ await submitForm();
236
338
 
237
339
  expect(screen.getByText('Price must be greater than 0')).toBeInTheDocument();
238
340
  expect(mockCreateBillableService).not.toHaveBeenCalled();
@@ -240,7 +342,7 @@ describe('AddBillableService', () => {
240
342
 
241
343
  test('should show "Service name is required" error when service name is empty', async () => {
242
344
  const user = userEvent.setup();
243
- renderAddBillableService();
345
+ renderBillableServicesForm();
244
346
 
245
347
  // Fill all fields except service name
246
348
  await user.type(screen.getByRole('textbox', { name: /Short name/i }), 'Test Short Name');
@@ -254,15 +356,15 @@ describe('AddBillableService', () => {
254
356
  const priceInput = screen.getByRole('spinbutton', { name: /Selling Price/i });
255
357
  await user.type(priceInput, '100');
256
358
 
257
- await submitForm(user);
359
+ await submitForm();
258
360
 
259
- expect(screen.getByText('Service name is required')).toBeInTheDocument();
361
+ expect(await screen.findByText('Service name is required')).toBeInTheDocument();
260
362
  expect(mockCreateBillableService).not.toHaveBeenCalled();
261
363
  });
262
364
 
263
365
  test('should accept valid decimal price values', async () => {
264
366
  const user = userEvent.setup();
265
- renderAddBillableService();
367
+ renderBillableServicesForm();
266
368
 
267
369
  await fillRequiredFields(user, { skipPrice: true });
268
370
 
@@ -271,7 +373,7 @@ describe('AddBillableService', () => {
271
373
 
272
374
  mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
273
375
 
274
- await submitForm(user);
376
+ await submitForm();
275
377
 
276
378
  expect(screen.queryByText('Price is required')).not.toBeInTheDocument();
277
379
  expect(screen.queryByText('Price must be greater than 0')).not.toBeInTheDocument();
@@ -294,7 +396,7 @@ describe('AddBillableService', () => {
294
396
 
295
397
  test('should show "Service type is required" error when not selected', async () => {
296
398
  const user = userEvent.setup();
297
- renderAddBillableService();
399
+ renderBillableServicesForm();
298
400
 
299
401
  await user.type(screen.getByRole('textbox', { name: /Service name/i }), 'Test Service');
300
402
  await user.type(screen.getByRole('textbox', { name: /Short name/i }), 'Test Short Name');
@@ -305,15 +407,15 @@ describe('AddBillableService', () => {
305
407
  const priceInput = screen.getByRole('spinbutton', { name: /Selling Price/i });
306
408
  await user.type(priceInput, '100');
307
409
 
308
- await submitForm(user);
410
+ await submitForm();
309
411
 
310
- expect(screen.getByText('Service type is required')).toBeInTheDocument();
412
+ expect(await screen.findByText('Service type is required')).toBeInTheDocument();
311
413
  expect(mockCreateBillableService).not.toHaveBeenCalled();
312
414
  });
313
415
 
314
416
  test('should show "Payment mode is required" error when not selected', async () => {
315
417
  const user = userEvent.setup();
316
- renderAddBillableService();
418
+ renderBillableServicesForm();
317
419
 
318
420
  await user.type(screen.getByRole('textbox', { name: /Service name/i }), 'Test Service');
319
421
  await user.type(screen.getByRole('textbox', { name: /Short name/i }), 'Test Short Name');
@@ -324,21 +426,21 @@ describe('AddBillableService', () => {
324
426
  const priceInput = screen.getByRole('spinbutton', { name: /Selling Price/i });
325
427
  await user.type(priceInput, '100');
326
428
 
327
- await submitForm(user);
429
+ await submitForm();
328
430
 
329
- expect(screen.getByText('Payment mode is required')).toBeInTheDocument();
431
+ expect(await screen.findByText('Payment mode is required')).toBeInTheDocument();
330
432
  expect(mockCreateBillableService).not.toHaveBeenCalled();
331
433
  });
332
434
 
333
435
  test('should show "Price is required" error when price field is empty', async () => {
334
436
  const user = userEvent.setup();
335
- renderAddBillableService();
437
+ renderBillableServicesForm();
336
438
 
337
439
  await fillRequiredFields(user, { skipPrice: true });
338
440
 
339
- await submitForm(user);
441
+ await submitForm();
340
442
 
341
- expect(screen.getByText('Price is required')).toBeInTheDocument();
443
+ expect(await screen.findByText('Price is required')).toBeInTheDocument();
342
444
  expect(mockCreateBillableService).not.toHaveBeenCalled();
343
445
  });
344
446
  });
@@ -368,17 +470,16 @@ describe('AddBillableService', () => {
368
470
  };
369
471
 
370
472
  test('should populate form with existing service data', () => {
371
- renderAddBillableService({ serviceToEdit: mockServiceToEdit });
473
+ renderBillableServicesForm({ serviceToEdit: mockServiceToEdit });
372
474
 
373
- expect(screen.getByText('Edit billable service')).toBeInTheDocument();
374
475
  expect(screen.getByText('X-Ray Service')).toBeInTheDocument(); // Service name shown as label
375
476
  expect(screen.getByDisplayValue('XRay')).toBeInTheDocument(); // Short name
376
477
  });
377
478
 
378
479
  test('should call updateBillableService instead of createBillableService', async () => {
379
480
  const user = userEvent.setup();
380
- const mockOnClose = jest.fn();
381
- renderAddBillableService({ serviceToEdit: mockServiceToEdit, onClose: mockOnClose });
481
+ const mockCloseWorkspace = jest.fn();
482
+ renderBillableServicesForm({ serviceToEdit: mockServiceToEdit, closeWorkspace: mockCloseWorkspace });
382
483
 
383
484
  const shortNameInput = screen.getByDisplayValue('XRay');
384
485
  await user.clear(shortNameInput);
@@ -386,7 +487,7 @@ describe('AddBillableService', () => {
386
487
 
387
488
  mockUpdateBillableService.mockResolvedValue({} as FetchResponse<any>);
388
489
 
389
- await submitForm(user);
490
+ await submitForm();
390
491
 
391
492
  expect(mockUpdateBillableService).toHaveBeenCalledTimes(1);
392
493
  expect(mockUpdateBillableService).toHaveBeenCalledWith('existing-service-uuid', {
@@ -404,23 +505,21 @@ describe('AddBillableService', () => {
404
505
  concept: undefined,
405
506
  });
406
507
  expect(mockCreateBillableService).not.toHaveBeenCalled();
407
- expect(mockOnClose).toHaveBeenCalledTimes(1);
408
508
  });
409
509
 
410
- test('should call onServiceUpdated callback after successful edit', async () => {
411
- const user = userEvent.setup();
412
- const mockOnServiceUpdated = jest.fn();
413
- renderAddBillableService({ serviceToEdit: mockServiceToEdit, onServiceUpdated: mockOnServiceUpdated });
510
+ test('should call onWorkspaceClose callback after successful edit', async () => {
511
+ const mockOnWorkspaceClose = jest.fn();
512
+ renderBillableServicesForm({ serviceToEdit: mockServiceToEdit, onWorkspaceClose: mockOnWorkspaceClose });
414
513
 
415
514
  mockUpdateBillableService.mockResolvedValue({} as FetchResponse<any>);
416
515
 
417
- await submitForm(user);
516
+ await submitForm();
418
517
 
419
- expect(mockOnServiceUpdated).toHaveBeenCalledTimes(1);
518
+ expect(mockOnWorkspaceClose).toHaveBeenCalledTimes(1);
420
519
  });
421
520
 
422
521
  test('should not allow editing service name in edit mode', () => {
423
- renderAddBillableService({ serviceToEdit: mockServiceToEdit });
522
+ renderBillableServicesForm({ serviceToEdit: mockServiceToEdit });
424
523
 
425
524
  // Service name should be displayed as a label, not an editable input
426
525
  expect(screen.getByText('X-Ray Service')).toBeInTheDocument();
@@ -431,12 +530,11 @@ describe('AddBillableService', () => {
431
530
  // Scenario: User opens edit form, but payment modes/service types haven't loaded yet
432
531
  // The form should wait for dependencies to load, then populate correctly
433
532
 
434
- renderAddBillableService({ serviceToEdit: mockServiceToEdit });
533
+ renderBillableServicesForm({ serviceToEdit: mockServiceToEdit });
435
534
 
436
- // After dependencies load (handled by renderAddBillableService's setupMocks),
535
+ // After dependencies load (handled by renderBillableServicesForm's setupMocks),
437
536
  // form should display with populated data
438
- await screen.findByText('Edit billable service');
439
- expect(screen.getByText('X-Ray Service')).toBeInTheDocument();
537
+ expect(await screen.findByText('X-Ray Service')).toBeInTheDocument();
440
538
  expect(screen.getByDisplayValue('XRay')).toBeInTheDocument();
441
539
 
442
540
  // This test verifies the useEffect that calls reset() when dependencies load
@@ -448,7 +546,7 @@ describe('AddBillableService', () => {
448
546
  describe('Dynamic Payment Options', () => {
449
547
  test('should add new payment option when clicking "Add payment option" button', async () => {
450
548
  const user = userEvent.setup();
451
- renderAddBillableService();
549
+ renderBillableServicesForm();
452
550
 
453
551
  const addButton = screen.getByRole('button', { name: /Add payment option/i });
454
552
  await user.click(addButton);
@@ -459,7 +557,7 @@ describe('AddBillableService', () => {
459
557
 
460
558
  test('should be able to add multiple payment options', async () => {
461
559
  const user = userEvent.setup();
462
- renderAddBillableService();
560
+ renderBillableServicesForm();
463
561
 
464
562
  // Add a second payment option
465
563
  const addButton = screen.getByRole('button', { name: /Add payment option/i });
@@ -471,7 +569,7 @@ describe('AddBillableService', () => {
471
569
 
472
570
  test('should allow adding multiple payment options with different payment modes', async () => {
473
571
  const user = userEvent.setup();
474
- renderAddBillableService();
572
+ renderBillableServicesForm();
475
573
 
476
574
  // Add second payment option
477
575
  const addButton = screen.getByRole('button', { name: /Add payment option/i });
@@ -497,7 +595,7 @@ describe('AddBillableService', () => {
497
595
  await user.click(screen.getByRole('option', { name: /Lab service/i }));
498
596
 
499
597
  mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
500
- await submitForm(user);
598
+ await submitForm();
501
599
 
502
600
  expect(mockCreateBillableService).toHaveBeenCalledWith(
503
601
  expect.objectContaining({
@@ -519,7 +617,7 @@ describe('AddBillableService', () => {
519
617
 
520
618
  test('should validate each payment option independently', async () => {
521
619
  const user = userEvent.setup();
522
- renderAddBillableService();
620
+ renderBillableServicesForm();
523
621
 
524
622
  // Add second payment option
525
623
  const addButton = screen.getByRole('button', { name: /Add payment option/i });
@@ -543,10 +641,10 @@ describe('AddBillableService', () => {
543
641
  await user.click(screen.getByRole('combobox', { name: /Service type/i }));
544
642
  await user.click(screen.getByRole('option', { name: /Lab service/i }));
545
643
 
546
- await submitForm(user);
644
+ await submitForm();
547
645
 
548
646
  // Should show error for the second payment option's missing price
549
- const priceErrors = screen.getAllByText('Price is required');
647
+ const priceErrors = await screen.findAllByText('Price is required');
550
648
  expect(priceErrors.length).toBeGreaterThan(0);
551
649
  expect(mockCreateBillableService).not.toHaveBeenCalled();
552
650
  });
@@ -555,20 +653,19 @@ describe('AddBillableService', () => {
555
653
  describe('Error Handling', () => {
556
654
  test('should display error snackbar when create API call fails', async () => {
557
655
  const user = userEvent.setup();
558
- renderAddBillableService();
656
+ renderBillableServicesForm();
559
657
 
560
658
  await fillRequiredFields(user);
561
659
 
562
660
  const errorMessage = 'Network error';
563
661
  mockCreateBillableService.mockRejectedValue(new Error(errorMessage));
564
662
 
565
- await submitForm(user);
663
+ await submitForm();
566
664
 
567
- // Wait for async operations
568
- await screen.findByRole('button', { name: /save/i });
665
+ // Wait for async operations to complete
666
+ await new Promise((resolve) => setTimeout(resolve, 100));
569
667
 
570
668
  expect(mockCreateBillableService).toHaveBeenCalledTimes(1);
571
- expect(navigate).not.toHaveBeenCalled();
572
669
  });
573
670
 
574
671
  test('should display error snackbar when update API call fails', async () => {
@@ -596,18 +693,17 @@ describe('AddBillableService', () => {
596
693
  ],
597
694
  };
598
695
 
599
- renderAddBillableService({ serviceToEdit: mockServiceToEdit });
696
+ renderBillableServicesForm({ serviceToEdit: mockServiceToEdit });
600
697
 
601
698
  const errorMessage = 'Update failed';
602
699
  mockUpdateBillableService.mockRejectedValue(new Error(errorMessage));
603
700
 
604
- await submitForm(user);
701
+ await submitForm();
605
702
 
606
- // Wait for async operations
607
- await screen.findByRole('button', { name: /save/i });
703
+ // Wait for async operations to complete
704
+ await new Promise((resolve) => setTimeout(resolve, 100));
608
705
 
609
706
  expect(mockUpdateBillableService).toHaveBeenCalledTimes(1);
610
- expect(navigate).not.toHaveBeenCalled();
611
707
  });
612
708
  });
613
709
  });