@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,244 @@
1
+ import {
2
+ ComboBox,
3
+ InlineLoading,
4
+ RadioButton,
5
+ RadioButtonGroup,
6
+ TextInput,
7
+ StructuredListSkeleton,
8
+ } from '@carbon/react';
9
+ import { useConfig, useLayoutType, usePagination } from '@openmrs/esm-framework';
10
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
11
+ import { Controller, useFormContext } from 'react-hook-form';
12
+ import { useTranslation } from 'react-i18next';
13
+ import classNames from 'classnames';
14
+ import { BillingConfig } from '../../config-schema';
15
+ import { PatientChartPagination } from '@openmrs/esm-patient-common-lib';
16
+ import styles from './visit-attributes-form.scss';
17
+ import { VisitAttributesFormValue } from '../billing-checkin-form-schema';
18
+ import { usePaymentModes } from './visit-attributes-form.resource';
19
+
20
+ type VisitAttributesFormProps = {
21
+ setAttributes: (state) => void;
22
+ };
23
+
24
+ const VisitAttributesForm: React.FC<VisitAttributesFormProps> = ({ setAttributes }) => {
25
+ const { t } = useTranslation();
26
+ const isTablet = useLayoutType() === 'tablet';
27
+ const { insuranceSchemes } = useConfig<BillingConfig>();
28
+ const { visitAttributeTypes, patientExemptionCategories, insurancePaymentMethod } = useConfig<BillingConfig>();
29
+ const { setValue, watch, control, getValues } = useFormContext<VisitAttributesFormValue>();
30
+ const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentModes();
31
+ const [isPatientExempted, paymentMethods] = watch(['isPatientExempted', 'paymentMethods']);
32
+ const [searchTerm, setSearchTerm] = useState('');
33
+
34
+ const patientExemptionOptions = useMemo(
35
+ () => [
36
+ { uuid: 'false', display: t('payingPatient', 'Yes, paying patient'), name: 'yes' },
37
+ { uuid: 'true', display: t('nonPayingPatient', 'No, non-paying patient (exempt)'), name: 'no' },
38
+ ],
39
+ [t],
40
+ );
41
+
42
+ const filteredOptions = useMemo(() => {
43
+ if (!searchTerm) {
44
+ return patientExemptionOptions;
45
+ }
46
+ return patientExemptionOptions.filter((option) => option.display.toLowerCase().includes(searchTerm.toLowerCase()));
47
+ }, [patientExemptionOptions, searchTerm]);
48
+
49
+ const { results, currentPage, goTo } = usePagination(filteredOptions, 5);
50
+ const hasNoMatchingSearchResults = filteredOptions.length === 0;
51
+
52
+ const resetFormFieldsForNonExemptedPatients = useCallback(() => {
53
+ setValue('insuranceScheme', '');
54
+ setValue('policyNumber', '');
55
+ setValue('exemptionCategory', '');
56
+ setValue('paymentMethods', '');
57
+ }, [setValue]);
58
+
59
+ useEffect(() => {
60
+ resetFormFieldsForNonExemptedPatients();
61
+ }, [isPatientExempted, resetFormFieldsForNonExemptedPatients]);
62
+
63
+ const createVisitAttributesPayload = useCallback(() => {
64
+ const values = getValues();
65
+ const formPayload = [
66
+ { uuid: visitAttributeTypes.isPatientExempted, value: values.isPatientExempted },
67
+ { uuid: visitAttributeTypes.paymentMethods, value: values.paymentMethods },
68
+ { uuid: visitAttributeTypes.policyNumber, value: values.policyNumber },
69
+ { uuid: visitAttributeTypes.insuranceScheme, value: values.insuranceScheme },
70
+ { uuid: visitAttributeTypes.exemptionCategory, value: values.exemptionCategory },
71
+ ];
72
+ const visitAttributesPayload = formPayload.filter(
73
+ (item) => item.value !== undefined && item.value !== null && item.value !== '',
74
+ );
75
+ return visitAttributesPayload.map(({ uuid, value }) => ({
76
+ attributeType: uuid,
77
+ value,
78
+ }));
79
+ }, [visitAttributeTypes, getValues]);
80
+
81
+ const [policyNumber, exemptionCategory, insuranceScheme] = watch([
82
+ 'policyNumber',
83
+ 'exemptionCategory',
84
+ 'insuranceScheme',
85
+ ]);
86
+
87
+ useEffect(() => {
88
+ setAttributes(createVisitAttributesPayload());
89
+ }, [
90
+ isPatientExempted,
91
+ paymentMethods,
92
+ setAttributes,
93
+ createVisitAttributesPayload,
94
+ policyNumber,
95
+ exemptionCategory,
96
+ insuranceScheme,
97
+ ]);
98
+
99
+ if (isLoadingPaymentModes) {
100
+ return (
101
+ <InlineLoading
102
+ status="active"
103
+ iconDescription={t('loadingDescription', 'Loading')}
104
+ description={t('loading', 'Loading data...')}
105
+ />
106
+ );
107
+ }
108
+
109
+ return (
110
+ <>
111
+ <section>
112
+ <div className={styles.sectionTitle}>{t('patientCategory', 'Payment Category')}</div>
113
+ <span className={styles.labelOption}>{t('isPatientExemptedLegend', 'Will the patient pay for services?')}</span>
114
+ <div className={styles.sectionField}>
115
+ <div className={classNames(styles.visitTypeOverviewWrapper, isTablet ? styles.tablet : styles.desktop)}>
116
+ {filteredOptions.length ? (
117
+ <>
118
+ <Controller
119
+ name="isPatientExempted"
120
+ control={control}
121
+ defaultValue=""
122
+ render={({ field: { onChange, value } }) => (
123
+ <RadioButtonGroup
124
+ className={styles.radioButtonGroup}
125
+ name="patient-exemption"
126
+ onChange={onChange}
127
+ orientation="vertical"
128
+ valueSelected={value}>
129
+ {results.map(({ uuid, display, name }) => (
130
+ <RadioButton
131
+ className={styles.radioButton}
132
+ id={`patient-exemption-${uuid}`}
133
+ key={uuid}
134
+ labelText={display}
135
+ value={uuid}
136
+ />
137
+ ))}
138
+ </RadioButtonGroup>
139
+ )}
140
+ />
141
+
142
+ {!hasNoMatchingSearchResults && filteredOptions.length > 5 && (
143
+ <div className={styles.paginationContainer}>
144
+ <PatientChartPagination
145
+ currentItems={results.length}
146
+ onPageNumberChange={({ page }) => goTo(page)}
147
+ pageNumber={currentPage}
148
+ pageSize={5}
149
+ totalItems={filteredOptions?.length}
150
+ />
151
+ </div>
152
+ )}
153
+ </>
154
+ ) : (
155
+ <StructuredListSkeleton className={styles.skeleton} />
156
+ )}
157
+ </div>
158
+
159
+ {isPatientExempted === 'true' && (
160
+ <div className={styles.sectionFieldLayer}>
161
+ <Controller
162
+ control={control}
163
+ name="exemptionCategory"
164
+ render={({ field }) => (
165
+ <ComboBox
166
+ className={styles.sectionField}
167
+ onChange={({ selectedItem }) => field.onChange(selectedItem?.value)}
168
+ id="exemptionCategory"
169
+ items={patientExemptionCategories}
170
+ itemToString={(item) => (item ? item.label : '')}
171
+ titleText={t('exemptionCategory', 'Exemption category')}
172
+ placeholder={t('selectExemptionCategory', 'Select exemption category')}
173
+ />
174
+ )}
175
+ />
176
+ </div>
177
+ )}
178
+
179
+ {isPatientExempted === 'false' && (
180
+ <div className={styles.sectionFieldLayer}>
181
+ <Controller
182
+ control={control}
183
+ name="paymentMethods"
184
+ render={({ field }) => (
185
+ <ComboBox
186
+ className={styles.sectionField}
187
+ onChange={({ selectedItem }) => field.onChange(selectedItem)}
188
+ initialSelectedItem={field.value}
189
+ id="paymentMethods"
190
+ items={paymentModes?.map((method) => method.uuid)}
191
+ itemToString={(item) => paymentModes.find((mode) => mode.uuid === item)?.name ?? ''}
192
+ titleText={t('paymentMethodsTitle', 'Payment method')}
193
+ placeholder={t('selectPaymentMethod', 'Select payment method')}
194
+ />
195
+ )}
196
+ />
197
+ </div>
198
+ )}
199
+
200
+ {paymentMethods === insurancePaymentMethod && isPatientExempted === 'false' && (
201
+ <>
202
+ <div className={styles.sectionFieldLayer}>
203
+ <Controller
204
+ control={control}
205
+ name="insuranceScheme"
206
+ render={({ field }) => (
207
+ <ComboBox
208
+ className={styles.sectionField}
209
+ onChange={({ selectedItem }) => field.onChange(selectedItem)}
210
+ id="insurance-scheme"
211
+ items={insuranceSchemes}
212
+ itemToString={(item) => (item ? item : '')}
213
+ titleText={t('insuranceScheme', 'Insurance scheme')}
214
+ placeholder={t('selectInsuranceScheme', 'Select insurance scheme')}
215
+ />
216
+ )}
217
+ />
218
+ </div>
219
+ <div className={styles.sectionFieldLayer}>
220
+ <Controller
221
+ control={control}
222
+ name="policyNumber"
223
+ render={({ field }) => (
224
+ <TextInput
225
+ {...field}
226
+ className={styles.sectionField}
227
+ onChange={(e) => field.onChange(e.target.value)}
228
+ id="policy-number"
229
+ type="text"
230
+ labelText={t('policyNumber', 'Policy number')}
231
+ placeholder={t('enterPolicyNumber', 'Enter policy number')}
232
+ />
233
+ )}
234
+ />
235
+ </div>
236
+ </>
237
+ )}
238
+ </div>
239
+ </section>
240
+ </>
241
+ );
242
+ };
243
+
244
+ export default VisitAttributesForm;
@@ -0,0 +1,23 @@
1
+ import { openmrsFetch, restBaseUrl, useConfig } from '@openmrs/esm-framework';
2
+ import { BillingConfig } from '../../config-schema';
3
+ import { PaymentMethod } from '../type';
4
+ import useSWR from 'swr';
5
+
6
+ export const usePaymentModes = (excludeWaiver: boolean = true) => {
7
+ const { excludedPaymentMode } = useConfig<BillingConfig>();
8
+ const url = `${restBaseUrl}/cashier/paymentMode?v=full`;
9
+ const { data, isLoading, error, mutate } = useSWR<{ data: { results: Array<PaymentMethod> } }>(url, openmrsFetch, {
10
+ errorRetryCount: 2,
11
+ });
12
+ const allowedPaymentModes =
13
+ excludedPaymentMode?.length > 0
14
+ ? (data?.data?.results.filter((mode) => !excludedPaymentMode.some((excluded) => excluded.uuid === mode.uuid)) ??
15
+ [])
16
+ : (data?.data?.results ?? []);
17
+ return {
18
+ paymentModes: excludeWaiver ? allowedPaymentModes : data?.data?.results,
19
+ isLoading,
20
+ mutate,
21
+ error,
22
+ };
23
+ };
@@ -0,0 +1,186 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+ @use '@openmrs/esm-styleguide/src/vars' as *;
5
+
6
+ .sectionTitle {
7
+ @include type.type-style('heading-compact-02');
8
+ color: colors.$gray-70;
9
+ margin: 0 0 layout.$spacing-03 0;
10
+ flex-basis: 30%;
11
+
12
+ :global(.omrs-breakpoint-lt-tablet) & {
13
+ flex-basis: 100%;
14
+ margin-bottom: layout.$spacing-05;
15
+ }
16
+ }
17
+
18
+ .desktop {
19
+ background-color: $ui-01;
20
+
21
+ .paginationContainer div {
22
+ background-color: $ui-01;
23
+ }
24
+ }
25
+
26
+ .sectionField {
27
+ flex-basis: 70%;
28
+
29
+ :global(.omrs-breakpoint-lt-tablet) & {
30
+ flex-basis: 100%;
31
+ }
32
+
33
+ :global(.omrs-breakpoint-lt-desktop) & {
34
+ flex-basis: 100%;
35
+ }
36
+ }
37
+
38
+ .sectionFieldLayer {
39
+ margin-bottom: 0.5rem;
40
+
41
+ :global(.omrs-breakpoint-lt-tablet) & {
42
+ margin-bottom: layout.$spacing-05;
43
+ }
44
+ }
45
+
46
+ .visitTypeOverviewWrapper {
47
+ margin: layout.$spacing-03 0;
48
+ border: 0.0625rem solid $grey-2;
49
+
50
+ &:has(.skeleton) {
51
+ border-bottom: none !important;
52
+ }
53
+
54
+ :global(.omrs-breakpoint-lt-tablet) & {
55
+ margin: layout.$spacing-05 0;
56
+ border: none;
57
+ }
58
+
59
+ :global(.omrs-breakpoint-lt-desktop) & {
60
+ margin: layout.$spacing-04 0;
61
+ }
62
+ }
63
+
64
+ .visitTypeOverviewWrapper div:nth-child(3) > div:nth-child(2) {
65
+ position: relative;
66
+ }
67
+
68
+ .visitTypeOverviewWrapper div:nth-child(3) span * {
69
+ display: none;
70
+ }
71
+
72
+ .radioButtonGroup {
73
+ display: flex;
74
+ flex-direction: column;
75
+ align-items: flex-start;
76
+ margin-top: layout.$spacing-03;
77
+ min-height: layout.$spacing-10;
78
+ width: 100%;
79
+ @include type.type-style('body-compact-01');
80
+
81
+ :global(.omrs-breakpoint-lt-tablet) & {
82
+ margin-top: layout.$spacing-02;
83
+ min-height: auto;
84
+ }
85
+ }
86
+
87
+ .radioButton {
88
+ padding: layout.$spacing-02 layout.$spacing-05;
89
+ margin: layout.$spacing-03 0;
90
+
91
+ :global(.omrs-breakpoint-lt-tablet) & {
92
+ padding: layout.$spacing-03 layout.$spacing-04;
93
+ margin: layout.$spacing-02 0;
94
+ width: 100%;
95
+ }
96
+
97
+ :global(.omrs-breakpoint-lt-desktop) & {
98
+ padding: layout.$spacing-03 layout.$spacing-04;
99
+ }
100
+ }
101
+
102
+ .tileContainer {
103
+ background-color: $ui-02;
104
+ padding: layout.$spacing-09 0;
105
+
106
+ :global(.omrs-breakpoint-lt-tablet) & {
107
+ padding: layout.$spacing-06 layout.$spacing-03;
108
+ }
109
+ }
110
+
111
+ .tile {
112
+ margin: auto;
113
+ width: fit-content;
114
+
115
+ :global(.omrs-breakpoint-lt-tablet) & {
116
+ width: 100%;
117
+ }
118
+ }
119
+
120
+ .tileContent {
121
+ display: flex;
122
+ flex-direction: column;
123
+ align-items: center;
124
+
125
+ :global(.omrs-breakpoint-lt-tablet) & {
126
+ padding: 0 layout.$spacing-03;
127
+ }
128
+ }
129
+
130
+ .content {
131
+ @include type.type-style('heading-compact-02');
132
+ color: $text-02;
133
+ margin-bottom: layout.$spacing-03;
134
+
135
+ :global(.omrs-breakpoint-lt-tablet) & {
136
+ @include type.type-style('heading-compact-01');
137
+ text-align: center;
138
+ }
139
+ }
140
+
141
+ .helper {
142
+ @include type.type-style('body-compact-01');
143
+ color: $text-02;
144
+
145
+ :global(.omrs-breakpoint-lt-tablet) & {
146
+ @include type.type-style('body-compact-02');
147
+ text-align: center;
148
+ }
149
+ }
150
+
151
+ .labelOption {
152
+ font-size: var(--cds-label-01-font-size, 0.75rem);
153
+ font-weight: var(--cds-label-01-font-weight, 400);
154
+ line-height: var(--cds-label-01-line-height, 1.33333);
155
+ letter-spacing: var(--cds-label-01-letter-spacing, 0.32px);
156
+ color: var(--cds-text-secondary, #525252);
157
+ vertical-align: baseline;
158
+ margin-bottom: 0.5rem;
159
+ font-weight: 400;
160
+ line-height: 1rem;
161
+ display: inline-block;
162
+
163
+ :global(.omrs-breakpoint-lt-tablet) & {
164
+ font-size: 0.875rem;
165
+ margin-bottom: layout.$spacing-04;
166
+ display: block;
167
+ }
168
+ }
169
+
170
+ .paginationContainer {
171
+ :global(.omrs-breakpoint-lt-tablet) & {
172
+ padding: layout.$spacing-03;
173
+
174
+ div {
175
+ flex-direction: column;
176
+ align-items: stretch;
177
+ gap: layout.$spacing-03;
178
+ }
179
+ }
180
+ }
181
+
182
+ .skeleton {
183
+ :global(.omrs-breakpoint-lt-tablet) & {
184
+ min-height: 10rem;
185
+ }
186
+ }
@@ -0,0 +1,224 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { PageHeader } from '../billing-header/page-header/page-header.component';
3
+ import styles from './billing-dashboard.scss';
4
+ import { InlineLoading, Tile, Button } from '@carbon/react';
5
+ import { useBillMetrics, useBills } from './metrics/metrics.resource';
6
+ import dayjs from 'dayjs';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { CardHeader } from '@openmrs/esm-patient-common-lib';
9
+ import { Alarm, ConnectionTwoWay, Login, Logout } from '@carbon/react/icons';
10
+ import { useCurrentTimesheet } from './billing-dashboard.resource';
11
+ import { showModal, showSnackbar, useSession } from '@openmrs/esm-framework';
12
+ import { clockOut } from '../billing-timesheet-modal/billing-timesheet-resource-modal';
13
+ import BillsTable from '../bills-table/bills-table.component';
14
+
15
+ function BillingDashboard() {
16
+ const { t } = useTranslation();
17
+ const session = useSession();
18
+ const { activeTimesheet, isClockedIn, mutate: mutateTimesheet } = useCurrentTimesheet();
19
+ const [isProcessing, setIsProcessing] = useState(false);
20
+
21
+ const startDate =
22
+ isClockedIn && activeTimesheet?.clockIn ? new Date(activeTimesheet.clockIn) : dayjs().startOf('day').toDate();
23
+
24
+ const endDate = dayjs().endOf('day').toDate();
25
+
26
+ const { bills, isLoading, error } = useBills('', '', startDate, endDate);
27
+
28
+ const filteredBills = useMemo(() => {
29
+ if (!isClockedIn || !activeTimesheet?.cashPoint?.uuid) {
30
+ return bills;
31
+ }
32
+ return bills.filter((bill) => bill.cashPointUuid === activeTimesheet.cashPoint.uuid);
33
+ }, [bills, isClockedIn, activeTimesheet]);
34
+
35
+ const { totalBills, pendingBills, paidBills, exemptedBills } = useBillMetrics(filteredBills);
36
+
37
+ const handleClockIn = () => {
38
+ const dispose = showModal('billing-timesheet-modal', {
39
+ closeModal: () => dispose(),
40
+ onClockInSuccess: (sessionData) => {
41
+ mutateTimesheet();
42
+ showSnackbar({
43
+ title: t('clockInSuccess', 'Clock In Successful'),
44
+ kind: 'success',
45
+ subtitle: t('clockInSuccessMessage', 'You have been clocked in successfully'),
46
+ });
47
+ },
48
+ });
49
+ };
50
+
51
+ const handleClockOut = async () => {
52
+ if (!activeTimesheet?.uuid) {
53
+ showSnackbar({
54
+ title: t('error', 'Error'),
55
+ subtitle: t('noActiveSession', 'No active session found'),
56
+ kind: 'error',
57
+ });
58
+ return;
59
+ }
60
+
61
+ setIsProcessing(true);
62
+
63
+ try {
64
+ await clockOut(activeTimesheet.uuid);
65
+ await mutateTimesheet();
66
+
67
+ showSnackbar({
68
+ title: t('clockOutSuccess', 'Clock Out Successful'),
69
+ subtitle: t('clockOutSuccessMessage', 'You have been clocked out successfully'),
70
+ kind: 'success',
71
+ timeoutInMs: 3000,
72
+ });
73
+ } catch (error) {
74
+ console.error('Clock out error:', error);
75
+
76
+ const errorMessage = error?.responseBody?.error?.message || t('clockOutFailed', 'Failed to clock out');
77
+
78
+ showSnackbar({
79
+ title: t('clockOutError', 'Clock Out Failed'),
80
+ subtitle: errorMessage,
81
+ kind: 'error',
82
+ timeoutInMs: 5000,
83
+ });
84
+ } finally {
85
+ setIsProcessing(false);
86
+ }
87
+ };
88
+
89
+ const handleSwitchCashPoint = () => {
90
+ if (!activeTimesheet?.uuid) {
91
+ showSnackbar({
92
+ title: t('error', 'Error'),
93
+ subtitle: t('noActiveSession', 'No active session found'),
94
+ kind: 'error',
95
+ });
96
+ return;
97
+ }
98
+
99
+ // Open modal in switch mode (modal will detect existing session)
100
+ const dispose = showModal('billing-timesheet-modal', {
101
+ closeModal: () => dispose(),
102
+ onClockInSuccess: (sessionData) => {
103
+ mutateTimesheet();
104
+ showSnackbar({
105
+ title: t('switchSuccess', 'Cash Point Switched'),
106
+ kind: 'success',
107
+ subtitle: t('switchSuccessMessage', 'You have successfully switched cash points'),
108
+ });
109
+ },
110
+ onClockOutSuccess: () => {
111
+ mutateTimesheet();
112
+ },
113
+ });
114
+ };
115
+
116
+ return (
117
+ <main>
118
+ <PageHeader />
119
+ <div className={styles.dashboardContainer}>
120
+ {/* Metrics Tile */}
121
+ <Tile className={styles.tileContainer}>
122
+ <div className={styles.displayDetails}>
123
+ <div className={styles.countLabel}>
124
+ {isClockedIn ? t('totalBilledShift', 'Total Billed (This Shift)') : t('totalBilled', 'Total Billed')}
125
+ </div>
126
+ <div className={styles.displayData}>{isLoading ? <InlineLoading description="" /> : totalBills}</div>
127
+ </div>
128
+ <div className={styles.displayDetails}>
129
+ <div className={styles.countLabel}>
130
+ {isClockedIn
131
+ ? t('amountCollectedShift', 'Cash Collected (This Shift)')
132
+ : t('amountCollected', 'Amount Collected')}
133
+ </div>
134
+ <div className={styles.displayData}>{isLoading ? <InlineLoading description="" /> : paidBills}</div>
135
+ </div>
136
+ <div className={styles.displayDetails}>
137
+ <div className={styles.countLabel}>{t('outstandingBills', 'Outstanding Bills')}</div>
138
+ <div className={styles.displayData}>{isLoading ? <InlineLoading description="" /> : pendingBills}</div>
139
+ </div>
140
+ <div className={styles.displayDetails}>
141
+ <div className={styles.countLabel}>{t('totalInvoices', 'Total Invoices')}</div>
142
+ <div className={styles.displayData}>
143
+ {isLoading ? <InlineLoading description="" /> : filteredBills.length}
144
+ </div>
145
+ </div>
146
+ </Tile>
147
+ {isClockedIn && activeTimesheet && (
148
+ <Tile className={styles.clockInBanner}>
149
+ <div className={styles.clockInInfo}>
150
+ <Alarm size={20} className={styles.alarmIcon} />
151
+ <div className={styles.clockInText}>
152
+ <strong>{t('clockedInTo', 'Clocked in to')}: </strong>
153
+ {activeTimesheet.cashPoint.name}
154
+ <span className={styles.clockInTime}>
155
+ {' '}
156
+ {t('since', 'since')} {new Date(activeTimesheet.clockIn).toLocaleTimeString()}
157
+ </span>
158
+ </div>
159
+ </div>
160
+ </Tile>
161
+ )}
162
+ <div className={styles.cardHeaderContainer}>
163
+ <CardHeader title={t('billingQueue', 'Billing Queue')}>
164
+ <div className={styles.headerActions}>
165
+ <Button
166
+ kind={isClockedIn ? 'danger' : 'primary'}
167
+ size="sm"
168
+ renderIcon={(props) => (isClockedIn ? <Logout size={16} {...props} /> : <Login size={16} {...props} />)}
169
+ onClick={isClockedIn ? handleClockOut : handleClockIn}
170
+ disabled={isProcessing}>
171
+ {isProcessing ? (
172
+ <InlineLoading
173
+ description={isClockedIn ? t('clockingOut', 'Clocking out') : t('clockingIn', 'Clocking in')}
174
+ />
175
+ ) : isClockedIn ? (
176
+ t('clockOut', 'Clock Out')
177
+ ) : (
178
+ t('clockIn', 'Clock In')
179
+ )}
180
+ </Button>
181
+
182
+ {isClockedIn && (
183
+ <Button
184
+ kind="secondary"
185
+ size="sm"
186
+ renderIcon={(props) => <ConnectionTwoWay size={16} {...props} />}
187
+ onClick={handleSwitchCashPoint}
188
+ disabled={isProcessing}>
189
+ {t('changeStation', 'Change Station')}
190
+ </Button>
191
+ )}
192
+ </div>
193
+ </CardHeader>
194
+ </div>
195
+
196
+ {/* Bills Table or Empty State */}
197
+ {isClockedIn && activeTimesheet ? (
198
+ <div className={styles.queueContainer}>
199
+ <BillsTable />
200
+ </div>
201
+ ) : (
202
+ <div className={styles.emptyStateContainer}>
203
+ <Tile className={styles.tile}>
204
+ <div className={styles.tileContent}>
205
+ <div className={styles.emptyStateIcon}>
206
+ <Login size={48} />
207
+ </div>
208
+ <p className={styles.content}>{t('pleaseClockIn', 'Please clock in to start billing')}</p>
209
+ <p className={styles.emptyStateHelperText}>
210
+ {t(
211
+ 'clockInPrompt',
212
+ 'Click the "Clock In" button above to select a cash point and start processing bills',
213
+ )}
214
+ </p>
215
+ </div>
216
+ </Tile>
217
+ </div>
218
+ )}
219
+ </div>
220
+ </main>
221
+ );
222
+ }
223
+
224
+ export default BillingDashboard;