@openmrs/esm-billing-app 1.0.1-pre.98 → 1.0.2-pre.56

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 (224) hide show
  1. package/.eslintignore +0 -1
  2. package/.eslintrc +33 -24
  3. package/.husky/pre-commit +1 -1
  4. package/.turbo.json +1 -1
  5. package/.tx/config +11 -0
  6. package/README.md +111 -1
  7. package/dist/1119.js +1 -0
  8. package/dist/1197.js +1 -0
  9. package/dist/1362.js +1 -0
  10. package/dist/1362.js.map +1 -0
  11. package/dist/2146.js +1 -0
  12. package/dist/2690.js +1 -0
  13. package/dist/3029.js +2 -0
  14. package/dist/3029.js.LICENSE.txt +7 -0
  15. package/dist/3029.js.map +1 -0
  16. package/dist/3099.js +1 -0
  17. package/dist/3511.js +1 -0
  18. package/dist/3511.js.map +1 -0
  19. package/dist/3584.js +1 -0
  20. package/dist/4055.js +1 -0
  21. package/dist/4132.js +1 -0
  22. package/dist/4225.js +1 -0
  23. package/dist/4225.js.map +1 -0
  24. package/dist/4300.js +1 -0
  25. package/dist/4335.js +1 -0
  26. package/dist/4618.js +1 -0
  27. package/dist/4652.js +1 -0
  28. package/dist/4817.js +2 -0
  29. package/dist/4817.js.LICENSE.txt +77 -0
  30. package/dist/4817.js.map +1 -0
  31. package/dist/4944.js +1 -0
  32. package/dist/4993.js +1 -0
  33. package/dist/4993.js.map +1 -0
  34. package/dist/5173.js +1 -0
  35. package/dist/5241.js +1 -0
  36. package/dist/5442.js +1 -0
  37. package/dist/5661.js +1 -0
  38. package/dist/6022.js +1 -0
  39. package/dist/6468.js +1 -0
  40. package/dist/6540.js +2 -0
  41. package/dist/6540.js.map +1 -0
  42. package/dist/6606.js +2 -0
  43. package/dist/{591.js.LICENSE.txt → 6606.js.LICENSE.txt} +2 -2
  44. package/dist/6606.js.map +1 -0
  45. package/dist/6679.js +1 -0
  46. package/dist/6840.js +1 -0
  47. package/dist/6859.js +1 -0
  48. package/dist/6941.js +1 -0
  49. package/dist/6941.js.map +1 -0
  50. package/dist/7097.js +1 -0
  51. package/dist/7159.js +1 -0
  52. package/dist/723.js +1 -0
  53. package/dist/7255.js +1 -0
  54. package/dist/7255.js.map +1 -0
  55. package/dist/7617.js +1 -0
  56. package/dist/763.js +1 -0
  57. package/dist/763.js.map +1 -0
  58. package/dist/8163.js +1 -0
  59. package/dist/8349.js +1 -0
  60. package/dist/8618.js +1 -0
  61. package/dist/890.js +1 -0
  62. package/dist/9055.js +1 -0
  63. package/dist/9055.js.map +1 -0
  64. package/dist/9214.js +1 -0
  65. package/dist/9538.js +1 -0
  66. package/dist/{935.js → 961.js} +2 -2
  67. package/dist/{935.js.map → 961.js.map} +1 -1
  68. package/dist/986.js +1 -0
  69. package/dist/9879.js +1 -0
  70. package/dist/9895.js +1 -0
  71. package/dist/9900.js +1 -0
  72. package/dist/9913.js +1 -0
  73. package/dist/main.js +1 -1
  74. package/dist/main.js.LICENSE.txt +31 -1
  75. package/dist/main.js.map +1 -1
  76. package/dist/openmrs-esm-billing-app.js +1 -1
  77. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +844 -165
  78. package/dist/openmrs-esm-billing-app.js.map +1 -1
  79. package/dist/routes.json +1 -1
  80. package/jest.config.js +4 -1
  81. package/package.json +19 -21
  82. package/src/bill-history/bill-history.component.tsx +5 -3
  83. package/src/bill-history/bill-history.scss +24 -9
  84. package/src/bill-history/bill-history.test.tsx +58 -16
  85. package/src/bill-item-actions/bill-item-actions.scss +26 -0
  86. package/src/bill-item-actions/edit-bill-item.component.tsx +221 -0
  87. package/src/bill-item-actions/edit-bill-item.test.tsx +137 -0
  88. package/src/billable-services/bill-waiver/bill-selection.component.tsx +1 -1
  89. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +2 -2
  90. package/src/billable-services/bill-waiver/bill-waiver-form.scss +4 -4
  91. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +4 -4
  92. package/src/billable-services/bill-waiver/patient-bills.component.tsx +1 -1
  93. package/src/billable-services/billable-service.resource.ts +19 -6
  94. package/src/billable-services/billable-services-home.component.tsx +19 -3
  95. package/src/billable-services/billable-services-menu-item/item.component.tsx +17 -0
  96. package/src/billable-services/billable-services-menu-item/item.scss +14 -0
  97. package/src/billable-services/billable-services.component.tsx +48 -9
  98. package/src/billable-services/billable-services.scss +10 -9
  99. package/src/billable-services/billable-services.test.tsx +172 -8
  100. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +276 -0
  101. package/src/billable-services/cash-point/cash-point-configuration.scss +23 -0
  102. package/src/billable-services/create-edit/add-billable-service.component.tsx +126 -47
  103. package/src/billable-services/create-edit/add-billable-service.scss +14 -8
  104. package/src/billable-services/create-edit/add-billable-service.test.tsx +12 -10
  105. package/src/billable-services/dashboard/dashboard.scss +3 -3
  106. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +280 -0
  107. package/src/billable-services/payyment-modes/payment-modes-config.scss +23 -0
  108. package/src/billing-dashboard/billing-dashboard.component.tsx +17 -4
  109. package/src/billing-dashboard/billing-dashboard.scss +3 -3
  110. package/src/billing-form/billing-form.component.tsx +31 -25
  111. package/src/billing-form/billing-form.scss +9 -10
  112. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +38 -14
  113. package/src/billing-header/billing-header.component.tsx +21 -5
  114. package/src/billing-header/billing-header.scss +1 -1
  115. package/src/billing.resource.ts +21 -4
  116. package/src/bills-table/bills-table.component.tsx +46 -36
  117. package/src/bills-table/bills-table.scss +6 -6
  118. package/src/bills-table/bills-table.test.tsx +108 -68
  119. package/src/config-schema.ts +36 -1
  120. package/src/constants.ts +2 -0
  121. package/src/dashboard.meta.ts +2 -1
  122. package/src/helpers/functions.ts +0 -2
  123. package/src/hooks/selectedDateContext.ts +10 -0
  124. package/src/index.ts +22 -27
  125. package/src/invoice/invoice-table.component.tsx +95 -56
  126. package/src/invoice/invoice-table.scss +7 -8
  127. package/src/invoice/invoice-table.test.tsx +151 -0
  128. package/src/invoice/invoice.component.tsx +7 -9
  129. package/src/invoice/invoice.scss +2 -2
  130. package/src/invoice/invoice.test.tsx +199 -169
  131. package/src/invoice/payments/payment-form/payment-form.component.tsx +84 -55
  132. package/src/invoice/payments/payment-form/payment-form.test.tsx +174 -0
  133. package/src/invoice/payments/payment-history/payment-history.component.tsx +9 -7
  134. package/src/invoice/payments/payment-history/payment-history.test.tsx +160 -0
  135. package/src/invoice/payments/payments.component.test.tsx +121 -0
  136. package/src/invoice/payments/payments.component.tsx +57 -48
  137. package/src/invoice/payments/utils.ts +17 -13
  138. package/src/invoice/printable-invoice/print-receipt.component.tsx +23 -8
  139. package/src/invoice/printable-invoice/print-receipt.test.tsx +50 -0
  140. package/src/metrics-cards/card.component.tsx +4 -2
  141. package/src/metrics-cards/metrics-cards.test.tsx +1 -1
  142. package/src/modal/require-payment-modal.component.tsx +2 -2
  143. package/src/modal/require-payment-modal.test.tsx +66 -0
  144. package/src/modal/require-payment.scss +2 -1
  145. package/src/routes.json +40 -8
  146. package/src/types/index.ts +15 -0
  147. package/{i18next-parser.config.js → tools/i18next-parser.config.js} +19 -19
  148. package/tools/update-openmrs-deps.mjs +42 -0
  149. package/translations/am.json +53 -0
  150. package/translations/ar.json +170 -0
  151. package/translations/ar_SY.json +170 -0
  152. package/translations/bn.json +170 -0
  153. package/translations/de.json +170 -0
  154. package/translations/en.json +53 -0
  155. package/translations/es.json +53 -0
  156. package/translations/es_MX.json +170 -0
  157. package/translations/fr.json +53 -0
  158. package/translations/he.json +53 -0
  159. package/translations/hi.json +170 -0
  160. package/translations/hi_IN.json +170 -0
  161. package/translations/id.json +170 -0
  162. package/translations/it.json +170 -0
  163. package/translations/km.json +53 -0
  164. package/translations/ku.json +170 -0
  165. package/translations/ky.json +170 -0
  166. package/translations/lg.json +170 -0
  167. package/translations/ne.json +170 -0
  168. package/translations/pl.json +170 -0
  169. package/translations/pt.json +170 -0
  170. package/translations/pt_BR.json +170 -0
  171. package/translations/qu.json +170 -0
  172. package/translations/ro_RO.json +170 -0
  173. package/translations/ru_RU.json +170 -0
  174. package/translations/si.json +170 -0
  175. package/translations/sw.json +170 -0
  176. package/translations/sw_KE.json +170 -0
  177. package/translations/tr.json +170 -0
  178. package/translations/tr_TR.json +170 -0
  179. package/translations/uk.json +170 -0
  180. package/translations/uz.json +170 -0
  181. package/translations/uz@Latn.json +170 -0
  182. package/translations/uz_UZ.json +170 -0
  183. package/translations/vi.json +170 -0
  184. package/translations/zh.json +170 -0
  185. package/translations/zh_CN.json +170 -0
  186. package/tsconfig.json +10 -8
  187. package/webpack.config.js +1 -1
  188. package/dist/146.js +0 -1
  189. package/dist/146.js.map +0 -1
  190. package/dist/294.js +0 -2
  191. package/dist/294.js.map +0 -1
  192. package/dist/319.js +0 -1
  193. package/dist/384.js +0 -1
  194. package/dist/384.js.map +0 -1
  195. package/dist/421.js +0 -1
  196. package/dist/421.js.map +0 -1
  197. package/dist/533.js +0 -1
  198. package/dist/533.js.map +0 -1
  199. package/dist/574.js +0 -1
  200. package/dist/591.js +0 -2
  201. package/dist/591.js.map +0 -1
  202. package/dist/614.js +0 -2
  203. package/dist/614.js.LICENSE.txt +0 -37
  204. package/dist/614.js.map +0 -1
  205. package/dist/753.js +0 -1
  206. package/dist/753.js.map +0 -1
  207. package/dist/757.js +0 -1
  208. package/dist/770.js +0 -1
  209. package/dist/770.js.map +0 -1
  210. package/dist/783.js +0 -1
  211. package/dist/783.js.map +0 -1
  212. package/dist/788.js +0 -1
  213. package/dist/800.js +0 -2
  214. package/dist/800.js.LICENSE.txt +0 -3
  215. package/dist/800.js.map +0 -1
  216. package/dist/807.js +0 -1
  217. package/dist/833.js +0 -1
  218. package/dist/992.js +0 -1
  219. package/dist/992.js.map +0 -1
  220. package/src/root.scss +0 -30
  221. /package/dist/{294.js.LICENSE.txt → 6540.js.LICENSE.txt} +0 -0
  222. /package/dist/{935.js.LICENSE.txt → 961.js.LICENSE.txt} +0 -0
  223. /package/{src → tools}/setup-tests.ts +0 -0
  224. /package/{test-helpers.tsx → tools/test-helpers.tsx} +0 -0
@@ -1,8 +1,12 @@
1
- import React from 'react';
1
+ import React, { useContext } from 'react';
2
+ import dayjs from 'dayjs';
3
+ import { DatePickerInput, DatePicker } from '@carbon/react';
2
4
  import { useTranslation } from 'react-i18next';
3
- import { Calendar, Location, UserFollow } from '@carbon/react/icons';
4
- import { formatDate, useSession } from '@openmrs/esm-framework';
5
+ import { Location, UserFollow } from '@carbon/react/icons';
6
+ import { useSession } from '@openmrs/esm-framework';
7
+ import { omrsDateFormat } from '../constants';
5
8
  import BillingIllustration from './billing-illustration.component';
9
+ import SelectedDateContext from '../hooks/selectedDateContext';
6
10
  import styles from './billing-header.scss';
7
11
 
8
12
  interface BillingHeaderProps {
@@ -13,6 +17,7 @@ const BillingHeader: React.FC<BillingHeaderProps> = ({ title }) => {
13
17
  const { t } = useTranslation();
14
18
  const session = useSession();
15
19
  const location = session?.sessionLocation?.display;
20
+ const { selectedDate, setSelectedDate } = useContext(SelectedDateContext);
16
21
 
17
22
  return (
18
23
  <div className={styles.header} data-testid="billing-header">
@@ -32,8 +37,19 @@ const BillingHeader: React.FC<BillingHeaderProps> = ({ title }) => {
32
37
  <Location size={16} />
33
38
  <span className={styles.value}>{location}</span>
34
39
  <span className={styles.middot}>&middot;</span>
35
- <Calendar size={16} />
36
- <span className={styles.value}>{formatDate(new Date(), { mode: 'standard' })}</span>
40
+ <DatePicker
41
+ onChange={([date]) => setSelectedDate(dayjs(date).startOf('day').format(omrsDateFormat))}
42
+ value={dayjs(selectedDate).format('DD MMM YYYY')}
43
+ dateFormat="d-M-Y"
44
+ datePickerType="single">
45
+ <DatePickerInput
46
+ style={{ cursor: 'pointer', backgroundColor: 'transparent', border: 'none', maxWidth: '10rem' }}
47
+ id="appointment-date-picker"
48
+ placeholder="DD-MMM-YYYY"
49
+ labelText=""
50
+ type="text"
51
+ />
52
+ </DatePicker>
37
53
  </div>
38
54
  </div>
39
55
  </div>
@@ -1,6 +1,6 @@
1
1
  @use '@carbon/layout';
2
2
  @use '@carbon/type';
3
- @import '~@openmrs/esm-styleguide/src/vars';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
4
 
5
5
  .header {
6
6
  @include type.type-style('body-compact-02');
@@ -1,12 +1,18 @@
1
+ import { useContext } from 'react';
2
+ import dayjs from 'dayjs';
3
+ import isEmpty from 'lodash-es/isEmpty';
4
+ import sortBy from 'lodash-es/sortBy';
1
5
  import useSWR from 'swr';
2
6
  import { formatDate, parseDate, openmrsFetch, useSession, useVisit, restBaseUrl } from '@openmrs/esm-framework';
7
+ import { apiBasePath, omrsDateFormat } from './constants';
3
8
  import type { FacilityDetail, MappedBill, PatientInvoice } from './types';
4
- import isEmpty from 'lodash-es/isEmpty';
5
- import sortBy from 'lodash-es/sortBy';
6
- import { apiBasePath } from './constants';
9
+ import SelectedDateContext from './hooks/selectedDateContext';
7
10
 
8
11
  export const useBills = (patientUuid: string = '', billStatus: string = '') => {
9
- const url = `${apiBasePath}bill?v=full`;
12
+ const { selectedDate } = useContext(SelectedDateContext);
13
+ const endDate = dayjs().endOf('day').format(omrsDateFormat);
14
+ const url = `${apiBasePath}bill?q=&v=full`;
15
+
10
16
  const patientUrl = `${apiBasePath}bill?patientUuid=${patientUuid}&v=full`;
11
17
 
12
18
  const { data, error, isLoading, isValidating, mutate } = useSWR<{ data: { results: Array<PatientInvoice> } }>(
@@ -146,3 +152,14 @@ export const processBillItems = (payload) => {
146
152
  },
147
153
  });
148
154
  };
155
+
156
+ export const updateBillItems = (payload) => {
157
+ const url = `${apiBasePath}bill/${payload.uuid}`;
158
+ return openmrsFetch(url, {
159
+ method: 'POST',
160
+ body: payload,
161
+ headers: {
162
+ 'Content-Type': 'application/json',
163
+ },
164
+ });
165
+ };
@@ -36,10 +36,10 @@ const BillsTable = () => {
36
36
  const config = useConfig();
37
37
  const layout = useLayoutType();
38
38
  const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
39
- const [billPaymentStatus, setBillPaymentStatus] = useState('');
39
+ const [billPaymentStatus, setBillPaymentStatus] = useState('PENDING');
40
40
  const pageSizes = config?.bills?.pageSizes ?? [10, 20, 30, 40, 50];
41
41
  const [pageSize, setPageSize] = useState(config?.bills?.pageSize ?? 10);
42
- const { bills, isLoading, isValidating, error } = useBills('', billPaymentStatus);
42
+ const { bills, isLoading, isValidating, error } = useBills('', '');
43
43
  const [searchString, setSearchString] = useState('');
44
44
 
45
45
  const headerData = [
@@ -62,22 +62,28 @@ const BillsTable = () => {
62
62
  ];
63
63
 
64
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
- }
65
+ if (!bills?.length) return bills;
78
66
 
79
- return bills;
80
- }, [searchString, bills]);
67
+ return bills
68
+ .map((bill) => {
69
+ if (bill.payments?.length > 0) {
70
+ const totalPaid = bill.payments.reduce((sum, payment) => sum + payment.amountTendered, 0);
71
+ if (totalPaid >= bill.totalAmount) {
72
+ bill.status = 'PAID';
73
+ }
74
+ }
75
+ return bill;
76
+ })
77
+ .filter((bill) => {
78
+ const statusMatch = billPaymentStatus === '' ? true : bill.status === billPaymentStatus;
79
+ const searchMatch = !searchString
80
+ ? true
81
+ : bill.patientName.toLowerCase().includes(searchString.toLowerCase()) ||
82
+ bill.identifier.toLowerCase().includes(searchString.toLowerCase());
83
+
84
+ return statusMatch && searchMatch;
85
+ });
86
+ }, [bills, searchString, billPaymentStatus]);
81
87
 
82
88
  const { paginated, goTo, results, currentPage } = usePagination(searchResults, pageSize);
83
89
 
@@ -86,23 +92,25 @@ const BillsTable = () => {
86
92
 
87
93
  const billingUrl = '${openmrsSpaBase}/home/billing/patient/${patientUuid}/${uuid}';
88
94
 
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
- }));
95
+ const rowData = results?.map((bill, index) => {
96
+ return {
97
+ id: `${index}`,
98
+ uuid: bill.uuid,
99
+ patientName: (
100
+ <ConfigurableLink
101
+ style={{ textDecoration: 'none', maxWidth: '50%' }}
102
+ to={billingUrl}
103
+ templateParams={{ patientUuid: bill.patientUuid, uuid: bill.uuid }}>
104
+ {bill.patientName}
105
+ </ConfigurableLink>
106
+ ),
107
+ visitTime: bill.dateCreated,
108
+ identifier: bill.identifier,
109
+ department: '--',
110
+ billedItems: setBilledItems(bill),
111
+ billingPrice: '--',
112
+ };
113
+ });
106
114
 
107
115
  const handleSearch = useCallback(
108
116
  (e) => {
@@ -118,7 +126,9 @@ const BillsTable = () => {
118
126
  { id: 'PAID', text: 'Paid bills' },
119
127
  ];
120
128
 
121
- const handleFilterChange = ({ selectedItem }) => setBillPaymentStatus(selectedItem.id);
129
+ const handleFilterChange = ({ selectedItem }) => {
130
+ setBillPaymentStatus(selectedItem.id);
131
+ };
122
132
 
123
133
  if (isLoading) {
124
134
  return (
@@ -152,7 +162,7 @@ const BillsTable = () => {
152
162
  className={styles.filterDropdown}
153
163
  direction="bottom"
154
164
  id={`filter-${id}`}
155
- initialSelectedItem={filterItems[0]}
165
+ initialSelectedItem={filterItems[1]}
156
166
  items={filterItems}
157
167
  itemToString={(item) => (item ? item.text : '')}
158
168
  label=""
@@ -1,9 +1,9 @@
1
1
  @use '@carbon/layout';
2
2
  @use '@carbon/type';
3
- @import '~@openmrs/esm-styleguide/src/vars';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
4
 
5
5
  .container {
6
- margin: 2rem 0;
6
+ margin: layout.$spacing-07 0;
7
7
  }
8
8
 
9
9
  .emptyStateContainer,
@@ -63,7 +63,7 @@
63
63
  justify-content: space-between;
64
64
 
65
65
  &:global(.cds--inline-loading) {
66
- max-height: 1rem;
66
+ max-height: layout.$spacing-05;
67
67
  }
68
68
  }
69
69
 
@@ -104,11 +104,11 @@
104
104
  }
105
105
 
106
106
  .expandedActiveVisitRow td {
107
- padding: 0 2rem;
107
+ padding: 0 layout.$spacing-05;
108
108
  }
109
109
 
110
110
  .expandedActiveVisitRow th[colspan] td[colspan] > div:first-child {
111
- padding: 0 1rem;
111
+ padding: 0 layout.$spacing-05;
112
112
  }
113
113
 
114
114
  .action {
@@ -172,7 +172,7 @@
172
172
  .filterEmptyStateContent {
173
173
  @include type.type-style('heading-compact-02');
174
174
  color: $text-02;
175
- margin-bottom: 0.5rem;
175
+ margin-bottom: layout.$spacing-03;
176
176
  }
177
177
 
178
178
  .filterEmptyStateHelper {
@@ -1,18 +1,39 @@
1
1
  import React from 'react';
2
2
  import userEvent from '@testing-library/user-event';
3
- import { render, screen } from '@testing-library/react';
3
+ import { render, screen, waitFor } from '@testing-library/react';
4
4
  import { useBills } from '../billing.resource';
5
5
  import BillsTable from './bills-table.component';
6
6
 
7
- const mockbills = useBills as jest.Mock;
7
+ jest.mock('react-i18next', () => ({
8
+ useTranslation: () => ({
9
+ t: (key: string) => key,
10
+ }),
11
+ }));
8
12
 
9
13
  const mockBillsData = [
10
- { uuid: '1', patientName: 'John Doe', identifier: '12345678', visitType: 'Checkup', patientUuid: 'uuid1' },
11
- { uuid: '2', patientName: 'Mary Smith', identifier: '98765432', visitType: 'Wake up', patientUuid: 'uuid2' },
14
+ {
15
+ uuid: '1',
16
+ patientName: 'John Doe',
17
+ identifier: '12345678',
18
+ visitType: 'Checkup',
19
+ patientUuid: 'uuid1',
20
+ dateCreated: '2024-01-01',
21
+ lineItems: [{ billableService: 'Service 1' }],
22
+ status: 'PENDING',
23
+ },
24
+ {
25
+ uuid: '2',
26
+ patientName: 'Mary Smith',
27
+ identifier: '98765432',
28
+ visitType: 'Wake up',
29
+ patientUuid: 'uuid2',
30
+ dateCreated: '2024-01-02',
31
+ lineItems: [{ billableService: 'Service 2' }],
32
+ status: 'PENDING',
33
+ },
12
34
  ];
13
35
 
14
36
  jest.mock('../billing.resource', () => ({
15
- ...jest.requireActual('../billing.resource'),
16
37
  useBills: jest.fn(() => ({
17
38
  bills: mockBillsData,
18
39
  isLoading: false,
@@ -21,47 +42,59 @@ jest.mock('../billing.resource', () => ({
21
42
  })),
22
43
  }));
23
44
 
45
+ jest.mock('@openmrs/esm-patient-common-lib', () => ({
46
+ EmptyDataIllustration: jest.fn(() => <div>Empty state illustration</div>),
47
+ }));
48
+
24
49
  jest.mock('@openmrs/esm-framework', () => ({
25
- ...jest.requireActual('@openmrs/esm-framework'),
26
- ErrorState: jest.fn(() => (
27
- <div>
28
- Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site
29
- administrator and quote the error code above.
30
- </div>
31
- )),
32
- useConfig: jest.fn(() => ({ bills: { pageSizes: [10, 20, 30, 40, 50], pageSize: 10 } })),
50
+ useLayoutType: jest.fn(() => 'desktop'),
51
+ isDesktop: jest.fn(() => true),
52
+ ErrorState: jest.fn(({ error }) => <div data-testid="error-state">{error?.message || error}</div>),
53
+ useConfig: jest.fn(() => ({
54
+ bills: {
55
+ pageSizes: [10, 20, 30, 40, 50],
56
+ pageSize: 10,
57
+ },
58
+ })),
33
59
  usePagination: jest.fn().mockImplementation((data) => ({
34
60
  currentPage: 1,
35
- goTo: () => {},
61
+ goTo: jest.fn(),
36
62
  results: data,
37
- paginated: false,
63
+ paginated: true,
38
64
  })),
65
+ ConfigurableLink: jest.fn(({ children, to, templateParams }) => {
66
+ const resolvedTo = '/home/billing/patient/' + templateParams.patientUuid + '/' + templateParams.uuid;
67
+ return <a href={resolvedTo}>{children}</a>;
68
+ }),
69
+ openmrsSpaBase: '',
39
70
  }));
40
71
 
41
72
  describe('BillsTable', () => {
73
+ const mockBills = useBills as jest.Mock;
42
74
  let user;
43
75
 
44
76
  beforeEach(() => {
45
77
  user = userEvent.setup();
78
+ mockBills.mockImplementation(() => ({
79
+ bills: mockBillsData,
80
+ isLoading: false,
81
+ isValidating: false,
82
+ error: null,
83
+ }));
46
84
  });
47
85
 
48
- xit('renders data table with pending bills', () => {
86
+ test('renders data table with pending bills', () => {
49
87
  render(<BillsTable />);
50
88
 
51
- expect(screen.getByText('Visit time')).toBeInTheDocument();
52
- expect(screen.getByText('Identifier')).toBeInTheDocument();
53
- const expectedColumnHeaders = [/Visit time/, /Identifier/, /Name/, /Billing service/];
54
- expectedColumnHeaders.forEach((header) => {
55
- expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument();
56
- });
89
+ expect(screen.getByText('visitTime')).toBeInTheDocument();
90
+ expect(screen.getByText('identifier')).toBeInTheDocument();
57
91
 
58
- const patientNameLink = screen.getByText('John Doe');
59
- expect(patientNameLink).toBeInTheDocument();
60
- expect(patientNameLink.tagName).toBe('A');
92
+ expect(screen.getByText(/John Doe/)).toBeInTheDocument();
93
+ expect(screen.getByText('12345678')).toBeInTheDocument();
61
94
  });
62
95
 
63
- it('displays empty state when there are no bills', () => {
64
- mockbills.mockImplementationOnce(() => ({
96
+ test('displays empty state when there are no bills', () => {
97
+ mockBills.mockImplementationOnce(() => ({
65
98
  bills: [],
66
99
  isLoading: false,
67
100
  isValidating: false,
@@ -69,12 +102,11 @@ describe('BillsTable', () => {
69
102
  }));
70
103
 
71
104
  render(<BillsTable />);
72
-
73
- expect(screen.getByText(/there are no bills to display/i)).toBeInTheDocument();
105
+ expect(screen.getByText('There are no bills to display.')).toBeInTheDocument();
74
106
  });
75
107
 
76
- it('should not display the table when the data is loading', () => {
77
- mockbills.mockImplementationOnce(() => ({
108
+ test('should show the loading spinner while retrieving data', () => {
109
+ mockBills.mockImplementationOnce(() => ({
78
110
  bills: undefined,
79
111
  isLoading: true,
80
112
  isValidating: false,
@@ -82,73 +114,81 @@ describe('BillsTable', () => {
82
114
  }));
83
115
 
84
116
  render(<BillsTable />);
85
-
86
- const expectedColumnHeaders = [/Visit time/, /Identifier/, /Name/, /Billing service/, /Department/];
87
- expectedColumnHeaders.forEach((header) => {
88
- expect(screen.queryByRole('columnheader', { name: new RegExp(header, 'i') })).not.toBeInTheDocument();
89
- });
117
+ const dataTableSkeleton = screen.getByRole('table');
118
+ expect(dataTableSkeleton).toBeInTheDocument();
119
+ expect(dataTableSkeleton).toHaveClass('cds--skeleton cds--data-table cds--data-table--zebra');
90
120
  });
91
121
 
92
- it('should display the error state when there is error', () => {
93
- mockbills.mockImplementationOnce(() => ({
94
- activeVisits: undefined,
122
+ test('should display the error state when there is error', () => {
123
+ mockBills.mockImplementationOnce(() => ({
124
+ bills: undefined,
95
125
  isLoading: false,
96
126
  isValidating: false,
97
- error: 'Error in fetching data',
127
+ error: new Error('Error in fetching data'),
98
128
  }));
99
129
 
100
130
  render(<BillsTable />);
101
-
102
- expect(screen.getByText(/sorry, there was a problem displaying this information/i)).toBeInTheDocument();
131
+ expect(screen.getByTestId('error-state')).toBeInTheDocument();
103
132
  expect(screen.queryByRole('table')).not.toBeInTheDocument();
104
133
  });
105
134
 
106
- test('should filter bills by search term and bill payment status', async () => {
135
+ test('should filter bills by search term', async () => {
107
136
  render(<BillsTable />);
108
137
 
109
138
  const searchInput = screen.getByRole('searchbox');
110
- await user.type(searchInput, 'John Doe');
139
+ expect(searchInput).toBeInTheDocument();
111
140
 
112
141
  expect(screen.getByText('John Doe')).toBeInTheDocument();
113
- expect(screen.queryByText('Mary Smith')).not.toBeInTheDocument();
142
+ expect(screen.getByText('Mary Smith')).toBeInTheDocument();
114
143
 
115
- await user.clear(searchInput);
116
- await user.type(searchInput, 'Mary Smith');
144
+ await user.type(searchInput, 'John Doe');
117
145
 
118
- expect(screen.getByText('Mary Smith')).toBeInTheDocument();
119
- expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
146
+ await waitFor(() => {
147
+ expect(screen.queryByText('Mary Smith')).not.toBeInTheDocument();
148
+ });
149
+
150
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
151
+ });
152
+
153
+ test('should render patient name as a link', () => {
154
+ render(<BillsTable />);
120
155
 
121
- // Should filter the table when bill payment status combobox is changed
122
- const billCategorySelect = screen.getByRole('combobox');
123
- expect(billCategorySelect).toBeInTheDocument();
124
- await user.click(billCategorySelect, { name: 'All bills' });
125
- expect(mockbills).toHaveBeenCalledWith('', '');
156
+ const patientNameLink = screen.getByRole('link', { name: 'John Doe' });
157
+ expect(patientNameLink).toBeInTheDocument();
126
158
 
127
- await user.click(screen.getByText('Pending bills'));
128
- expect(screen.getByText('Pending bills')).toBeInTheDocument();
129
- expect(mockbills).toHaveBeenCalledWith('', 'PENDING');
159
+ expect(patientNameLink.getAttribute('href')).toEqual('/home/billing/patient/uuid1/1');
130
160
  });
131
161
 
132
- test('should show the loading spinner while retrieving data', () => {
133
- mockbills.mockImplementationOnce(() => ({
134
- bills: undefined,
135
- isLoading: true,
162
+ test('should filter bills by payment status', async () => {
163
+ mockBills.mockImplementationOnce(() => ({
164
+ bills: mockBillsData.map((bill) => ({ ...bill, status: 'PENDING' })),
165
+ isLoading: false,
136
166
  isValidating: false,
137
167
  error: null,
138
168
  }));
139
169
 
140
170
  render(<BillsTable />);
141
171
 
142
- const dataTableSkeleton = screen.getByRole('table');
143
- expect(dataTableSkeleton).toBeInTheDocument();
144
- expect(dataTableSkeleton).toHaveClass('cds--skeleton cds--data-table cds--data-table--zebra');
172
+ const filterDropdown = screen.getByText('Pending bills');
173
+ await user.click(filterDropdown);
174
+
175
+ const paidBillsOption = screen.getAllByText('Paid bills')[0];
176
+ await user.click(paidBillsOption);
177
+
178
+ expect(screen.getByText('noMatchingBillsToDisplay')).toBeInTheDocument();
145
179
  });
146
180
 
147
- test('should render patient name as a link', async () => {
181
+ test('should show loading state during background updates', () => {
182
+ mockBills.mockImplementationOnce(() => ({
183
+ bills: mockBillsData,
184
+ isLoading: false,
185
+ isValidating: true,
186
+ error: null,
187
+ }));
188
+
148
189
  render(<BillsTable />);
149
190
 
150
- const patientNameLink = screen.getByRole('link', { name: 'John Doe' });
151
- expect(patientNameLink).toBeInTheDocument();
152
- expect(patientNameLink).toHaveAttribute('href', '/openmrs/spa/home/billing/patient/uuid1/1');
191
+ const loadingIndicator = screen.getByTitle('loading');
192
+ expect(loadingIndicator).toBeInTheDocument();
153
193
  });
154
194
  });
@@ -23,11 +23,36 @@ export const configSchema = {
23
23
  payingDetails: '44b34972-6630-4e5a-a9f6-a6eb0f109650',
24
24
  nonPayingDetails: 'f3fb2d88-cccd-422c-8766-be101ba7bd2e',
25
25
  insuranceDetails: 'beac329b-f1dc-4a33-9e7c-d95821a137a6',
26
+ },
27
+ },
28
+
29
+ nonPayingPatientCategories: {
30
+ _type: Type.Object,
31
+ _description: 'Concept UUIDs for non-paying patient categories',
32
+ _default: {
26
33
  childUnder5: '1528AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
27
34
  student: '159465AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
28
35
  },
29
36
  },
30
37
 
38
+ postBilledItems: {
39
+ _type: Type.Object,
40
+ _description: 'Post Bill Items such as cashPoints, cashier, priceUUid when submitting a bill',
41
+ _default: {
42
+ cashPoint: '54065383-b4d4-42d2-af4d-d250a1fd2590',
43
+ cashier: 'f9badd80-ab76-11e2-9e96-0800200c9a66',
44
+ priceUuid: '7b9171ac-d3c1-49b4-beff-c9902aee5245',
45
+ },
46
+ },
47
+
48
+ serviceTypes: {
49
+ _type: Type.Object,
50
+ _description: 'Post Bill Items such as cashPoints, cashier, priceUUid when submitting a bill',
51
+ _default: {
52
+ billableService: '21b8cf43-9f9f-4d02-9f4a-d710ece54261',
53
+ },
54
+ },
55
+
31
56
  defaultCurrency: {
32
57
  _type: Type.String,
33
58
  _description: 'The default currency for the application. Specify the currency code (e.g., KES, UGX, GBP).',
@@ -35,10 +60,16 @@ export const configSchema = {
35
60
  },
36
61
 
37
62
  pageSize: {
38
- _type: Type.String,
63
+ _type: Type.Number,
39
64
  _description: 'The default page size',
40
65
  _default: 10,
41
66
  },
67
+
68
+ showEditBillButton: {
69
+ _type: Type.Boolean,
70
+ _description: 'Whether to show the edit bill button or not.',
71
+ _default: false,
72
+ },
42
73
  };
43
74
 
44
75
  export interface ConfigObject {
@@ -47,4 +78,8 @@ export interface ConfigObject {
47
78
  catergoryConcepts: Object;
48
79
  pageSize;
49
80
  object;
81
+ showEditBillButton: boolean;
82
+ postBilledItems: Object;
83
+ serviceTypes: Object;
84
+ nonPayingPatientCategories: Object;
50
85
  }
package/src/constants.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  import { restBaseUrl } from '@openmrs/esm-framework';
2
2
 
3
3
  export const apiBasePath = `${restBaseUrl}/billing/`;
4
+
5
+ export const omrsDateFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZZ';
@@ -2,6 +2,7 @@ export const dashboardMeta = {
2
2
  slot: 'patient-chart-billing-dashboard-slot',
3
3
  columns: 1,
4
4
  title: 'Billing history',
5
- path: 'Billing history',
6
5
  hideDashboardTitle: true,
6
+ icon: '',
7
+ path: 'Billing history',
7
8
  };
@@ -1,6 +1,4 @@
1
1
  import { type Payment, type LineItem } from '../types';
2
- import { type ConfigObject } from '../config-schema';
3
- import { useConfig } from '@openmrs/esm-framework';
4
2
 
5
3
  // amount already paid
6
4
  export function calculateTotalAmountTendered(payments: Array<Payment>) {
@@ -0,0 +1,10 @@
1
+ import { createContext } from 'react';
2
+ import dayjs from 'dayjs';
3
+ import { omrsDateFormat } from '../constants';
4
+
5
+ const SelectedDateContext = createContext({
6
+ selectedDate: dayjs().startOf('day').format(omrsDateFormat),
7
+ setSelectedDate: (date: string) => {},
8
+ });
9
+
10
+ export default SelectedDateContext;