@openmrs/esm-billing-app 1.0.2-pre.74 → 1.0.2-pre.749

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 (196) hide show
  1. package/README.md +55 -9
  2. package/__mocks__/bills.mock.ts +12 -0
  3. package/__mocks__/react-i18next.js +6 -5
  4. package/dist/1119.js +1 -1
  5. package/dist/1146.js +1 -2
  6. package/dist/1146.js.map +1 -1
  7. package/dist/1197.js +1 -1
  8. package/dist/1856.js +1 -0
  9. package/dist/1856.js.map +1 -0
  10. package/dist/2146.js +1 -1
  11. package/dist/2177.js +2 -0
  12. package/dist/2177.js.LICENSE.txt +9 -0
  13. package/dist/2177.js.map +1 -0
  14. package/dist/2524.js +1 -0
  15. package/dist/2524.js.map +1 -0
  16. package/dist/2690.js +1 -1
  17. package/dist/3041.js +1 -0
  18. package/dist/3041.js.map +1 -0
  19. package/dist/3099.js +1 -1
  20. package/dist/3584.js +1 -1
  21. package/dist/4055.js +1 -1
  22. package/dist/4132.js +1 -1
  23. package/dist/4225.js +1 -0
  24. package/dist/4225.js.map +1 -0
  25. package/dist/4300.js +1 -1
  26. package/dist/4335.js +1 -1
  27. package/dist/4618.js +1 -1
  28. package/dist/4652.js +1 -1
  29. package/dist/4724.js +1 -0
  30. package/dist/4724.js.map +1 -0
  31. package/dist/4739.js +1 -1
  32. package/dist/4739.js.map +1 -1
  33. package/dist/4944.js +1 -1
  34. package/dist/5173.js +1 -1
  35. package/dist/5241.js +1 -1
  36. package/dist/5422.js +1 -0
  37. package/dist/5422.js.map +1 -0
  38. package/dist/5442.js +1 -1
  39. package/dist/5661.js +1 -1
  40. package/dist/6022.js +1 -1
  41. package/dist/6468.js +1 -1
  42. package/dist/6540.js +1 -1
  43. package/dist/6540.js.map +1 -1
  44. package/dist/6606.js +1 -0
  45. package/dist/6606.js.map +1 -0
  46. package/dist/6679.js +1 -1
  47. package/dist/6840.js +1 -1
  48. package/dist/6859.js +1 -1
  49. package/dist/7097.js +1 -1
  50. package/dist/7159.js +1 -1
  51. package/dist/723.js +1 -1
  52. package/dist/7452.js +2 -0
  53. package/dist/7452.js.map +1 -0
  54. package/dist/7617.js +1 -1
  55. package/dist/795.js +1 -0
  56. package/dist/8163.js +1 -1
  57. package/dist/8349.js +1 -1
  58. package/dist/8618.js +1 -1
  59. package/dist/890.js +1 -1
  60. package/dist/8930.js +2 -0
  61. package/dist/{6525.js.LICENSE.txt → 8930.js.LICENSE.txt} +16 -4
  62. package/dist/8930.js.map +1 -0
  63. package/dist/9214.js +1 -1
  64. package/dist/942.js +1 -0
  65. package/dist/942.js.map +1 -0
  66. package/dist/9538.js +1 -1
  67. package/dist/9569.js +1 -0
  68. package/dist/961.js +1 -1
  69. package/dist/961.js.map +1 -1
  70. package/dist/986.js +1 -1
  71. package/dist/9879.js +1 -1
  72. package/dist/9895.js +1 -1
  73. package/dist/9900.js +1 -1
  74. package/dist/9913.js +1 -1
  75. package/dist/main.js +1 -1
  76. package/dist/main.js.map +1 -1
  77. package/dist/openmrs-esm-billing-app.js +1 -1
  78. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +381 -231
  79. package/dist/openmrs-esm-billing-app.js.map +1 -1
  80. package/dist/routes.json +1 -1
  81. package/e2e/README.md +19 -18
  82. package/e2e/specs/sample-test.spec.ts +0 -1
  83. package/package.json +10 -10
  84. package/src/bill-history/bill-history.component.tsx +17 -25
  85. package/src/bill-history/bill-history.scss +4 -94
  86. package/src/bill-history/bill-history.test.tsx +37 -78
  87. package/src/bill-item-actions/bill-item-actions.scss +0 -4
  88. package/src/bill-item-actions/{edit-bill-item.component.tsx → edit-bill-item.modal.tsx} +58 -56
  89. package/src/bill-item-actions/edit-bill-item.test.tsx +22 -24
  90. package/src/billable-services/bill-waiver/bill-selection.component.tsx +2 -2
  91. package/src/billable-services/bill-waiver/patient-bills.component.tsx +3 -3
  92. package/src/billable-services/billable-service.resource.ts +4 -3
  93. package/src/billable-services/billable-services-home.component.tsx +1 -1
  94. package/src/billable-services/billable-services.component.tsx +115 -132
  95. package/src/billable-services/billable-services.scss +3 -0
  96. package/src/billable-services/billable-services.test.tsx +2 -45
  97. package/src/billable-services/cash-point/add-cash-point.modal.tsx +168 -0
  98. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +17 -192
  99. package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
  100. package/src/billable-services/create-edit/add-billable-service.component.tsx +28 -24
  101. package/src/billable-services/create-edit/add-billable-service.scss +2 -5
  102. package/src/billable-services/create-edit/add-billable-service.test.tsx +6 -6
  103. package/src/billable-services/create-edit/edit-billable-service.modal.tsx +50 -0
  104. package/src/billable-services/payment-modes/add-payment-mode.modal.tsx +121 -0
  105. package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +72 -0
  106. package/src/billable-services/payment-modes/payment-modes-config.component.tsx +125 -0
  107. package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -1
  108. package/src/billing-form/billing-checkin-form.component.tsx +2 -3
  109. package/src/billing-form/billing-checkin-form.test.tsx +0 -2
  110. package/src/billing-form/billing-form.component.tsx +210 -268
  111. package/src/billing-form/billing-form.scss +143 -0
  112. package/src/billing.resource.ts +16 -19
  113. package/src/bills-table/bills-table.test.tsx +97 -53
  114. package/src/config-schema.ts +52 -18
  115. package/src/dashboard.meta.ts +4 -2
  116. package/src/helpers/functions.ts +5 -4
  117. package/src/index.ts +17 -6
  118. package/src/invoice/invoice-table.component.tsx +24 -54
  119. package/src/invoice/invoice-table.scss +1 -5
  120. package/src/invoice/invoice-table.test.tsx +21 -47
  121. package/src/invoice/invoice.component.tsx +36 -29
  122. package/src/invoice/invoice.scss +7 -4
  123. package/src/invoice/invoice.test.tsx +22 -48
  124. package/src/invoice/payments/payment-form/payment-form.component.tsx +2 -9
  125. package/src/invoice/payments/payment-form/payment-form.test.tsx +14 -46
  126. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  127. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  128. package/src/invoice/payments/payments.component.tsx +16 -27
  129. package/src/invoice/payments/{payments.component.test.tsx → payments.test.tsx} +24 -10
  130. package/src/invoice/payments/utils.ts +4 -22
  131. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
  132. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  133. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  134. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  135. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
  136. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
  137. package/src/invoice/printable-invoice/printable-invoice.component.tsx +19 -33
  138. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  139. package/src/modal/require-payment-modal.test.tsx +25 -20
  140. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +17 -18
  141. package/src/routes.json +22 -2
  142. package/src/types/index.ts +13 -12
  143. package/translations/am.json +33 -16
  144. package/translations/ar.json +33 -16
  145. package/translations/ar_SY.json +33 -16
  146. package/translations/bn.json +38 -21
  147. package/translations/de.json +33 -16
  148. package/translations/en.json +33 -16
  149. package/translations/en_US.json +187 -0
  150. package/translations/es.json +48 -31
  151. package/translations/es_MX.json +33 -16
  152. package/translations/fr.json +47 -30
  153. package/translations/he.json +33 -16
  154. package/translations/hi.json +33 -16
  155. package/translations/hi_IN.json +33 -16
  156. package/translations/id.json +180 -163
  157. package/translations/it.json +70 -53
  158. package/translations/ka.json +187 -0
  159. package/translations/km.json +33 -16
  160. package/translations/ku.json +33 -16
  161. package/translations/ky.json +33 -16
  162. package/translations/lg.json +33 -16
  163. package/translations/ne.json +33 -16
  164. package/translations/pl.json +33 -16
  165. package/translations/pt.json +33 -16
  166. package/translations/pt_BR.json +33 -16
  167. package/translations/qu.json +33 -16
  168. package/translations/ro_RO.json +182 -165
  169. package/translations/ru_RU.json +33 -16
  170. package/translations/si.json +33 -16
  171. package/translations/sw.json +33 -16
  172. package/translations/sw_KE.json +33 -16
  173. package/translations/tr.json +33 -16
  174. package/translations/tr_TR.json +33 -16
  175. package/translations/uk.json +33 -16
  176. package/translations/uz.json +33 -16
  177. package/translations/uz@Latn.json +33 -16
  178. package/translations/uz_UZ.json +33 -16
  179. package/translations/vi.json +33 -16
  180. package/translations/zh.json +33 -16
  181. package/translations/zh_CN.json +91 -74
  182. package/dist/1146.js.LICENSE.txt +0 -21
  183. package/dist/2352.js +0 -1
  184. package/dist/2352.js.map +0 -1
  185. package/dist/246.js +0 -1
  186. package/dist/246.js.map +0 -1
  187. package/dist/6525.js +0 -2
  188. package/dist/6525.js.map +0 -1
  189. package/dist/8556.js +0 -2
  190. package/dist/8556.js.map +0 -1
  191. package/dist/8638.js +0 -1
  192. package/dist/8638.js.map +0 -1
  193. package/dist/9968.js +0 -1
  194. package/dist/9968.js.map +0 -1
  195. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  196. /package/dist/{8556.js.LICENSE.txt → 7452.js.LICENSE.txt} +0 -0
@@ -6,7 +6,6 @@ import {
6
6
  DataTable,
7
7
  InlineLoading,
8
8
  Layer,
9
- Modal,
10
9
  OverflowMenu,
11
10
  OverflowMenuItem,
12
11
  Pagination,
@@ -21,25 +20,31 @@ import {
21
20
  Tile,
22
21
  } from '@carbon/react';
23
22
  import { ArrowRight } from '@carbon/react/icons';
24
- import { useLayoutType, isDesktop, useConfig, usePagination, ErrorState, navigate } from '@openmrs/esm-framework';
23
+ import {
24
+ useLayoutType,
25
+ isDesktop,
26
+ useConfig,
27
+ usePagination,
28
+ ErrorState,
29
+ navigate,
30
+ showModal,
31
+ getCoreTranslation,
32
+ } from '@openmrs/esm-framework';
25
33
  import { EmptyState } from '@openmrs/esm-patient-common-lib';
26
34
  import { type BillableService } from '../types/index';
27
35
  import { useBillableServices } from './billable-service.resource';
28
- import AddBillableService from './create-edit/add-billable-service.component';
36
+ import type { BillingConfig } from '../config-schema';
29
37
  import styles from './billable-services.scss';
30
38
 
31
39
  const BillableServices = () => {
32
40
  const { t } = useTranslation();
33
41
  const { billableServices, isLoading, isValidating, error, mutate } = useBillableServices();
34
42
  const layout = useLayoutType();
35
- const config = useConfig();
43
+ const { pageSize: configuredPageSize } = useConfig<BillingConfig>();
36
44
  const [searchString, setSearchString] = useState('');
37
45
  const responsiveSize = isDesktop(layout) ? 'lg' : 'sm';
38
- const pageSizes = config?.billableServices?.pageSizes ?? [10, 20, 30, 40, 50];
39
- const [pageSize, setPageSize] = useState(config?.billableServices?.pageSize ?? 10);
40
-
41
- const [showOverlay, setShowOverlay] = useState(false);
42
- const [editingService, setEditingService] = useState(null);
46
+ const pageSizes = [10, 20, 30, 40, 50];
47
+ const [pageSize, setPageSize] = useState(configuredPageSize ?? 10);
43
48
 
44
49
  const headerData = [
45
50
  {
@@ -55,7 +60,7 @@ const BillableServices = () => {
55
60
  key: 'serviceType',
56
61
  },
57
62
  {
58
- header: t('status', 'Service Status'),
63
+ header: t('serviceStatus', 'Service Status'),
59
64
  key: 'status',
60
65
  },
61
66
  {
@@ -63,15 +68,13 @@ const BillableServices = () => {
63
68
  key: 'prices',
64
69
  },
65
70
  {
66
- header: t('actions', 'Actions'),
71
+ header: getCoreTranslation('actions'),
67
72
  key: 'actions',
68
73
  },
69
74
  ];
70
75
 
71
76
  const launchBillableServiceForm = useCallback(() => {
72
77
  navigate({ to: window.getOpenmrsSpaBase() + 'billable-services/add-service' });
73
- setEditingService(null);
74
- setShowOverlay(true);
75
78
  }, []);
76
79
 
77
80
  const searchResults: BillableService[] = useMemo(() => {
@@ -103,16 +106,6 @@ const BillableServices = () => {
103
106
  serviceType: service?.serviceType?.display,
104
107
  status: service.serviceStatus,
105
108
  prices: '--',
106
- actions: (
107
- <TableCell>
108
- <OverflowMenu size="sm" flipped>
109
- <OverflowMenuItem
110
- itemText={t('editBillableService', 'Edit Billable Service')}
111
- onClick={() => handleEditService(service)}
112
- />
113
- </OverflowMenu>
114
- </TableCell>
115
- ),
116
109
  };
117
110
  let cost = '';
118
111
  service.servicePrices.forEach((price) => {
@@ -130,128 +123,118 @@ const BillableServices = () => {
130
123
  },
131
124
  [goTo, setSearchString],
132
125
  );
133
- const handleEditService = useCallback((service) => {
134
- setEditingService(service);
135
- setShowOverlay(true);
136
- }, []);
137
-
138
- const closeModal = useCallback(() => {
139
- setShowOverlay(false);
140
- setEditingService(null);
141
- }, []);
126
+ const handleEditService = useCallback(
127
+ (service) => {
128
+ showModal('edit-billable-service-modal', {
129
+ editingService: service,
130
+ onServiceUpdated: mutate,
131
+ });
132
+ },
133
+ [mutate],
134
+ );
142
135
 
143
136
  if (isLoading) {
144
- <InlineLoading status="active" iconDescription="Loading" description="Loading data..." />;
137
+ return <InlineLoading status="active" iconDescription="Loading" description="Loading data..." />;
145
138
  }
139
+
146
140
  if (error) {
147
- <ErrorState headerTitle={t('billableService', 'Billable Service')} error={error} />;
141
+ return <ErrorState headerTitle={t('billableService', 'Billable Service')} error={error} />;
148
142
  }
143
+
149
144
  if (billableServices.length === 0) {
150
- <EmptyState
151
- displayText={t('billableService', 'Billable Service')}
152
- headerTitle={t('billableService', 'Billable Service')}
153
- launchForm={launchBillableServiceForm}
154
- />;
145
+ return (
146
+ <EmptyState
147
+ displayText={t('billableServices__lower', 'billable services')}
148
+ headerTitle={t('billableService', 'Billable Service')}
149
+ launchForm={launchBillableServiceForm}
150
+ />
151
+ );
155
152
  }
156
153
 
157
154
  return (
158
- <>
159
- {billableServices?.length > 0 ? (
160
- <div className={styles.serviceContainer}>
161
- <FilterableTableHeader
162
- handleSearch={handleSearch}
163
- isValidating={isValidating}
164
- layout={layout}
165
- responsiveSize={responsiveSize}
166
- t={t}
167
- />
168
- <DataTable
169
- isSortable
170
- rows={rowData}
171
- headers={headerData}
172
- size={responsiveSize}
173
- useZebraStyles={rowData?.length > 1 ? true : false}>
174
- {({ rows, headers, getRowProps, getTableProps }) => (
175
- <TableContainer>
176
- <Table {...getTableProps()} aria-label="service list">
177
- <TableHead>
178
- <TableRow>
179
- {headers.map((header) => (
180
- <TableHeader key={header.key}>{header.header}</TableHeader>
181
- ))}
182
- </TableRow>
183
- </TableHead>
184
- <TableBody>
185
- {rows.map((row) => (
186
- <TableRow
187
- key={row.id}
188
- {...getRowProps({
189
- row,
190
- })}>
191
- {row.cells.map((cell) => (
192
- <TableCell key={cell.id}>{cell.value}</TableCell>
193
- ))}
194
- </TableRow>
155
+ <div className={styles.serviceContainer}>
156
+ <FilterableTableHeader
157
+ handleSearch={handleSearch}
158
+ isValidating={isValidating}
159
+ layout={layout}
160
+ responsiveSize={responsiveSize}
161
+ t={t}
162
+ />
163
+ <DataTable
164
+ isSortable
165
+ rows={rowData}
166
+ headers={headerData}
167
+ size={responsiveSize}
168
+ useZebraStyles={rowData?.length > 1 ? true : false}>
169
+ {({ rows, headers, getRowProps, getTableProps }) => (
170
+ <TableContainer>
171
+ <Table {...getTableProps()} aria-label="service list">
172
+ <TableHead>
173
+ <TableRow>
174
+ {headers.map((header) => (
175
+ <TableHeader key={header.key}>{header.header}</TableHeader>
176
+ ))}
177
+ </TableRow>
178
+ </TableHead>
179
+ <TableBody>
180
+ {rows.map((row) => (
181
+ <TableRow
182
+ key={row.id}
183
+ {...getRowProps({
184
+ row,
185
+ })}>
186
+ {row.cells.map((cell) => (
187
+ <TableCell key={cell.id}>{cell.value}</TableCell>
195
188
  ))}
196
- </TableBody>
197
- </Table>
198
- </TableContainer>
199
- )}
200
- </DataTable>
201
- {searchResults?.length === 0 && (
202
- <div className={styles.filterEmptyState}>
203
- <Layer level={0}>
204
- <Tile className={styles.filterEmptyStateTile}>
205
- <p className={styles.filterEmptyStateContent}>
206
- {t('noMatchingServicesToDisplay', 'No matching services to display')}
207
- </p>
208
- <p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
209
- </Tile>
210
- </Layer>
211
- </div>
212
- )}
213
- {paginated && (
214
- <Pagination
215
- forwardText="Next page"
216
- backwardText="Previous page"
217
- page={currentPage}
218
- pageSize={pageSize}
219
- pageSizes={pageSizes}
220
- totalItems={searchResults?.length}
221
- className={styles.pagination}
222
- size={responsiveSize}
223
- onChange={({ pageSize: newPageSize, page: newPage }) => {
224
- if (newPageSize !== pageSize) {
225
- setPageSize(newPageSize);
226
- }
227
- if (newPage !== currentPage) {
228
- goTo(newPage);
229
- }
230
- }}
231
- />
232
- )}
189
+ <TableCell className="cds--table-column-menu">
190
+ <OverflowMenu size="sm" flipped>
191
+ <OverflowMenuItem
192
+ className={styles.menuItem}
193
+ itemText={t('editBillableService', 'Edit Billable Service')}
194
+ onClick={() => handleEditService(results.find((service) => service.uuid === row.id))}
195
+ />
196
+ </OverflowMenu>
197
+ </TableCell>
198
+ </TableRow>
199
+ ))}
200
+ </TableBody>
201
+ </Table>
202
+ </TableContainer>
203
+ )}
204
+ </DataTable>
205
+ {searchResults?.length === 0 && (
206
+ <div className={styles.filterEmptyState}>
207
+ <Layer level={0}>
208
+ <Tile className={styles.filterEmptyStateTile}>
209
+ <p className={styles.filterEmptyStateContent}>
210
+ {t('noMatchingServicesToDisplay', 'No matching services to display')}
211
+ </p>
212
+ <p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
213
+ </Tile>
214
+ </Layer>
233
215
  </div>
234
- ) : (
235
- <EmptyState
236
- launchForm={launchBillableServiceForm}
237
- displayText={t('noServicesToDisplay', 'There are no services to display')}
238
- headerTitle={t('billableService', 'Billable service')}
239
- />
240
216
  )}
241
- {showOverlay && (
242
- <Modal
243
- open={showOverlay}
244
- modalHeading={t('billableService', 'Billable Service')}
245
- primaryButtonText={null}
246
- secondaryButtonText={t('cancel', 'Cancel')}
247
- onRequestClose={closeModal}
248
- onSecondarySubmit={closeModal}
249
- size="lg"
250
- passiveModal={true}>
251
- <AddBillableService editingService={editingService} onClose={closeModal} />
252
- </Modal>
217
+ {paginated && (
218
+ <Pagination
219
+ forwardText="Next page"
220
+ backwardText="Previous page"
221
+ page={currentPage}
222
+ pageSize={pageSize}
223
+ pageSizes={pageSizes}
224
+ totalItems={searchResults?.length}
225
+ className={styles.pagination}
226
+ size={responsiveSize}
227
+ onChange={({ pageSize: newPageSize, page: newPage }) => {
228
+ if (newPageSize !== pageSize) {
229
+ setPageSize(newPageSize);
230
+ }
231
+ if (newPage !== currentPage) {
232
+ goTo(newPage);
233
+ }
234
+ }}
235
+ />
253
236
  )}
254
- </>
237
+ </div>
255
238
  );
256
239
  };
257
240
 
@@ -217,3 +217,6 @@
217
217
  grid-template-columns: 16rem 1fr;
218
218
  }
219
219
 
220
+ .menuItem {
221
+ max-width: none;
222
+ }
@@ -4,55 +4,13 @@ import userEvent from '@testing-library/user-event';
4
4
  import BillableServices from './billable-services.component';
5
5
  import { useBillableServices } from './billable-service.resource';
6
6
 
7
- // Mock the resource
8
7
  jest.mock('./billable-service.resource', () => ({
9
8
  useBillableServices: jest.fn(),
10
9
  }));
11
10
 
12
- // Mock the empty state component
13
- jest.mock('@openmrs/esm-patient-common-lib', () => ({
14
- EmptyState: jest.fn(({ displayText, headerTitle }) => (
15
- <div data-testid="empty-state">
16
- <h1>{headerTitle}</h1>
17
- <p>{displayText}</p>
18
- </div>
19
- )),
20
- }));
21
-
22
- // Mock navigation
23
- jest.mock('@openmrs/esm-framework', () => ({
24
- useLayoutType: jest.fn(() => 'desktop'),
25
- isDesktop: jest.fn(() => true),
26
- useConfig: jest.fn(() => ({
27
- billableServices: {
28
- pageSizes: [10, 20, 30, 40, 50],
29
- pageSize: 10,
30
- },
31
- })),
32
- usePagination: jest.fn().mockImplementation((data) => ({
33
- currentPage: 1,
34
- goTo: jest.fn(),
35
- results: data,
36
- paginated: true,
37
- })),
38
- navigate: jest.fn(),
39
- ErrorState: jest.fn(({ error }) => <div>Error: {error?.message || error}</div>),
40
- }));
41
-
42
- // Mock i18next
43
- jest.mock('react-i18next', () => ({
44
- useTranslation: () => ({
45
- t: (key: string, fallback: string) => fallback || key,
46
- }),
47
- }));
48
-
49
11
  describe('BillableService', () => {
50
12
  const mockedUseBillableServices = useBillableServices as jest.Mock;
51
13
 
52
- beforeEach(() => {
53
- jest.clearAllMocks();
54
- });
55
-
56
14
  it('renders an empty state when there are no billable services', () => {
57
15
  mockedUseBillableServices.mockReturnValue({
58
16
  billableServices: [],
@@ -64,9 +22,8 @@ describe('BillableService', () => {
64
22
 
65
23
  render(<BillableServices />);
66
24
 
67
- expect(screen.getByTestId('empty-state')).toBeInTheDocument();
68
- expect(screen.getByText('Billable service')).toBeInTheDocument();
69
- expect(screen.getByText('There are no services to display')).toBeInTheDocument();
25
+ expect(screen.getByRole('button', { name: /record.*billable services/i })).toBeInTheDocument();
26
+ expect(screen.queryByRole('table')).not.toBeInTheDocument();
70
27
  });
71
28
 
72
29
  it('renders billable services table correctly', () => {
@@ -0,0 +1,168 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useForm, Controller } from 'react-hook-form';
4
+ import { z } from 'zod';
5
+ import { zodResolver } from '@hookform/resolvers/zod';
6
+ import { Button, Dropdown, Form, ModalBody, ModalFooter, ModalHeader, TextInput } from '@carbon/react';
7
+ import { showSnackbar, openmrsFetch, restBaseUrl, getCoreTranslation } from '@openmrs/esm-framework';
8
+
9
+ type CashPointFormValues = {
10
+ name: string;
11
+ uuid: string;
12
+ location: string;
13
+ };
14
+
15
+ interface AddCashPointModalProps {
16
+ closeModal: () => void;
17
+ onCashPointAdded: () => void;
18
+ }
19
+
20
+ const AddCashPointModal: React.FC<AddCashPointModalProps> = ({ closeModal, onCashPointAdded }) => {
21
+ const { t } = useTranslation();
22
+ const [locations, setLocations] = useState([]);
23
+
24
+ const cashPointSchema = z.object({
25
+ name: z.string().min(1, t('cashPointNameRequired', 'Cash Point Name is required')),
26
+ uuid: z
27
+ .string()
28
+ .min(1, t('uuidRequired', 'UUID is required'))
29
+ .regex(
30
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
31
+ t('invalidUuidFormat', 'Invalid UUID format'),
32
+ ),
33
+ location: z.string().min(1, t('locationRequired', 'Location is required')),
34
+ });
35
+
36
+ const {
37
+ control,
38
+ handleSubmit,
39
+ reset,
40
+ formState: { errors, isSubmitting },
41
+ } = useForm<CashPointFormValues>({
42
+ resolver: zodResolver(cashPointSchema),
43
+ defaultValues: {
44
+ name: '',
45
+ uuid: '',
46
+ location: '',
47
+ },
48
+ });
49
+
50
+ const fetchLocations = useCallback(async () => {
51
+ try {
52
+ const response = await openmrsFetch(`${restBaseUrl}/location?v=default`);
53
+ const allLocations = response.data.results.map((loc: any) => ({
54
+ id: loc.uuid,
55
+ label: loc.display,
56
+ }));
57
+ setLocations(allLocations);
58
+ } catch (err) {
59
+ showSnackbar({
60
+ title: getCoreTranslation('error'),
61
+ subtitle: t('errorFetchingLocations', 'An error occurred while fetching locations.'),
62
+ kind: 'error',
63
+ isLowContrast: false,
64
+ });
65
+ }
66
+ }, [t]);
67
+
68
+ useEffect(() => {
69
+ fetchLocations();
70
+ }, [fetchLocations]);
71
+
72
+ const onSubmit = async (data: CashPointFormValues) => {
73
+ try {
74
+ await openmrsFetch(`${restBaseUrl}/billing/cashPoint`, {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json',
78
+ },
79
+ body: {
80
+ name: data.name,
81
+ uuid: data.uuid,
82
+ location: { uuid: data.location },
83
+ },
84
+ });
85
+
86
+ showSnackbar({
87
+ title: t('success', 'Success'),
88
+ subtitle: t('cashPointSaved', 'Cash point was successfully saved.'),
89
+ kind: 'success',
90
+ });
91
+
92
+ closeModal();
93
+ reset({ name: '', uuid: '', location: '' });
94
+ onCashPointAdded();
95
+ } catch (err) {
96
+ showSnackbar({
97
+ title: getCoreTranslation('error'),
98
+ subtitle: err?.message || t('errorSavingCashPoint', 'An error occurred while saving the cash point.'),
99
+ kind: 'error',
100
+ isLowContrast: false,
101
+ });
102
+ }
103
+ };
104
+
105
+ return (
106
+ <>
107
+ <ModalHeader closeModal={closeModal} title={t('addCashPoint', 'Add Cash Point')} />
108
+ <Form onSubmit={handleSubmit(onSubmit)}>
109
+ <ModalBody>
110
+ <Controller
111
+ name="name"
112
+ control={control}
113
+ render={({ field }) => (
114
+ <TextInput
115
+ id="cash-point-name"
116
+ labelText={t('cashPointName', 'Cash Point Name')}
117
+ placeholder={t('cashPointNamePlaceholder', 'e.g., Pharmacy Cash Point')}
118
+ invalid={!!errors.name}
119
+ invalidText={errors.name?.message}
120
+ {...field}
121
+ />
122
+ )}
123
+ />
124
+ <Controller
125
+ name="uuid"
126
+ control={control}
127
+ render={({ field }) => (
128
+ <TextInput
129
+ id="cash-point-uuid"
130
+ labelText={t('cashPointUuid', 'Cash Point UUID')}
131
+ placeholder={t('cashPointUuidPlaceholder', 'Enter UUID')}
132
+ invalid={!!errors.uuid}
133
+ invalidText={errors.uuid?.message}
134
+ {...field}
135
+ />
136
+ )}
137
+ />
138
+ <Controller
139
+ name="location"
140
+ control={control}
141
+ render={({ field }) => (
142
+ <Dropdown
143
+ id="cash-point-location"
144
+ label={t('selectLocation', 'Select Location')}
145
+ titleText={t('cashPointLocation', 'Cash Point Location')}
146
+ items={locations}
147
+ selectedItem={locations.find((loc) => loc.id === field.value)}
148
+ onChange={({ selectedItem }) => field.onChange(selectedItem?.id)}
149
+ invalid={!!errors.location}
150
+ invalidText={errors.location?.message}
151
+ />
152
+ )}
153
+ />
154
+ </ModalBody>
155
+ <ModalFooter>
156
+ <Button kind="secondary" onClick={closeModal}>
157
+ {getCoreTranslation('cancel')}
158
+ </Button>
159
+ <Button type="submit" disabled={isSubmitting}>
160
+ {isSubmitting ? t('saving', 'Saving') + '...' : getCoreTranslation('save')}
161
+ </Button>
162
+ </ModalFooter>
163
+ </Form>
164
+ </>
165
+ );
166
+ };
167
+
168
+ export default AddCashPointModal;