@openmrs/esm-billing-app 1.0.1-pre.14

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 (167) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc +57 -0
  4. package/.husky/pre-commit +7 -0
  5. package/.husky/pre-push +6 -0
  6. package/.prettierignore +14 -0
  7. package/.turbo.json +18 -0
  8. package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
  9. package/LICENSE +401 -0
  10. package/README.md +7 -0
  11. package/__mocks__/bills.mock.ts +392 -0
  12. package/__mocks__/delivery-summary.mock.ts +87 -0
  13. package/__mocks__/encounter-observation.mock.ts +10649 -0
  14. package/__mocks__/encounter-observations.mock.ts +6187 -0
  15. package/__mocks__/hiv-summary.mock.ts +22 -0
  16. package/__mocks__/patient-summary.mock.ts +32 -0
  17. package/__mocks__/patient.mock.ts +59 -0
  18. package/__mocks__/program-summary.mock.ts +43 -0
  19. package/__mocks__/react-i18next.js +57 -0
  20. package/dist/294.js +2 -0
  21. package/dist/294.js.LICENSE.txt +9 -0
  22. package/dist/294.js.map +1 -0
  23. package/dist/319.js +1 -0
  24. package/dist/384.js +1 -0
  25. package/dist/384.js.map +1 -0
  26. package/dist/421.js +1 -0
  27. package/dist/421.js.map +1 -0
  28. package/dist/450.js +1 -0
  29. package/dist/450.js.map +1 -0
  30. package/dist/476.js +1 -0
  31. package/dist/476.js.map +1 -0
  32. package/dist/574.js +1 -0
  33. package/dist/757.js +1 -0
  34. package/dist/788.js +1 -0
  35. package/dist/800.js +2 -0
  36. package/dist/800.js.LICENSE.txt +3 -0
  37. package/dist/800.js.map +1 -0
  38. package/dist/807.js +1 -0
  39. package/dist/833.js +1 -0
  40. package/dist/935.js +2 -0
  41. package/dist/935.js.LICENSE.txt +19 -0
  42. package/dist/935.js.map +1 -0
  43. package/dist/96.js +2 -0
  44. package/dist/96.js.LICENSE.txt +47 -0
  45. package/dist/96.js.map +1 -0
  46. package/dist/main.js +2 -0
  47. package/dist/main.js.LICENSE.txt +47 -0
  48. package/dist/main.js.map +1 -0
  49. package/dist/openmrs-esm-billing-app.js +1 -0
  50. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +462 -0
  51. package/dist/openmrs-esm-billing-app.js.map +1 -0
  52. package/dist/routes.json +1 -0
  53. package/e2e/README.md +115 -0
  54. package/e2e/core/global-setup.ts +32 -0
  55. package/e2e/core/index.ts +1 -0
  56. package/e2e/core/test.ts +20 -0
  57. package/e2e/fixtures/api.ts +26 -0
  58. package/e2e/fixtures/index.ts +1 -0
  59. package/e2e/pages/home-page.ts +9 -0
  60. package/e2e/pages/index.ts +1 -0
  61. package/e2e/specs/sample-test.spec.ts +11 -0
  62. package/e2e/support/github/Dockerfile +34 -0
  63. package/e2e/support/github/docker-compose.yml +24 -0
  64. package/e2e/support/github/run-e2e-docker-env.sh +49 -0
  65. package/example.env +6 -0
  66. package/i18next-parser.config.js +89 -0
  67. package/jest.config.js +34 -0
  68. package/package.json +123 -0
  69. package/playwright.config.ts +32 -0
  70. package/prettier.config.js +8 -0
  71. package/src/bill-history/bill-history.component.tsx +187 -0
  72. package/src/bill-history/bill-history.scss +151 -0
  73. package/src/bill-history/bill-history.test.tsx +122 -0
  74. package/src/billable-services/bill-waiver/bill-selection.component.tsx +72 -0
  75. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +108 -0
  76. package/src/billable-services/bill-waiver/bill-waiver-form.scss +34 -0
  77. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +32 -0
  78. package/src/billable-services/bill-waiver/bill-waiver.scss +10 -0
  79. package/src/billable-services/bill-waiver/patient-bills.component.tsx +135 -0
  80. package/src/billable-services/bill-waiver/utils.ts +41 -0
  81. package/src/billable-services/billable-service.resource.ts +71 -0
  82. package/src/billable-services/billable-services-home.component.tsx +51 -0
  83. package/src/billable-services/billable-services.component.tsx +255 -0
  84. package/src/billable-services/billable-services.scss +218 -0
  85. package/src/billable-services/billable-services.test.tsx +16 -0
  86. package/src/billable-services/create-edit/add-billable-service.component.tsx +322 -0
  87. package/src/billable-services/create-edit/add-billable-service.scss +131 -0
  88. package/src/billable-services/create-edit/add-billable-service.test.tsx +152 -0
  89. package/src/billable-services/dashboard/dashboard.component.tsx +15 -0
  90. package/src/billable-services/dashboard/dashboard.scss +27 -0
  91. package/src/billable-services/dashboard/dashboard.test.tsx +11 -0
  92. package/src/billable-services/dashboard/service-metrics.component.tsx +42 -0
  93. package/src/billable-services-admin-card-link.component.test.tsx +21 -0
  94. package/src/billable-services-admin-card-link.component.tsx +25 -0
  95. package/src/billing-dashboard/billing-dashboard.component.tsx +20 -0
  96. package/src/billing-dashboard/billing-dashboard.scss +27 -0
  97. package/src/billing-dashboard/billing-dashboard.test.tsx +13 -0
  98. package/src/billing-form/billing-checkin-form.component.tsx +131 -0
  99. package/src/billing-form/billing-checkin-form.scss +13 -0
  100. package/src/billing-form/billing-checkin-form.test.tsx +134 -0
  101. package/src/billing-form/billing-form.component.tsx +25 -0
  102. package/src/billing-form/billing-form.resource.ts +31 -0
  103. package/src/billing-form/billing-form.scss +5 -0
  104. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +173 -0
  105. package/src/billing-form/visit-attributes/visit-attributes-form.scss +22 -0
  106. package/src/billing-header/billing-header.component.tsx +43 -0
  107. package/src/billing-header/billing-header.scss +83 -0
  108. package/src/billing-header/billing-illustration.component.tsx +30 -0
  109. package/src/billing.resource.ts +120 -0
  110. package/src/bills-table/bills-table.component.tsx +280 -0
  111. package/src/bills-table/bills-table.scss +181 -0
  112. package/src/bills-table/bills-table.test.tsx +154 -0
  113. package/src/config-schema.ts +3 -0
  114. package/src/dashboard.meta.ts +6 -0
  115. package/src/declarations.d.ts +4 -0
  116. package/src/helpers/functions.ts +63 -0
  117. package/src/helpers/index.ts +1 -0
  118. package/src/index.ts +56 -0
  119. package/src/invoice/invoice-table.component.tsx +185 -0
  120. package/src/invoice/invoice-table.scss +91 -0
  121. package/src/invoice/invoice.component.tsx +138 -0
  122. package/src/invoice/invoice.scss +93 -0
  123. package/src/invoice/invoice.test.tsx +242 -0
  124. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.component.tsx +17 -0
  125. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +29 -0
  126. package/src/invoice/payments/payment-form/payment-form.component.tsx +105 -0
  127. package/src/invoice/payments/payment-form/payment-form.scss +54 -0
  128. package/src/invoice/payments/payment-history/payment-history.component.tsx +68 -0
  129. package/src/invoice/payments/payment.resource.ts +43 -0
  130. package/src/invoice/payments/payments.component.tsx +140 -0
  131. package/src/invoice/payments/payments.scss +46 -0
  132. package/src/invoice/payments/utils.ts +30 -0
  133. package/src/invoice/payments/visit-tags/visit-attribute.component.tsx +21 -0
  134. package/src/invoice/printable-invoice/print-receipt.component.tsx +28 -0
  135. package/src/invoice/printable-invoice/print-receipt.scss +14 -0
  136. package/src/invoice/printable-invoice/printable-footer.component.tsx +19 -0
  137. package/src/invoice/printable-invoice/printable-footer.scss +17 -0
  138. package/src/invoice/printable-invoice/printable-footer.test.tsx +30 -0
  139. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +63 -0
  140. package/src/invoice/printable-invoice/printable-invoice-header.scss +61 -0
  141. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +58 -0
  142. package/src/invoice/printable-invoice/printable-invoice.component.tsx +146 -0
  143. package/src/invoice/printable-invoice/printable-invoice.scss +50 -0
  144. package/src/left-panel-link.component.tsx +41 -0
  145. package/src/left-panel-link.test.tsx +38 -0
  146. package/src/metrics-cards/card.component.tsx +11 -0
  147. package/src/metrics-cards/card.scss +20 -0
  148. package/src/metrics-cards/metrics-cards.component.tsx +42 -0
  149. package/src/metrics-cards/metrics-cards.scss +12 -0
  150. package/src/metrics-cards/metrics-cards.test.tsx +41 -0
  151. package/src/metrics-cards/metrics.resource.ts +45 -0
  152. package/src/modal/require-payment-modal.component.tsx +81 -0
  153. package/src/modal/require-payment.scss +6 -0
  154. package/src/root.component.tsx +19 -0
  155. package/src/root.scss +30 -0
  156. package/src/routes.json +79 -0
  157. package/src/setup-tests.ts +13 -0
  158. package/src/types/index.ts +167 -0
  159. package/test-helpers.tsx +23 -0
  160. package/translations/am.json +107 -0
  161. package/translations/en.json +107 -0
  162. package/translations/es.json +107 -0
  163. package/translations/fr.json +107 -0
  164. package/translations/he.json +107 -0
  165. package/translations/km.json +107 -0
  166. package/tsconfig.json +16 -0
  167. package/webpack.config.js +1 -0
@@ -0,0 +1,22 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+
5
+ .sectionContainer {
6
+ margin: 0 layout.$spacing-03;
7
+ }
8
+
9
+ .sectionTitle {
10
+ @include type.type-style('heading-compact-02');
11
+ color: colors.$gray-70;
12
+ margin: 0 0 layout.$spacing-03 0;
13
+ }
14
+ .visitAttributesContainer {
15
+ row-gap: layout.$spacing-03;
16
+ display: flex;
17
+ flex-direction: column;
18
+ }
19
+
20
+ .sectionField {
21
+ margin-top: layout.$spacing-03;
22
+ }
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Calendar, Location, UserFollow } from '@carbon/react/icons';
4
+ import { formatDate, useSession } from '@openmrs/esm-framework';
5
+ import BillingIllustration from './billing-illustration.component';
6
+ import styles from './billing-header.scss';
7
+
8
+ interface BillingHeaderProps {
9
+ title: string;
10
+ }
11
+
12
+ const BillingHeader: React.FC<BillingHeaderProps> = ({ title }) => {
13
+ const { t } = useTranslation();
14
+ const session = useSession();
15
+ const location = session?.sessionLocation?.display;
16
+
17
+ return (
18
+ <div className={styles.header} data-testid="billing-header">
19
+ <div className={styles['left-justified-items']}>
20
+ <BillingIllustration />
21
+ <div className={styles['page-labels']}>
22
+ <p>{t('billing', 'Billing')}</p>
23
+ <p className={styles['page-name']}>{title}</p>
24
+ </div>
25
+ </div>
26
+ <div className={styles['right-justified-items']}>
27
+ <div className={styles.userContainer}>
28
+ <p>{session?.user?.person?.display}</p>
29
+ <UserFollow size={16} className={styles.userIcon} />
30
+ </div>
31
+ <div className={styles['date-and-location']}>
32
+ <Location size={16} />
33
+ <span className={styles.value}>{location}</span>
34
+ <span className={styles.middot}>&middot;</span>
35
+ <Calendar size={16} />
36
+ <span className={styles.value}>{formatDate(new Date(), { mode: 'standard' })}</span>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ );
41
+ };
42
+
43
+ export default BillingHeader;
@@ -0,0 +1,83 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .header {
6
+ @include type.type-style('body-compact-02');
7
+ color: $text-02;
8
+ height: layout.$spacing-12;
9
+ background-color: $ui-02;
10
+ border-bottom: 1px solid $ui-03;
11
+ display: flex;
12
+ justify-content: space-between;
13
+ padding: layout.$spacing-05;
14
+ }
15
+
16
+ .left-justified-items {
17
+ display: flex;
18
+ flex-direction: row;
19
+ align-items: center;
20
+ cursor: pointer;
21
+ align-items: center;
22
+ }
23
+
24
+ .right-justified-items {
25
+ @include type.type-style('body-compact-02');
26
+ color: $text-02;
27
+ display: flex;
28
+ flex-direction: column;
29
+ justify-content: space-between;
30
+ }
31
+
32
+ .page-name {
33
+ @include type.type-style('heading-04');
34
+ }
35
+
36
+ .page-labels {
37
+ margin: layout.$spacing-05;
38
+
39
+ p:first-of-type {
40
+ margin-bottom: layout.$spacing-02;
41
+ }
42
+ }
43
+
44
+ .date-and-location {
45
+ display: flex;
46
+ justify-content: flex-end;
47
+ align-items: center;
48
+ }
49
+
50
+ .userContainer {
51
+ display: flex;
52
+ justify-content: flex-end;
53
+ gap: layout.$spacing-05;
54
+ }
55
+
56
+ .value {
57
+ margin-left: layout.$spacing-02;
58
+ }
59
+
60
+ .middot {
61
+ margin: 0 layout.$spacing-03;
62
+ }
63
+
64
+ .view {
65
+ @include type.type-style('label-01');
66
+ }
67
+
68
+ // Overriding styles for RTL support
69
+ html[dir='rtl'] {
70
+ .date-and-location {
71
+ & > svg {
72
+ order: -1;
73
+ }
74
+ & > span:nth-child(2) {
75
+ order: -2;
76
+ }
77
+ }
78
+ }
79
+
80
+ .userIcon {
81
+ fill: $ui-05;
82
+ margin: layout.$spacing-01;
83
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+
3
+ const BillingIllustration: React.FC = () => {
4
+ return (
5
+ <svg width="64" height="64" viewBox="10 10 60 60.02">
6
+ <title>Billing module illustration</title>
7
+ <g fill="none" fillRule="evenodd">
8
+ <path
9
+ opacity="0.5"
10
+ d="M58.75 68.639C60.8886 66.8242 64.1114 66.8242 66.25 68.639C67.703 69.8719 70 68.8873 70 67.0314V12.9686C70 11.1128 67.703 10.1281 66.25 11.3611C64.1114 13.1758 60.8886 13.1758 58.75 11.3611C56.6114 9.54632 53.3886 9.54632 51.25 11.3611C49.1114 13.1758 45.8886 13.1758 43.75 11.3611C41.6114 9.54632 38.3886 9.54632 36.25 11.3611C34.1114 13.1758 30.8886 13.1758 28.75 11.3611C26.6114 9.54632 23.3886 9.54632 21.25 11.3611C19.1114 13.1758 15.8886 13.1758 13.75 11.3611C12.297 10.1281 10 11.1128 10 12.9686V67.0314C10 68.8873 12.297 69.8719 13.75 68.639C15.8886 66.8242 19.1114 66.8242 21.25 68.639C23.3886 70.4537 26.6114 70.4537 28.75 68.639C30.8886 66.8242 34.1114 66.8242 36.25 68.639C38.3886 70.4537 41.6114 70.4537 43.75 68.639C45.8886 66.8242 49.1114 66.8242 51.25 68.639C53.3886 70.4537 56.6114 70.4537 58.75 68.639Z"
11
+ fill="#CEE6E5"
12
+ />
13
+ <path
14
+ d="M22.5 51.6666C22.5 50.2859 23.6193 49.1666 25 49.1666H55C56.3807 49.1666 57.5 50.2859 57.5 51.6666C57.5 53.0474 56.3807 54.1666 55 54.1666H25C23.6193 54.1666 22.5 53.0474 22.5 51.6666Z"
15
+ fill="#7BBCB9"
16
+ />
17
+ <path
18
+ d="M22.5 40C22.5 38.6193 23.6193 37.5 25 37.5H55C56.3807 37.5 57.5 38.6193 57.5 40C57.5 41.3807 56.3807 42.5 55 42.5H25C23.6193 42.5 22.5 41.3807 22.5 40Z"
19
+ fill="#7BBCB9"
20
+ />
21
+ <path
22
+ d="M22.5 28.3333C22.5 26.9526 23.6193 25.8333 25 25.8333H55C56.3807 25.8333 57.5 26.9526 57.5 28.3333C57.5 29.714 56.3807 30.8333 55 30.8333H25C23.6193 30.8333 22.5 29.714 22.5 28.3333Z"
23
+ fill="#7BBCB9"
24
+ />
25
+ </g>
26
+ </svg>
27
+ );
28
+ };
29
+
30
+ export default BillingIllustration;
@@ -0,0 +1,120 @@
1
+ import useSWR from 'swr';
2
+ import { formatDate, parseDate, openmrsFetch, useSession, useVisit } from '@openmrs/esm-framework';
3
+ import type { FacilityDetail, MappedBill, PatientInvoice } from './types';
4
+ import isEmpty from 'lodash-es/isEmpty';
5
+ import sortBy from 'lodash-es/sortBy';
6
+
7
+ export const useBills = (patientUuid: string = '', billStatus: string = '') => {
8
+ const url = `/ws/rest/v1/cashier/bill?v=full`;
9
+
10
+ const { data, error, isLoading, isValidating, mutate } = useSWR<{ data: { results: Array<PatientInvoice> } }>(
11
+ url,
12
+ openmrsFetch,
13
+ );
14
+
15
+ const mapBillProperties = (bill: PatientInvoice): MappedBill => ({
16
+ id: bill?.id,
17
+ uuid: bill?.uuid,
18
+ patientName: bill?.patient?.display.split('-')?.[1],
19
+ identifier: bill?.patient?.display.split('-')?.[0],
20
+ patientUuid: bill?.patient?.uuid,
21
+ status: bill?.status,
22
+ receiptNumber: bill?.receiptNumber,
23
+ cashier: bill?.cashier,
24
+ cashPointUuid: bill?.cashPoint?.uuid,
25
+ cashPointName: bill?.cashPoint?.name,
26
+ cashPointLocation: bill?.cashPoint?.location?.display,
27
+ dateCreated: bill?.dateCreated ? formatDate(parseDate(bill.dateCreated), { mode: 'wide' }) : '--',
28
+ lineItems: bill.lineItems,
29
+ billingService: bill.lineItems.map((bill) => bill.item || bill.billableService || '--').join(' '),
30
+ payments: bill.payments,
31
+ display: bill.display,
32
+ totalAmount: bill?.lineItems?.map((item) => item.price * item.quantity).reduce((prev, curr) => prev + curr, 0),
33
+ });
34
+
35
+ const sortedBills = sortBy(data?.data?.results ?? [], ['dateCreated']).reverse();
36
+ const filteredBills = billStatus === '' ? sortedBills : sortedBills?.filter((bill) => bill.status === billStatus);
37
+ const mappedResults = filteredBills?.map((bill) => mapBillProperties(bill));
38
+ const filteredResults = mappedResults?.filter((res) => res.patientUuid === patientUuid);
39
+ const formattedBills = isEmpty(patientUuid) ? mappedResults : filteredResults || [];
40
+
41
+ return {
42
+ bills: formattedBills,
43
+ error,
44
+ isLoading,
45
+ isValidating,
46
+ mutate,
47
+ };
48
+ };
49
+
50
+ export const useBill = (billUuid: string) => {
51
+ const url = `/ws/rest/v1/cashier/bill/${billUuid}`;
52
+ const { data, error, isLoading, isValidating, mutate } = useSWR<{ data: PatientInvoice }>(
53
+ billUuid ? url : null,
54
+ openmrsFetch,
55
+ );
56
+
57
+ const mapBillProperties = (bill: PatientInvoice): MappedBill => ({
58
+ id: bill?.id,
59
+ uuid: bill?.uuid,
60
+ patientName: bill?.patient?.display.split('-')?.[1],
61
+ identifier: bill?.patient?.display.split('-')?.[0],
62
+ patientUuid: bill?.patient?.uuid,
63
+ status: bill?.status,
64
+ receiptNumber: bill?.receiptNumber,
65
+ cashier: bill?.cashier,
66
+ cashPointUuid: bill?.cashPoint?.uuid,
67
+ cashPointName: bill?.cashPoint?.name,
68
+ cashPointLocation: bill?.cashPoint?.location?.display,
69
+ dateCreated: bill?.dateCreated ? formatDate(parseDate(bill.dateCreated), { mode: 'wide' }) : '--',
70
+ lineItems: bill.lineItems,
71
+ billingService: bill.lineItems.map((bill) => bill.item).join(' '),
72
+ payments: bill.payments,
73
+ totalAmount: bill?.lineItems?.map((item) => item.price * item.quantity).reduce((prev, curr) => prev + curr, 0),
74
+ tenderedAmount: bill?.payments?.map((item) => item.amountTendered).reduce((prev, curr) => prev + curr, 0),
75
+ });
76
+
77
+ const formattedBill = data?.data ? mapBillProperties(data?.data) : null;
78
+
79
+ return {
80
+ bill: formattedBill,
81
+ error,
82
+ isLoading,
83
+ isValidating,
84
+ mutate,
85
+ };
86
+ };
87
+
88
+ export const processBillPayment = (payload, billUuid: string) => {
89
+ const url = `/ws/rest/v1/cashier/bill/${billUuid}`;
90
+
91
+ return openmrsFetch(url, {
92
+ method: 'POST',
93
+ body: payload,
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ },
97
+ });
98
+ };
99
+
100
+ export function useDefaultFacility() {
101
+ const url = '/ws/rest/v1/kenyaemr/default-facility';
102
+ const { authenticated } = useSession();
103
+
104
+ const { data, isLoading } = useSWR<{ data: FacilityDetail }>(authenticated ? url : null, openmrsFetch);
105
+
106
+ return { data: data?.data, isLoading: isLoading };
107
+ }
108
+
109
+ export const usePatientPaymentInfo = (patientUuid: string) => {
110
+ const { currentVisit } = useVisit(patientUuid);
111
+ const attributes = currentVisit?.attributes ?? [];
112
+ const paymentInformation = attributes
113
+ .map((attribute) => ({
114
+ name: attribute.attributeType.name,
115
+ value: attribute.value,
116
+ }))
117
+ .filter(({ name }) => name === 'Insurance scheme' || name === 'Policy Number');
118
+
119
+ return paymentInformation;
120
+ };
@@ -0,0 +1,280 @@
1
+ import React, { useCallback, useId, useMemo, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import {
4
+ DataTable,
5
+ DataTableSkeleton,
6
+ Dropdown,
7
+ InlineLoading,
8
+ Layer,
9
+ Pagination,
10
+ Search,
11
+ Table,
12
+ TableBody,
13
+ TableCell,
14
+ TableContainer,
15
+ TableHead,
16
+ TableHeader,
17
+ TableRow,
18
+ Tile,
19
+ } from '@carbon/react';
20
+ import { useTranslation } from 'react-i18next';
21
+ import {
22
+ useLayoutType,
23
+ isDesktop,
24
+ useConfig,
25
+ usePagination,
26
+ ErrorState,
27
+ ConfigurableLink,
28
+ } from '@openmrs/esm-framework';
29
+ import { EmptyDataIllustration } from '@openmrs/esm-patient-common-lib';
30
+ import { useBills } from '../billing.resource';
31
+ import styles from './bills-table.scss';
32
+
33
+ const BillsTable = () => {
34
+ const { t } = useTranslation();
35
+ const id = useId();
36
+ const config = useConfig();
37
+ const layout = useLayoutType();
38
+ const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
39
+ const [billPaymentStatus, setBillPaymentStatus] = useState('');
40
+ const pageSizes = config?.bills?.pageSizes ?? [10, 20, 30, 40, 50];
41
+ const [pageSize, setPageSize] = useState(config?.bills?.pageSize ?? 10);
42
+ const { bills, isLoading, isValidating, error } = useBills('', billPaymentStatus);
43
+ const [searchString, setSearchString] = useState('');
44
+
45
+ const headerData = [
46
+ {
47
+ header: t('visitTime', 'Visit time'),
48
+ key: 'visitTime',
49
+ },
50
+ {
51
+ header: t('identifier', 'Identifier'),
52
+ key: 'identifier',
53
+ },
54
+ {
55
+ header: t('name', 'Name'),
56
+ key: 'patientName',
57
+ },
58
+ {
59
+ header: t('billedItems', 'Billed Items'),
60
+ key: 'billedItems',
61
+ },
62
+ ];
63
+
64
+ const searchResults = useMemo(() => {
65
+ if (bills !== undefined && bills.length > 0) {
66
+ if (searchString && searchString.trim() !== '') {
67
+ const search = searchString.toLowerCase();
68
+ return bills?.filter((activeBillRow) =>
69
+ Object.entries(activeBillRow).some(([header, value]) => {
70
+ if (header === 'patientUuid') {
71
+ return false;
72
+ }
73
+ return `${value}`.toLowerCase().includes(search);
74
+ }),
75
+ );
76
+ }
77
+ }
78
+
79
+ return bills;
80
+ }, [searchString, bills]);
81
+
82
+ const { paginated, goTo, results, currentPage } = usePagination(searchResults, pageSize);
83
+
84
+ const setBilledItems = (bill) =>
85
+ bill?.lineItems?.reduce((acc, item) => acc + (acc ? ' & ' : '') + (item.billableService || item.item || ''), '');
86
+
87
+ const billingUrl = '${openmrsSpaBase}/home/billing/patient/${patientUuid}/${uuid}';
88
+
89
+ const rowData = results?.map((bill, index) => ({
90
+ id: `${index}`,
91
+ uuid: bill.uuid,
92
+ patientName: (
93
+ <ConfigurableLink
94
+ style={{ textDecoration: 'none', maxWidth: '50%' }}
95
+ to={billingUrl}
96
+ templateParams={{ patientUuid: bill.patientUuid, uuid: bill.uuid }}>
97
+ {bill.patientName}
98
+ </ConfigurableLink>
99
+ ),
100
+ visitTime: bill.dateCreated,
101
+ identifier: bill.identifier,
102
+ department: '--',
103
+ billedItems: setBilledItems(bill),
104
+ billingPrice: '--',
105
+ }));
106
+
107
+ const handleSearch = useCallback(
108
+ (e) => {
109
+ goTo(1);
110
+ setSearchString(e.target.value);
111
+ },
112
+ [goTo, setSearchString],
113
+ );
114
+
115
+ const filterItems = [
116
+ { id: '', text: 'All bills' },
117
+ { id: 'PENDING', text: 'Pending bills' },
118
+ { id: 'PAID', text: 'Paid bills' },
119
+ ];
120
+
121
+ const handleFilterChange = ({ selectedItem }) => setBillPaymentStatus(selectedItem.id);
122
+
123
+ if (isLoading) {
124
+ return (
125
+ <div className={styles.loaderContainer}>
126
+ <DataTableSkeleton
127
+ rowCount={pageSize}
128
+ showHeader={false}
129
+ showToolbar={false}
130
+ zebra
131
+ columnCount={headerData?.length}
132
+ size={responsiveSize}
133
+ />
134
+ </div>
135
+ );
136
+ }
137
+
138
+ if (error) {
139
+ return (
140
+ <div className={styles.errorContainer}>
141
+ <Layer>
142
+ <ErrorState error={error} headerTitle={t('billsList', 'Bill list')} />
143
+ </Layer>
144
+ </div>
145
+ );
146
+ }
147
+
148
+ return (
149
+ <>
150
+ <div className={styles.filterContainer}>
151
+ <Dropdown
152
+ className={styles.filterDropdown}
153
+ direction="bottom"
154
+ id={`filter-${id}`}
155
+ initialSelectedItem={filterItems[0]}
156
+ items={filterItems}
157
+ itemToString={(item) => (item ? item.text : '')}
158
+ label=""
159
+ onChange={handleFilterChange}
160
+ size={responsiveSize}
161
+ titleText={t('filterBy', 'Filter by') + ':'}
162
+ type="inline"
163
+ />
164
+ </div>
165
+
166
+ {bills?.length > 0 ? (
167
+ <div className={styles.billListContainer}>
168
+ <FilterableTableHeader
169
+ handleSearch={handleSearch}
170
+ isValidating={isValidating}
171
+ layout={layout}
172
+ responsiveSize={responsiveSize}
173
+ t={t}
174
+ />
175
+ <DataTable
176
+ isSortable
177
+ rows={rowData}
178
+ headers={headerData}
179
+ size={responsiveSize}
180
+ useZebraStyles={rowData?.length > 1 ? true : false}>
181
+ {({ rows, headers, getRowProps, getTableProps }) => (
182
+ <TableContainer>
183
+ <Table {...getTableProps()} aria-label="bill list">
184
+ <TableHead>
185
+ <TableRow>
186
+ {headers.map((header) => (
187
+ <TableHeader key={header.key}>{header.header}</TableHeader>
188
+ ))}
189
+ </TableRow>
190
+ </TableHead>
191
+ <TableBody>
192
+ {rows.map((row) => (
193
+ <TableRow
194
+ key={row.id}
195
+ {...getRowProps({
196
+ row,
197
+ })}>
198
+ {row.cells.map((cell) => (
199
+ <TableCell key={cell.id}>{cell.value}</TableCell>
200
+ ))}
201
+ </TableRow>
202
+ ))}
203
+ </TableBody>
204
+ </Table>
205
+ </TableContainer>
206
+ )}
207
+ </DataTable>
208
+ {searchResults?.length === 0 && (
209
+ <div className={styles.filterEmptyState}>
210
+ <Layer level={0}>
211
+ <Tile className={styles.filterEmptyStateTile}>
212
+ <p className={styles.filterEmptyStateContent}>
213
+ {t('noMatchingBillsToDisplay', 'No matching bills to display')}
214
+ </p>
215
+ <p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
216
+ </Tile>
217
+ </Layer>
218
+ </div>
219
+ )}
220
+ {paginated && (
221
+ <Pagination
222
+ forwardText="Next page"
223
+ backwardText="Previous page"
224
+ page={currentPage}
225
+ pageSize={pageSize}
226
+ pageSizes={pageSizes}
227
+ totalItems={searchResults?.length}
228
+ className={styles.pagination}
229
+ size={responsiveSize}
230
+ onChange={({ pageSize: newPageSize, page: newPage }) => {
231
+ if (newPageSize !== pageSize) {
232
+ setPageSize(newPageSize);
233
+ }
234
+ if (newPage !== currentPage) {
235
+ goTo(newPage);
236
+ }
237
+ }}
238
+ />
239
+ )}
240
+ </div>
241
+ ) : (
242
+ <Layer className={styles.emptyStateContainer}>
243
+ <Tile className={styles.tile}>
244
+ <div className={styles.illo}>
245
+ <EmptyDataIllustration />
246
+ </div>
247
+ <p className={styles.content}>There are no bills to display.</p>
248
+ </Tile>
249
+ </Layer>
250
+ )}
251
+ </>
252
+ );
253
+ };
254
+
255
+ function FilterableTableHeader({ layout, handleSearch, isValidating, responsiveSize, t }) {
256
+ return (
257
+ <>
258
+ <div className={styles.headerContainer}>
259
+ <div
260
+ className={classNames({
261
+ [styles.tabletHeading]: !isDesktop(layout),
262
+ [styles.desktopHeading]: isDesktop(layout),
263
+ })}>
264
+ <h4>{t('billList', 'Bill list')}</h4>
265
+ </div>
266
+ <div className={styles.backgroundDataFetchingIndicator}>
267
+ <span>{isValidating ? <InlineLoading /> : null}</span>
268
+ </div>
269
+ </div>
270
+ <Search
271
+ labelText=""
272
+ placeholder={t('filterTable', 'Filter table')}
273
+ onChange={handleSearch}
274
+ size={responsiveSize}
275
+ />
276
+ </>
277
+ );
278
+ }
279
+
280
+ export default BillsTable;