@openmrs/esm-billing-app 1.1.1 → 1.1.2-pre.1

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 (193) hide show
  1. package/.turbo/cache/53e233a916ffe7d2-meta.json +1 -0
  2. package/.turbo/cache/53e233a916ffe7d2.tar.zst +0 -0
  3. package/.turbo/turbo-build.log +44 -0
  4. package/__mocks__/bills.mock.ts +6 -5
  5. package/dist/1119.js +1 -1
  6. package/dist/1197.js +1 -1
  7. package/dist/1435.js +1 -0
  8. package/dist/1435.js.map +1 -0
  9. package/dist/1807.js +1 -0
  10. package/dist/1807.js.map +1 -0
  11. package/dist/2146.js +1 -1
  12. package/dist/2177.js +1 -1
  13. package/dist/2177.js.map +1 -1
  14. package/dist/2690.js +1 -1
  15. package/dist/2704.js +1 -0
  16. package/dist/2704.js.map +1 -0
  17. package/dist/3002.js +1 -0
  18. package/dist/3002.js.map +1 -0
  19. package/dist/3041.js +1 -1
  20. package/dist/3041.js.map +1 -1
  21. package/dist/3099.js +1 -1
  22. package/dist/3184.js +1 -1
  23. package/dist/3184.js.map +1 -1
  24. package/dist/3584.js +1 -1
  25. package/dist/4055.js +1 -1
  26. package/dist/4132.js +1 -1
  27. package/dist/4225.js +1 -1
  28. package/dist/4225.js.map +1 -1
  29. package/dist/4300.js +1 -1
  30. package/dist/4335.js +1 -1
  31. package/dist/439.js +1 -1
  32. package/dist/4618.js +1 -1
  33. package/dist/4652.js +1 -1
  34. package/dist/4944.js +1 -1
  35. package/dist/5173.js +1 -1
  36. package/dist/5241.js +1 -1
  37. package/dist/5422.js +1 -1
  38. package/dist/5422.js.map +1 -1
  39. package/dist/5442.js +1 -1
  40. package/dist/5661.js +1 -1
  41. package/dist/6022.js +1 -1
  42. package/dist/6404.js +1 -0
  43. package/dist/6404.js.map +1 -0
  44. package/dist/6468.js +1 -1
  45. package/dist/6540.js +1 -1
  46. package/dist/6540.js.map +1 -1
  47. package/dist/6589.js +1 -1
  48. package/dist/6606.js +1 -1
  49. package/dist/6606.js.map +1 -1
  50. package/dist/6679.js +1 -1
  51. package/dist/6792.js +1 -0
  52. package/dist/6792.js.map +1 -0
  53. package/dist/6840.js +1 -1
  54. package/dist/6859.js +1 -1
  55. package/dist/7097.js +1 -1
  56. package/dist/7159.js +1 -1
  57. package/dist/723.js +1 -1
  58. package/dist/7255.js +1 -1
  59. package/dist/7255.js.map +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/8341.js +2 -0
  64. package/dist/{1907.js.LICENSE.txt → 8341.js.LICENSE.txt} +0 -15
  65. package/dist/8341.js.map +1 -0
  66. package/dist/8349.js +1 -1
  67. package/dist/8371.js +1 -1
  68. package/dist/8421.js +1 -0
  69. package/dist/8421.js.map +1 -0
  70. package/dist/8618.js +1 -1
  71. package/dist/890.js +1 -1
  72. package/dist/9214.js +1 -1
  73. package/dist/9538.js +1 -1
  74. package/dist/9569.js +1 -1
  75. package/dist/961.js +1 -1
  76. package/dist/961.js.map +1 -1
  77. package/dist/986.js +1 -1
  78. package/dist/9879.js +1 -1
  79. package/dist/9895.js +1 -1
  80. package/dist/9900.js +1 -1
  81. package/dist/9913.js +1 -1
  82. package/dist/main.js +1 -1
  83. package/dist/main.js.LICENSE.txt +0 -15
  84. package/dist/main.js.map +1 -1
  85. package/dist/openmrs-esm-billing-app.js +1 -1
  86. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +284 -259
  87. package/dist/openmrs-esm-billing-app.js.map +1 -1
  88. package/dist/routes.json +1 -1
  89. package/e2e/commands/patient-operations.ts +1 -1
  90. package/e2e/pages/billing-dashboard-page.ts +3 -1
  91. package/e2e/pages/billing-form-page.ts +5 -0
  92. package/e2e/pages/invoice-page.ts +10 -0
  93. package/e2e/specs/billing-dashboard.spec.ts +126 -3
  94. package/e2e/specs/billing-patient-chart.spec.ts +95 -9
  95. package/package.json +3 -6
  96. package/src/bill-history/bill-action-menu.component.tsx +41 -0
  97. package/src/bill-history/bill-action-menu.scss +3 -0
  98. package/src/bill-history/bill-history.component.tsx +15 -5
  99. package/src/bill-history/bill-history.scss +0 -1
  100. package/src/bill-history/bill-history.test.tsx +78 -1
  101. package/src/bill-item-actions/edit-bill-item.modal.tsx +1 -1
  102. package/src/bill-item-actions/edit-bill-item.test.tsx +40 -0
  103. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +3 -1
  104. package/src/billing-dashboard/billing-dashboard.component.tsx +3 -16
  105. package/src/billing-form/billing-checkin-form.component.tsx +116 -57
  106. package/src/billing-form/billing-checkin-form.scss +26 -2
  107. package/src/billing-form/billing-checkin-form.test.tsx +51 -1
  108. package/src/billing-form/billing-form.resource.test.ts +87 -0
  109. package/src/billing-form/billing-form.resource.ts +33 -0
  110. package/src/billing-form/billing-form.scss +54 -7
  111. package/src/billing-form/billing-form.test.tsx +547 -0
  112. package/src/billing-form/billing-form.workspace.tsx +150 -45
  113. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +25 -2
  114. package/src/billing-form/visit-attributes/visit-attributes-form.scss +29 -0
  115. package/src/billing-header/billing-header.component.tsx +1 -34
  116. package/src/billing-header/billing-header.scss +0 -50
  117. package/src/billing.resource.test.ts +11 -11
  118. package/src/billing.resource.ts +42 -12
  119. package/src/bills-table/bills-table.component.tsx +16 -12
  120. package/src/bills-table/bills-table.test.tsx +84 -7
  121. package/src/index.ts +5 -0
  122. package/src/invoice/invoice.component.tsx +46 -16
  123. package/src/invoice/invoice.scss +9 -8
  124. package/src/invoice/invoice.test.tsx +128 -7
  125. package/src/invoice/line-item-action-menu.component.tsx +2 -2
  126. package/src/invoice/payments/payments.component.tsx +2 -2
  127. package/src/invoice/payments/payments.test.tsx +31 -2
  128. package/src/metrics-cards/metrics.resource.ts +3 -4
  129. package/src/modal/finalize-bill-confirmation.modal.test.tsx +209 -0
  130. package/src/modal/finalize-bill-confirmation.modal.tsx +86 -0
  131. package/src/modal/require-payment.modal.tsx +2 -1
  132. package/src/routes.json +4 -0
  133. package/src/types/index.ts +10 -1
  134. package/tools/setup-tests.ts +7 -6
  135. package/translations/am.json +28 -0
  136. package/translations/ar.json +28 -0
  137. package/translations/ar_SY.json +28 -0
  138. package/translations/bn.json +28 -0
  139. package/translations/cs.json +28 -0
  140. package/translations/de.json +266 -238
  141. package/translations/en.json +29 -0
  142. package/translations/en_US.json +28 -0
  143. package/translations/es.json +28 -0
  144. package/translations/es_MX.json +28 -0
  145. package/translations/fr.json +28 -0
  146. package/translations/he.json +28 -0
  147. package/translations/hi.json +28 -0
  148. package/translations/hi_IN.json +28 -0
  149. package/translations/id.json +28 -0
  150. package/translations/it.json +28 -0
  151. package/translations/ka.json +28 -0
  152. package/translations/km.json +28 -0
  153. package/translations/ku.json +28 -0
  154. package/translations/ky.json +28 -0
  155. package/translations/lg.json +28 -0
  156. package/translations/ne.json +28 -0
  157. package/translations/pl.json +28 -0
  158. package/translations/pt.json +28 -0
  159. package/translations/pt_BR.json +28 -0
  160. package/translations/qu.json +28 -0
  161. package/translations/ro_RO.json +28 -0
  162. package/translations/ru_RU.json +28 -0
  163. package/translations/si.json +28 -0
  164. package/translations/sq.json +28 -0
  165. package/translations/sw.json +28 -0
  166. package/translations/sw_KE.json +28 -0
  167. package/translations/tr.json +28 -0
  168. package/translations/tr_TR.json +28 -0
  169. package/translations/uk.json +28 -0
  170. package/translations/uz.json +28 -0
  171. package/translations/uz@Latn.json +28 -0
  172. package/translations/uz_UZ.json +28 -0
  173. package/translations/vi.json +28 -0
  174. package/translations/zh.json +268 -240
  175. package/translations/zh_CN.json +30 -2
  176. package/translations/zh_TW.json +28 -0
  177. package/turbo.json +29 -0
  178. package/dist/1537.js +0 -1
  179. package/dist/1537.js.map +0 -1
  180. package/dist/1907.js +0 -2
  181. package/dist/1907.js.map +0 -1
  182. package/dist/1981.js +0 -1
  183. package/dist/1981.js.map +0 -1
  184. package/dist/2820.js +0 -1
  185. package/dist/2820.js.map +0 -1
  186. package/dist/8025.js +0 -1
  187. package/dist/8025.js.map +0 -1
  188. package/dist/9727.js +0 -2
  189. package/dist/9727.js.LICENSE.txt +0 -14
  190. package/dist/9727.js.map +0 -1
  191. package/dist/9756.js +0 -1
  192. package/dist/9756.js.map +0 -1
  193. package/src/hooks/selectedDateContext.ts +0 -10
@@ -1,5 +1,4 @@
1
- import { calculateTotalAmount, convertToCurrency } from '../helpers';
2
- import { type MappedBill } from '../types';
1
+ import { BillStatus, type MappedBill } from '../types';
3
2
 
4
3
  /**
5
4
  * A custom hook for calculating bill metrics.
@@ -31,9 +30,9 @@ const calculateBillTotals = (bills: Array<MappedBill>) => {
31
30
  let cumulativeTotal = 0;
32
31
 
33
32
  bills.forEach((bill) => {
34
- if (bill?.status === 'PAID') {
33
+ if (bill?.status === BillStatus.PAID) {
35
34
  paidTotal += bill?.totalAmount;
36
- } else if (bill?.status === 'PENDING') {
35
+ } else if (bill?.status === BillStatus.PENDING) {
37
36
  pendingTotal += bill?.totalAmount;
38
37
  }
39
38
  cumulativeTotal += bill?.totalAmount; // Add to cumulative total regardless of status
@@ -0,0 +1,209 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen, waitFor } from '@testing-library/react';
4
+ import { showSnackbar } from '@openmrs/esm-framework';
5
+ import { finalizeBill } from '../billing.resource';
6
+ import FinalizeBillModal from './finalize-bill-confirmation.modal';
7
+ import type { MappedBill } from '../types';
8
+ import { BillStatus } from '../types';
9
+
10
+ const mockFinalizeBill = jest.mocked(finalizeBill);
11
+ const mockShowSnackbar = jest.mocked(showSnackbar);
12
+
13
+ jest.mock('../billing.resource', () => ({
14
+ finalizeBill: jest.fn(),
15
+ }));
16
+
17
+ const mockBill: MappedBill = {
18
+ uuid: 'bill-uuid',
19
+ id: 1,
20
+ patientUuid: 'patient-uuid',
21
+ patientName: 'John Doe',
22
+ cashPointUuid: 'cash-point-uuid',
23
+ cashPointName: 'Main Cashier',
24
+ cashPointLocation: 'Main Hospital',
25
+ cashier: { uuid: 'cashier-uuid', display: 'Jane Cashier', links: [] },
26
+ receiptNumber: 'RCPT-001',
27
+ status: BillStatus.PENDING,
28
+ identifier: '12345678',
29
+ dateCreated: '2024-01-01',
30
+ lineItems: [
31
+ {
32
+ uuid: 'item-1',
33
+ item: 'X-Ray',
34
+ quantity: 1,
35
+ price: 500,
36
+ paymentStatus: BillStatus.PENDING,
37
+ billableService: 'X-Ray Service',
38
+ },
39
+ ],
40
+ billingService: 'X-Ray Service',
41
+ payments: [],
42
+ totalAmount: 500,
43
+ tenderedAmount: 0,
44
+ };
45
+
46
+ describe('FinalizeBillModal', () => {
47
+ const mockCloseModal = jest.fn();
48
+ const mockMutate = jest.fn();
49
+
50
+ it('renders the confirmation modal with correct content', () => {
51
+ render(<FinalizeBillModal closeModal={mockCloseModal} bill={mockBill} onMutate={mockMutate} />);
52
+
53
+ expect(screen.getByText(/finalize bill/i)).toBeInTheDocument();
54
+ expect(
55
+ screen.getByText(/are you sure you want to finalize this bill\? once finalized, no further modifications/i),
56
+ ).toBeInTheDocument();
57
+ expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
58
+ expect(screen.getByRole('button', { name: /finalize/i })).toBeInTheDocument();
59
+ });
60
+
61
+ it('calls closeModal when cancel is clicked', async () => {
62
+ const user = userEvent.setup();
63
+ render(<FinalizeBillModal closeModal={mockCloseModal} bill={mockBill} onMutate={mockMutate} />);
64
+
65
+ await user.click(screen.getByRole('button', { name: /cancel/i }));
66
+
67
+ expect(mockCloseModal).toHaveBeenCalled();
68
+ });
69
+
70
+ it('calls finalizeBill with bill uuid and shows success snackbar', async () => {
71
+ const user = userEvent.setup();
72
+ mockFinalizeBill.mockResolvedValueOnce({} as any);
73
+
74
+ render(<FinalizeBillModal closeModal={mockCloseModal} bill={mockBill} onMutate={mockMutate} />);
75
+
76
+ await user.click(screen.getByRole('button', { name: /finalize/i }));
77
+
78
+ await waitFor(() => {
79
+ expect(mockFinalizeBill).toHaveBeenCalledWith('bill-uuid');
80
+ expect(mockMutate).toHaveBeenCalled();
81
+ expect(mockShowSnackbar).toHaveBeenCalledWith({
82
+ kind: 'success',
83
+ title: 'Bill finalized',
84
+ subtitle: 'Bill has been finalized successfully',
85
+ });
86
+ expect(mockCloseModal).toHaveBeenCalled();
87
+ });
88
+ });
89
+
90
+ it('calls onMutate before closeModal on success', async () => {
91
+ const user = userEvent.setup();
92
+ const callOrder: string[] = [];
93
+ mockFinalizeBill.mockResolvedValueOnce({} as any);
94
+ mockMutate.mockImplementation(() => callOrder.push('onMutate'));
95
+ mockCloseModal.mockImplementation(() => callOrder.push('closeModal'));
96
+
97
+ render(<FinalizeBillModal closeModal={mockCloseModal} bill={mockBill} onMutate={mockMutate} />);
98
+ await user.click(screen.getByRole('button', { name: /finalize/i }));
99
+
100
+ await waitFor(() => expect(mockCloseModal).toHaveBeenCalled());
101
+ expect(callOrder).toEqual(['onMutate', 'closeModal']);
102
+ });
103
+
104
+ it('shows error snackbar when finalize fails', async () => {
105
+ const user = userEvent.setup();
106
+ mockFinalizeBill.mockRejectedValueOnce({ message: 'Network error' });
107
+
108
+ render(<FinalizeBillModal closeModal={mockCloseModal} bill={mockBill} onMutate={mockMutate} />);
109
+
110
+ await user.click(screen.getByRole('button', { name: /finalize/i }));
111
+
112
+ await waitFor(() => {
113
+ expect(mockShowSnackbar).toHaveBeenCalledWith({
114
+ kind: 'error',
115
+ title: 'Failed to finalize bill',
116
+ subtitle: 'Network error',
117
+ });
118
+ });
119
+ expect(mockCloseModal).not.toHaveBeenCalled();
120
+ });
121
+
122
+ it('shows error from responseBody when available', async () => {
123
+ const user = userEvent.setup();
124
+ mockFinalizeBill.mockRejectedValueOnce({
125
+ responseBody: { error: { message: 'Bill cannot be finalized' } },
126
+ });
127
+
128
+ render(<FinalizeBillModal closeModal={mockCloseModal} bill={mockBill} onMutate={mockMutate} />);
129
+
130
+ await user.click(screen.getByRole('button', { name: /finalize/i }));
131
+
132
+ await waitFor(() => {
133
+ expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ subtitle: 'Bill cannot be finalized' }));
134
+ });
135
+ });
136
+
137
+ it('shows fallback error message when error has no message', async () => {
138
+ const user = userEvent.setup();
139
+ mockFinalizeBill.mockRejectedValueOnce({});
140
+
141
+ render(<FinalizeBillModal closeModal={mockCloseModal} bill={mockBill} onMutate={mockMutate} />);
142
+
143
+ await user.click(screen.getByRole('button', { name: /finalize/i }));
144
+
145
+ await waitFor(() => {
146
+ expect(mockShowSnackbar).toHaveBeenCalledWith(
147
+ expect.objectContaining({ subtitle: 'Unable to finalize bill. Please try again.' }),
148
+ );
149
+ });
150
+ });
151
+
152
+ it('disables buttons and shows loading state while finalizing', async () => {
153
+ const user = userEvent.setup();
154
+ mockFinalizeBill.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));
155
+
156
+ render(<FinalizeBillModal closeModal={mockCloseModal} bill={mockBill} onMutate={mockMutate} />);
157
+
158
+ const finalizeButton = screen.getByRole('button', { name: /finalize/i });
159
+ await user.click(finalizeButton);
160
+
161
+ expect(finalizeButton).toBeDisabled();
162
+ expect(screen.getByRole('button', { name: /cancel/i })).toBeDisabled();
163
+ expect(screen.getByText(/finalizing/i)).toBeInTheDocument();
164
+
165
+ await waitFor(() => expect(mockCloseModal).toHaveBeenCalled());
166
+ });
167
+
168
+ it('re-enables finalize button after failed finalization', async () => {
169
+ const user = userEvent.setup();
170
+ mockFinalizeBill.mockRejectedValueOnce({ message: 'error' });
171
+
172
+ render(<FinalizeBillModal closeModal={mockCloseModal} bill={mockBill} onMutate={mockMutate} />);
173
+
174
+ const finalizeButton = screen.getByRole('button', { name: /finalize/i });
175
+ await user.click(finalizeButton);
176
+
177
+ await waitFor(() => expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'error' })));
178
+
179
+ expect(finalizeButton).toBeEnabled();
180
+ });
181
+
182
+ it('does not call onMutate when finalization fails', async () => {
183
+ const user = userEvent.setup();
184
+ mockFinalizeBill.mockRejectedValueOnce({ message: 'error' });
185
+
186
+ render(<FinalizeBillModal closeModal={mockCloseModal} bill={mockBill} onMutate={mockMutate} />);
187
+
188
+ await user.click(screen.getByRole('button', { name: /finalize/i }));
189
+
190
+ await waitFor(() => expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'error' })));
191
+
192
+ expect(mockMutate).not.toHaveBeenCalled();
193
+ });
194
+
195
+ it('works correctly when onMutate is not provided', async () => {
196
+ const user = userEvent.setup();
197
+ mockFinalizeBill.mockResolvedValueOnce({} as any);
198
+
199
+ render(<FinalizeBillModal closeModal={mockCloseModal} bill={mockBill} />);
200
+
201
+ await user.click(screen.getByRole('button', { name: /finalize/i }));
202
+
203
+ await waitFor(() => {
204
+ expect(mockFinalizeBill).toHaveBeenCalled();
205
+ expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'success' }));
206
+ expect(mockCloseModal).toHaveBeenCalled();
207
+ });
208
+ });
209
+ });
@@ -0,0 +1,86 @@
1
+ import React, { useState } from 'react';
2
+ import { Button, InlineLoading, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { getCoreTranslation, showSnackbar } from '@openmrs/esm-framework';
5
+ import { finalizeBill } from '../billing.resource';
6
+ import { type MappedBill } from '../types';
7
+ import styles from './delete-line-item-confirmation.scss';
8
+
9
+ interface FinalizeBillModalParams {
10
+ closeModal: () => void;
11
+ bill: MappedBill;
12
+ onMutate?: () => void;
13
+ }
14
+
15
+ const FinalizeBillModal: React.FC<FinalizeBillModalParams> = ({ closeModal, bill, onMutate }) => {
16
+ const { t } = useTranslation();
17
+ const [isFinalizing, setIsFinalizing] = useState(false);
18
+
19
+ const handleFinalize = async () => {
20
+ if (!bill?.uuid) {
21
+ return;
22
+ }
23
+
24
+ setIsFinalizing(true);
25
+
26
+ try {
27
+ await finalizeBill(bill.uuid);
28
+
29
+ showSnackbar({
30
+ title: t('billFinalized', 'Bill finalized'),
31
+ subtitle: t('billFinalizedSuccess', 'Bill has been finalized successfully'),
32
+ kind: 'success',
33
+ });
34
+
35
+ onMutate?.();
36
+ closeModal();
37
+ } catch (err: any) {
38
+ const message =
39
+ err?.responseBody?.error?.message ||
40
+ err?.message ||
41
+ t('finalizeFailedTryAgain', 'Unable to finalize bill. Please try again.');
42
+
43
+ // eslint-disable-next-line no-console
44
+ console.error('[FinalizeBillModal] Bill finalization failed', err);
45
+
46
+ showSnackbar({
47
+ title: t('billFinalizeFailed', 'Failed to finalize bill'),
48
+ subtitle: message,
49
+ kind: 'error',
50
+ });
51
+ } finally {
52
+ setIsFinalizing(false);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <>
58
+ <ModalHeader closeModal={closeModal} title={t('finalizeBill', 'Finalize bill')} />
59
+
60
+ <ModalBody className={styles.modalBody}>
61
+ <p>
62
+ {t(
63
+ 'finalizeBillConfirmation',
64
+ 'Are you sure you want to finalize this bill? Once finalized, no further modifications to the bill will be allowed.',
65
+ )}
66
+ </p>
67
+ </ModalBody>
68
+
69
+ <ModalFooter>
70
+ <Button kind="secondary" onClick={closeModal} disabled={isFinalizing}>
71
+ {getCoreTranslation('cancel')}
72
+ </Button>
73
+
74
+ <Button kind="primary" onClick={handleFinalize} disabled={isFinalizing}>
75
+ {isFinalizing ? (
76
+ <InlineLoading className={styles.spinner} description={t('finalizing', 'Finalizing') + '...'} />
77
+ ) : (
78
+ t('finalize', 'Finalize')
79
+ )}
80
+ </Button>
81
+ </ModalFooter>
82
+ </>
83
+ );
84
+ };
85
+
86
+ export default FinalizeBillModal;
@@ -15,6 +15,7 @@ import {
15
15
  import { getCoreTranslation, useConfig } from '@openmrs/esm-framework';
16
16
  import { useBills } from '../billing.resource';
17
17
  import { convertToCurrency } from '../helpers';
18
+ import { BillStatus } from '../types';
18
19
  import styles from './require-payment.scss';
19
20
 
20
21
  type RequirePaymentModalProps = {
@@ -26,7 +27,7 @@ const RequirePaymentModal: React.FC<RequirePaymentModalProps> = ({ closeModal, p
26
27
  const { t } = useTranslation();
27
28
  const { defaultCurrency } = useConfig();
28
29
  const { bills, isLoading } = useBills(patientUuid);
29
- const lineItems = bills.filter((bill) => bill?.status !== 'PAID').flatMap((bill) => bill?.lineItems);
30
+ const lineItems = bills.filter((bill) => bill?.status !== BillStatus.PAID).flatMap((bill) => bill?.lineItems);
30
31
 
31
32
  return (
32
33
  <>
package/src/routes.json CHANGED
@@ -112,6 +112,10 @@
112
112
  {
113
113
  "name": "delete-line-item-confirmation-modal",
114
114
  "component": "deleteLineItemConfirmationModal"
115
+ },
116
+ {
117
+ "name": "finalize-bill-confirmation-modal",
118
+ "component": "finalizeBillConfirmationModal"
115
119
  }
116
120
  ],
117
121
  "workspaces2": [
@@ -1,5 +1,14 @@
1
1
  import { type OpenmrsResource } from '@openmrs/esm-framework';
2
2
 
3
+ export const BillStatus = {
4
+ PENDING: 'PENDING',
5
+ POSTED: 'POSTED',
6
+ PAID: 'PAID',
7
+ ADJUSTED: 'ADJUSTED',
8
+ } as const;
9
+
10
+ export type BillStatus = (typeof BillStatus)[keyof typeof BillStatus];
11
+
3
12
  export interface MappedBill {
4
13
  uuid: string;
5
14
  id: number;
@@ -10,7 +19,7 @@ export interface MappedBill {
10
19
  cashPointLocation: string;
11
20
  cashier: Provider;
12
21
  receiptNumber: string;
13
- status: string;
22
+ status: BillStatus;
14
23
  identifier: string;
15
24
  dateCreated: string;
16
25
  lineItems: Array<LineItem>;
@@ -12,12 +12,13 @@ window.spaBase = '/spa';
12
12
  window.getOpenmrsSpaBase = () => '/openmrs/spa/';
13
13
  window.HTMLElement.prototype.scrollIntoView = jest.fn();
14
14
 
15
- // Mock ResizeObserver for Carbon components that use it (e.g., TextArea)
16
- global.ResizeObserver = jest.fn().mockImplementation(() => ({
17
- observe: jest.fn(),
18
- unobserve: jest.fn(),
19
- disconnect: jest.fn(),
20
- }));
15
+ class ResizeObserver {
16
+ observe() {}
17
+ unobserve() {}
18
+ disconnect() {}
19
+ }
20
+ window.ResizeObserver = ResizeObserver;
21
+ global.ResizeObserver = ResizeObserver;
21
22
 
22
23
  // Suppress single-spa warnings in tests (these are expected when using framework mocks)
23
24
  const originalWarn = console.warn;
@@ -3,6 +3,7 @@
3
3
  "addBillableService": "Add billable service",
4
4
  "addBillItems": "Add bill items",
5
5
  "addCashPoint": "Add cash point",
6
+ "addItemsToBill": "Add items to bill",
6
7
  "addNewBillableService": "Add new billable service",
7
8
  "addNewCashPoint": "Add new cash point",
8
9
  "addNewPaymentMode": "Add new payment mode",
@@ -38,6 +39,9 @@
38
39
  "billDate": "Bill date",
39
40
  "billedItems": "Billed items",
40
41
  "billErrorService": "Billing service error",
42
+ "billFinalized": "Bill finalized",
43
+ "billFinalizedSuccess": "Bill has been finalized successfully",
44
+ "billFinalizeFailed": "Failed to finalize bill",
41
45
  "billing": "Billing",
42
46
  "billingHistory": "Billing history",
43
47
  "billingSettings": "Billing settings",
@@ -72,6 +76,7 @@
72
76
  "childUnder5": "Child under 5",
73
77
  "clearSearch": "Clear",
74
78
  "confirmDeleteMessage": "Are you sure you want to delete this payment mode? Proceed cautiously.",
79
+ "createBill": "Create bill",
75
80
  "cumulativeBills": "Cumulative bills",
76
81
  "currentPrice": "Current price",
77
82
  "date": "Date",
@@ -100,16 +105,24 @@
100
105
  "errorDeletingPaymentMode": "An error occurred while deleting the payment mode.",
101
106
  "errorFetchingCashPoints": "An error occurred while fetching cash points.",
102
107
  "errorFetchingLocations": "An error occurred while fetching locations.",
108
+ "errorLoadingBill": "Error loading bill",
103
109
  "errorLoadingBillableServices": "Error loading billable services",
104
110
  "errorLoadingBillServices": "Error loading bill services",
111
+ "errorLoadingLastVisit": "An error occurred while loading the last visit",
105
112
  "errorLoadingPaymentModes": "Error loading payment modes",
106
113
  "errorPrintingInvoice": "Error printing invoice",
107
114
  "errorProcessingPayment": "Error processing payment",
108
115
  "errorSavingCashPoint": "An error occurred while saving the cash point.",
109
116
  "errorSavingPaymentMode": "An error occurred while saving the payment mode.",
117
+ "existingItems": "Existing items",
110
118
  "filterBillsByPatient": "Filter bills by patient name or identifier",
111
119
  "filterBy": "Filter by:",
112
120
  "filterTable": "Filter table",
121
+ "finalize": "Finalize",
122
+ "finalizeBill": "Finalize bill",
123
+ "finalizeBillConfirmation": "Are you sure you want to finalize this bill? Once finalized, no further modifications to the bill will be allowed.",
124
+ "finalizeFailedTryAgain": "Unable to finalize bill. Please try again.",
125
+ "finalizing": "Finalizing",
113
126
  "gender": "Gender",
114
127
  "grandTotal": "Grand total",
115
128
  "home": "Home",
@@ -125,7 +138,13 @@
125
138
  "invoiceStatus": "Invoice status",
126
139
  "invoiceSummary": "Invoice summary",
127
140
  "item": "Item",
141
+ "itemsAddedToBill": "Items added to bill",
142
+ "itemsAddedToBillSuccessfully": "Items have been added to the bill successfully",
128
143
  "itemsToBeBilled": "Items to be billed",
144
+ "lastVisitError": "Last visit error",
145
+ "lastVisitInfo": "Last Visit Information",
146
+ "lastVisitMsg_one": "The last visit was a {{type}} {{count}} day ago at {{location}}",
147
+ "lastVisitMsg_other": "The last visit was a {{type}} {{count}} days ago at {{location}}",
129
148
  "lineItemDeleted": "Line item deleted",
130
149
  "lineItemDeleteFailed": "Failed to delete line item",
131
150
  "lineItemDeleteSuccess": "Bill line item deleted successfully",
@@ -144,6 +163,7 @@
144
163
  "locationRequired": "Location is required",
145
164
  "manageBillableServices": "Manage billable services",
146
165
  "name": "Name",
166
+ "newItems": "New items",
147
167
  "nextPage": "Next page",
148
168
  "no": "No",
149
169
  "noBillsToDisplay": "There are no bills to display.",
@@ -152,6 +172,7 @@
152
172
  "noMatchingItemsToDisplay": "No matching items to display",
153
173
  "noMatchingServicesToDisplay": "No matching services to display",
154
174
  "nonPaying": "Non paying",
175
+ "nonPayingInfo": "Any services rendered to non-paying patients will not be billed",
155
176
  "noResultsFor": "No results for {{searchTerm}}",
156
177
  "number": "Number",
157
178
  "ok": "OK",
@@ -184,11 +205,14 @@
184
205
  "paymentProcessedSuccessfully": "Payment processed successfully",
185
206
  "payments": "Payments",
186
207
  "pendingBills": "Pending bills",
208
+ "pendingConfirmationBills": "Pending confirmation",
209
+ "pendingPaymentBills": "Pending payment",
187
210
  "policyNumber": "Policy number",
188
211
  "postWaiver": "Post waiver",
189
212
  "previousPage": "Previous page",
190
213
  "price": "Price",
191
214
  "priceIsRequired": "Price is required",
215
+ "priceMustBeNonNegative": "Price must be 0 or greater",
192
216
  "priceMustBeNumber": "Price must be a valid number",
193
217
  "priceMustBePositive": "Price must be greater than 0",
194
218
  "prices": "Prices",
@@ -222,15 +246,18 @@
222
246
  "serviceName": "Service name",
223
247
  "serviceNameExceedsLimit": "Service name cannot exceed {{MAX_NAME_LENGTH}} characters",
224
248
  "serviceNameRequired": "Service name is required",
249
+ "serviceResolutionError": "Could not resolve service \"{{service}}\"",
225
250
  "servicesList": "Services list",
226
251
  "serviceStatus": "Service status",
227
252
  "serviceType": "Service type",
228
253
  "serviceTypeRequired": "Service type is required",
229
254
  "shortName": "Short name",
230
255
  "shortNameExceedsLimit": "Short name cannot exceed {{MAX_NAME_LENGTH}} characters",
256
+ "showInformation": "Show information",
231
257
  "status": "Service status",
232
258
  "student": "Student",
233
259
  "submitting": "Submitting",
260
+ "subtotal": "Subtotal",
234
261
  "success": "Success",
235
262
  "total": "Total",
236
263
  "totalAmount": "Total amount",
@@ -238,6 +265,7 @@
238
265
  "totalTendered": "Total tendered",
239
266
  "unitPrice": "Unit price",
240
267
  "unitPriceHelperText": "This is the unit price for this item",
268
+ "unknownBillError": "An unexpected error occurred",
241
269
  "uuid": "UUID",
242
270
  "uuidRequired": "UUID is required",
243
271
  "validationError": "Validation error",
@@ -3,6 +3,7 @@
3
3
  "addBillableService": "إضافة خدمة قابلة للفوترة",
4
4
  "addBillItems": "Add bill items",
5
5
  "addCashPoint": "Add cash point",
6
+ "addItemsToBill": "Add items to bill",
6
7
  "addNewBillableService": "إضافة خدمة جديدة قابلة للفوترة",
7
8
  "addNewCashPoint": "Add new cash point",
8
9
  "addNewPaymentMode": "Add new payment mode",
@@ -38,6 +39,9 @@
38
39
  "billDate": "تاريخ الفاتورة",
39
40
  "billedItems": "العناصر المفوترة",
40
41
  "billErrorService": "خطأ في خدمة الفوترة",
42
+ "billFinalized": "Bill finalized",
43
+ "billFinalizedSuccess": "Bill has been finalized successfully",
44
+ "billFinalizeFailed": "Failed to finalize bill",
41
45
  "billing": "الفواتير",
42
46
  "billingHistory": "تاريخ الفواتير",
43
47
  "billingSettings": "إعدادات الفواتير",
@@ -72,6 +76,7 @@
72
76
  "childUnder5": "طفل أقل من 5 سنوات",
73
77
  "clearSearch": "مسح",
74
78
  "confirmDeleteMessage": "هل أنت متأكد من حذف طريقة الدفع هذه؟ يرجى المتابعة بحذر.",
79
+ "createBill": "Create bill",
75
80
  "cumulativeBills": "الفواتير التراكمية",
76
81
  "currentPrice": "السعر الحالي",
77
82
  "date": "التاريخ",
@@ -100,16 +105,24 @@
100
105
  "errorDeletingPaymentMode": "حدث خطأ أثناء حذف طريقة الدفع",
101
106
  "errorFetchingCashPoints": "حدث خطأ أثناء جلب نقاط التحصيل للمال",
102
107
  "errorFetchingLocations": "حدث خطأ أثناء جلب المواقع",
108
+ "errorLoadingBill": "Error loading bill",
103
109
  "errorLoadingBillableServices": "خطأ أثناء تحميل الخدمات القابلة للفوترة",
104
110
  "errorLoadingBillServices": "خطأ أثناء تحميل خدمات الفاتورة",
111
+ "errorLoadingLastVisit": "An error occurred while loading the last visit",
105
112
  "errorLoadingPaymentModes": "خطأ أثناء تحميل طرق الدفع",
106
113
  "errorPrintingInvoice": "خطأ أثناء طباعة الفاتورة",
107
114
  "errorProcessingPayment": "خطأ أثناء معالجة الدفع",
108
115
  "errorSavingCashPoint": "حدث خطأ أثناء حفظ نقطة التحصيل",
109
116
  "errorSavingPaymentMode": "حدث خطأ أثناء حفظ طريقة الدفع",
117
+ "existingItems": "Existing items",
110
118
  "filterBillsByPatient": "فلترة الفواتير حسب اسم المريض أو المعرّف",
111
119
  "filterBy": "فلترة حسب",
112
120
  "filterTable": "جدول الفلترة",
121
+ "finalize": "Finalize",
122
+ "finalizeBill": "Finalize bill",
123
+ "finalizeBillConfirmation": "Are you sure you want to finalize this bill? Once finalized, no further modifications to the bill will be allowed.",
124
+ "finalizeFailedTryAgain": "Unable to finalize bill. Please try again.",
125
+ "finalizing": "Finalizing",
113
126
  "gender": "الجنس",
114
127
  "grandTotal": "الإجمالي الكلي",
115
128
  "home": "الصفحة الرئيسية",
@@ -125,7 +138,13 @@
125
138
  "invoiceStatus": "حالة الفاتورة",
126
139
  "invoiceSummary": "ملخص الفاتورة",
127
140
  "item": "عنصر",
141
+ "itemsAddedToBill": "Items added to bill",
142
+ "itemsAddedToBillSuccessfully": "Items have been added to the bill successfully",
128
143
  "itemsToBeBilled": "العناصر المطلوب فوترتها",
144
+ "lastVisitError": "Last visit error",
145
+ "lastVisitInfo": "Last Visit Information",
146
+ "lastVisitMsg_one": "The last visit was a {{type}} {{count}} day ago at {{location}}",
147
+ "lastVisitMsg_other": "The last visit was a {{type}} {{count}} days ago at {{location}}",
129
148
  "lineItemDeleted": "تم حذف العنصر",
130
149
  "lineItemDeleteFailed": "فشل في حذف العنصر",
131
150
  "lineItemDeleteSuccess": "تم حذف عنصر الفاتورة بنجاح",
@@ -144,6 +163,7 @@
144
163
  "locationRequired": "الموقع مطلوب",
145
164
  "manageBillableServices": "إدارة الخدمات القابلة للفوترة",
146
165
  "name": "الاسم",
166
+ "newItems": "New items",
147
167
  "nextPage": "الصفحة التالية",
148
168
  "no": "لا",
149
169
  "noBillsToDisplay": "لا توجد فواتير للعرض",
@@ -152,6 +172,7 @@
152
172
  "noMatchingItemsToDisplay": "لا توجد عناصر للعرض",
153
173
  "noMatchingServicesToDisplay": "لا توجد عناصر مطابقة للعرض",
154
174
  "nonPaying": "غير دافع",
175
+ "nonPayingInfo": "Any services rendered to non-paying patients will not be billed",
155
176
  "noResultsFor": "لا توجد نتائج لـ {{searchTerm}}",
156
177
  "number": "رقم",
157
178
  "ok": "موافق",
@@ -184,11 +205,14 @@
184
205
  "paymentProcessedSuccessfully": "تمت معالجة الدفع بنجاح",
185
206
  "payments": "المدفوعات",
186
207
  "pendingBills": "الفواتير المعلقة",
208
+ "pendingConfirmationBills": "Pending confirmation",
209
+ "pendingPaymentBills": "Pending payment",
187
210
  "policyNumber": "رقم الوثيقة",
188
211
  "postWaiver": "تأكيد الإعفاء",
189
212
  "previousPage": "الصفحة السابقة",
190
213
  "price": "السعر",
191
214
  "priceIsRequired": "السعر مطلوب",
215
+ "priceMustBeNonNegative": "Price must be 0 or greater",
192
216
  "priceMustBeNumber": "يجب أن يكون السعر رقمًا صالحًا",
193
217
  "priceMustBePositive": "يجب أن يكون السعر أكبر من 0",
194
218
  "prices": "الأسعار",
@@ -222,15 +246,18 @@
222
246
  "serviceName": "اسم الخدمة",
223
247
  "serviceNameExceedsLimit": "لا يمكن أن يتجاوز اسم الخدمة {{MAX_NAME_LENGTH}} حرفًا",
224
248
  "serviceNameRequired": "اسم الخدمة مطلوب",
249
+ "serviceResolutionError": "Could not resolve service \"{{service}}\"",
225
250
  "servicesList": "قائمة الخدمات",
226
251
  "serviceStatus": "حالة الخدمة",
227
252
  "serviceType": "نوع الخدمة",
228
253
  "serviceTypeRequired": "نوع الخدمة مطلوب",
229
254
  "shortName": "أسم مختصر",
230
255
  "shortNameExceedsLimit": "لا يمكن أن يتجاوز الاسم المختصر {{MAX_NAME_LENGTH}} حرفًا",
256
+ "showInformation": "Show information",
231
257
  "status": "حالة الخدمة",
232
258
  "student": "طالب",
233
259
  "submitting": "جارٍ الإرسال",
260
+ "subtotal": "Subtotal",
234
261
  "success": "نجاح",
235
262
  "total": "الإجمالي",
236
263
  "totalAmount": "إجمالي المبلغ",
@@ -238,6 +265,7 @@
238
265
  "totalTendered": "إجمالي المُسدَّد",
239
266
  "unitPrice": "سعر الوحدة",
240
267
  "unitPriceHelperText": "هذا هو سعر الوحدة لهذا البند",
268
+ "unknownBillError": "An unexpected error occurred",
241
269
  "uuid": "المعرّف الفريد UUID",
242
270
  "uuidRequired": "المعرّف الفريد UUID مطلوب",
243
271
  "validationError": "خطأ في التحقق",