@openmrs/esm-billing-app 1.0.2-pre.84 → 1.0.2-pre.849

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.eslintrc +16 -2
  2. package/README.md +54 -9
  3. package/__mocks__/bills.mock.ts +12 -0
  4. package/__mocks__/react-i18next.js +6 -5
  5. package/dist/1119.js +1 -1
  6. package/dist/1146.js +1 -2
  7. package/dist/1146.js.map +1 -1
  8. package/dist/1197.js +1 -1
  9. package/dist/1856.js +1 -0
  10. package/dist/1856.js.map +1 -0
  11. package/dist/2146.js +1 -1
  12. package/dist/2177.js +2 -0
  13. package/dist/2177.js.LICENSE.txt +9 -0
  14. package/dist/2177.js.map +1 -0
  15. package/dist/2524.js +1 -0
  16. package/dist/2524.js.map +1 -0
  17. package/dist/2690.js +1 -1
  18. package/dist/3041.js +1 -0
  19. package/dist/3041.js.map +1 -0
  20. package/dist/3099.js +1 -1
  21. package/dist/3584.js +1 -1
  22. package/dist/3717.js +2 -0
  23. package/dist/3717.js.map +1 -0
  24. package/dist/4055.js +1 -1
  25. package/dist/4132.js +1 -1
  26. package/dist/4225.js +1 -0
  27. package/dist/4225.js.map +1 -0
  28. package/dist/4300.js +1 -1
  29. package/dist/4335.js +1 -1
  30. package/dist/4344.js +1 -0
  31. package/dist/4344.js.map +1 -0
  32. package/dist/4618.js +1 -1
  33. package/dist/4652.js +1 -1
  34. package/dist/4724.js +1 -0
  35. package/dist/4724.js.map +1 -0
  36. package/dist/4739.js +1 -1
  37. package/dist/4739.js.map +1 -1
  38. package/dist/4944.js +1 -1
  39. package/dist/5173.js +1 -1
  40. package/dist/5241.js +1 -1
  41. package/dist/5422.js +1 -0
  42. package/dist/5422.js.map +1 -0
  43. package/dist/5442.js +1 -1
  44. package/dist/5661.js +1 -1
  45. package/dist/6022.js +1 -1
  46. package/dist/6295.js +2 -0
  47. package/dist/{6525.js.LICENSE.txt → 6295.js.LICENSE.txt} +16 -4
  48. package/dist/6295.js.map +1 -0
  49. package/dist/6468.js +1 -1
  50. package/dist/6540.js +1 -1
  51. package/dist/6540.js.map +1 -1
  52. package/dist/6606.js +1 -0
  53. package/dist/6606.js.map +1 -0
  54. package/dist/6679.js +1 -1
  55. package/dist/6840.js +1 -1
  56. package/dist/6859.js +1 -1
  57. package/dist/7097.js +1 -1
  58. package/dist/7159.js +1 -1
  59. package/dist/723.js +1 -1
  60. package/dist/7617.js +1 -1
  61. package/dist/795.js +1 -1
  62. package/dist/8163.js +1 -1
  63. package/dist/8349.js +1 -1
  64. package/dist/8618.js +1 -1
  65. package/dist/890.js +1 -1
  66. package/dist/9214.js +1 -1
  67. package/dist/9538.js +1 -1
  68. package/dist/9569.js +1 -1
  69. package/dist/961.js +1 -1
  70. package/dist/961.js.map +1 -1
  71. package/dist/986.js +1 -1
  72. package/dist/9879.js +1 -1
  73. package/dist/9895.js +1 -1
  74. package/dist/9900.js +1 -1
  75. package/dist/9913.js +1 -1
  76. package/dist/main.js +1 -1
  77. package/dist/main.js.map +1 -1
  78. package/dist/openmrs-esm-billing-app.js +1 -1
  79. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +388 -282
  80. package/dist/openmrs-esm-billing-app.js.map +1 -1
  81. package/dist/routes.json +1 -1
  82. package/e2e/README.md +19 -18
  83. package/e2e/core/test.ts +1 -1
  84. package/e2e/fixtures/api.ts +1 -1
  85. package/e2e/specs/sample-test.spec.ts +0 -1
  86. package/e2e/support/github/Dockerfile +1 -1
  87. package/package.json +13 -10
  88. package/src/bill-history/bill-history.component.tsx +20 -28
  89. package/src/bill-history/bill-history.scss +4 -94
  90. package/src/bill-history/bill-history.test.tsx +37 -78
  91. package/src/bill-item-actions/bill-item-actions.scss +21 -5
  92. package/src/bill-item-actions/edit-bill-item.modal.tsx +225 -0
  93. package/src/bill-item-actions/edit-bill-item.test.tsx +214 -40
  94. package/src/billable-services/bill-waiver/bill-selection.component.tsx +5 -5
  95. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +28 -32
  96. package/src/billable-services/bill-waiver/patient-bills.component.tsx +7 -7
  97. package/src/billable-services/bill-waiver/utils.ts +13 -3
  98. package/src/billable-services/billable-service.resource.ts +28 -12
  99. package/src/billable-services/billable-services-home.component.tsx +4 -4
  100. package/src/billable-services/billable-services.component.tsx +149 -148
  101. package/src/billable-services/billable-services.scss +3 -0
  102. package/src/billable-services/billable-services.test.tsx +6 -49
  103. package/src/billable-services/cash-point/add-cash-point.modal.tsx +168 -0
  104. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +19 -193
  105. package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
  106. package/src/billable-services/create-edit/add-billable-service.component.tsx +356 -300
  107. package/src/billable-services/create-edit/add-billable-service.scss +6 -65
  108. package/src/billable-services/create-edit/add-billable-service.test.tsx +167 -81
  109. package/src/billable-services/create-edit/edit-billable-service.modal.tsx +51 -0
  110. package/src/billable-services/dashboard/service-metrics.component.tsx +11 -3
  111. package/src/billable-services/payment-modes/add-payment-mode.modal.tsx +121 -0
  112. package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +72 -0
  113. package/src/billable-services/payment-modes/payment-modes-config.component.tsx +125 -0
  114. package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -4
  115. package/src/billing-dashboard/billing-dashboard.scss +1 -1
  116. package/src/billing-form/billing-checkin-form.component.tsx +21 -17
  117. package/src/billing-form/billing-checkin-form.test.tsx +99 -26
  118. package/src/billing-form/billing-form.component.tsx +222 -292
  119. package/src/billing-form/billing-form.scss +143 -0
  120. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +1 -1
  121. package/src/billing.resource.ts +69 -74
  122. package/src/bills-table/bills-table.component.tsx +3 -3
  123. package/src/bills-table/bills-table.test.tsx +98 -54
  124. package/src/config-schema.ts +52 -24
  125. package/src/dashboard.meta.ts +4 -2
  126. package/src/helpers/functions.ts +5 -4
  127. package/src/index.ts +17 -6
  128. package/src/invoice/invoice-table.component.tsx +36 -70
  129. package/src/invoice/invoice-table.scss +8 -5
  130. package/src/invoice/invoice-table.test.tsx +273 -62
  131. package/src/invoice/invoice.component.tsx +39 -32
  132. package/src/invoice/invoice.scss +11 -4
  133. package/src/invoice/invoice.test.tsx +324 -120
  134. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +9 -9
  135. package/src/invoice/payments/payment-form/payment-form.component.tsx +43 -34
  136. package/src/invoice/payments/payment-form/payment-form.scss +5 -6
  137. package/src/invoice/payments/payment-form/payment-form.test.tsx +216 -66
  138. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  139. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  140. package/src/invoice/payments/payments.component.tsx +55 -67
  141. package/src/invoice/payments/payments.scss +4 -3
  142. package/src/invoice/payments/payments.test.tsx +282 -0
  143. package/src/invoice/payments/utils.ts +15 -27
  144. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
  145. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  146. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  147. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  148. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
  149. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
  150. package/src/invoice/printable-invoice/printable-invoice.component.tsx +20 -34
  151. package/src/left-panel-link.test.tsx +1 -4
  152. package/src/metrics-cards/metrics-cards.component.tsx +12 -2
  153. package/src/metrics-cards/metrics-cards.scss +4 -0
  154. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  155. package/src/modal/require-payment-modal.test.tsx +27 -22
  156. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +18 -19
  157. package/src/routes.json +25 -7
  158. package/src/types/index.ts +80 -18
  159. package/translations/am.json +125 -74
  160. package/translations/ar.json +126 -75
  161. package/translations/ar_SY.json +126 -75
  162. package/translations/bn.json +128 -77
  163. package/translations/de.json +126 -75
  164. package/translations/en.json +126 -75
  165. package/translations/en_US.json +126 -75
  166. package/translations/es.json +125 -74
  167. package/translations/es_MX.json +126 -75
  168. package/translations/fr.json +131 -80
  169. package/translations/he.json +125 -74
  170. package/translations/hi.json +126 -75
  171. package/translations/hi_IN.json +126 -75
  172. package/translations/id.json +126 -75
  173. package/translations/it.json +152 -101
  174. package/translations/ka.json +126 -75
  175. package/translations/km.json +125 -74
  176. package/translations/ku.json +126 -75
  177. package/translations/ky.json +126 -75
  178. package/translations/lg.json +126 -75
  179. package/translations/ne.json +126 -75
  180. package/translations/pl.json +126 -75
  181. package/translations/pt.json +126 -75
  182. package/translations/pt_BR.json +126 -75
  183. package/translations/qu.json +126 -75
  184. package/translations/ro_RO.json +216 -165
  185. package/translations/ru_RU.json +126 -75
  186. package/translations/si.json +126 -75
  187. package/translations/sw.json +126 -75
  188. package/translations/sw_KE.json +126 -75
  189. package/translations/tr.json +126 -75
  190. package/translations/tr_TR.json +126 -75
  191. package/translations/uk.json +126 -75
  192. package/translations/uz.json +126 -75
  193. package/translations/uz@Latn.json +126 -75
  194. package/translations/uz_UZ.json +126 -75
  195. package/translations/vi.json +126 -75
  196. package/translations/zh.json +126 -75
  197. package/translations/zh_CN.json +158 -107
  198. package/dist/1146.js.LICENSE.txt +0 -21
  199. package/dist/2352.js +0 -1
  200. package/dist/2352.js.map +0 -1
  201. package/dist/246.js +0 -1
  202. package/dist/246.js.map +0 -1
  203. package/dist/6525.js +0 -2
  204. package/dist/6525.js.map +0 -1
  205. package/dist/8556.js +0 -2
  206. package/dist/8556.js.map +0 -1
  207. package/dist/8638.js +0 -1
  208. package/dist/8638.js.map +0 -1
  209. package/dist/9968.js +0 -1
  210. package/dist/9968.js.map +0 -1
  211. package/src/bill-item-actions/edit-bill-item.component.tsx +0 -221
  212. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  213. package/src/invoice/payments/payments.component.test.tsx +0 -121
  214. /package/dist/{8556.js.LICENSE.txt → 3717.js.LICENSE.txt} +0 -0
@@ -6,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,65 +20,75 @@ 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
+ ErrorState,
25
+ getCoreTranslation,
26
+ isDesktop,
27
+ navigate,
28
+ showModal,
29
+ useConfig,
30
+ useLayoutType,
31
+ usePagination,
32
+ type LayoutType,
33
+ } from '@openmrs/esm-framework';
25
34
  import { EmptyState } from '@openmrs/esm-patient-common-lib';
26
35
  import { type BillableService } from '../types/index';
27
36
  import { useBillableServices } from './billable-service.resource';
28
- import AddBillableService from './create-edit/add-billable-service.component';
37
+ import type { BillingConfig } from '../config-schema';
29
38
  import styles from './billable-services.scss';
30
39
 
40
+ interface FilterableTableHeaderProps {
41
+ layout: LayoutType;
42
+ handleSearch: (e: React.ChangeEvent<HTMLInputElement>) => void;
43
+ isValidating: boolean;
44
+ responsiveSize: 'sm' | 'md' | 'lg';
45
+ t: (key: string, fallback: string) => string;
46
+ }
47
+
31
48
  const BillableServices = () => {
32
49
  const { t } = useTranslation();
33
50
  const { billableServices, isLoading, isValidating, error, mutate } = useBillableServices();
34
51
  const layout = useLayoutType();
35
- const config = useConfig();
52
+ const { pageSize: configuredPageSize } = useConfig<BillingConfig>();
36
53
  const [searchString, setSearchString] = useState('');
37
54
  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);
55
+ const pageSizes = [10, 20, 30, 40, 50];
56
+ const [pageSize, setPageSize] = useState(configuredPageSize ?? 10);
43
57
 
44
58
  const headerData = [
45
59
  {
46
- header: t('serviceName', 'Service Name'),
60
+ header: t('serviceName', 'Service name'),
47
61
  key: 'serviceName',
48
62
  },
49
63
  {
50
- header: t('shortName', 'Short Name'),
64
+ header: t('shortName', 'Short name'),
51
65
  key: 'shortName',
52
66
  },
53
67
  {
54
- header: t('serviceType', 'Service Type'),
68
+ header: t('serviceType', 'Service type'),
55
69
  key: 'serviceType',
56
70
  },
57
71
  {
58
- header: t('status', 'Service Status'),
72
+ header: t('serviceStatus', 'Service status'),
59
73
  key: 'status',
60
74
  },
61
75
  {
62
76
  header: t('prices', 'Prices'),
63
77
  key: 'prices',
64
78
  },
65
- {
66
- header: t('actions', 'Actions'),
67
- key: 'actions',
68
- },
69
79
  ];
70
80
 
71
81
  const launchBillableServiceForm = useCallback(() => {
72
82
  navigate({ to: window.getOpenmrsSpaBase() + 'billable-services/add-service' });
73
- setEditingService(null);
74
- setShowOverlay(true);
75
83
  }, []);
76
84
 
77
85
  const searchResults: BillableService[] = useMemo(() => {
78
86
  const flatBillableServices = Array.isArray(billableServices) ? billableServices.flat() : billableServices;
79
87
 
80
88
  if (flatBillableServices !== undefined && flatBillableServices.length > 0) {
81
- if (searchString && searchString.trim() !== '') {
82
- const search = searchString.toLowerCase();
89
+ const trimmedSearch = searchString.trim();
90
+ if (trimmedSearch) {
91
+ const search = trimmedSearch.toLowerCase();
83
92
  return flatBillableServices.filter((service: BillableService) =>
84
93
  Object.entries(service).some(([header, value]) => {
85
94
  return header === 'uuid' ? false : `${value}`.toLowerCase().includes(search);
@@ -94,31 +103,16 @@ const BillableServices = () => {
94
103
  const rowData = [];
95
104
 
96
105
  if (results) {
97
- results.forEach((service, index) => {
106
+ results.forEach((service) => {
98
107
  const s = {
99
- id: `${index}`,
108
+ id: service.uuid,
100
109
  uuid: service.uuid,
101
110
  serviceName: service.name,
102
111
  shortName: service.shortName,
103
112
  serviceType: service?.serviceType?.display,
104
113
  status: service.serviceStatus,
105
- 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
- ),
114
+ prices: service.servicePrices.map((price) => `${price.name} (${price.price})`).join(', ') || '--',
116
115
  };
117
- let cost = '';
118
- service.servicePrices.forEach((price) => {
119
- cost += `${price.name} (${price.price}) `;
120
- });
121
- s.prices = cost;
122
116
  rowData.push(s);
123
117
  });
124
118
  }
@@ -130,132 +124,138 @@ const BillableServices = () => {
130
124
  },
131
125
  [goTo, setSearchString],
132
126
  );
133
- const handleEditService = useCallback((service) => {
134
- setEditingService(service);
135
- setShowOverlay(true);
136
- }, []);
137
127
 
138
- const closeModal = useCallback(() => {
139
- setShowOverlay(false);
140
- setEditingService(null);
141
- }, []);
128
+ const handleEditService = useCallback(
129
+ (service: BillableService) => {
130
+ const dispose = showModal('edit-billable-service-modal', {
131
+ serviceToEdit: service,
132
+ onServiceUpdated: mutate,
133
+ closeModal: () => dispose(),
134
+ });
135
+ },
136
+ [mutate],
137
+ );
142
138
 
143
139
  if (isLoading) {
144
- <InlineLoading status="active" iconDescription="Loading" description="Loading data..." />;
140
+ return (
141
+ <InlineLoading
142
+ status="active"
143
+ iconDescription={getCoreTranslation('loading')}
144
+ description={t('loading', 'Loading data') + '...'}
145
+ />
146
+ );
145
147
  }
148
+
146
149
  if (error) {
147
- <ErrorState headerTitle={t('billableService', 'Billable Service')} error={error} />;
150
+ return <ErrorState headerTitle={t('billableService', 'Billable service')} error={error} />;
148
151
  }
152
+
149
153
  if (billableServices.length === 0) {
150
- <EmptyState
151
- displayText={t('billableService', 'Billable Service')}
152
- headerTitle={t('billableService', 'Billable Service')}
153
- launchForm={launchBillableServiceForm}
154
- />;
154
+ return (
155
+ <EmptyState
156
+ displayText={t('billableServices__lower', 'billable services')}
157
+ headerTitle={t('billableService', 'Billable service')}
158
+ launchForm={launchBillableServiceForm}
159
+ />
160
+ );
155
161
  }
156
162
 
157
163
  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>
164
+ <div className={styles.serviceContainer}>
165
+ <FilterableTableHeader
166
+ handleSearch={handleSearch}
167
+ isValidating={isValidating}
168
+ layout={layout}
169
+ responsiveSize={responsiveSize}
170
+ t={t}
171
+ />
172
+ <DataTable
173
+ isSortable
174
+ rows={rowData}
175
+ headers={headerData}
176
+ overflowMenuOnHover={isDesktop(layout)}
177
+ size={responsiveSize}
178
+ useZebraStyles={rowData?.length > 1}>
179
+ {({ rows, headers, getHeaderProps, getRowProps, getTableProps }) => (
180
+ <TableContainer>
181
+ <Table {...getTableProps()} aria-label={t('serviceList', 'Service list')}>
182
+ <TableHead>
183
+ <TableRow>
184
+ {headers.map((header) => (
185
+ <TableHeader
186
+ {...getHeaderProps({
187
+ header,
188
+ })}
189
+ key={header.key}>
190
+ {header.header}
191
+ </TableHeader>
192
+ ))}
193
+ <TableHeader aria-label={getCoreTranslation('actions')} />
194
+ </TableRow>
195
+ </TableHead>
196
+ <TableBody>
197
+ {rows.map((row) => (
198
+ <TableRow
199
+ key={row.id}
200
+ {...getRowProps({
201
+ row,
202
+ })}>
203
+ {row.cells.map((cell) => (
204
+ <TableCell key={cell.id}>{cell.value}</TableCell>
195
205
  ))}
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
- )}
206
+ <TableCell className="cds--table-column-menu">
207
+ <OverflowMenu size="lg" flipped>
208
+ <OverflowMenuItem
209
+ className={styles.menuItem}
210
+ itemText={t('editBillableService', 'Edit billable service')}
211
+ onClick={() => handleEditService(results.find((service) => service.uuid === row.id))}
212
+ />
213
+ </OverflowMenu>
214
+ </TableCell>
215
+ </TableRow>
216
+ ))}
217
+ </TableBody>
218
+ </Table>
219
+ </TableContainer>
220
+ )}
221
+ </DataTable>
222
+ {searchResults?.length === 0 && (
223
+ <div className={styles.filterEmptyState}>
224
+ <Layer level={0}>
225
+ <Tile className={styles.filterEmptyStateTile}>
226
+ <p className={styles.filterEmptyStateContent}>
227
+ {t('noMatchingServicesToDisplay', 'No matching services to display')}
228
+ </p>
229
+ <p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
230
+ </Tile>
231
+ </Layer>
233
232
  </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
233
  )}
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>
234
+ {paginated && (
235
+ <Pagination
236
+ forwardText={t('nextPage', 'Next page')}
237
+ backwardText={t('previousPage', 'Previous page')}
238
+ page={currentPage}
239
+ pageSize={pageSize}
240
+ pageSizes={pageSizes}
241
+ totalItems={searchResults?.length}
242
+ className={styles.pagination}
243
+ size={responsiveSize}
244
+ onChange={({ pageSize: newPageSize, page: newPage }) => {
245
+ if (newPageSize !== pageSize) {
246
+ setPageSize(newPageSize);
247
+ }
248
+ if (newPage !== currentPage) {
249
+ goTo(newPage);
250
+ }
251
+ }}
252
+ />
253
253
  )}
254
- </>
254
+ </div>
255
255
  );
256
256
  };
257
257
 
258
- function FilterableTableHeader({ layout, handleSearch, isValidating, responsiveSize, t }) {
258
+ function FilterableTableHeader({ layout, handleSearch, isValidating, responsiveSize, t }: FilterableTableHeaderProps) {
259
259
  return (
260
260
  <>
261
261
  <div className={styles.headerContainer}>
@@ -291,4 +291,5 @@ function FilterableTableHeader({ layout, handleSearch, isValidating, responsiveS
291
291
  </>
292
292
  );
293
293
  }
294
+
294
295
  export default BillableServices;
@@ -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', () => {
@@ -100,10 +57,10 @@ describe('BillableService', () => {
100
57
  render(<BillableServices />);
101
58
 
102
59
  // Check table headers
103
- expect(screen.getByText('Service Name')).toBeInTheDocument();
104
- expect(screen.getByText('Short Name')).toBeInTheDocument();
105
- expect(screen.getByText('Service Type')).toBeInTheDocument();
106
- expect(screen.getByText('Service Status')).toBeInTheDocument();
60
+ expect(screen.getByText('Service name')).toBeInTheDocument();
61
+ expect(screen.getByText('Short name')).toBeInTheDocument();
62
+ expect(screen.getByText('Service type')).toBeInTheDocument();
63
+ expect(screen.getByText('Service status')).toBeInTheDocument();
107
64
 
108
65
  // Check service data
109
66
  expect(screen.getByText('Service 1')).toBeInTheDocument();
@@ -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', 'For example, 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;