@digihmis/esm-billing-app 1.0.0

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 (115) hide show
  1. package/README.md +4 -0
  2. package/dist/117.js +1 -0
  3. package/dist/117.js.map +1 -0
  4. package/dist/132.js +1 -0
  5. package/dist/132.js.map +1 -0
  6. package/dist/152.js +1 -0
  7. package/dist/152.js.map +1 -0
  8. package/dist/16.js +1 -0
  9. package/dist/16.js.map +1 -0
  10. package/dist/209.js +1 -0
  11. package/dist/209.js.map +1 -0
  12. package/dist/317.js +1 -0
  13. package/dist/317.js.map +1 -0
  14. package/dist/349.js +1 -0
  15. package/dist/349.js.map +1 -0
  16. package/dist/358.js +1 -0
  17. package/dist/358.js.map +1 -0
  18. package/dist/371.js +1 -0
  19. package/dist/371.js.map +1 -0
  20. package/dist/378.js +1 -0
  21. package/dist/378.js.map +1 -0
  22. package/dist/442.js +1 -0
  23. package/dist/442.js.map +1 -0
  24. package/dist/45.js +1 -0
  25. package/dist/45.js.map +1 -0
  26. package/dist/466.js +1 -0
  27. package/dist/466.js.map +1 -0
  28. package/dist/496.js +12 -0
  29. package/dist/496.js.map +1 -0
  30. package/dist/508.js +1 -0
  31. package/dist/508.js.map +1 -0
  32. package/dist/61.js +1 -0
  33. package/dist/61.js.map +1 -0
  34. package/dist/640.js +1 -0
  35. package/dist/640.js.map +1 -0
  36. package/dist/689.js +1 -0
  37. package/dist/689.js.map +1 -0
  38. package/dist/709.js +1 -0
  39. package/dist/709.js.map +1 -0
  40. package/dist/712.js +1 -0
  41. package/dist/712.js.map +1 -0
  42. package/dist/720.js +1 -0
  43. package/dist/720.js.map +1 -0
  44. package/dist/742.js +1 -0
  45. package/dist/742.js.map +1 -0
  46. package/dist/771.js +1 -0
  47. package/dist/771.js.map +1 -0
  48. package/dist/806.js +1 -0
  49. package/dist/806.js.map +1 -0
  50. package/dist/839.js +1 -0
  51. package/dist/839.js.map +1 -0
  52. package/dist/912.js +1 -0
  53. package/dist/912.js.map +1 -0
  54. package/dist/913.js +1 -0
  55. package/dist/913.js.map +1 -0
  56. package/dist/940.js +1 -0
  57. package/dist/940.js.map +1 -0
  58. package/dist/965.js +15 -0
  59. package/dist/965.js.map +1 -0
  60. package/dist/digihmis-esm-billing-app.js +6 -0
  61. package/dist/digihmis-esm-billing-app.js.buildmanifest.json +827 -0
  62. package/dist/digihmis-esm-billing-app.js.map +1 -0
  63. package/dist/main.js +17 -0
  64. package/dist/main.js.map +1 -0
  65. package/dist/routes.json +1 -0
  66. package/jest.config.js +26 -0
  67. package/package.json +51 -0
  68. package/rspack.config.js +1 -0
  69. package/src/billing-checkin-form/billing-checkin-form-schema/index.ts +11 -0
  70. package/src/billing-checkin-form/billing-checkin-form.extension.tsx +179 -0
  71. package/src/billing-checkin-form/billing-checkin-form.resource.ts +42 -0
  72. package/src/billing-checkin-form/billing-checkin-form.scss +82 -0
  73. package/src/billing-checkin-form/billing-patient-type-tag/billing-patient-type-tag.component.tsx +39 -0
  74. package/src/billing-checkin-form/billing-patient-type-tag/billing-patient-type-tag.resource.ts +57 -0
  75. package/src/billing-checkin-form/billing-patient-type-tag/billing-patient-type-tag.scss +4 -0
  76. package/src/billing-checkin-form/helper/index.ts +2 -0
  77. package/src/billing-checkin-form/type/index.ts +42 -0
  78. package/src/billing-checkin-form/visit-attributes/visit-attributes-form.component.tsx +244 -0
  79. package/src/billing-checkin-form/visit-attributes/visit-attributes-form.resource.ts +23 -0
  80. package/src/billing-checkin-form/visit-attributes/visit-attributes-form.scss +186 -0
  81. package/src/billing-dashboard/billing-dashboard.component.tsx +224 -0
  82. package/src/billing-dashboard/billing-dashboard.resource.ts +101 -0
  83. package/src/billing-dashboard/billing-dashboard.scss +166 -0
  84. package/src/billing-dashboard/metrics/metrics.resource.ts +142 -0
  85. package/src/billing-dashboard/type/index.ts +176 -0
  86. package/src/billing-header/page-header/page-header.component.tsx +35 -0
  87. package/src/billing-header/page-header/page-header.scss +76 -0
  88. package/src/billing-header/page-header/page-icon/icon-wrapper.extension.tsx +17 -0
  89. package/src/billing-timesheet-modal/billing-timesheet-modal.scss +56 -0
  90. package/src/billing-timesheet-modal/billing-timesheet-modal.tsx +281 -0
  91. package/src/billing-timesheet-modal/billing-timesheet-resource-modal.ts +37 -0
  92. package/src/bills-table/bills-table.component.tsx +323 -0
  93. package/src/bills-table/bills-table.scss +252 -0
  94. package/src/config-schema.ts +148 -0
  95. package/src/constants/index.ts +2 -0
  96. package/src/dashboard.meta.ts +16 -0
  97. package/src/declarations.d.ts +6 -0
  98. package/src/index.ts +37 -0
  99. package/src/leftpanel/left-panel.component.tsx +98 -0
  100. package/src/leftpanel/left-panel.scss +13 -0
  101. package/src/routes.json +58 -0
  102. package/src/setup-tests.ts +1 -0
  103. package/src/utils/index.ts +270 -0
  104. package/translations/am.json +18 -0
  105. package/translations/ar.json +18 -0
  106. package/translations/en.json +18 -0
  107. package/translations/es.json +18 -0
  108. package/translations/fr.json +18 -0
  109. package/translations/he.json +18 -0
  110. package/translations/km.json +18 -0
  111. package/translations/pt.json +18 -0
  112. package/translations/sw.json +18 -0
  113. package/translations/vi.json +18 -0
  114. package/translations/zh.json +18 -0
  115. package/tsconfig.json +5 -0
@@ -0,0 +1,56 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+ @use '@openmrs/esm-styleguide/src/vars' as *;
5
+
6
+ .patientHeader {
7
+ padding: layout.$spacing-05;
8
+ border-bottom: 1px solid colors.$gray-20;
9
+ background-color: colors.$gray-10;
10
+
11
+ .patientName {
12
+ @include type.type-style('heading-compact-01');
13
+ font-weight: 600;
14
+ margin-right: layout.$spacing-03;
15
+ }
16
+
17
+ .patientId {
18
+ @include type.type-style('body-compact-01');
19
+ font-weight: 600;
20
+ margin-right: layout.$spacing-02;
21
+ }
22
+
23
+ .patientDetails {
24
+ @include type.type-style('body-compact-01');
25
+ color: colors.$gray-70;
26
+ }
27
+ .currentLocation {
28
+ @include type.type-style('body-compact-01');
29
+ color: red;
30
+ float: right;
31
+ }
32
+ }
33
+ :global(.cds--select-input) {
34
+ background-color: colors.$gray-20;
35
+ }
36
+
37
+ @media (min-width: 82rem) {
38
+ :global(.cds--modal-container) {
39
+ width: 38%;
40
+ }
41
+ }
42
+
43
+ .providerInfo {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: layout.$spacing-03;
47
+ background-color: $ui-01;
48
+ }
49
+
50
+ .providerLabel {
51
+ margin: 0;
52
+ }
53
+
54
+ .providerName {
55
+ margin: 0;
56
+ }
@@ -0,0 +1,281 @@
1
+ import {
2
+ Button,
3
+ ModalFooter,
4
+ ModalHeader,
5
+ ModalBody,
6
+ InlineLoading,
7
+ Select,
8
+ SelectItem,
9
+ InlineNotification,
10
+ } from '@carbon/react';
11
+ import React, { useState } from 'react';
12
+ import { useTranslation } from 'react-i18next';
13
+ import { showSnackbar, useSession } from '@openmrs/esm-framework';
14
+ import { Logout } from '@carbon/react/icons';
15
+ import styles from './billing-timesheet-modal.scss';
16
+ import { useCashPoints, useCurrentTimesheet } from '../billing-dashboard/billing-dashboard.resource';
17
+ import { clockIn, clockOut } from './billing-timesheet-resource-modal';
18
+
19
+ interface BillingTimesheetModalProps {
20
+ closeModal?: () => void;
21
+ onClockInSuccess?: (sessionData: any) => void;
22
+ onClockOutSuccess?: () => void;
23
+ }
24
+
25
+ export const BillingTimesheetModal: React.FC<BillingTimesheetModalProps> = ({
26
+ closeModal,
27
+ onClockInSuccess,
28
+ onClockOutSuccess,
29
+ }) => {
30
+ const { t } = useTranslation();
31
+ const session = useSession();
32
+ const currentProvider = session?.currentProvider;
33
+ const providerUuid = currentProvider?.uuid;
34
+
35
+ const { activeTimesheet, isClockedIn, mutate: mutateTimesheet } = useCurrentTimesheet();
36
+ const { cashPoints, isLoading: loadingCashPoints, error: cashPointsError } = useCashPoints();
37
+
38
+ const [selectedCashPoint, setSelectedCashPoint] = useState('');
39
+ const [isSubmitting, setIsSubmitting] = useState(false);
40
+ const [errorMessage, setErrorMessage] = useState('');
41
+
42
+ const handleCashPointChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
43
+ setSelectedCashPoint(event.target.value);
44
+ setErrorMessage('');
45
+ };
46
+
47
+ const handleClockIn = async () => {
48
+ if (!selectedCashPoint) {
49
+ setErrorMessage(t('selectCashPointRequired', 'Please select a cash point'));
50
+ return;
51
+ }
52
+
53
+ if (!providerUuid) {
54
+ setErrorMessage(t('providerNotFound', 'Provider information not found'));
55
+ return;
56
+ }
57
+
58
+ setErrorMessage('');
59
+ setIsSubmitting(true);
60
+
61
+ try {
62
+ const response = await clockIn(providerUuid, selectedCashPoint);
63
+ await mutateTimesheet();
64
+
65
+ showSnackbar({
66
+ title: t('clockedIn', 'Clocked In'),
67
+ kind: 'success',
68
+ subtitle: t('successfullyClocked', 'You have successfully clocked in'),
69
+ });
70
+
71
+ if (onClockInSuccess) {
72
+ onClockInSuccess(response?.data);
73
+ }
74
+
75
+ closeModal?.();
76
+ } catch (error) {
77
+ const errorMsg =
78
+ error?.responseBody?.error?.message || t('clockInFailed', 'Failed to clock in. Please try again.');
79
+ setErrorMessage(errorMsg);
80
+
81
+ showSnackbar({
82
+ title: t('clockInError', 'Clock In Error'),
83
+ kind: 'error',
84
+ subtitle: errorMsg,
85
+ });
86
+
87
+ console.error('Clock in error:', error);
88
+ } finally {
89
+ setIsSubmitting(false);
90
+ }
91
+ };
92
+
93
+ const handleSwitchCashPoint = async () => {
94
+ if (!selectedCashPoint) {
95
+ setErrorMessage(t('selectCashPointRequired', 'Please select a cash point'));
96
+ return;
97
+ }
98
+
99
+ if (!activeTimesheet?.uuid) {
100
+ setErrorMessage(t('noActiveSession', 'No active session found'));
101
+ return;
102
+ }
103
+
104
+ if (!providerUuid) {
105
+ setErrorMessage(t('providerNotFound', 'Provider information not found'));
106
+ return;
107
+ }
108
+
109
+ setErrorMessage('');
110
+ setIsSubmitting(true);
111
+
112
+ try {
113
+ // Clock out from current cash point
114
+ await clockOut(activeTimesheet.uuid);
115
+
116
+ // Clock in to new cash point
117
+ const response = await clockIn(providerUuid, selectedCashPoint);
118
+ await mutateTimesheet();
119
+
120
+ showSnackbar({
121
+ title: t('switchedCashPoint', 'Cash Point Switched'),
122
+ kind: 'success',
123
+ subtitle: t('successfullySwitched', 'You have successfully switched cash points'),
124
+ });
125
+
126
+ if (onClockInSuccess) {
127
+ onClockInSuccess(response?.data);
128
+ }
129
+
130
+ closeModal?.();
131
+ } catch (error) {
132
+ const errorMsg =
133
+ error?.responseBody?.error?.message || t('switchFailed', 'Failed to switch cash points. Please try again.');
134
+ setErrorMessage(errorMsg);
135
+
136
+ showSnackbar({
137
+ title: t('switchError', 'Switch Error'),
138
+ kind: 'error',
139
+ subtitle: errorMsg,
140
+ });
141
+
142
+ console.error('Switch cash point error:', error);
143
+ } finally {
144
+ setIsSubmitting(false);
145
+ }
146
+ };
147
+
148
+ // Handle loading and error states
149
+ if (cashPointsError) {
150
+ return (
151
+ <>
152
+ <ModalHeader closeModal={closeModal} title={t('billingSession', 'Billing Session')} />
153
+ <ModalBody>
154
+ <InlineNotification
155
+ kind="error"
156
+ title={t('error', 'Error')}
157
+ subtitle={t('errorLoadingCashPoints', 'Error loading cash points. Please try again.')}
158
+ lowContrast
159
+ hideCloseButton
160
+ />
161
+ </ModalBody>
162
+ <ModalFooter>
163
+ <Button kind="secondary" onClick={closeModal} type="button">
164
+ {t('close', 'Close')}
165
+ </Button>
166
+ </ModalFooter>
167
+ </>
168
+ );
169
+ }
170
+
171
+ return (
172
+ <>
173
+ <ModalHeader closeModal={closeModal}>
174
+ <div className={styles.patientHeader}>
175
+ <span className={styles.patientName}>
176
+ {isClockedIn ? t('switchCashPoint', 'Switch Cash Point') : t('clockInToBilling', 'Clock In to Billing')}
177
+ </span>
178
+ {isClockedIn && activeTimesheet && (
179
+ <span className={styles.currentLocation}>
180
+ ·{' '}
181
+ {t('currentlyActiveAt', 'Currently active at {{location}}', {
182
+ location: activeTimesheet.cashPoint?.name || t('unknown', 'Unknown'),
183
+ })}
184
+ </span>
185
+ )}
186
+ </div>
187
+ </ModalHeader>
188
+
189
+ <ModalBody>
190
+ {errorMessage && (
191
+ <InlineNotification
192
+ kind="error"
193
+ title={t('error', 'Error')}
194
+ subtitle={errorMessage}
195
+ lowContrast
196
+ hideCloseButton
197
+ className={styles.errorNotification}
198
+ />
199
+ )}
200
+
201
+ {loadingCashPoints ? (
202
+ <InlineLoading description={t('loadingCashPoints', 'Loading cash points...')} />
203
+ ) : (
204
+ <div className={styles.modalContent}>
205
+ {isClockedIn && activeTimesheet && (
206
+ <div
207
+ style={{
208
+ marginBottom: '1rem',
209
+ padding: '0.75rem',
210
+ backgroundColor: '#fff3cd',
211
+ borderLeft: '4px solid #ffc107',
212
+ borderRadius: '4px',
213
+ }}>
214
+ <p style={{ margin: 0, fontSize: '0.875rem' }}>
215
+ <strong>{t('switchingCashPoints', 'Switching Cash Points')}</strong>
216
+ </p>
217
+ <p style={{ margin: '0.25rem 0 0 0', fontSize: '0.875rem' }}>
218
+ {t(
219
+ 'willAutoTimesheetClockOut',
220
+ 'You will be automatically clocked out from {{current}} and clocked in to your selected cash point.',
221
+ {
222
+ current: activeTimesheet?.cashPoint?.name,
223
+ },
224
+ )}
225
+ </p>
226
+ </div>
227
+ )}
228
+
229
+ <Select
230
+ id="cashPoint"
231
+ labelText={
232
+ isClockedIn
233
+ ? t('selectCashPointToSwitch', 'Select a cash point to switch to:')
234
+ : t('selectCashPointToClockIn', 'Select a cash point to clock in:')
235
+ }
236
+ value={selectedCashPoint}
237
+ onChange={handleCashPointChange}
238
+ disabled={isSubmitting}
239
+ className={styles.cashPointSelect}>
240
+ <SelectItem value="" text={t('chooseCashPoint', 'Choose a cash point')} />
241
+ {cashPoints?.map((cashPoint) => (
242
+ <SelectItem key={cashPoint.uuid} value={cashPoint.uuid} text={`${cashPoint.name}`} />
243
+ ))}
244
+ </Select>
245
+ </div>
246
+ )}
247
+ </ModalBody>
248
+
249
+ <ModalFooter>
250
+ <Button kind="secondary" onClick={closeModal} type="button" disabled={isSubmitting}>
251
+ {t('cancel', 'Cancel')}
252
+ </Button>
253
+
254
+ {isClockedIn ? (
255
+ <Button
256
+ onClick={handleSwitchCashPoint}
257
+ type="button"
258
+ disabled={isSubmitting || !selectedCashPoint || loadingCashPoints}
259
+ renderIcon={Logout}>
260
+ {isSubmitting ? (
261
+ <InlineLoading description={t('switchingCashPoint', 'Switching cash point...')} />
262
+ ) : (
263
+ t('switchCashPoint', 'Switch Cash Point')
264
+ )}
265
+ </Button>
266
+ ) : (
267
+ <Button
268
+ onClick={handleClockIn}
269
+ type="button"
270
+ disabled={isSubmitting || !selectedCashPoint || loadingCashPoints}>
271
+ {isSubmitting ? (
272
+ <InlineLoading description={t('clockingIn', 'Clocking in...')} />
273
+ ) : (
274
+ t('clockIn', 'Clock In')
275
+ )}
276
+ </Button>
277
+ )}
278
+ </ModalFooter>
279
+ </>
280
+ );
281
+ };
@@ -0,0 +1,37 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+
3
+ /**
4
+ * Clock in a cashier to a cash point
5
+ */
6
+ export const clockIn = async (cashierUuid: string, cashPointUuid: string) => {
7
+ const url = `${restBaseUrl}/cashier/timesheet`;
8
+
9
+ return openmrsFetch(url, {
10
+ method: 'POST',
11
+ headers: {
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ body: {
15
+ cashier: cashierUuid,
16
+ cashPoint: cashPointUuid,
17
+ clockIn: new Date().toISOString(),
18
+ },
19
+ });
20
+ };
21
+
22
+ /**
23
+ * Clock out the current cashier (close the timesheet)
24
+ */
25
+ export const clockOut = async (timesheetUuid: string) => {
26
+ const url = `${restBaseUrl}/cashier/timesheet/${timesheetUuid}`;
27
+
28
+ return openmrsFetch(url, {
29
+ method: 'POST',
30
+ headers: {
31
+ 'Content-Type': 'application/json',
32
+ },
33
+ body: {
34
+ clockOut: new Date().toISOString(),
35
+ },
36
+ });
37
+ };
@@ -0,0 +1,323 @@
1
+ import React, { useCallback, useId, useMemo, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import dayjs from 'dayjs';
4
+ import {
5
+ DataTable,
6
+ DataTableSkeleton,
7
+ Dropdown,
8
+ InlineLoading,
9
+ Layer,
10
+ Pagination,
11
+ Search,
12
+ Table,
13
+ TableBody,
14
+ TableCell,
15
+ TableContainer,
16
+ TableHead,
17
+ TableHeader,
18
+ TableRow,
19
+ Tile,
20
+ } from '@carbon/react';
21
+ import { useTranslation } from 'react-i18next';
22
+ import {
23
+ useLayoutType,
24
+ isDesktop,
25
+ useConfig,
26
+ usePagination,
27
+ ErrorState,
28
+ ConfigurableLink,
29
+ } from '@openmrs/esm-framework';
30
+ import { EmptyDataIllustration } from '@openmrs/esm-patient-common-lib';
31
+ import styles from './bills-table.scss';
32
+ import { useBills } from '../billing-dashboard/metrics/metrics.resource';
33
+ import { extractString, useCurrencyFormatting } from '../utils';
34
+
35
+ const filterItems = [
36
+ { id: '', text: 'All bills' },
37
+ { id: 'PENDING', text: 'Pending bills' },
38
+ { id: 'PAID', text: 'Paid bills' },
39
+ // { id: 'EXEMPTED', text: 'Exempted bills' },
40
+ ];
41
+
42
+ type BillTableProps = {
43
+ defaultBillPaymentStatus?: string;
44
+ };
45
+
46
+ const BillsTable: React.FC<BillTableProps> = ({ defaultBillPaymentStatus = '' }) => {
47
+ const { t } = useTranslation();
48
+ const id = useId();
49
+ const config = useConfig();
50
+ const layout = useLayoutType();
51
+ const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
52
+ const [billPaymentStatus, setBillPaymentStatus] = useState(defaultBillPaymentStatus);
53
+ const pageSizes = config?.bills?.pageSizes ?? [10, 20, 30, 40, 50];
54
+ const [pageSize, setPageSize] = useState(config?.bills?.pageSize ?? 10);
55
+ const [searchString, setSearchString] = useState('');
56
+ const { format: formatCurrency } = useCurrencyFormatting();
57
+
58
+ const { bills, isLoading, isValidating, error } = useBills(
59
+ '',
60
+ billPaymentStatus,
61
+ dayjs().startOf('day').toDate(),
62
+ dayjs().endOf('day').toDate(),
63
+ );
64
+
65
+ const headerData = [
66
+ {
67
+ header: t('visitTime', 'Visit time'),
68
+ key: 'visitTime',
69
+ },
70
+ {
71
+ header: t('identifier', 'Identifier'),
72
+ key: 'identifier',
73
+ },
74
+ {
75
+ header: t('name', 'Name'),
76
+ key: 'patientName',
77
+ },
78
+ {
79
+ header: t('billedItems', 'Billed Items'),
80
+ key: 'billedItems',
81
+ },
82
+ {
83
+ header: t('totalAmount', 'Total Amount'),
84
+ key: 'totalAmount',
85
+ },
86
+ {
87
+ header: t('status', 'Status'),
88
+ key: 'status',
89
+ },
90
+ ];
91
+
92
+ const searchResults = useMemo(() => {
93
+ if (bills !== undefined && bills.length > 0) {
94
+ if (searchString && searchString.trim() !== '') {
95
+ const search = searchString.toLowerCase();
96
+ return bills?.filter((activeBillRow) =>
97
+ Object.entries(activeBillRow).some(([header, value]) => {
98
+ if (header === 'patientUuid') {
99
+ return false;
100
+ }
101
+ return `${value}`.toLowerCase().includes(search);
102
+ }),
103
+ );
104
+ }
105
+ }
106
+
107
+ return bills;
108
+ }, [searchString, bills]);
109
+
110
+ const { paginated, goTo, results, currentPage } = usePagination(searchResults, pageSize);
111
+
112
+ // Improved function using extractString utility
113
+ const setBilledItems = (bill) => {
114
+ if (!bill?.lineItems || bill.lineItems.length === 0) {
115
+ return '--';
116
+ }
117
+
118
+ return bill.lineItems
119
+ .map((item) => extractString(item?.billableService || item?.item || ''))
120
+ .filter(Boolean)
121
+ .join(' & ');
122
+ };
123
+
124
+ const rowData = results?.map((bill, index) => ({
125
+ id: `${index}`,
126
+ uuid: bill.uuid,
127
+ patientName: (
128
+ <ConfigurableLink
129
+ style={{ textDecoration: 'none', maxWidth: '50%' }}
130
+ to={`\${openmrsSpaBase}/home/accounting/patient/\${patientUuid}/\${uuid}`}
131
+ templateParams={{ patientUuid: bill.patientUuid, uuid: bill.uuid }}>
132
+ {bill.patientName}
133
+ </ConfigurableLink>
134
+ ),
135
+ visitTime: bill.dateCreated,
136
+ identifier: bill.identifier,
137
+ billedItems: setBilledItems(bill),
138
+ totalAmount: formatCurrency(bill.totalAmount || 0),
139
+ status: (
140
+ <span
141
+ className={classNames(styles.statusBadge, {
142
+ [styles.statusPaid]: bill.status === 'PAID',
143
+ [styles.statusPending]: bill.status === 'PENDING',
144
+ [styles.statusExempted]: bill.status === 'EXEMPTED',
145
+ })}>
146
+ {bill.status}
147
+ </span>
148
+ ),
149
+ }));
150
+
151
+ const handleSearch = useCallback(
152
+ (e) => {
153
+ goTo(1);
154
+ setSearchString(e.target.value);
155
+ },
156
+ [goTo, setSearchString],
157
+ );
158
+
159
+ const handleFilterChange = ({ selectedItem }) => setBillPaymentStatus(selectedItem.id);
160
+
161
+ if (isLoading) {
162
+ return (
163
+ <div className={styles.loaderContainer}>
164
+ <DataTableSkeleton
165
+ rowCount={pageSize}
166
+ showHeader={false}
167
+ showToolbar={false}
168
+ zebra
169
+ columnCount={headerData?.length}
170
+ />
171
+ </div>
172
+ );
173
+ }
174
+
175
+ if (error) {
176
+ return (
177
+ <div className={styles.errorContainer}>
178
+ <Layer>
179
+ <ErrorState error={error} headerTitle={t('billsList', 'Bill list')} />
180
+ </Layer>
181
+ </div>
182
+ );
183
+ }
184
+
185
+ return (
186
+ <>
187
+ <div className={styles.filterContainer}>
188
+ {bills?.length > 0 ? (
189
+ <Dropdown
190
+ className={styles.filterDropdown}
191
+ direction="bottom"
192
+ id={`filter-${id}`}
193
+ initialSelectedItem={filterItems.find((item) => item.id === billPaymentStatus)}
194
+ items={filterItems}
195
+ itemToString={(item) => (item ? item.text : '')}
196
+ label=""
197
+ onChange={handleFilterChange}
198
+ size={responsiveSize}
199
+ titleText={t('filterBy', 'Filter by') + ':'}
200
+ type="inline"
201
+ />
202
+ ) : null}
203
+ </div>
204
+
205
+ {bills?.length > 0 ? (
206
+ <div className={styles.billListContainer}>
207
+ <FilterableTableHeader
208
+ handleSearch={handleSearch}
209
+ isValidating={isValidating}
210
+ layout={layout}
211
+ responsiveSize={responsiveSize}
212
+ t={t}
213
+ />
214
+ <DataTable
215
+ isSortable
216
+ rows={rowData}
217
+ headers={headerData}
218
+ size={responsiveSize}
219
+ useZebraStyles={rowData?.length > 1 ? true : false}>
220
+ {({ rows, headers, getRowProps, getTableProps }) => (
221
+ <TableContainer>
222
+ <Table {...getTableProps()} aria-label="bill list">
223
+ <TableHead>
224
+ <TableRow>
225
+ {headers.map((header) => (
226
+ <TableHeader key={header.key}>{header.header}</TableHeader>
227
+ ))}
228
+ </TableRow>
229
+ </TableHead>
230
+ <TableBody>
231
+ {rows.map((row) => (
232
+ <TableRow
233
+ key={row.id}
234
+ {...getRowProps({
235
+ row,
236
+ })}>
237
+ {row.cells.map((cell) => (
238
+ <TableCell key={cell.id}>{cell.value}</TableCell>
239
+ ))}
240
+ </TableRow>
241
+ ))}
242
+ </TableBody>
243
+ </Table>
244
+ </TableContainer>
245
+ )}
246
+ </DataTable>
247
+ {searchResults?.length === 0 && (
248
+ <>
249
+ <div className={styles.emptyStateContainer}>
250
+ <Tile className={styles.tile}>
251
+ <div className={styles.tileContent}>
252
+ <p className={styles.content}>{t('noMatchingBillsToDisplay', 'No matching bills to display')}</p>
253
+ <p className={styles.emptyStateHelperText}>{t('checkFilters', 'Check the filters above')}</p>
254
+ </div>
255
+ </Tile>
256
+ </div>
257
+ </>
258
+ )}
259
+ {paginated && (
260
+ <Pagination
261
+ forwardText="Next page"
262
+ backwardText="Previous page"
263
+ page={currentPage}
264
+ pageSize={pageSize}
265
+ pageSizes={pageSizes}
266
+ totalItems={searchResults?.length}
267
+ className={styles.pagination}
268
+ size={responsiveSize}
269
+ onChange={({ pageSize: newPageSize, page: newPage }) => {
270
+ if (newPageSize !== pageSize) {
271
+ setPageSize(newPageSize);
272
+ }
273
+ if (newPage !== currentPage) {
274
+ goTo(newPage);
275
+ }
276
+ }}
277
+ />
278
+ )}
279
+ </div>
280
+ ) : (
281
+ <>
282
+ <div className={styles.emptyStateContainer}>
283
+ <Tile className={styles.tile}>
284
+ <div className={styles.tileContent}>
285
+ <p className={styles.content}> {t('noBillsAvailable', 'No bills available')}</p>
286
+ <p className={styles.emptyStateHelperText}>
287
+ {t('noBillsToDisplay', 'There are currently no bills to display.')}
288
+ </p>
289
+ </div>
290
+ </Tile>
291
+ </div>
292
+ </>
293
+ )}
294
+ </>
295
+ );
296
+ };
297
+
298
+ function FilterableTableHeader({ layout, handleSearch, isValidating, responsiveSize, t }) {
299
+ return (
300
+ <>
301
+ <div className={styles.headerContainer}>
302
+ <div
303
+ className={classNames({
304
+ [styles.tabletHeading]: !isDesktop(layout),
305
+ [styles.desktopHeading]: isDesktop(layout),
306
+ })}>
307
+ <h4>{t('billList', 'Bill list')}</h4>
308
+ </div>
309
+ <div className={styles.backgroundDataFetchingIndicator}>
310
+ <span>{isValidating ? <InlineLoading /> : null}</span>
311
+ </div>
312
+ </div>
313
+ <Search
314
+ labelText=""
315
+ placeholder={t('filterTable', 'Filter table')}
316
+ onChange={handleSearch}
317
+ size={responsiveSize}
318
+ />
319
+ </>
320
+ );
321
+ }
322
+
323
+ export default BillsTable;