@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
@@ -1,17 +1,23 @@
1
1
  import React from 'react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { render, screen } from '@testing-library/react';
4
+ import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
5
+ import { configSchema, type BillingConfig } from '../config-schema';
4
6
  import { useBills } from '../billing.resource';
5
7
  import BillHistory from './bill-history.component';
6
8
 
7
- // Mock i18next
8
- jest.mock('react-i18next', () => ({
9
- useTranslation: () => ({
10
- t: (key: string) => key,
11
- }),
9
+ const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
10
+ const mockUseBills = jest.mocked(useBills);
11
+
12
+ jest.mock('../billing.resource', () => ({
13
+ useBills: jest.fn(() => ({
14
+ bills: mockBillData,
15
+ isLoading: false,
16
+ isValidating: false,
17
+ error: null,
18
+ })),
12
19
  }));
13
20
 
14
- // Mock window.i18next
15
21
  window.i18next = {
16
22
  language: 'en-US',
17
23
  } as any;
@@ -20,9 +26,7 @@ const testProps = {
20
26
  patientUuid: 'some-uuid',
21
27
  };
22
28
 
23
- const mockbills = useBills as jest.MockedFunction<typeof useBills>;
24
-
25
- const mockBillsData = [
29
+ const mockBillData = [
26
30
  { uuid: '1', patientName: 'John Doe', identifier: '12345678', billingService: 'Checkup', totalAmount: 500 },
27
31
  { uuid: '2', patientName: 'John Doe', identifier: '12345678', billingService: 'Consulatation', totalAmount: 600 },
28
32
  { uuid: '3', patientName: 'John Doe', identifier: '12345678', billingService: 'Child services', totalAmount: 700 },
@@ -37,70 +41,19 @@ const mockBillsData = [
37
41
  { uuid: '12', patientName: 'John Doe', identifier: '12345678', billingService: 'MCH', totalAmount: 1300 },
38
42
  ];
39
43
 
40
- // Mock the invoice table component
41
- jest.mock('../invoice/invoice-table.component', () => jest.fn(() => <div>Invoice table</div>));
42
-
43
- // Mock the billing resource
44
- jest.mock('../billing.resource', () => ({
45
- useBills: jest.fn(() => ({
46
- bills: mockBillsData,
47
- isLoading: false,
48
- isValidating: false,
49
- error: null,
50
- })),
51
- }));
52
-
53
- // Mock esm-patient-common-lib
54
- jest.mock('@openmrs/esm-patient-common-lib', () => ({
55
- CardHeader: jest.fn(({ children }) => <div>{children}</div>),
56
- EmptyDataIllustration: jest.fn(() => <div>Empty state illustration</div>),
57
- ErrorState: jest.fn(({ error }) => <div>Error: {error?.message}</div>),
58
- launchPatientWorkspace: jest.fn(),
59
- usePaginationInfo: jest.fn(() => ({
60
- pageSizes: [10, 20, 30],
61
- currentPage: 1,
62
- })),
63
- }));
64
-
65
- // Mock esm-framework
66
- jest.mock('@openmrs/esm-framework', () => ({
67
- useLayoutType: jest.fn(() => 'small-desktop'),
68
- isDesktop: jest.fn(() => true),
69
- usePagination: jest.fn().mockImplementation((data) => ({
70
- currentPage: 1,
71
- goTo: jest.fn(),
72
- results: data,
73
- paginated: true,
74
- })),
75
- showToast: jest.fn(),
76
- showNotification: jest.fn(),
77
- createErrorHandler: jest.fn(),
78
- createGlobalStore: jest.fn(),
79
- getGlobalStore: jest.fn(() => ({
80
- subscribe: jest.fn(),
81
- getState: jest.fn(),
82
- setState: jest.fn(),
83
- })),
84
- useConfig: jest.fn(() => ({
85
- pageSize: 10,
86
- defaultCurrency: 'USD',
87
- })),
88
- useSession: jest.fn(() => ({
89
- sessionLocation: { uuid: 'some-uuid', display: 'Location' },
90
- })),
91
- formatDate: jest.fn((date) => date?.toString() ?? ''),
92
- formatDatetime: jest.fn((date) => date?.toString() ?? ''),
93
- parseDate: jest.fn((dateString) => new Date(dateString)),
94
- ExtensionSlot: jest.fn(({ children }) => <>{children}</>),
95
- }));
96
-
97
44
  describe('BillHistory', () => {
98
- afterEach(() => {
99
- jest.clearAllMocks();
45
+ beforeEach(() => {
46
+ mockUseConfig.mockReturnValue({ ...getDefaultsFromConfigSchema(configSchema), defaultCurrency: 'USD' });
100
47
  });
101
48
 
102
49
  test('should render loading datatable skeleton', () => {
103
- mockbills.mockReturnValueOnce({ isLoading: true, isValidating: false, error: null, bills: [], mutate: jest.fn() });
50
+ mockUseBills.mockReturnValueOnce({
51
+ isLoading: true,
52
+ isValidating: false,
53
+ error: null,
54
+ bills: [],
55
+ mutate: jest.fn(),
56
+ });
104
57
  render(<BillHistory {...testProps} />);
105
58
  const loadingSkeleton = screen.getByRole('table');
106
59
  expect(loadingSkeleton).toBeInTheDocument();
@@ -108,7 +61,7 @@ describe('BillHistory', () => {
108
61
  });
109
62
 
110
63
  test('should render error state when API call fails', () => {
111
- mockbills.mockReturnValueOnce({
64
+ mockUseBills.mockReturnValueOnce({
112
65
  isLoading: false,
113
66
  isValidating: false,
114
67
  error: new Error('some error'),
@@ -116,31 +69,31 @@ describe('BillHistory', () => {
116
69
  mutate: jest.fn(),
117
70
  });
118
71
  render(<BillHistory {...testProps} />);
119
- const errorState = screen.getByText('Error: some error');
72
+ const errorState = screen.getByText(/Error/);
120
73
  expect(errorState).toBeInTheDocument();
121
74
  });
122
75
 
123
76
  test('should render bills table', async () => {
124
77
  const user = userEvent.setup();
125
- mockbills.mockReturnValueOnce({
78
+ mockUseBills.mockReturnValueOnce({
126
79
  isLoading: false,
127
80
  isValidating: false,
128
81
  error: null,
129
- bills: mockBillsData as any,
82
+ bills: mockBillData as any,
130
83
  mutate: jest.fn(),
131
84
  });
132
85
  render(<BillHistory {...testProps} />);
133
86
 
134
87
  // Verify headers
135
- expect(screen.getByText('visitTime')).toBeInTheDocument();
136
- expect(screen.getByText('identifier')).toBeInTheDocument();
88
+ expect(screen.getByText('Visit time')).toBeInTheDocument();
89
+ expect(screen.getByText('Identifier')).toBeInTheDocument();
137
90
 
138
91
  const tableRowGroup = screen.getAllByRole('rowgroup');
139
92
  expect(tableRowGroup).toHaveLength(2);
140
93
 
141
94
  // Page navigation should work as expected
142
- const nextPageButton = screen.getByRole('button', { name: /nextPage/ });
143
- const prevPageButton = screen.getByRole('button', { name: /previousPage/ });
95
+ const nextPageButton = screen.getByRole('button', { name: /Next page/ });
96
+ const prevPageButton = screen.getByRole('button', { name: /Previous page/ });
144
97
 
145
98
  expect(nextPageButton).toBeInTheDocument();
146
99
  expect(prevPageButton).toBeInTheDocument();
@@ -156,7 +109,13 @@ describe('BillHistory', () => {
156
109
  });
157
110
 
158
111
  test('should render empty state view when there are no bills', () => {
159
- mockbills.mockReturnValueOnce({ isLoading: false, isValidating: false, error: null, bills: [], mutate: jest.fn() });
112
+ mockUseBills.mockReturnValueOnce({
113
+ isLoading: false,
114
+ isValidating: false,
115
+ error: null,
116
+ bills: [],
117
+ mutate: jest.fn(),
118
+ });
160
119
  render(<BillHistory {...testProps} />);
161
120
  const emptyState = screen.getByText(/There are no bills to display./);
162
121
  expect(emptyState).toBeInTheDocument();
@@ -1,11 +1,7 @@
1
+ @use '@carbon/colors';
1
2
  @use '@carbon/layout';
2
3
  @use '@carbon/type';
3
- @use '@carbon/colors';
4
4
 
5
- .section {
6
- margin: layout.$spacing-03;
7
- }
8
-
9
5
  .sectionTitle {
10
6
  @include type.type-style('heading-compact-02');
11
7
  color: colors.$gray-70;
@@ -16,6 +12,16 @@
16
12
  padding-bottom: layout.$spacing-05;
17
13
  }
18
14
 
15
+ .billInfo {
16
+ @include type.type-style('body-compact-01');
17
+ color: colors.$gray-70;
18
+
19
+ .separator {
20
+ margin: 0 layout.$spacing-02;
21
+ color: colors.$gray-50;
22
+ }
23
+ }
24
+
19
25
  .label {
20
26
  @include type.type-style('heading-compact-01');
21
27
  margin-bottom: layout.$spacing-05;
@@ -24,3 +30,13 @@
24
30
  .controlField {
25
31
  margin-bottom: layout.$spacing-05;
26
32
  }
33
+
34
+ .loader {
35
+ &:global(.cds--inline-loading) {
36
+ min-height: layout.$spacing-05;
37
+ }
38
+
39
+ :global(.cds--inline-loading__text) {
40
+ font-size: unset;
41
+ }
42
+ }
@@ -0,0 +1,225 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ Button,
4
+ Form,
5
+ InlineLoading,
6
+ ModalBody,
7
+ ModalFooter,
8
+ ModalHeader,
9
+ NumberInput,
10
+ Stack,
11
+ TextInput,
12
+ } from '@carbon/react';
13
+ import { useTranslation } from 'react-i18next';
14
+ import { Controller, useForm } from 'react-hook-form';
15
+ import { mutate } from 'swr';
16
+ import { z } from 'zod';
17
+ import { zodResolver } from '@hookform/resolvers/zod';
18
+ import { getCoreTranslation, showSnackbar, useConfig } from '@openmrs/esm-framework';
19
+ import { apiBasePath } from '../constants';
20
+ import { getBillableServiceUuid } from '../invoice/payments/utils';
21
+ import { type LineItem, type MappedBill } from '../types';
22
+ import { updateBillItems } from '../billing.resource';
23
+ import { useBillableServices } from '../billable-services/billable-service.resource';
24
+ import { type BillingConfig } from '../config-schema';
25
+ import { convertToCurrency } from '../helpers';
26
+ import styles from './bill-item-actions.scss';
27
+
28
+ interface EditBillLineItemModalProps {
29
+ bill: MappedBill;
30
+ closeModal: () => void;
31
+ item: LineItem;
32
+ }
33
+
34
+ const EditBillLineItemModal: React.FC<EditBillLineItemModalProps> = ({ bill, closeModal, item }) => {
35
+ const { t } = useTranslation();
36
+ const { defaultCurrency } = useConfig<BillingConfig>();
37
+ const { billableServices } = useBillableServices();
38
+ const [total, setTotal] = useState(0);
39
+
40
+ const schema = useMemo(
41
+ () =>
42
+ z.object({
43
+ // NOTE: Frontend-only validation - quantities <1 or >100 can still be submitted via API.
44
+ // Backend (BillServiceImpl.java:100) has empty validate() method.
45
+ // TODO: Add server-side validation to enforce data integrity
46
+ quantity: z.coerce
47
+ .number({
48
+ required_error: t('quantityRequired', 'Quantity is required'),
49
+ invalid_type_error: t('quantityMustBeNumber', 'Quantity must be a valid number'),
50
+ })
51
+ .int(t('quantityMustBeInteger', 'Quantity must be a whole number'))
52
+ .min(1, t('quantityMustBeAtLeastOne', 'Quantity must be at least 1'))
53
+ .max(100, t('quantityCannotExceed100', 'Quantity cannot exceed 100')),
54
+ price: z.coerce
55
+ .number({
56
+ required_error: t('priceIsRequired', 'Price is required'),
57
+ invalid_type_error: t('priceMustBeNumber', 'Price must be a valid number'),
58
+ })
59
+ .positive(t('priceMustBePositive', 'Price must be greater than 0')),
60
+ }),
61
+ [t],
62
+ );
63
+
64
+ type BillLineItemForm = z.infer<typeof schema>;
65
+
66
+ const {
67
+ control,
68
+ handleSubmit,
69
+ formState: { isSubmitting, errors },
70
+ watch,
71
+ } = useForm<BillLineItemForm>({
72
+ defaultValues: {
73
+ quantity: item.quantity,
74
+ price: item.price,
75
+ },
76
+ resolver: zodResolver(schema),
77
+ });
78
+
79
+ const quantity = watch('quantity');
80
+ const price = watch('price');
81
+
82
+ useEffect(() => {
83
+ const quantityNum = typeof quantity === 'number' ? quantity : parseFloat(quantity) || 0;
84
+ const priceNum = typeof price === 'number' ? price : parseFloat(price) || 0;
85
+ const newTotal = quantityNum * priceNum;
86
+ setTotal(isNaN(newTotal) ? 0 : newTotal);
87
+ }, [quantity, price]);
88
+
89
+ const onSubmit = async (data: BillLineItemForm) => {
90
+ const url = `${apiBasePath}bill`;
91
+
92
+ const newItem = {
93
+ ...item,
94
+ quantity: data.quantity,
95
+ price: data.price,
96
+ billableService: getBillableServiceUuid(billableServices, item.billableService),
97
+ item: item?.item,
98
+ };
99
+
100
+ const previousLineitems = bill?.lineItems
101
+ .filter((currItem) => currItem.uuid !== item?.uuid)
102
+ .map((currItem) => ({
103
+ ...currItem,
104
+ billableService: getBillableServiceUuid(billableServices, currItem.billableService),
105
+ }));
106
+
107
+ const updatedLineItems = previousLineitems.concat(newItem);
108
+
109
+ const payload = {
110
+ cashPoint: bill.cashPointUuid,
111
+ cashier: bill.cashier.uuid,
112
+ lineItems: updatedLineItems,
113
+ patient: bill.patientUuid,
114
+ status: bill.status,
115
+ uuid: bill.uuid,
116
+ };
117
+
118
+ try {
119
+ await updateBillItems(payload);
120
+ mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
121
+ showSnackbar({
122
+ title: t('lineItemUpdated', 'Line item updated'),
123
+ subtitle: t('lineItemUpdateSuccess', 'The bill line item has been updated successfully'),
124
+ kind: 'success',
125
+ });
126
+ closeModal();
127
+ } catch (error) {
128
+ showSnackbar({
129
+ title: t('lineItemUpdateFailed', 'Failed to update line item'),
130
+ subtitle:
131
+ error?.message || t('lineItemUpdateErrorDefault', 'Unable to update the bill line item. Please try again.'),
132
+ kind: 'error',
133
+ });
134
+ }
135
+ };
136
+
137
+ if (Object.keys(bill)?.length === 0) {
138
+ return <ModalHeader closeModal={closeModal} title={t('billLineItemEmpty', 'This bill has no line items')} />;
139
+ }
140
+
141
+ return (
142
+ <>
143
+ <ModalHeader closeModal={closeModal} title={t('editBillLineItem', 'Edit bill line item')} />
144
+ <Form onSubmit={handleSubmit(onSubmit)}>
145
+ <ModalBody>
146
+ <Stack gap={5}>
147
+ <div className={styles.modalBody}>
148
+ <div className={styles.billInfo}>
149
+ {bill?.patientName}
150
+ <span className={styles.separator}>·</span>
151
+ {bill?.cashPointName}
152
+ <span className={styles.separator}>·</span>
153
+ {bill?.receiptNumber}
154
+ </div>
155
+ </div>
156
+ <section>
157
+ <p className={styles.label}>
158
+ {t('item', 'Item')}: {item?.billableService ? item?.billableService : item?.item}
159
+ </p>
160
+ <p className={styles.label}>
161
+ {t('currentPrice', 'Current price')}: {convertToCurrency(item?.price, defaultCurrency)}
162
+ </p>
163
+ <p className={styles.label}>
164
+ {t('serviceStatus', 'Service status')}: {item?.paymentStatus}
165
+ </p>
166
+ <Controller
167
+ name="quantity"
168
+ control={control}
169
+ render={({ field: { onChange, value } }) => (
170
+ <NumberInput
171
+ disableWheel
172
+ className={styles.controlField}
173
+ hideSteppers
174
+ id="quantityInput"
175
+ invalid={!!errors.quantity}
176
+ invalidText={errors.quantity?.message}
177
+ label={t('quantity', 'Quantity')}
178
+ onChange={(_event, state: { value: number | string; direction: string }) => {
179
+ onChange(state.value);
180
+ }}
181
+ value={value}
182
+ />
183
+ )}
184
+ />
185
+
186
+ <Controller
187
+ name="price"
188
+ control={control}
189
+ render={({ field: { value } }) => (
190
+ <TextInput
191
+ className={styles.controlField}
192
+ helperText={t('unitPriceHelperText', 'This is the unit price for this item')}
193
+ id="priceInput"
194
+ labelText={t('unitPrice', 'Unit price')}
195
+ readOnly
196
+ value={value}
197
+ />
198
+ )}
199
+ />
200
+ <p className={styles.label} aria-live="polite">
201
+ {t('total', 'Total')}: {convertToCurrency(total, defaultCurrency)}
202
+ </p>
203
+ </section>
204
+ </Stack>
205
+ </ModalBody>
206
+ <ModalFooter>
207
+ <Button kind="secondary" onClick={closeModal}>
208
+ {getCoreTranslation('cancel')}
209
+ </Button>
210
+ <Button type="submit" disabled={isSubmitting}>
211
+ {isSubmitting ? (
212
+ <div className={styles.inline}>
213
+ <InlineLoading className={styles.loader} description={`${t('submitting', 'Submitting')}...`} />
214
+ </div>
215
+ ) : (
216
+ getCoreTranslation('save')
217
+ )}
218
+ </Button>
219
+ </ModalFooter>
220
+ </Form>
221
+ </>
222
+ );
223
+ };
224
+
225
+ export default EditBillLineItemModal;