@openmrs/esm-billing-app 1.1.1 → 1.1.2-pre.1

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 (193) hide show
  1. package/.turbo/cache/53e233a916ffe7d2-meta.json +1 -0
  2. package/.turbo/cache/53e233a916ffe7d2.tar.zst +0 -0
  3. package/.turbo/turbo-build.log +44 -0
  4. package/__mocks__/bills.mock.ts +6 -5
  5. package/dist/1119.js +1 -1
  6. package/dist/1197.js +1 -1
  7. package/dist/1435.js +1 -0
  8. package/dist/1435.js.map +1 -0
  9. package/dist/1807.js +1 -0
  10. package/dist/1807.js.map +1 -0
  11. package/dist/2146.js +1 -1
  12. package/dist/2177.js +1 -1
  13. package/dist/2177.js.map +1 -1
  14. package/dist/2690.js +1 -1
  15. package/dist/2704.js +1 -0
  16. package/dist/2704.js.map +1 -0
  17. package/dist/3002.js +1 -0
  18. package/dist/3002.js.map +1 -0
  19. package/dist/3041.js +1 -1
  20. package/dist/3041.js.map +1 -1
  21. package/dist/3099.js +1 -1
  22. package/dist/3184.js +1 -1
  23. package/dist/3184.js.map +1 -1
  24. package/dist/3584.js +1 -1
  25. package/dist/4055.js +1 -1
  26. package/dist/4132.js +1 -1
  27. package/dist/4225.js +1 -1
  28. package/dist/4225.js.map +1 -1
  29. package/dist/4300.js +1 -1
  30. package/dist/4335.js +1 -1
  31. package/dist/439.js +1 -1
  32. package/dist/4618.js +1 -1
  33. package/dist/4652.js +1 -1
  34. package/dist/4944.js +1 -1
  35. package/dist/5173.js +1 -1
  36. package/dist/5241.js +1 -1
  37. package/dist/5422.js +1 -1
  38. package/dist/5422.js.map +1 -1
  39. package/dist/5442.js +1 -1
  40. package/dist/5661.js +1 -1
  41. package/dist/6022.js +1 -1
  42. package/dist/6404.js +1 -0
  43. package/dist/6404.js.map +1 -0
  44. package/dist/6468.js +1 -1
  45. package/dist/6540.js +1 -1
  46. package/dist/6540.js.map +1 -1
  47. package/dist/6589.js +1 -1
  48. package/dist/6606.js +1 -1
  49. package/dist/6606.js.map +1 -1
  50. package/dist/6679.js +1 -1
  51. package/dist/6792.js +1 -0
  52. package/dist/6792.js.map +1 -0
  53. package/dist/6840.js +1 -1
  54. package/dist/6859.js +1 -1
  55. package/dist/7097.js +1 -1
  56. package/dist/7159.js +1 -1
  57. package/dist/723.js +1 -1
  58. package/dist/7255.js +1 -1
  59. package/dist/7255.js.map +1 -1
  60. package/dist/7617.js +1 -1
  61. package/dist/795.js +1 -1
  62. package/dist/8163.js +1 -1
  63. package/dist/8341.js +2 -0
  64. package/dist/{1907.js.LICENSE.txt → 8341.js.LICENSE.txt} +0 -15
  65. package/dist/8341.js.map +1 -0
  66. package/dist/8349.js +1 -1
  67. package/dist/8371.js +1 -1
  68. package/dist/8421.js +1 -0
  69. package/dist/8421.js.map +1 -0
  70. package/dist/8618.js +1 -1
  71. package/dist/890.js +1 -1
  72. package/dist/9214.js +1 -1
  73. package/dist/9538.js +1 -1
  74. package/dist/9569.js +1 -1
  75. package/dist/961.js +1 -1
  76. package/dist/961.js.map +1 -1
  77. package/dist/986.js +1 -1
  78. package/dist/9879.js +1 -1
  79. package/dist/9895.js +1 -1
  80. package/dist/9900.js +1 -1
  81. package/dist/9913.js +1 -1
  82. package/dist/main.js +1 -1
  83. package/dist/main.js.LICENSE.txt +0 -15
  84. package/dist/main.js.map +1 -1
  85. package/dist/openmrs-esm-billing-app.js +1 -1
  86. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +284 -259
  87. package/dist/openmrs-esm-billing-app.js.map +1 -1
  88. package/dist/routes.json +1 -1
  89. package/e2e/commands/patient-operations.ts +1 -1
  90. package/e2e/pages/billing-dashboard-page.ts +3 -1
  91. package/e2e/pages/billing-form-page.ts +5 -0
  92. package/e2e/pages/invoice-page.ts +10 -0
  93. package/e2e/specs/billing-dashboard.spec.ts +126 -3
  94. package/e2e/specs/billing-patient-chart.spec.ts +95 -9
  95. package/package.json +3 -6
  96. package/src/bill-history/bill-action-menu.component.tsx +41 -0
  97. package/src/bill-history/bill-action-menu.scss +3 -0
  98. package/src/bill-history/bill-history.component.tsx +15 -5
  99. package/src/bill-history/bill-history.scss +0 -1
  100. package/src/bill-history/bill-history.test.tsx +78 -1
  101. package/src/bill-item-actions/edit-bill-item.modal.tsx +1 -1
  102. package/src/bill-item-actions/edit-bill-item.test.tsx +40 -0
  103. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +3 -1
  104. package/src/billing-dashboard/billing-dashboard.component.tsx +3 -16
  105. package/src/billing-form/billing-checkin-form.component.tsx +116 -57
  106. package/src/billing-form/billing-checkin-form.scss +26 -2
  107. package/src/billing-form/billing-checkin-form.test.tsx +51 -1
  108. package/src/billing-form/billing-form.resource.test.ts +87 -0
  109. package/src/billing-form/billing-form.resource.ts +33 -0
  110. package/src/billing-form/billing-form.scss +54 -7
  111. package/src/billing-form/billing-form.test.tsx +547 -0
  112. package/src/billing-form/billing-form.workspace.tsx +150 -45
  113. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +25 -2
  114. package/src/billing-form/visit-attributes/visit-attributes-form.scss +29 -0
  115. package/src/billing-header/billing-header.component.tsx +1 -34
  116. package/src/billing-header/billing-header.scss +0 -50
  117. package/src/billing.resource.test.ts +11 -11
  118. package/src/billing.resource.ts +42 -12
  119. package/src/bills-table/bills-table.component.tsx +16 -12
  120. package/src/bills-table/bills-table.test.tsx +84 -7
  121. package/src/index.ts +5 -0
  122. package/src/invoice/invoice.component.tsx +46 -16
  123. package/src/invoice/invoice.scss +9 -8
  124. package/src/invoice/invoice.test.tsx +128 -7
  125. package/src/invoice/line-item-action-menu.component.tsx +2 -2
  126. package/src/invoice/payments/payments.component.tsx +2 -2
  127. package/src/invoice/payments/payments.test.tsx +31 -2
  128. package/src/metrics-cards/metrics.resource.ts +3 -4
  129. package/src/modal/finalize-bill-confirmation.modal.test.tsx +209 -0
  130. package/src/modal/finalize-bill-confirmation.modal.tsx +86 -0
  131. package/src/modal/require-payment.modal.tsx +2 -1
  132. package/src/routes.json +4 -0
  133. package/src/types/index.ts +10 -1
  134. package/tools/setup-tests.ts +7 -6
  135. package/translations/am.json +28 -0
  136. package/translations/ar.json +28 -0
  137. package/translations/ar_SY.json +28 -0
  138. package/translations/bn.json +28 -0
  139. package/translations/cs.json +28 -0
  140. package/translations/de.json +266 -238
  141. package/translations/en.json +29 -0
  142. package/translations/en_US.json +28 -0
  143. package/translations/es.json +28 -0
  144. package/translations/es_MX.json +28 -0
  145. package/translations/fr.json +28 -0
  146. package/translations/he.json +28 -0
  147. package/translations/hi.json +28 -0
  148. package/translations/hi_IN.json +28 -0
  149. package/translations/id.json +28 -0
  150. package/translations/it.json +28 -0
  151. package/translations/ka.json +28 -0
  152. package/translations/km.json +28 -0
  153. package/translations/ku.json +28 -0
  154. package/translations/ky.json +28 -0
  155. package/translations/lg.json +28 -0
  156. package/translations/ne.json +28 -0
  157. package/translations/pl.json +28 -0
  158. package/translations/pt.json +28 -0
  159. package/translations/pt_BR.json +28 -0
  160. package/translations/qu.json +28 -0
  161. package/translations/ro_RO.json +28 -0
  162. package/translations/ru_RU.json +28 -0
  163. package/translations/si.json +28 -0
  164. package/translations/sq.json +28 -0
  165. package/translations/sw.json +28 -0
  166. package/translations/sw_KE.json +28 -0
  167. package/translations/tr.json +28 -0
  168. package/translations/tr_TR.json +28 -0
  169. package/translations/uk.json +28 -0
  170. package/translations/uz.json +28 -0
  171. package/translations/uz@Latn.json +28 -0
  172. package/translations/uz_UZ.json +28 -0
  173. package/translations/vi.json +28 -0
  174. package/translations/zh.json +268 -240
  175. package/translations/zh_CN.json +30 -2
  176. package/translations/zh_TW.json +28 -0
  177. package/turbo.json +29 -0
  178. package/dist/1537.js +0 -1
  179. package/dist/1537.js.map +0 -1
  180. package/dist/1907.js +0 -2
  181. package/dist/1907.js.map +0 -1
  182. package/dist/1981.js +0 -1
  183. package/dist/1981.js.map +0 -1
  184. package/dist/2820.js +0 -1
  185. package/dist/2820.js.map +0 -1
  186. package/dist/8025.js +0 -1
  187. package/dist/8025.js.map +0 -1
  188. package/dist/9727.js +0 -2
  189. package/dist/9727.js.LICENSE.txt +0 -14
  190. package/dist/9727.js.map +0 -1
  191. package/dist/9756.js +0 -1
  192. package/dist/9756.js.map +0 -1
  193. package/src/hooks/selectedDateContext.ts +0 -10
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React, { useMemo, useState } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import {
4
4
  Button,
@@ -19,10 +19,12 @@ import {
19
19
  type Workspace2DefinitionProps,
20
20
  Workspace2,
21
21
  } from '@openmrs/esm-framework';
22
- import { processBillItems, useBillableServices } from '../billing.resource';
22
+ import { processBillItems, updateBillItems, useBill, useBillableServices } from '../billing.resource';
23
+ import { useBillableServices as useBillableServicesList } from '../billable-services/billable-service.resource';
24
+ import { getBillableServiceUuid } from '../invoice/payments/utils';
23
25
  import { calculateTotalAmount, convertToCurrency } from '../helpers/functions';
24
26
  import type { BillingConfig } from '../config-schema';
25
- import type { BillableItem, LineItem, ServicePrice } from '../types';
27
+ import { BillStatus, type BillableItem, type LineItem, type ServicePrice } from '../types';
26
28
  import styles from './billing-form.scss';
27
29
 
28
30
  interface ExtendedLineItem extends LineItem {
@@ -34,10 +36,11 @@ type BillingFormProps = {
34
36
  patientUuid: string;
35
37
  closeWorkspace: () => void;
36
38
  onMutate?: () => void;
39
+ billUuid?: string;
37
40
  };
38
41
 
39
42
  const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
40
- workspaceProps: { patientUuid, onMutate },
43
+ workspaceProps: { patientUuid, onMutate, billUuid },
41
44
  closeWorkspace,
42
45
  }) => {
43
46
  const isTablet = useLayoutType() === 'tablet';
@@ -46,6 +49,27 @@ const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
46
49
  const [isSubmitting, setIsSubmitting] = useState(false);
47
50
  const [selectedItems, setSelectedItems] = useState<ExtendedLineItem[]>([]);
48
51
  const { data, error, isLoading } = useBillableServices();
52
+ const { bill, isLoading: isLoadingBill, error: billError } = useBill(billUuid);
53
+ const {
54
+ billableServices,
55
+ isLoading: isLoadingBillableServices,
56
+ error: billableServicesError,
57
+ } = useBillableServicesList();
58
+ const isEditMode = !!billUuid && !!bill;
59
+ const existingItemsTotal = useMemo(
60
+ () => (isEditMode ? calculateTotalAmount(bill.lineItems) : 0),
61
+ [isEditMode, bill?.lineItems],
62
+ );
63
+
64
+ const availableBillableItems = useMemo(() => {
65
+ if (!data) return [];
66
+ if (!isEditMode) return data;
67
+ const lineItems = bill.lineItems ?? [];
68
+ const existingNames = new Set(
69
+ lineItems.flatMap((lineItem) => [lineItem.billableService, lineItem.item].filter(Boolean)),
70
+ );
71
+ return data.filter((item) => item.name && !existingNames.has(item.name));
72
+ }, [data, isEditMode, bill?.lineItems]);
49
73
 
50
74
  const selectBillableItem = (item: BillableItem) => {
51
75
  if (!item) {
@@ -77,7 +101,7 @@ const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
77
101
  quantity: 1,
78
102
  price: defaultPrice,
79
103
  billableService: item.uuid,
80
- paymentStatus: 'PENDING',
104
+ paymentStatus: BillStatus.PENDING,
81
105
  lineItemOrder: 0,
82
106
  selectedPaymentMethod: selectedPaymentMethod,
83
107
  availablePaymentMethods: availablePaymentMethods,
@@ -135,49 +159,84 @@ const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
135
159
  if (isSubmitting || selectedItems.length === 0) {
136
160
  return;
137
161
  }
162
+ if (isEditMode && (isLoadingBillableServices || billableServicesError)) {
163
+ return;
164
+ }
138
165
  if (!validateSelectedItems()) {
139
166
  return;
140
167
  }
141
168
 
142
169
  setIsSubmitting(true);
143
- const bill = {
144
- cashPoint: postBilledItems.cashPoint,
145
- cashier: postBilledItems.cashier,
146
- lineItems: [],
147
- payments: [],
148
- patient: patientUuid,
149
- status: 'PENDING',
150
- };
151
170
 
152
- selectedItems.forEach((item) => {
153
- const lineItem: LineItem = {
154
- quantity: item.quantity,
155
- price: item.price,
156
- lineItemOrder: 0,
157
- paymentStatus: 'PENDING',
158
- billableService: item.uuid,
159
- };
160
-
161
- bill.lineItems.push(lineItem);
162
- });
171
+ const newLineItems: Array<LineItem> = selectedItems.map((item) => ({
172
+ quantity: item.quantity,
173
+ price: item.price,
174
+ lineItemOrder: 0,
175
+ paymentStatus: BillStatus.PENDING,
176
+ billableService: item.uuid,
177
+ }));
163
178
 
164
179
  try {
165
- await processBillItems(bill);
166
- closeWorkspace({ discardUnsavedChanges: true });
180
+ if (isEditMode) {
181
+ const existingLineItems = bill.lineItems.map((item) => {
182
+ const serviceUuid = getBillableServiceUuid(billableServices, item.billableService || item.item);
183
+ if (!serviceUuid) {
184
+ throw new Error(
185
+ t('serviceResolutionError', 'Could not resolve service "{{service}}"', {
186
+ service: item.billableService || item.item,
187
+ }),
188
+ );
189
+ }
190
+ return {
191
+ uuid: item.uuid,
192
+ quantity: item.quantity,
193
+ price: item.price,
194
+ lineItemOrder: item.lineItemOrder,
195
+ paymentStatus: item.paymentStatus,
196
+ billableService: serviceUuid,
197
+ priceName: item.priceName,
198
+ priceUuid: item.priceUuid,
199
+ };
200
+ });
167
201
 
168
- // Call the mutate function from parent to revalidate bill list
202
+ const payload = {
203
+ cashPoint: bill.cashPointUuid,
204
+ cashier: bill.cashier.uuid,
205
+ lineItems: [...existingLineItems, ...newLineItems],
206
+ patient: bill.patientUuid,
207
+ status: bill.status,
208
+ uuid: bill.uuid,
209
+ };
210
+
211
+ await updateBillItems(payload);
212
+ } else {
213
+ const payload = {
214
+ cashPoint: postBilledItems.cashPoint,
215
+ cashier: postBilledItems.cashier,
216
+ lineItems: newLineItems,
217
+ payments: [],
218
+ patient: patientUuid,
219
+ status: BillStatus.PENDING,
220
+ };
221
+
222
+ await processBillItems(payload);
223
+ }
224
+
225
+ closeWorkspace({ discardUnsavedChanges: true });
169
226
  onMutate?.();
170
227
 
171
228
  showSnackbar({
172
- title: t('billProcessed', 'Bill processed'),
173
- subtitle: t('billProcessedSuccessfully', 'Bill processed successfully'),
229
+ title: isEditMode ? t('itemsAddedToBill', 'Items added to bill') : t('billProcessed', 'Bill processed'),
230
+ subtitle: isEditMode
231
+ ? t('itemsAddedToBillSuccessfully', 'Items have been added to the bill successfully')
232
+ : t('billProcessedSuccessfully', 'Bill processed successfully'),
174
233
  kind: 'success',
175
234
  });
176
- } catch (error) {
235
+ } catch (err) {
177
236
  showSnackbar({
178
237
  title: t('billProcessingError', 'Bill processing error'),
179
238
  kind: 'error',
180
- subtitle: error?.message,
239
+ subtitle: err instanceof Error ? err.message : t('unknownBillError', 'An unexpected error occurred'),
181
240
  });
182
241
  } finally {
183
242
  setIsSubmitting(false);
@@ -190,30 +249,76 @@ const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
190
249
  };
191
250
 
192
251
  return (
193
- <Workspace2 title={t('addBillItems', 'Add bill items')} hasUnsavedChanges={selectedItems.length > 0}>
252
+ <Workspace2
253
+ title={isEditMode ? t('addItemsToBill', 'Add items to bill') : t('addBillItems', 'Add bill items')}
254
+ hasUnsavedChanges={selectedItems.length > 0}>
194
255
  <Form className={styles.form} onSubmit={handleSubmit}>
195
256
  <div className={styles.grid}>
196
- {isLoading ? (
257
+ {billUuid && isLoadingBill ? (
197
258
  <InlineLoading description={getCoreTranslation('loading') + '...'} />
198
- ) : error ? (
259
+ ) : billUuid && billError ? (
260
+ <InlineNotification
261
+ kind="error"
262
+ lowContrast
263
+ title={t('errorLoadingBill', 'Error loading bill')}
264
+ subtitle={billError?.message}
265
+ />
266
+ ) : isEditMode && billableServicesError ? (
199
267
  <InlineNotification
200
268
  kind="error"
201
269
  lowContrast
202
270
  title={t('errorLoadingBillableServices', 'Error loading billable services')}
203
- subtitle={error?.message}
271
+ subtitle={billableServicesError?.message}
204
272
  />
205
273
  ) : (
206
- <ComboBox
207
- id="searchItems"
208
- onChange={({ selectedItem: item }: { selectedItem: BillableItem }) => selectBillableItem(item)}
209
- itemToString={(item: BillableItem) => item?.name || ''}
210
- items={data ?? []}
211
- titleText={t('searchItems', 'Search items and services')}
212
- />
274
+ <>
275
+ {isEditMode && (
276
+ <div className={styles.existingItemsContainer}>
277
+ <h4 className={styles.sectionHeading}>{t('existingItems', 'Existing items')}</h4>
278
+ {bill.lineItems.map((item) => (
279
+ <div key={item.uuid} className={styles.existingItemRow}>
280
+ <span className={styles.existingItemName}>
281
+ {item.billableService || item.item || item.display}
282
+ </span>
283
+ <span className={styles.existingItemDetail}>
284
+ {item.quantity} x {convertToCurrency(item.price, defaultCurrency)}
285
+ </span>
286
+ <span className={styles.existingItemTotal}>
287
+ {convertToCurrency(item.price * item.quantity, defaultCurrency)}
288
+ </span>
289
+ </div>
290
+ ))}
291
+ <div className={styles.existingItemsSubtotal}>
292
+ <strong>
293
+ {t('subtotal', 'Subtotal')}: {convertToCurrency(existingItemsTotal, defaultCurrency)}
294
+ </strong>
295
+ </div>
296
+ </div>
297
+ )}
298
+ {isEditMode && <h4 className={styles.sectionHeading}>{t('newItems', 'New items')}</h4>}
299
+ {isLoading ? (
300
+ <InlineLoading description={getCoreTranslation('loading') + '...'} />
301
+ ) : error ? (
302
+ <InlineNotification
303
+ kind="error"
304
+ lowContrast
305
+ title={t('errorLoadingBillableServices', 'Error loading billable services')}
306
+ subtitle={error?.message}
307
+ />
308
+ ) : (
309
+ <ComboBox
310
+ id="searchItems"
311
+ onChange={({ selectedItem: item }: { selectedItem: BillableItem }) => selectBillableItem(item)}
312
+ itemToString={(item: BillableItem) => item?.name || ''}
313
+ items={availableBillableItems}
314
+ titleText={t('searchItems', 'Search items and services')}
315
+ />
316
+ )}
317
+ </>
213
318
  )}
214
319
  {selectedItems && selectedItems.length > 0 && (
215
320
  <div className={styles.selectedItemsContainer}>
216
- <h4>{t('selectedItems', 'Selected items')}</h4>
321
+ <h4 className={styles.sectionHeading}>{t('selectedItems', 'Selected items')}</h4>
217
322
  {selectedItems.map((item) => (
218
323
  <div key={item.uuid} className={styles.itemCard}>
219
324
  <div className={styles.itemHeader}>
@@ -289,7 +394,7 @@ const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
289
394
  <div className={styles.grandTotal}>
290
395
  <strong>
291
396
  {t('grandTotal', 'Grand total')}:{' '}
292
- {convertToCurrency(calculateTotalAmount(selectedItems), defaultCurrency)}
397
+ {convertToCurrency(existingItemsTotal + calculateTotalAmount(selectedItems), defaultCurrency)}
293
398
  </strong>
294
399
  </div>
295
400
  </div>
@@ -307,7 +412,7 @@ const BillingForm: React.FC<Workspace2DefinitionProps<BillingFormProps>> = ({
307
412
  <Button
308
413
  className={styles.button}
309
414
  kind="primary"
310
- disabled={isSubmitting || selectedItems.length === 0}
415
+ disabled={isSubmitting || selectedItems.length === 0 || (isEditMode && isLoadingBillableServices)}
311
416
  type="submit">
312
417
  {isSubmitting ? (
313
418
  <InlineLoading description={t('saving', 'Saving') + '...'} />
@@ -3,7 +3,18 @@ import { z } from 'zod';
3
3
  import { zodResolver } from '@hookform/resolvers/zod';
4
4
  import { Controller, useForm } from 'react-hook-form';
5
5
  import { useTranslation } from 'react-i18next';
6
- import { ComboBox, InlineLoading, RadioButton, RadioButtonGroup, Stack, TextInput } from '@carbon/react';
6
+ import {
7
+ ComboBox,
8
+ InlineLoading,
9
+ RadioButton,
10
+ RadioButtonGroup,
11
+ Stack,
12
+ TextInput,
13
+ Toggletip,
14
+ ToggletipButton,
15
+ ToggletipContent,
16
+ } from '@carbon/react';
17
+ import { Information } from '@carbon/react/icons';
7
18
  import { useConfig } from '@openmrs/esm-framework';
8
19
  import { usePaymentMethods } from '../billing-form.resource';
9
20
  import styles from './visit-attributes-form.scss';
@@ -121,7 +132,19 @@ const VisitAttributesForm: React.FC<VisitAttributesFormProps> = ({ setAttributes
121
132
  render={({ field }) => (
122
133
  <RadioButtonGroup
123
134
  className={styles.radioButtonGroup}
124
- legendText={t('paymentDetails', 'Payment details')}
135
+ legendText={
136
+ <div className={styles.paymentDetailsLegend}>
137
+ {t('paymentDetails', 'Payment details')}
138
+ <Toggletip autoAlign align="bottom">
139
+ <ToggletipButton label={t('showInformation', 'Show information')}>
140
+ <Information />
141
+ </ToggletipButton>
142
+ <ToggletipContent>
143
+ <p>{t('nonPayingInfo', 'Any services rendered to non-paying patients will not be billed')}</p>
144
+ </ToggletipContent>
145
+ </Toggletip>
146
+ </div>
147
+ }
125
148
  name="payment-details"
126
149
  onChange={(selected) => field.onChange(selected)}
127
150
  orientation="vertical">
@@ -19,7 +19,11 @@
19
19
  }
20
20
 
21
21
  .radioButtonGroup {
22
+ overflow: visible;
23
+
22
24
  :global(.cds--radio-button-group) {
25
+ overflow: visible;
26
+
23
27
  :global(.cds--radio-button-wrapper) {
24
28
  margin-bottom: layout.$spacing-03;
25
29
 
@@ -28,8 +32,33 @@
28
32
  }
29
33
  }
30
34
  }
35
+
36
+ :global(fieldset.cds--fieldset) {
37
+ overflow: visible;
38
+ }
31
39
  }
32
40
 
33
41
  .stack {
34
42
  margin-bottom: layout.$spacing-05;
43
+ overflow: visible;
44
+ }
45
+
46
+ .paymentDetailsLegend {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: layout.$spacing-03;
50
+ overflow: visible;
51
+
52
+ :global(.cds--toggletip) {
53
+ overflow: visible;
54
+ }
55
+
56
+ :global(.cds--popover-content) {
57
+ z-index: 9100;
58
+ }
59
+
60
+ :global(.cds--toggletip-content) {
61
+ min-width: 16rem;
62
+ white-space: normal;
63
+ }
35
64
  }
@@ -1,12 +1,6 @@
1
- import React, { useContext } from 'react';
2
- import dayjs from 'dayjs';
3
- import { DatePickerInput, DatePicker } from '@carbon/react';
1
+ import React from 'react';
4
2
  import { useTranslation } from 'react-i18next';
5
- import { Location, UserFollow } from '@carbon/react/icons';
6
- import { useSession } from '@openmrs/esm-framework';
7
- import { omrsDateFormat } from '../constants';
8
3
  import BillingIllustration from './billing-illustration.component';
9
- import SelectedDateContext from '../hooks/selectedDateContext';
10
4
  import styles from './billing-header.scss';
11
5
 
12
6
  interface BillingHeaderProps {
@@ -15,9 +9,6 @@ interface BillingHeaderProps {
15
9
 
16
10
  const BillingHeader: React.FC<BillingHeaderProps> = ({ title }) => {
17
11
  const { t } = useTranslation();
18
- const session = useSession();
19
- const location = session?.sessionLocation?.display;
20
- const { selectedDate, setSelectedDate } = useContext(SelectedDateContext);
21
12
 
22
13
  return (
23
14
  <div className={styles.header} data-testid="billing-header">
@@ -28,30 +19,6 @@ const BillingHeader: React.FC<BillingHeaderProps> = ({ title }) => {
28
19
  <p className={styles['page-name']}>{title}</p>
29
20
  </div>
30
21
  </div>
31
- <div className={styles['right-justified-items']}>
32
- <div className={styles.userContainer}>
33
- <p>{session?.user?.person?.display}</p>
34
- <UserFollow size={16} className={styles.userIcon} />
35
- </div>
36
- <div className={styles['date-and-location']}>
37
- <Location size={16} />
38
- <span className={styles.value}>{location}</span>
39
- <span className={styles.middot}>&middot;</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>
53
- </div>
54
- </div>
55
22
  </div>
56
23
  );
57
24
  };
@@ -18,15 +18,6 @@
18
18
  flex-direction: row;
19
19
  align-items: center;
20
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
21
  }
31
22
 
32
23
  .page-name {
@@ -40,44 +31,3 @@
40
31
  margin-bottom: layout.$spacing-02;
41
32
  }
42
33
  }
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
- }
@@ -194,8 +194,8 @@ describe('mapBillProperties', () => {
194
194
  patient: createPatient({ display: '12345 - John Doe' }),
195
195
  });
196
196
  const result = mapBillProperties(bill);
197
- expect(result.identifier).toBe('12345 ');
198
- expect(result.patientName).toBe(' John Doe');
197
+ expect(result.identifier).toBe('12345');
198
+ expect(result.patientName).toBe('John Doe');
199
199
  expect(result.patientUuid).toBe('patient-uuid');
200
200
  });
201
201
 
@@ -204,8 +204,8 @@ describe('mapBillProperties', () => {
204
204
  patient: createPatient({ display: 'John Doe' }),
205
205
  });
206
206
  const result = mapBillProperties(bill);
207
- expect(result.identifier).toBe('John Doe');
208
- expect(result.patientName).toBeUndefined();
207
+ expect(result.identifier).toBe('');
208
+ expect(result.patientName).toBe('John Doe');
209
209
  });
210
210
 
211
211
  it('Handles patient display with multiple dashes', () => {
@@ -213,9 +213,9 @@ describe('mapBillProperties', () => {
213
213
  patient: createPatient({ display: '12345 - John - Doe - Jr' }),
214
214
  });
215
215
  const result = mapBillProperties(bill);
216
- expect(result.identifier).toBe('12345 ');
216
+ expect(result.identifier).toBe('12345');
217
217
  // Note: split('-')[1] only takes the second element, not everything after first dash
218
- expect(result.patientName).toBe(' John ');
218
+ expect(result.patientName).toBe('John - Doe - Jr');
219
219
  });
220
220
 
221
221
  it('Handles empty patient display string', () => {
@@ -224,7 +224,7 @@ describe('mapBillProperties', () => {
224
224
  });
225
225
  const result = mapBillProperties(bill);
226
226
  expect(result.identifier).toBe('');
227
- expect(result.patientName).toBeUndefined();
227
+ expect(result.patientName).toBe('');
228
228
  });
229
229
 
230
230
  it('Handles undefined patient gracefully', () => {
@@ -233,8 +233,8 @@ describe('mapBillProperties', () => {
233
233
  });
234
234
  const result = mapBillProperties(bill);
235
235
  expect(result.patientUuid).toBeUndefined();
236
- expect(result.identifier).toBeUndefined();
237
- expect(result.patientName).toBeUndefined();
236
+ expect(result.identifier).toBe('');
237
+ expect(result.patientName).toBe('');
238
238
  });
239
239
 
240
240
  it('Handles patient with undefined display', () => {
@@ -245,8 +245,8 @@ describe('mapBillProperties', () => {
245
245
  },
246
246
  });
247
247
  const result = mapBillProperties(bill);
248
- expect(result.identifier).toBeUndefined();
249
- expect(result.patientName).toBeUndefined();
248
+ expect(result.identifier).toBe('');
249
+ expect(result.patientName).toBe('');
250
250
  });
251
251
  });
252
252
 
@@ -1,8 +1,6 @@
1
1
  import useSWR from 'swr';
2
2
  import sortBy from 'lodash-es/sortBy';
3
3
  import {
4
- formatDate,
5
- parseDate,
6
4
  openmrsFetch,
7
5
  useSession,
8
6
  useVisit,
@@ -11,26 +9,47 @@ import {
11
9
  useOpenmrsPagination,
12
10
  } from '@openmrs/esm-framework';
13
11
  import { apiBasePath } from './constants';
14
- import type {
15
- MappedBill,
16
- PatientInvoice,
17
- BillableItem,
18
- PaymentRequestPayload,
19
- CreateBillPayload,
20
- UpdateBillPayload,
12
+ import {
13
+ type MappedBill,
14
+ type PatientInvoice,
15
+ type BillableItem,
16
+ type PaymentRequestPayload,
17
+ type CreateBillPayload,
18
+ type UpdateBillPayload,
19
+ BillStatus,
21
20
  } from './types';
22
21
 
22
+ const parsePatientDisplay = (display: string | undefined): { identifier: string; name: string } => {
23
+ if (!display) {
24
+ return { identifier: '', name: '' };
25
+ }
26
+
27
+ const separator = ' - ';
28
+ const index = display.indexOf(separator);
29
+
30
+ if (index === -1) {
31
+ return { identifier: '', name: display.trim() };
32
+ }
33
+
34
+ return {
35
+ identifier: display.substring(0, index).trim(),
36
+ name: display.substring(index + separator.length).trim(),
37
+ };
38
+ };
39
+
23
40
  export const mapBillProperties = (bill: PatientInvoice): MappedBill => {
24
- const activeLineItems = bill?.lineItems?.filter((item) => !item.voided) || [];
41
+ const activeLineItems = bill?.lineItems?.filter((item) => !item.voided) ?? [];
42
+ const { identifier, name } = parsePatientDisplay(bill?.patient?.display);
25
43
 
26
44
  return {
27
45
  ...bill,
28
- patientName: bill?.patient?.display?.split('-')?.[1],
29
- identifier: bill?.patient?.display?.split('-')?.[0],
46
+ patientName: name,
47
+ identifier: identifier,
30
48
  patientUuid: bill?.patient?.uuid,
31
49
  cashPointUuid: bill?.cashPoint?.uuid,
32
50
  cashPointName: bill?.cashPoint?.name,
33
51
  cashPointLocation: bill?.cashPoint?.location?.display,
52
+ status: bill.status as BillStatus,
34
53
  lineItems: activeLineItems,
35
54
  billingService: activeLineItems.map((lineItem) => lineItem?.item || lineItem?.billableService || '--').join(' '),
36
55
  totalAmount: activeLineItems
@@ -175,6 +194,17 @@ export const updateBillItems = (payload: UpdateBillPayload) => {
175
194
  });
176
195
  };
177
196
 
197
+ export const finalizeBill = (billUuid: string) => {
198
+ const url = `${apiBasePath}bill/${billUuid}`;
199
+ return openmrsFetch(url, {
200
+ method: 'POST',
201
+ body: { status: BillStatus.POSTED },
202
+ headers: {
203
+ 'Content-Type': 'application/json',
204
+ },
205
+ });
206
+ };
207
+
178
208
  export const deleteBillItem = (itemUuid: string, voidReason: string) => {
179
209
  const url = `${apiBasePath}billLineItem/${itemUuid}?reason=${encodeURIComponent(voidReason)}`;
180
210