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

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 (179) 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 +394 -0
  12. package/__mocks__/delivery-summary.mock.ts +89 -0
  13. package/__mocks__/encounter-observation.mock.ts +10651 -0
  14. package/__mocks__/encounter-observations.mock.ts +6189 -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/146.js +1 -0
  21. package/dist/146.js.map +1 -0
  22. package/dist/294.js +2 -0
  23. package/dist/294.js.LICENSE.txt +9 -0
  24. package/dist/294.js.map +1 -0
  25. package/dist/319.js +1 -0
  26. package/dist/384.js +1 -0
  27. package/dist/384.js.map +1 -0
  28. package/dist/421.js +1 -0
  29. package/dist/421.js.map +1 -0
  30. package/dist/533.js +1 -0
  31. package/dist/533.js.map +1 -0
  32. package/dist/574.js +1 -0
  33. package/dist/591.js +2 -0
  34. package/dist/591.js.LICENSE.txt +9 -0
  35. package/dist/591.js.map +1 -0
  36. package/dist/614.js +2 -0
  37. package/dist/614.js.LICENSE.txt +37 -0
  38. package/dist/614.js.map +1 -0
  39. package/dist/753.js +1 -0
  40. package/dist/753.js.map +1 -0
  41. package/dist/757.js +1 -0
  42. package/dist/770.js +1 -0
  43. package/dist/770.js.map +1 -0
  44. package/dist/783.js +1 -0
  45. package/dist/783.js.map +1 -0
  46. package/dist/788.js +1 -0
  47. package/dist/800.js +2 -0
  48. package/dist/800.js.LICENSE.txt +3 -0
  49. package/dist/800.js.map +1 -0
  50. package/dist/807.js +1 -0
  51. package/dist/833.js +1 -0
  52. package/dist/935.js +2 -0
  53. package/dist/935.js.LICENSE.txt +19 -0
  54. package/dist/935.js.map +1 -0
  55. package/dist/992.js +1 -0
  56. package/dist/992.js.map +1 -0
  57. package/dist/main.js +2 -0
  58. package/dist/main.js.LICENSE.txt +47 -0
  59. package/dist/main.js.map +1 -0
  60. package/dist/openmrs-esm-billing-app.js +1 -0
  61. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +609 -0
  62. package/dist/openmrs-esm-billing-app.js.map +1 -0
  63. package/dist/routes.json +1 -0
  64. package/e2e/README.md +115 -0
  65. package/e2e/core/global-setup.ts +32 -0
  66. package/e2e/core/index.ts +1 -0
  67. package/e2e/core/test.ts +20 -0
  68. package/e2e/fixtures/api.ts +27 -0
  69. package/e2e/fixtures/index.ts +1 -0
  70. package/e2e/pages/home-page.ts +9 -0
  71. package/e2e/pages/index.ts +1 -0
  72. package/e2e/specs/sample-test.spec.ts +11 -0
  73. package/e2e/support/github/Dockerfile +34 -0
  74. package/e2e/support/github/docker-compose.yml +24 -0
  75. package/e2e/support/github/run-e2e-docker-env.sh +49 -0
  76. package/example.env +6 -0
  77. package/i18next-parser.config.js +89 -0
  78. package/jest.config.js +34 -0
  79. package/package.json +124 -0
  80. package/playwright.config.ts +32 -0
  81. package/prettier.config.js +8 -0
  82. package/src/bill-history/bill-history.component.tsx +199 -0
  83. package/src/bill-history/bill-history.scss +151 -0
  84. package/src/bill-history/bill-history.test.tsx +122 -0
  85. package/src/billable-services/bill-waiver/bill-selection.component.tsx +76 -0
  86. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +110 -0
  87. package/src/billable-services/bill-waiver/bill-waiver-form.scss +34 -0
  88. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +32 -0
  89. package/src/billable-services/bill-waiver/bill-waiver.scss +10 -0
  90. package/src/billable-services/bill-waiver/patient-bills.component.tsx +137 -0
  91. package/src/billable-services/bill-waiver/utils.ts +41 -0
  92. package/src/billable-services/billable-service.resource.ts +72 -0
  93. package/src/billable-services/billable-services-home.component.tsx +51 -0
  94. package/src/billable-services/billable-services.component.tsx +255 -0
  95. package/src/billable-services/billable-services.scss +218 -0
  96. package/src/billable-services/billable-services.test.tsx +16 -0
  97. package/src/billable-services/create-edit/add-billable-service.component.tsx +322 -0
  98. package/src/billable-services/create-edit/add-billable-service.scss +131 -0
  99. package/src/billable-services/create-edit/add-billable-service.test.tsx +152 -0
  100. package/src/billable-services/dashboard/dashboard.component.tsx +15 -0
  101. package/src/billable-services/dashboard/dashboard.scss +27 -0
  102. package/src/billable-services/dashboard/dashboard.test.tsx +11 -0
  103. package/src/billable-services/dashboard/service-metrics.component.tsx +41 -0
  104. package/src/billable-services-admin-card-link.component.test.tsx +21 -0
  105. package/src/billable-services-admin-card-link.component.tsx +25 -0
  106. package/src/billing-dashboard/billing-dashboard.component.tsx +20 -0
  107. package/src/billing-dashboard/billing-dashboard.scss +27 -0
  108. package/src/billing-dashboard/billing-dashboard.test.tsx +13 -0
  109. package/src/billing-form/billing-checkin-form.component.tsx +127 -0
  110. package/src/billing-form/billing-checkin-form.scss +13 -0
  111. package/src/billing-form/billing-checkin-form.test.tsx +134 -0
  112. package/src/billing-form/billing-form.component.tsx +347 -0
  113. package/src/billing-form/billing-form.resource.ts +32 -0
  114. package/src/billing-form/billing-form.scss +88 -0
  115. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +173 -0
  116. package/src/billing-form/visit-attributes/visit-attributes-form.scss +22 -0
  117. package/src/billing-header/billing-header.component.tsx +43 -0
  118. package/src/billing-header/billing-header.scss +83 -0
  119. package/src/billing-header/billing-illustration.component.tsx +30 -0
  120. package/src/billing.resource.ts +148 -0
  121. package/src/bills-table/bills-table.component.tsx +280 -0
  122. package/src/bills-table/bills-table.scss +181 -0
  123. package/src/bills-table/bills-table.test.tsx +154 -0
  124. package/src/config-schema.ts +50 -0
  125. package/src/constants.ts +3 -0
  126. package/src/dashboard.meta.ts +7 -0
  127. package/src/declarations.d.ts +4 -0
  128. package/src/helpers/functions.ts +66 -0
  129. package/src/helpers/index.ts +1 -0
  130. package/src/index.ts +72 -0
  131. package/src/invoice/invoice-table.component.tsx +189 -0
  132. package/src/invoice/invoice-table.scss +91 -0
  133. package/src/invoice/invoice.component.tsx +144 -0
  134. package/src/invoice/invoice.scss +93 -0
  135. package/src/invoice/invoice.test.tsx +242 -0
  136. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.component.tsx +17 -0
  137. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +29 -0
  138. package/src/invoice/payments/payment-form/payment-form.component.tsx +105 -0
  139. package/src/invoice/payments/payment-form/payment-form.scss +54 -0
  140. package/src/invoice/payments/payment-history/payment-history.component.tsx +69 -0
  141. package/src/invoice/payments/payment.resource.ts +44 -0
  142. package/src/invoice/payments/payments.component.tsx +147 -0
  143. package/src/invoice/payments/payments.scss +46 -0
  144. package/src/invoice/payments/utils.ts +68 -0
  145. package/src/invoice/payments/visit-tags/visit-attribute.component.tsx +21 -0
  146. package/src/invoice/printable-invoice/print-receipt.component.tsx +29 -0
  147. package/src/invoice/printable-invoice/print-receipt.scss +14 -0
  148. package/src/invoice/printable-invoice/printable-footer.component.tsx +19 -0
  149. package/src/invoice/printable-invoice/printable-footer.scss +17 -0
  150. package/src/invoice/printable-invoice/printable-footer.test.tsx +30 -0
  151. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +63 -0
  152. package/src/invoice/printable-invoice/printable-invoice-header.scss +61 -0
  153. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +58 -0
  154. package/src/invoice/printable-invoice/printable-invoice.component.tsx +146 -0
  155. package/src/invoice/printable-invoice/printable-invoice.scss +50 -0
  156. package/src/left-panel-link.component.tsx +41 -0
  157. package/src/left-panel-link.test.tsx +38 -0
  158. package/src/metrics-cards/card.component.tsx +14 -0
  159. package/src/metrics-cards/card.scss +20 -0
  160. package/src/metrics-cards/metrics-cards.component.tsx +42 -0
  161. package/src/metrics-cards/metrics-cards.scss +12 -0
  162. package/src/metrics-cards/metrics-cards.test.tsx +44 -0
  163. package/src/metrics-cards/metrics.resource.ts +45 -0
  164. package/src/modal/require-payment-modal.component.tsx +85 -0
  165. package/src/modal/require-payment.scss +6 -0
  166. package/src/root.component.tsx +19 -0
  167. package/src/root.scss +30 -0
  168. package/src/routes.json +78 -0
  169. package/src/setup-tests.ts +13 -0
  170. package/src/types/index.ts +181 -0
  171. package/test-helpers.tsx +23 -0
  172. package/translations/am.json +117 -0
  173. package/translations/en.json +117 -0
  174. package/translations/es.json +117 -0
  175. package/translations/fr.json +117 -0
  176. package/translations/he.json +117 -0
  177. package/translations/km.json +117 -0
  178. package/tsconfig.json +16 -0
  179. package/webpack.config.js +1 -0
@@ -0,0 +1,146 @@
1
+ import React, { useMemo } from 'react';
2
+ import {
3
+ DataTable,
4
+ Table,
5
+ TableContainer,
6
+ TableHead,
7
+ TableRow,
8
+ TableBody,
9
+ TableHeader,
10
+ TableCell,
11
+ DataTableSkeleton,
12
+ } from '@carbon/react';
13
+ import { age, isDesktop, useLayoutType } from '@openmrs/esm-framework';
14
+ import { getGender } from '../../helpers';
15
+ import { type MappedBill } from '../../types';
16
+ import { useTranslation } from 'react-i18next';
17
+ import PrintableFooter from './printable-footer.component';
18
+ import PrintableInvoiceHeader from './printable-invoice-header.component';
19
+ import styles from './printable-invoice.scss';
20
+
21
+ type PrintableInvoiceProps = {
22
+ bill: MappedBill;
23
+ patient: fhir.Patient;
24
+ isLoading: boolean;
25
+ };
26
+
27
+ const PrintableInvoice: React.FC<PrintableInvoiceProps> = ({ bill, patient, isLoading }) => {
28
+ const { t } = useTranslation();
29
+ const layout = useLayoutType();
30
+ const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
31
+ const headerData = [
32
+ { header: 'Inventory item', key: 'billItem' },
33
+ { header: 'Quantity', key: 'quantity' },
34
+ { header: 'Unit price', key: 'price' },
35
+ { header: 'Total', key: 'total' },
36
+ ];
37
+
38
+ const rowData =
39
+ bill?.lineItems?.map((item) => {
40
+ return {
41
+ id: `${item.uuid}`,
42
+ billItem: item.item,
43
+ quantity: item.quantity,
44
+ price: item.price,
45
+ total: item.price * item.quantity,
46
+ };
47
+ }) ?? [];
48
+
49
+ const invoiceTotal = {
50
+ 'Total Amount': bill?.totalAmount,
51
+ 'Amount Tendered': bill?.tenderedAmount,
52
+ 'Discount Amount': 0,
53
+ 'Amount due': bill?.totalAmount - bill?.tenderedAmount,
54
+ };
55
+
56
+ const patientDetails = useMemo(() => {
57
+ return {
58
+ name: `${patient?.name?.[0]?.given?.join(' ')} ${patient?.name?.[0].family}`,
59
+ age: age(patient?.birthDate),
60
+ gender: getGender(patient?.gender, t),
61
+ city: patient?.address?.[0].city,
62
+ county: patient?.address?.[0].district,
63
+ subCounty: patient?.address?.[0].state,
64
+ };
65
+ }, [patient, t]);
66
+
67
+ const invoiceDetails = {
68
+ 'Invoice #': bill?.receiptNumber,
69
+ 'Invoice date': bill.dateCreated,
70
+ Status: bill?.status,
71
+ };
72
+
73
+ if (isLoading) {
74
+ return (
75
+ <div className={styles.loaderContainer}>
76
+ <DataTableSkeleton
77
+ columnCount={headerData?.length ?? 0}
78
+ showHeader={false}
79
+ showToolbar={false}
80
+ size={responsiveSize}
81
+ zebra
82
+ />
83
+ </div>
84
+ );
85
+ }
86
+
87
+ return (
88
+ <div className={styles.container}>
89
+ <PrintableInvoiceHeader patientDetails={patientDetails} />
90
+ <div className={styles.printableInvoiceContainer}>
91
+ <div className={styles.detailsContainer}>
92
+ {Object.entries(invoiceDetails).map(([key, val]) => (
93
+ <div key={key} className={styles.item}>
94
+ <p className={styles.itemHeading}>{key}</p>
95
+ <span>{val}</span>
96
+ </div>
97
+ ))}
98
+ </div>
99
+
100
+ <div className={styles.itemsContainer}>
101
+ <div className={styles.tableContainer}>
102
+ <DataTable isSortable rows={rowData} headers={headerData} size={responsiveSize} useZebraStyles={false}>
103
+ {({ rows, headers, getRowProps, getTableProps }) => (
104
+ <TableContainer>
105
+ <Table {...getTableProps()} aria-label="Invoice line items">
106
+ <TableHead>
107
+ <TableRow>
108
+ {headers.map((header) => (
109
+ <TableHeader key={header.key}>{header.header}</TableHeader>
110
+ ))}
111
+ </TableRow>
112
+ </TableHead>
113
+ <TableBody>
114
+ {rows.map((row) => (
115
+ <TableRow
116
+ key={row.id}
117
+ {...getRowProps({
118
+ row,
119
+ })}>
120
+ {row.cells.map((cell) => (
121
+ <TableCell key={cell.id}>{cell.value}</TableCell>
122
+ ))}
123
+ </TableRow>
124
+ ))}
125
+ </TableBody>
126
+ </Table>
127
+ </TableContainer>
128
+ )}
129
+ </DataTable>
130
+ </div>
131
+
132
+ <div className={styles.totalContainer}>
133
+ {Object.entries(invoiceTotal).map(([key, val]) => (
134
+ <p key={key} className={styles.itemTotal}>
135
+ <span className={styles.itemHeading}>{key}</span>: <span className={styles.itemLabel}>{val}</span>
136
+ </p>
137
+ ))}
138
+ </div>
139
+ </div>
140
+ </div>
141
+ <PrintableFooter />
142
+ </div>
143
+ );
144
+ };
145
+
146
+ export default PrintableInvoice;
@@ -0,0 +1,50 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .printableInvoiceContainer {
6
+ display: flex;
7
+ flex-direction: row;
8
+ }
9
+
10
+ .itemsContainer {
11
+ display: flex;
12
+ flex-direction: column;
13
+ width: 80%;
14
+ padding: layout.$spacing-05;
15
+ margin: layout.$spacing-05;
16
+ }
17
+
18
+ .detailsContainer {
19
+ margin: layout.$spacing-05 0;
20
+ width: 20%;
21
+ }
22
+
23
+ .tableContainer {
24
+ min-height: 50vh;
25
+ }
26
+
27
+ .totalContainer {
28
+ display: flex;
29
+ flex-direction: column;
30
+ }
31
+
32
+ .item {
33
+ margin: layout.$spacing-04;
34
+ }
35
+
36
+ .itemHeading {
37
+ @include type.type-style('body-compact-02');
38
+ font-weight: bold;
39
+ color: colors.$cool-gray-90;
40
+ margin-bottom: layout.$spacing-02;
41
+ }
42
+
43
+ .itemTotal {
44
+ border-top: solid 0.1pt colors.$cool-gray-30;
45
+ padding: layout.$spacing-02;
46
+ }
47
+
48
+ .itemLabel {
49
+ float: right;
50
+ }
@@ -0,0 +1,41 @@
1
+ import React, { useMemo } from 'react';
2
+ import last from 'lodash-es/last';
3
+ import { BrowserRouter, useLocation } from 'react-router-dom';
4
+ import { ConfigurableLink } from '@openmrs/esm-framework';
5
+
6
+ export interface LinkConfig {
7
+ name: string;
8
+ title: string;
9
+ }
10
+
11
+ export function LinkExtension({ config }: { config: LinkConfig }) {
12
+ const { name, title } = config;
13
+ const location = useLocation();
14
+ const spaBasePath = window.getOpenmrsSpaBase() + 'home';
15
+
16
+ let urlSegment = useMemo(() => decodeURIComponent(last(location.pathname.split('/'))), [location.pathname]);
17
+
18
+ const isUUID = (value) => {
19
+ const regex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/;
20
+ return regex.test(value);
21
+ };
22
+
23
+ if (isUUID(urlSegment)) {
24
+ urlSegment = 'billing';
25
+ }
26
+
27
+ return (
28
+ <ConfigurableLink
29
+ to={spaBasePath + '/' + name}
30
+ className={`cds--side-nav__link ${name === urlSegment && 'active-left-nav-link'}`}>
31
+ {title}
32
+ </ConfigurableLink>
33
+ );
34
+ }
35
+
36
+ export const createLeftPanelLink = (config: LinkConfig) => () =>
37
+ (
38
+ <BrowserRouter>
39
+ <LinkExtension config={config} />
40
+ </BrowserRouter>
41
+ );
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ import { LinkExtension, createLeftPanelLink } from './left-panel-link.component';
5
+ import userEvent from '@testing-library/user-event';
6
+
7
+ window.getOpenmrsSpaBase = () => '/openmrs/spa/';
8
+
9
+ describe('LinkExtension Component', () => {
10
+ const renderWithRouter = (component, { route = '/' } = {}) => {
11
+ window.history.pushState({}, 'Test page', route);
12
+ return render(component, { wrapper: MemoryRouter });
13
+ };
14
+
15
+ test('renders correctly', () => {
16
+ const config = { name: 'billing', title: 'Billing' };
17
+ renderWithRouter(<LinkExtension config={config} />, {
18
+ route: '/billing/6eb8d678-514d-46ad-9554-51e48d96d567',
19
+ });
20
+
21
+ expect(screen.getByText('Billing')).toBeInTheDocument();
22
+ });
23
+ });
24
+
25
+ describe('createLeftPanelLink Function', () => {
26
+ const user = userEvent.setup();
27
+ test('returns a component that renders LinkExtension', () => {
28
+ const config = { name: 'billing', title: 'Billing' };
29
+ const TestComponent = createLeftPanelLink(config);
30
+
31
+ render(<TestComponent />);
32
+ expect(screen.getByText('Billing')).toBeInTheDocument();
33
+ const testLink = screen.getByRole('link', { name: 'Billing' });
34
+ user.click(testLink);
35
+ expect(window.location.pathname).toBe('/billing/6eb8d678-514d-46ad-9554-51e48d96d567');
36
+ // expect(testLink).toHaveClass('active-left-nav-link');
37
+ });
38
+ });
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import styles from './card.scss';
3
+ import { useConfig } from '@openmrs/esm-framework';
4
+ import { convertToCurrency } from '../helpers';
5
+
6
+ export default function Card({ count, title }) {
7
+ const { defaultCurrency } = useConfig();
8
+ return (
9
+ <div className={styles.container}>
10
+ <h1 className={styles.title}>{title}</h1>
11
+ <span className={styles.count}>{convertToCurrency(count, defaultCurrency)}</span>
12
+ </div>
13
+ );
14
+ }
@@ -0,0 +1,20 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ border: 1px solid colors.$gray-20;
7
+ padding: layout.$spacing-05;
8
+ flex: 1;
9
+ }
10
+
11
+ .title {
12
+ @include type.type-style('heading-compact-02');
13
+ }
14
+
15
+ .count {
16
+ @include type.type-style('heading-05');
17
+ display: inline-block;
18
+ font-weight: 300;
19
+ margin-top: layout.$spacing-05;
20
+ }
@@ -0,0 +1,42 @@
1
+ import React, { useMemo } from 'react';
2
+ import { InlineLoading } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { ErrorState } from '@openmrs/esm-patient-common-lib';
5
+ import { useBills } from '../billing.resource';
6
+ import { useBillMetrics } from './metrics.resource';
7
+ import Card from './card.component';
8
+ import styles from './metrics-cards.scss';
9
+
10
+ export default function MetricsCards() {
11
+ const { t } = useTranslation();
12
+ const { bills, isLoading, error } = useBills('');
13
+ const { cumulativeBills, pendingBills, paidBills } = useBillMetrics(bills);
14
+
15
+ const cards = useMemo(
16
+ () => [
17
+ { title: 'Cumulative Bills', count: cumulativeBills },
18
+ { title: 'Pending Bills', count: pendingBills },
19
+ { title: 'Paid Bills', count: paidBills },
20
+ ],
21
+ [cumulativeBills, pendingBills, paidBills],
22
+ );
23
+
24
+ if (isLoading) {
25
+ return (
26
+ <section className={styles.container}>
27
+ <InlineLoading status="active" iconDescription="Loading" description="Loading bill metrics..." />
28
+ </section>
29
+ );
30
+ }
31
+
32
+ if (error) {
33
+ return <ErrorState headerTitle={t('billMetrics', 'Bill metrics')} error={error} />;
34
+ }
35
+ return (
36
+ <section className={styles.container}>
37
+ {cards.map((card) => (
38
+ <Card key={card.title} title={card.title} count={card.count} />
39
+ ))}
40
+ </section>
41
+ );
42
+ }
@@ -0,0 +1,12 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ display: flex;
7
+ margin: layout.$spacing-05;
8
+ justify-content: space-between;
9
+ column-gap: layout.$spacing-09;
10
+ row-gap: layout.$spacing-05;
11
+ flex-flow: row wrap;
12
+ }
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { billsSummary } from '../../__mocks__/bills.mock';
4
+ import { useBills } from '../billing.resource';
5
+ import MetricsCards from './metrics-cards.component';
6
+ import { useConfig } from '@openmrs/esm-framework';
7
+
8
+ const mockUseBills = useBills as jest.Mock;
9
+ const mockUseConfig = useConfig as jest.Mock;
10
+
11
+ jest.mock('../billing.resource', () => ({
12
+ useBills: jest.fn(),
13
+ }));
14
+
15
+ describe('MetricsCards', () => {
16
+ test('renders loading state', () => {
17
+ mockUseBills.mockReturnValue({ isLoading: true, bills: [], error: null });
18
+ renderMetricsCards();
19
+ expect(screen.getByText(/Loading bill metrics.../i)).toBeInTheDocument();
20
+ });
21
+
22
+ test('renders error state', () => {
23
+ mockUseBills.mockReturnValue({ isLoading: false, bills: [], error: new Error('Internal server error') });
24
+ renderMetricsCards();
25
+ expect(
26
+ screen.getByText(
27
+ /Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site administrator and quote the error code above./i,
28
+ ),
29
+ ).toBeInTheDocument();
30
+ });
31
+
32
+ test('renders metrics cards', () => {
33
+ mockUseBills.mockReturnValue({ isLoading: false, bills: billsSummary, error: null });
34
+ mockUseConfig.mockImplementation(() => ({ defaultCurrency: 'USD' }));
35
+ renderMetricsCards();
36
+ expect(screen.getByRole('heading', { name: /cumulative bills/i })).toBeInTheDocument();
37
+ expect(screen.getByRole('heading', { name: /pending bills/i })).toBeInTheDocument();
38
+ expect(screen.getByRole('heading', { name: /paid bills/i })).toBeInTheDocument();
39
+ });
40
+ });
41
+
42
+ function renderMetricsCards() {
43
+ render(<MetricsCards />);
44
+ }
@@ -0,0 +1,45 @@
1
+ import { calculateTotalAmount, convertToCurrency } from '../helpers';
2
+ import { type MappedBill } from '../types';
3
+
4
+ /**
5
+ * A custom hook for calculating bill metrics.
6
+ *
7
+ * This hook takes in an array of bills and calculates the total amount for different
8
+ * bill statuses (cumulative, pending, paid) using provided helper functions.
9
+ *
10
+ * @param {Array<Object>} bills - An array of bill objects. Each bill object should have a `status` and `lineItems` properties.
11
+ *
12
+ * @returns {{
13
+ * cumulativeBills: string,
14
+ * pendingBills: string,
15
+ * paidBills: string
16
+ * }}
17
+ */
18
+
19
+ export const useBillMetrics = (bills: Array<MappedBill>) => {
20
+ const { paidTotal, pendingTotal, cumulativeTotal } = calculateBillTotals(bills);
21
+ return {
22
+ cumulativeBills: cumulativeTotal,
23
+ pendingBills: pendingTotal,
24
+ paidBills: paidTotal,
25
+ };
26
+ };
27
+
28
+ const calculateBillTotals = (bills: Array<MappedBill>) => {
29
+ let paidTotal = 0;
30
+ let pendingTotal = 0;
31
+ let cumulativeTotal = 0;
32
+
33
+ bills.forEach((bill) => {
34
+ if (bill?.status === 'PAID') {
35
+ paidTotal += bill?.totalAmount;
36
+ } else if (bill?.status === 'PENDING') {
37
+ pendingTotal += bill?.totalAmount;
38
+ }
39
+ cumulativeTotal += bill?.totalAmount; // Add to cumulative total regardless of status
40
+ });
41
+
42
+ return { paidTotal, pendingTotal, cumulativeTotal };
43
+ };
44
+
45
+ export default calculateBillTotals;
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import {
4
+ Button,
5
+ InlineLoading,
6
+ ModalBody,
7
+ ModalFooter,
8
+ ModalHeader,
9
+ StructuredListBody,
10
+ StructuredListCell,
11
+ StructuredListHead,
12
+ StructuredListRow,
13
+ StructuredListWrapper,
14
+ } from '@carbon/react';
15
+ import { useBills } from '../billing.resource';
16
+ import { convertToCurrency } from '../helpers';
17
+ import styles from './require-payment.scss';
18
+ import { useConfig } from '@openmrs/esm-framework';
19
+
20
+ type RequirePaymentModalProps = {
21
+ closeModal: () => void;
22
+ patientUuid: string;
23
+ };
24
+
25
+ const RequirePaymentModal: React.FC<RequirePaymentModalProps> = ({ closeModal, patientUuid }) => {
26
+ const { t } = useTranslation();
27
+ const { defaultCurrency } = useConfig();
28
+ const { bills, isLoading, error } = useBills(patientUuid);
29
+ const lineItems = bills.filter((bill) => bill?.status !== 'PAID').flatMap((bill) => bill?.lineItems);
30
+
31
+ return (
32
+ <div>
33
+ <ModalHeader closeModal={closeModal} title={t('patientBillingAlert', 'Patient Billing Alert')} />
34
+ <ModalBody>
35
+ <p className={styles.bodyShort02}>
36
+ {t(
37
+ 'billPaymentRequiredMessage',
38
+ 'The current patient has pending bill. Advice patient to settle bill before receiving services',
39
+ )}
40
+ </p>
41
+ {isLoading && (
42
+ <InlineLoading
43
+ status="active"
44
+ iconDescription="Loading"
45
+ description={t('inlineLoading', 'Loading bill items...')}
46
+ />
47
+ )}
48
+ <StructuredListWrapper isCondensed>
49
+ <StructuredListHead>
50
+ <StructuredListRow head>
51
+ <StructuredListCell head>{t('item', 'Item')}</StructuredListCell>
52
+ <StructuredListCell head>{t('quantity', 'Quantity')}</StructuredListCell>
53
+ <StructuredListCell head>{t('unitPrice', 'Unit price')}</StructuredListCell>
54
+ <StructuredListCell head>{t('total', 'Total')}</StructuredListCell>
55
+ </StructuredListRow>
56
+ </StructuredListHead>
57
+ <StructuredListBody>
58
+ {lineItems.map((lineItem) => {
59
+ return (
60
+ <StructuredListRow>
61
+ <StructuredListCell>{lineItem.billableService || lineItem.item}</StructuredListCell>
62
+ <StructuredListCell>{lineItem.quantity}</StructuredListCell>
63
+ <StructuredListCell>{convertToCurrency(lineItem.price, defaultCurrency)}</StructuredListCell>
64
+ <StructuredListCell>
65
+ {convertToCurrency(lineItem.quantity * lineItem.price, defaultCurrency)}
66
+ </StructuredListCell>
67
+ </StructuredListRow>
68
+ );
69
+ })}
70
+ </StructuredListBody>
71
+ </StructuredListWrapper>
72
+ </ModalBody>
73
+ <ModalFooter>
74
+ <Button kind="secondary" onClick={closeModal}>
75
+ {t('cancel', 'Cancel')}
76
+ </Button>
77
+ <Button kind="primary" onClick={closeModal}>
78
+ {t('ok', 'OK')}
79
+ </Button>
80
+ </ModalFooter>
81
+ </div>
82
+ );
83
+ };
84
+
85
+ export default RequirePaymentModal;
@@ -0,0 +1,6 @@
1
+ @use '@carbon/type';
2
+
3
+ .bodyShort02 {
4
+ margin-bottom: 0.5rem;
5
+ @include type.type-style('body-compact-02');
6
+ }
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { BrowserRouter, Route, Routes } from 'react-router-dom';
3
+ import { BillingDashboard } from './billing-dashboard/billing-dashboard.component';
4
+ import Invoice from './invoice/invoice.component';
5
+
6
+ const RootComponent: React.FC = () => {
7
+ const baseName = window.getOpenmrsSpaBase() + 'home/billing';
8
+
9
+ return (
10
+ <BrowserRouter basename={baseName}>
11
+ <Routes>
12
+ <Route path="/" element={<BillingDashboard />} />
13
+ <Route path="/patient/:patientUuid/:billUuid" element={<Invoice />} />
14
+ </Routes>
15
+ </BrowserRouter>
16
+ );
17
+ };
18
+
19
+ export default RootComponent;
package/src/root.scss ADDED
@@ -0,0 +1,30 @@
1
+ @use '@carbon/type';
2
+ @import '~@openmrs/esm-styleguide/src/vars';
3
+
4
+ .productiveHeading01 {
5
+ @include type.type-style('heading-compact-01');
6
+ }
7
+
8
+ .productiveHeading02 {
9
+ @include type.type-style('heading-compact-02');
10
+ }
11
+
12
+ .bodyLong01 {
13
+ @include type.type-style('body-01');
14
+ }
15
+
16
+ .caption01 {
17
+ @include type.type-style('legal-01');
18
+ }
19
+
20
+ .bodyShort02 {
21
+ @include type.type-style('body-compact-02');
22
+ }
23
+
24
+ .text02 {
25
+ color: $text-02;
26
+ }
27
+
28
+ .text01 {
29
+ color: $ui-05;
30
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "$schema": "https://json.openmrs.org/routes.schema.json",
3
+ "backendDependencies": {
4
+ "webservices.rest": ">=2.24.0",
5
+ "fhir2": "^1.2.0"
6
+ },
7
+ "pages": [
8
+ {
9
+ "component": "billableServicesHome",
10
+ "route":"billable-services"
11
+ }
12
+ ],
13
+ "extensions": [
14
+ {
15
+ "component": "billingDashboardLink",
16
+ "name": "billing-dashboard-link",
17
+ "slot": "homepage-dashboard-slot",
18
+ "meta": {
19
+ "name": "billing",
20
+ "title": "billing",
21
+ "slot": "billing-dashboard-slot"
22
+ },
23
+ "featureFlag": "billing"
24
+ },
25
+ {
26
+ "component": "root",
27
+ "name": "billing-dashboard-root",
28
+ "slot": "billing-dashboard-slot"
29
+ },
30
+ {
31
+ "name": "billing-patient-summary",
32
+ "component": "billingPatientSummary",
33
+ "slot": "patient-chart-billing-dashboard-slot",
34
+ "order": 10,
35
+ "meta": {
36
+ "columnSpan": 4
37
+ }
38
+ },
39
+ {
40
+ "name": "billing-summary-dashboard-link",
41
+ "component": "billingSummaryDashboardLink",
42
+ "slot": "patient-chart-dashboard-slot",
43
+ "order": 11,
44
+ "meta": {
45
+ "columns": 1,
46
+ "columnSpan": 1,
47
+ "slot": "patient-chart-billing-dashboard-slot",
48
+ "path": "Billing history"
49
+ },
50
+ "featureFlag": "billing"
51
+ },
52
+ {
53
+ "name": "billing-checkin-form",
54
+ "slot": "extra-visit-attribute-slot",
55
+ "component": "billingCheckInForm"
56
+ },
57
+ {
58
+ "slot": "system-admin-page-card-link-slot",
59
+ "component": "billableServicesCardLink",
60
+ "name": "billable-services-admin-card-link"
61
+ },
62
+ {
63
+ "name": "require-billing-modal",
64
+ "component": "requirePaymentModal"
65
+ },
66
+ {
67
+ "name": "patient-banner-billing-tags",
68
+ "component": "visitAttributeTags",
69
+ "slot": "patient-banner-tags-slot",
70
+ "order": 2
71
+ },
72
+ {
73
+ "name": "billing-home-tiles-ext",
74
+ "slot": "billing-home-tiles-slot",
75
+ "component": "serviceMetrics"
76
+ }
77
+ ]
78
+ }