@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,41 @@
1
+ import { type OpenmrsResource } from '@openmrs/esm-framework';
2
+ import type { LineItem, MappedBill } from '../../types';
3
+
4
+ const WAIVER_UUID = 'eb6173cb-9678-4614-bbe1-0ccf7ed9d1d4';
5
+
6
+ export const createBillWaiverPayload = (
7
+ bill: MappedBill,
8
+ amountWaived: number,
9
+ totalAmount: number,
10
+ lineItems: Array<LineItem>,
11
+ billableLineItems: Array<OpenmrsResource>,
12
+ ) => {
13
+ const { cashier } = bill;
14
+
15
+ const billPayment = {
16
+ amount: parseFloat(totalAmount.toFixed(2)),
17
+ amountTendered: parseFloat(Number(amountWaived).toFixed(2)),
18
+ attributes: [],
19
+ instanceType: WAIVER_UUID,
20
+ };
21
+
22
+ const processedLineItems = lineItems.map((lineItem) => ({
23
+ ...lineItem,
24
+ billableService: findBillableServiceUuid(billableLineItems, lineItem),
25
+ paymentStatus: 'PAID',
26
+ }));
27
+
28
+ const processedPayment = {
29
+ cashPoint: bill.cashPointUuid,
30
+ cashier: cashier.uuid,
31
+ lineItems: processedLineItems,
32
+ payments: [...bill.payments, billPayment],
33
+ patient: bill.patientUuid,
34
+ };
35
+
36
+ return processedPayment;
37
+ };
38
+
39
+ const findBillableServiceUuid = (billableService: Array<OpenmrsResource>, lineItems: LineItem) => {
40
+ return billableService.find((service) => service.name === lineItems.billableService)?.uuid ?? null;
41
+ };
@@ -0,0 +1,71 @@
1
+ import useSWR from 'swr';
2
+ import { type OpenmrsResource, openmrsFetch } from '@openmrs/esm-framework';
3
+ import { type ServiceConcept } from '../types';
4
+
5
+ type ResponseObject = {
6
+ results: Array<OpenmrsResource>;
7
+ };
8
+
9
+ export const useBillableServices = () => {
10
+ const url = `/ws/rest/v1/cashier/billableService?v=custom:(uuid,name,shortName,serviceStatus,serviceType:(display),servicePrices:(uuid,name,price))`;
11
+
12
+ const { data, isLoading, isValidating, error, mutate } = useSWR<{ data: ResponseObject }>(url, openmrsFetch);
13
+
14
+ return {
15
+ billableServices: data?.data.results ?? [],
16
+ isLoading,
17
+ isValidating,
18
+ error,
19
+ mutate,
20
+ };
21
+ };
22
+
23
+ export function useServiceTypes() {
24
+ const url = `/ws/rest/v1/concept/d7bd4cc0-90b1-4f22-90f2-ab7fde936727?v=custom:(setMembers:(uuid,display))`;
25
+
26
+ const { data, error, isLoading } = useSWR<{ data }>(url, openmrsFetch);
27
+
28
+ return {
29
+ serviceTypes: data?.data.setMembers ?? [],
30
+ error,
31
+ isLoading,
32
+ };
33
+ }
34
+
35
+ export const usePaymentModes = () => {
36
+ const url = `/ws/rest/v1/cashier/paymentMode`;
37
+
38
+ const { data, error, isLoading } = useSWR<{ data: ResponseObject }>(url, openmrsFetch);
39
+
40
+ return {
41
+ paymentModes: data?.data.results ?? [],
42
+ error,
43
+ isLoading,
44
+ };
45
+ };
46
+
47
+ export const createBillableSerice = (payload: any) => {
48
+ const url = `/ws/rest/v1/cashier/api/billable-service`;
49
+ return openmrsFetch(url, {
50
+ method: 'POST',
51
+ body: payload,
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ },
55
+ });
56
+ };
57
+
58
+ export function useConceptsSearch(conceptToLookup: string) {
59
+ const conditionsSearchUrl = `/ws/rest/v1/conceptsearch?q=${conceptToLookup}`;
60
+
61
+ const { data, error, isLoading } = useSWR<{ data: { results: Array<ServiceConcept> } }, Error>(
62
+ conceptToLookup ? conditionsSearchUrl : null,
63
+ openmrsFetch,
64
+ );
65
+
66
+ return {
67
+ searchResults: data?.data?.results ?? [],
68
+ error: error,
69
+ isSearching: isLoading,
70
+ };
71
+ }
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import { BrowserRouter, Routes, Route } from 'react-router-dom';
3
+ import { SideNav, SideNavItems, SideNavLink } from '@carbon/react';
4
+ import { Wallet, Money } from '@carbon/react/icons';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { UserHasAccess, navigate } from '@openmrs/esm-framework';
7
+ import AddBillableService from './create-edit/add-billable-service.component';
8
+ import BillWaiver from './bill-waiver/bill-waiver.component';
9
+ import BillableServicesDashboard from './dashboard/dashboard.component';
10
+ import BillingHeader from '../billing-header/billing-header.component';
11
+ import styles from './billable-services.scss';
12
+
13
+ const BillableServiceHome: React.FC = () => {
14
+ const { t } = useTranslation();
15
+ const basePath = `${window.spaBase}/billable-services`;
16
+
17
+ const handleNavigation = (path: string) => {
18
+ navigate({ to: `${basePath}/${path}` });
19
+ };
20
+
21
+ return (
22
+ <BrowserRouter basename={`${window.spaBase}/billable-services`}>
23
+ <main className={styles.mainSection}>
24
+ <section>
25
+ <SideNav>
26
+ <SideNavItems>
27
+ <SideNavLink onClick={() => handleNavigation('')} renderIcon={Wallet} isActive>
28
+ {t('billableServices', 'Billable Services')}
29
+ </SideNavLink>
30
+ <UserHasAccess privilege="coreapps.systemAdministration">
31
+ <SideNavLink onClick={() => handleNavigation('waive-bill')} renderIcon={Money}>
32
+ {t('billWaiver', 'Bill waiver')}
33
+ </SideNavLink>
34
+ </UserHasAccess>
35
+ </SideNavItems>
36
+ </SideNav>
37
+ </section>
38
+ <section>
39
+ <BillingHeader title={t('billServicesManagement', 'Bill services management')} />
40
+ <Routes>
41
+ <Route path="/" element={<BillableServicesDashboard />} />
42
+ <Route path="/add-service" element={<AddBillableService />} />
43
+ <Route path="/waive-bill" element={<BillWaiver />} />
44
+ </Routes>
45
+ </section>
46
+ </main>
47
+ </BrowserRouter>
48
+ );
49
+ };
50
+
51
+ export default BillableServiceHome;
@@ -0,0 +1,255 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import classNames from 'classnames';
4
+ import {
5
+ Button,
6
+ DataTable,
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 { ArrowRight } from '@carbon/react/icons';
21
+ import { useLayoutType, isDesktop, useConfig, usePagination, ErrorState, navigate } from '@openmrs/esm-framework';
22
+ import { EmptyState } from '@openmrs/esm-patient-common-lib';
23
+ import { useBillableServices } from './billable-service.resource';
24
+ import styles from './billable-services.scss';
25
+
26
+ const BillableServices = () => {
27
+ const { t } = useTranslation();
28
+ const { billableServices, isLoading, isValidating, error, mutate } = useBillableServices();
29
+ const layout = useLayoutType();
30
+ const config = useConfig();
31
+ const [searchString, setSearchString] = useState('');
32
+ const responsiveSize = isDesktop(layout) ? 'lg' : 'sm';
33
+ const pageSizes = config?.billableServices?.pageSizes ?? [10, 20, 30, 40, 50];
34
+ const [pageSize, setPageSize] = useState(config?.billableServices?.pageSize ?? 10);
35
+
36
+ //creating service state
37
+ const [showOverlay, setShowOverlay] = useState(false);
38
+ const [overlayHeader, setOverlayTitle] = useState('');
39
+
40
+ const headerData = [
41
+ {
42
+ header: t('serviceName', 'Service Name'),
43
+ key: 'serviceName',
44
+ },
45
+ {
46
+ header: t('shortName', 'Short Name'),
47
+ key: 'shortName',
48
+ },
49
+ {
50
+ header: t('serviceType', 'Service Type'),
51
+ key: 'serviceType',
52
+ },
53
+ {
54
+ header: t('status', 'Service Status'),
55
+ key: 'status',
56
+ },
57
+ {
58
+ header: t('prices', 'Prices'),
59
+ key: 'prices',
60
+ },
61
+ {
62
+ header: t('actions', 'Actions'),
63
+ key: 'actions',
64
+ },
65
+ ];
66
+
67
+ const launchBillableServiceForm = useCallback(() => {
68
+ navigate({ to: window.getOpenmrsSpaBase() + 'billable-services/add-service' });
69
+ }, []);
70
+
71
+ const searchResults = useMemo(() => {
72
+ if (billableServices !== undefined && billableServices.length > 0) {
73
+ if (searchString && searchString.trim() !== '') {
74
+ const search = searchString.toLowerCase();
75
+ return billableServices?.filter((service) =>
76
+ Object.entries(service).some(([header, value]) => {
77
+ return header === 'uuid' ? false : `${value}`.toLowerCase().includes(search);
78
+ }),
79
+ );
80
+ }
81
+ }
82
+ return billableServices;
83
+ }, [searchString, billableServices]);
84
+
85
+ const { paginated, goTo, results, currentPage } = usePagination(searchResults, pageSize);
86
+
87
+ let rowData = [];
88
+ if (results) {
89
+ results.forEach((service, index) => {
90
+ const s = {
91
+ id: `${index}`,
92
+ uuid: service.uuid,
93
+ serviceName: service.name,
94
+ shortName: service.shortName,
95
+ serviceType: service?.serviceType?.display,
96
+ status: service.serviceStatus,
97
+ prices: '--',
98
+ actions: '--',
99
+ };
100
+ let cost = '';
101
+ service.servicePrices.forEach((price) => {
102
+ cost += `${price.name} (${price.price}) `;
103
+ });
104
+ s.prices = cost;
105
+ rowData.push(s);
106
+ });
107
+ }
108
+
109
+ const handleSearch = useCallback(
110
+ (e) => {
111
+ goTo(1);
112
+ setSearchString(e.target.value);
113
+ },
114
+ [goTo, setSearchString],
115
+ );
116
+
117
+ if (isLoading) {
118
+ <InlineLoading status="active" iconDescription="Loading" description="Loading data..." />;
119
+ }
120
+ if (error) {
121
+ <ErrorState headerTitle={t('billableService', 'Billable Service')} error={error} />;
122
+ }
123
+ if (billableServices.length === 0) {
124
+ <EmptyState
125
+ displayText={t('billableService', 'Billable Service')}
126
+ headerTitle={t('billableService', 'Billable Service')}
127
+ launchForm={launchBillableServiceForm}
128
+ />;
129
+ }
130
+
131
+ return (
132
+ <>
133
+ {billableServices?.length > 0 ? (
134
+ <div className={styles.serviceContainer}>
135
+ <FilterableTableHeader
136
+ handleSearch={handleSearch}
137
+ isValidating={isValidating}
138
+ layout={layout}
139
+ responsiveSize={responsiveSize}
140
+ t={t}
141
+ />
142
+ <DataTable
143
+ isSortable
144
+ rows={rowData}
145
+ headers={headerData}
146
+ size={responsiveSize}
147
+ useZebraStyles={rowData?.length > 1 ? true : false}>
148
+ {({ rows, headers, getRowProps, getTableProps }) => (
149
+ <TableContainer>
150
+ <Table {...getTableProps()} aria-label="service list">
151
+ <TableHead>
152
+ <TableRow>
153
+ {headers.map((header) => (
154
+ <TableHeader key={header.key}>{header.header}</TableHeader>
155
+ ))}
156
+ </TableRow>
157
+ </TableHead>
158
+ <TableBody>
159
+ {rows.map((row) => (
160
+ <TableRow
161
+ key={row.id}
162
+ {...getRowProps({
163
+ row,
164
+ })}>
165
+ {row.cells.map((cell) => (
166
+ <TableCell key={cell.id}>{cell.value}</TableCell>
167
+ ))}
168
+ </TableRow>
169
+ ))}
170
+ </TableBody>
171
+ </Table>
172
+ </TableContainer>
173
+ )}
174
+ </DataTable>
175
+ {searchResults?.length === 0 && (
176
+ <div className={styles.filterEmptyState}>
177
+ <Layer level={0}>
178
+ <Tile className={styles.filterEmptyStateTile}>
179
+ <p className={styles.filterEmptyStateContent}>
180
+ {t('noMatchingServicesToDisplay', 'No matching services to display')}
181
+ </p>
182
+ <p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
183
+ </Tile>
184
+ </Layer>
185
+ </div>
186
+ )}
187
+ {paginated && (
188
+ <Pagination
189
+ forwardText="Next page"
190
+ backwardText="Previous page"
191
+ page={currentPage}
192
+ pageSize={pageSize}
193
+ pageSizes={pageSizes}
194
+ totalItems={searchResults?.length}
195
+ className={styles.pagination}
196
+ size={responsiveSize}
197
+ onChange={({ pageSize: newPageSize, page: newPage }) => {
198
+ if (newPageSize !== pageSize) {
199
+ setPageSize(newPageSize);
200
+ }
201
+ if (newPage !== currentPage) {
202
+ goTo(newPage);
203
+ }
204
+ }}
205
+ />
206
+ )}
207
+ </div>
208
+ ) : (
209
+ <EmptyState
210
+ launchForm={launchBillableServiceForm}
211
+ displayText={t('noServicesToDisplay', 'There are no services to display')}
212
+ headerTitle={t('billableService', 'Billable service')}
213
+ />
214
+ )}
215
+ </>
216
+ );
217
+ };
218
+
219
+ function FilterableTableHeader({ layout, handleSearch, isValidating, responsiveSize, t }) {
220
+ return (
221
+ <>
222
+ <div className={styles.headerContainer}>
223
+ <div
224
+ className={classNames({
225
+ [styles.tabletHeading]: !isDesktop(layout),
226
+ [styles.desktopHeading]: isDesktop(layout),
227
+ })}>
228
+ <h4>{t('servicesList', 'Services list')}</h4>
229
+ </div>
230
+ <div className={styles.backgroundDataFetchingIndicator}>
231
+ <span>{isValidating ? <InlineLoading /> : null}</span>
232
+ </div>
233
+ </div>
234
+ <div className={styles.actionsContainer}>
235
+ <Search
236
+ labelText=""
237
+ placeholder={t('filterTable', 'Filter table')}
238
+ onChange={handleSearch}
239
+ size={responsiveSize}
240
+ />
241
+ <Button
242
+ size={responsiveSize}
243
+ kind="primary"
244
+ renderIcon={(props) => <ArrowRight size={16} {...props} />}
245
+ onClick={() => {
246
+ navigate({ to: window.getOpenmrsSpaBase() + 'billable-services/add-service' });
247
+ }}
248
+ iconDescription={t('addNewBillableService', 'Add new billable service')}>
249
+ {t('addNewService', 'Add new service')}
250
+ </Button>
251
+ </div>
252
+ </>
253
+ );
254
+ }
255
+ export default BillableServices;
@@ -0,0 +1,218 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .container {
6
+ margin: 2rem 0;
7
+ }
8
+
9
+ .emptyStateContainer,
10
+ .loaderContainer {
11
+ @extend .container;
12
+ }
13
+
14
+ .serviceContainer {
15
+ background-color: $ui-02;
16
+ border: 1px solid $ui-03;
17
+ width: 100%;
18
+ margin: 0 auto;
19
+ max-width: 95vw;
20
+ padding-bottom: 0;
21
+
22
+ :has(.filterEmptyState) {
23
+ border-bottom: none;
24
+ }
25
+ }
26
+ .left-justified-items {
27
+ display: flex;
28
+ flex-direction: row;
29
+ align-items: center;
30
+ cursor: pointer;
31
+ align-items: center;
32
+ }
33
+
34
+ .filterContainer {
35
+ flex: 1;
36
+
37
+ :global(.cds--dropdown__wrapper--inline) {
38
+ gap: 0;
39
+ }
40
+
41
+ :global(.cds--list-box__menu-icon) {
42
+ height: 1rem;
43
+ }
44
+
45
+ :global(.cds--list-box__menu) {
46
+ min-width: max-content;
47
+ }
48
+
49
+ :global(.cds--list-box) {
50
+ margin-left: layout.$spacing-03;
51
+ }
52
+ }
53
+
54
+ .menu {
55
+ margin-left: layout.$spacing-03;
56
+ }
57
+
58
+ .headerContainer {
59
+ display: flex;
60
+ justify-content: space-between;
61
+ align-items: center;
62
+ padding: layout.$spacing-04 layout.$spacing-05;
63
+ background-color: $ui-02;
64
+ }
65
+
66
+ .backgroundDataFetchingIndicator {
67
+ align-items: center;
68
+ display: flex;
69
+ flex: 1;
70
+ justify-content: space-between;
71
+
72
+ &:global(.cds--inline-loading) {
73
+ max-height: 1rem;
74
+ }
75
+ }
76
+
77
+ .tableContainer section {
78
+ position: relative;
79
+ }
80
+
81
+ .tableContainer a {
82
+ text-decoration: none;
83
+ }
84
+
85
+ .pagination {
86
+ overflow: hidden;
87
+
88
+ &:global(.cds--pagination) {
89
+ border-top: none;
90
+ }
91
+ }
92
+
93
+ .hiddenRow {
94
+ display: none;
95
+ }
96
+
97
+ .emptyRow {
98
+ padding: 0 1rem;
99
+ display: flex;
100
+ align-items: center;
101
+ }
102
+
103
+ .visitSummaryContainer {
104
+ width: 100%;
105
+ max-width: 768px;
106
+ margin: 1rem auto;
107
+ }
108
+
109
+ .expandedActiveVisitRow > td > div {
110
+ max-height: max-content !important;
111
+ }
112
+
113
+ .expandedActiveVisitRow td {
114
+ padding: 0 2rem;
115
+ }
116
+
117
+ .expandedActiveVisitRow th[colspan] td[colspan] > div:first-child {
118
+ padding: 0 1rem;
119
+ }
120
+
121
+ .action {
122
+ margin-bottom: layout.$spacing-03;
123
+ }
124
+
125
+ .illo {
126
+ margin-top: layout.$spacing-05;
127
+ }
128
+
129
+ .content {
130
+ @include type.type-style('heading-compact-01');
131
+ color: $text-02;
132
+ margin-top: layout.$spacing-05;
133
+ margin-bottom: layout.$spacing-03;
134
+ }
135
+
136
+ .desktopHeading,
137
+ .tabletHeading {
138
+ text-align: left;
139
+ text-transform: capitalize;
140
+ flex: 1;
141
+
142
+ h4 {
143
+ @include type.type-style('heading-compact-02');
144
+ color: $text-02;
145
+
146
+ &:after {
147
+ content: '';
148
+ display: block;
149
+ width: 2rem;
150
+ padding-top: 3px;
151
+ border-bottom: 0.375rem solid;
152
+ @include brand-03(border-bottom-color);
153
+ }
154
+ }
155
+ }
156
+
157
+ .tile {
158
+ text-align: center;
159
+ border: 1px solid $ui-03;
160
+ }
161
+
162
+ .menuitem {
163
+ max-width: none;
164
+ }
165
+
166
+ .filterEmptyState {
167
+ display: flex;
168
+ justify-content: center;
169
+ align-items: center;
170
+ padding: layout.$spacing-05;
171
+ margin: layout.$spacing-09;
172
+ text-align: center;
173
+ }
174
+
175
+ .filterEmptyStateTile {
176
+ margin: auto;
177
+ }
178
+
179
+ .filterEmptyStateContent {
180
+ @include type.type-style('heading-compact-02');
181
+ color: $text-02;
182
+ margin-bottom: 0.5rem;
183
+ }
184
+
185
+ .filterEmptyStateHelper {
186
+ @include type.type-style('body-compact-01');
187
+ color: $text-02;
188
+ }
189
+
190
+ .metricsContainer {
191
+ display: flex;
192
+ justify-content: space-between;
193
+ background-color: $ui-02;
194
+ height: layout.$spacing-10;
195
+ align-items: center;
196
+ padding: 0 layout.$spacing-05;
197
+ }
198
+
199
+ .metricsTitle {
200
+ @include type.type-style('heading-03');
201
+ color: $ui-05;
202
+ }
203
+
204
+ .actionsContainer {
205
+ display: flex;
206
+ justify-content: space-between;
207
+ align-items: center;
208
+ background-color: $ui-02;
209
+ }
210
+ .actionBtn {
211
+ display: flex;
212
+ column-gap: 0.5rem;
213
+ }
214
+
215
+ .mainSection {
216
+ display: grid;
217
+ grid-template-columns: 16rem 1fr;
218
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import BillableServices from './billable-services.component';
4
+
5
+ describe('BillableService', () => {
6
+ test('renders an empty state when there are no billable services', () => {
7
+ renderBillableServices();
8
+
9
+ expect(screen.getByText(/Empty data illustration/i)).toBeInTheDocument();
10
+ expect(screen.getByText(/There are no services to display to display for this patient/i)).toBeInTheDocument();
11
+ });
12
+ });
13
+
14
+ function renderBillableServices() {
15
+ render(<BillableServices />);
16
+ }