@kenyaemr/esm-billing-app 5.4.2-pre.2265 → 5.4.2-pre.2271

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.
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"},{"component":"requirePaymentModal","routeRegex":"^patient/.+/chart","online":true,"offline":false}],"extensions":[{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","slot":"patient-chart-dashboard-slot","meta":{"name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot","path":"insurance-benefits","columns":1,"columnSpan":1},"featureFlag":"healthInformationExchange"},{"component":"benefitsPackage","name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"component":"benefitsEligibilyRequestForm","name":"benefits-eligibility-request-form"},{"component":"benefitsPreAuthForm","name":"benefits-pre-auth-form"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing","layoutMode":"anchored"}},{"name":"billing-check-in-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"name":"require-billing-modal","component":"requirePaymentModal"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"initiate-payment-modal","component":"initiatePaymentDialog"},{"name":"delete-billableservice-modal","component":"deleteBillableServiceModal"},{"name":"refund-bill-modal","component":"refundBillModal"},{"name":"delete-bill-modal","component":"deleteBillModal"},{"name":"lab-order-billable-item","component":"labOrder","slot":"top-of-lab-order-form-slot"},{"name":"procedure-order-billable-item","component":"procedureOrder","slot":"top-of-procedure-order-form-slot"},{"name":"imaging-order-billable-item","component":"imagingOrder","slot":"top-of-imaging-order-form-slot"},{"name":"price-info-order","component":"priceInfoOrder"},{"name":"drug-order-billable-item","component":"drugOrder","slot":"medication-info-slot"},{"name":"order-action-button","component":"orderActionButton","slots":["prescription-action-button-slot","imaging-orders-action","procedure-orders-action","tests-ordered-actions-slot"],"order":0},{"component":"billingOverviewLink","name":"billing-overview-link","order":0,"slot":"billing-dashboard-group-nav-slot"},{"component":"billDepositDashboardLink","name":"bill-deposit-dashboard-link","slot":"billing-dashboard-group-nav-slot"},{"component":"paymentHistoryLink","name":"payment-history-link","slot":"billing-dashboard-group-nav-slot"},{"component":"paymentPointsLink","name":"payment-points-link","slot":"billing-dashboard-group-nav-slot"},{"component":"paymentModesLink","name":"payment-modes-link","slot":"billing-dashboard-group-nav-slot"},{"component":"billManagerLink","name":"bill-manager-link","slot":"billing-dashboard-group-nav-slot"},{"component":"chargeableItemsLink","name":"chargeable-items-link","slot":"billing-dashboard-group-nav-slot"},{"component":"billableExemptionsLink","name":"billable-exemptions-link","slot":"billing-dashboard-group-nav-slot"},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","order":0,"slot":"claims-management-dashboard-link-slot"},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slot":"claims-management-dashboard-link-slot"},{"component":"claimsOverview","name":"claims-overview-dashboard-link","slot":"claims-management-overview-slot"},{"component":"waiveBillActionButton","name":"waive-bill-action-button","slot":"bill-actions-slot"},{"component":"deleteBillActionButton","name":"delete-bill-action-button","slot":"bill-actions-slot"},{"component":"refundLineItem","name":"refund-line-item","slot":"bill-actions-overflow-menu-slot"},{"name":"edit-line-item","component":"editLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"cancel-line-item","component":"cancelLineItem","slot":"bill-actions-overflow-menu-slot"}],"workspaces":[{"name":"create-bill-workspace","component":"createBillWorkspace","title":"Create Bill Workspace","type":"other-form"},{"name":"waive-bill-form","component":"waiveBillForm","title":"Waive Bill Form","type":"other-form"},{"name":"edit-bill-form","component":"editBillForm","title":"Edit Bill Form","type":"other-form"},{"name":"billable-service-form","component":"addServiceForm","title":"Create Charge Item Form","type":"other-form"},{"name":"commodity-form","component":"addCommodityForm","title":"Create Charge Item Form","type":"other-form"},{"name":"billing-form","component":"billingForm","title":"Billing Form","type":"other-form","width":"extra-wide"},{"name":"payment-mode-workspace","component":"paymentModeWorkspace","title":"Payment Mode Workspace","type":"other-form"},{"name":"cancel-bill-workspace","component":"cancelBillWorkspace","title":"Cancel Bill Workspace","type":"other-form"},{"name":"add-deposit-workspace","component":"addDepositWorkspace","title":"Add Deposit","type":"other-form"},{"name":"deposit-transaction-workspace","component":"depositTransactionWorkspace","title":"Deposit Transaction","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"manage-claim-request-modal","component":"manageClaimRequestModal"},{"name":"clock-in-modal","component":"clockIn"},{"name":"create-bill-item-modal","component":"createBillItemModal"},{"name":"delete-deposit-modal","component":"deleteDepositModal"},{"name":"reverse-transaction-modal","component":"reverseTransactionModal"},{"name":"print-preview-modal","component":"printPreviewModal"}],"version":"5.4.2-pre.2265"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"},{"component":"requirePaymentModal","routeRegex":"^patient/.+/chart","online":true,"offline":false}],"extensions":[{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","slot":"patient-chart-dashboard-slot","meta":{"name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot","path":"insurance-benefits","columns":1,"columnSpan":1},"featureFlag":"healthInformationExchange"},{"component":"benefitsPackage","name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"component":"benefitsEligibilyRequestForm","name":"benefits-eligibility-request-form"},{"component":"benefitsPreAuthForm","name":"benefits-pre-auth-form"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing","layoutMode":"anchored"}},{"name":"billing-check-in-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"name":"require-billing-modal","component":"requirePaymentModal"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"initiate-payment-modal","component":"initiatePaymentDialog"},{"name":"delete-billableservice-modal","component":"deleteBillableServiceModal"},{"name":"refund-bill-modal","component":"refundBillModal"},{"name":"delete-bill-modal","component":"deleteBillModal"},{"name":"lab-order-billable-item","component":"labOrder","slot":"top-of-lab-order-form-slot"},{"name":"procedure-order-billable-item","component":"procedureOrder","slot":"top-of-procedure-order-form-slot"},{"name":"imaging-order-billable-item","component":"imagingOrder","slot":"top-of-imaging-order-form-slot"},{"name":"price-info-order","component":"priceInfoOrder"},{"name":"drug-order-billable-item","component":"drugOrder","slot":"medication-info-slot"},{"name":"order-action-button","component":"orderActionButton","slots":["prescription-action-button-slot","imaging-orders-action","procedure-orders-action","tests-ordered-actions-slot"],"order":0},{"component":"billingOverviewLink","name":"billing-overview-link","order":0,"slot":"billing-dashboard-group-nav-slot"},{"component":"billDepositDashboardLink","name":"bill-deposit-dashboard-link","slot":"billing-dashboard-group-nav-slot"},{"component":"paymentHistoryLink","name":"payment-history-link","slot":"billing-dashboard-group-nav-slot"},{"component":"paymentPointsLink","name":"payment-points-link","slot":"billing-dashboard-group-nav-slot"},{"component":"paymentModesLink","name":"payment-modes-link","slot":"billing-dashboard-group-nav-slot"},{"component":"billManagerLink","name":"bill-manager-link","slot":"billing-dashboard-group-nav-slot"},{"component":"chargeableItemsLink","name":"chargeable-items-link","slot":"billing-dashboard-group-nav-slot"},{"component":"billableExemptionsLink","name":"billable-exemptions-link","slot":"billing-dashboard-group-nav-slot"},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","order":0,"slot":"claims-management-dashboard-link-slot"},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slot":"claims-management-dashboard-link-slot"},{"component":"claimsOverview","name":"claims-overview-dashboard-link","slot":"claims-management-overview-slot"},{"component":"waiveBillActionButton","name":"waive-bill-action-button","slot":"bill-actions-slot"},{"component":"deleteBillActionButton","name":"delete-bill-action-button","slot":"bill-actions-slot"},{"component":"refundLineItem","name":"refund-line-item","slot":"bill-actions-overflow-menu-slot"},{"name":"edit-line-item","component":"editLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"cancel-line-item","component":"cancelLineItem","slot":"bill-actions-overflow-menu-slot"}],"workspaces":[{"name":"create-bill-workspace","component":"createBillWorkspace","title":"Create Bill Workspace","type":"other-form"},{"name":"waive-bill-form","component":"waiveBillForm","title":"Waive Bill Form","type":"other-form"},{"name":"edit-bill-form","component":"editBillForm","title":"Edit Bill Form","type":"other-form"},{"name":"billable-service-form","component":"addServiceForm","title":"Create Charge Item Form","type":"other-form"},{"name":"commodity-form","component":"addCommodityForm","title":"Create Charge Item Form","type":"other-form"},{"name":"billing-form","component":"billingForm","title":"Billing Form","type":"other-form","width":"extra-wide"},{"name":"payment-mode-workspace","component":"paymentModeWorkspace","title":"Payment Mode Workspace","type":"other-form"},{"name":"cancel-bill-workspace","component":"cancelBillWorkspace","title":"Cancel Bill Workspace","type":"other-form"},{"name":"add-deposit-workspace","component":"addDepositWorkspace","title":"Add Deposit","type":"other-form"},{"name":"deposit-transaction-workspace","component":"depositTransactionWorkspace","title":"Deposit Transaction","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"manage-claim-request-modal","component":"manageClaimRequestModal"},{"name":"clock-in-modal","component":"clockIn"},{"name":"create-bill-item-modal","component":"createBillItemModal"},{"name":"delete-deposit-modal","component":"deleteDepositModal"},{"name":"reverse-transaction-modal","component":"reverseTransactionModal"},{"name":"print-preview-modal","component":"printPreviewModal"},{"name":"bill-action-modal","component":"billActionModal"}],"version":"5.4.2-pre.2271"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-billing-app",
3
- "version": "5.4.2-pre.2265",
3
+ "version": "5.4.2-pre.2271",
4
4
  "description": "Billing app for KenyaEMR",
5
5
  "browser": "dist/kenyaemr-esm-billing-app.js",
6
6
  "main": "src/index.ts",
@@ -12,8 +12,6 @@ import {
12
12
  TableExpandRow,
13
13
  TableCell,
14
14
  TableExpandedRow,
15
- Tile,
16
- Button,
17
15
  } from '@carbon/react';
18
16
  import { convertToCurrency } from '../../helpers';
19
17
  import { useTranslation } from 'react-i18next';
@@ -21,8 +19,7 @@ import { EmptyState } from '@openmrs/esm-patient-common-lib';
21
19
  import { MappedBill, PaymentStatus } from '../../types';
22
20
  import styles from '../../bills-table/bills-table.scss';
23
21
  import BillLineItems from './bill-line-items.component';
24
- import { Scalpel, ShoppingCartMinus, TrashCan } from '@carbon/react/icons';
25
- import { ExtensionSlot, launchWorkspace, showModal } from '@openmrs/esm-framework';
22
+ import { ExtensionSlot } from '@openmrs/esm-framework';
26
23
 
27
24
  type PatientBillsProps = {
28
25
  bills: Array<MappedBill>;
@@ -63,6 +63,7 @@ export const mapBillProperties = (bill: PatientInvoice): MappedBill => {
63
63
  totalPayments: bill?.totalPayments,
64
64
  totalDeposits: bill?.totalDeposits,
65
65
  totalExempted: bill?.totalExempted,
66
+ closed: bill?.closed,
66
67
  };
67
68
 
68
69
  return mappedBill;
@@ -142,6 +143,7 @@ export const useBill = (billUuid: string) => {
142
143
  totalDeposits: bill?.totalDeposits,
143
144
  totalExempted: bill?.totalExempted,
144
145
  balance: bill?.balance,
146
+ closed: bill?.closed,
145
147
  };
146
148
 
147
149
  return mappedBill;
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ import RefundLineItem from './billable-services/bill-manager/bill-actions/refund
33
33
  import WaiveBillActionButton from './billable-services/bill-manager/bill-actions/waive-bill-action-button.component';
34
34
  import { DeleteBillModal } from './billable-services/bill-manager/modals/delete-bill.modal';
35
35
  import { RefundBillModal } from './billable-services/bill-manager/modals/refund-bill.modal';
36
+ import BillActionModal from './modal/bill-action.modal';
36
37
  import DeleteBillableServiceModal from './billable-services/bill-manager/modals/delete-billable-service.modal';
37
38
  import CancelBillWorkspace from './billable-services/bill-manager/workspaces/cancel-bill/cancel-bill.workspace';
38
39
  import { EditBillForm } from './billable-services/bill-manager/workspaces/edit-bill/edit-bill-form.workspace';
@@ -188,6 +189,7 @@ export const deleteBillModal = getSyncLifecycle(DeleteBillModal, options);
188
189
  export const waiveBillForm = getSyncLifecycle(WaiveBillForm, options);
189
190
  export const editBillForm = getSyncLifecycle(EditBillForm, options);
190
191
  export const refundBillModal = getSyncLifecycle(RefundBillModal, options);
192
+ export const billActionModal = getSyncLifecycle(BillActionModal, options);
191
193
  export const cancelBillWorkspace = getSyncLifecycle(CancelBillWorkspace, options);
192
194
  export const waiveBillActionButton = getSyncLifecycle(WaiveBillActionButton, options);
193
195
  export const deleteBillActionButton = getSyncLifecycle(DeleteBillActionButton, options);
@@ -1,5 +1,5 @@
1
- import { Button, InlineLoading } from '@carbon/react';
2
- import { BaggageClaim, Printer, Wallet } from '@carbon/react/icons';
1
+ import { Button, InlineLoading, Popover, PopoverContent } from '@carbon/react';
2
+ import { BaggageClaim, Close, Printer, Wallet, FolderOpen } from '@carbon/react/icons';
3
3
  import {
4
4
  defaultVisitCustomRepresentation,
5
5
  ExtensionSlot,
@@ -13,6 +13,7 @@ import {
13
13
  updateVisit,
14
14
  useFeatureFlag,
15
15
  usePatient,
16
+ UserHasAccess,
16
17
  useVisit,
17
18
  useVisitContextStore,
18
19
  } from '@openmrs/esm-framework';
@@ -29,7 +30,6 @@ import InvoiceTable from './invoice-table.component';
29
30
  import { useShaFacilityStatus } from './invoice.resource';
30
31
  import styles from './invoice.scss';
31
32
  import Payments from './payments/payments.component';
32
- import ReceiptPrintButton from './print-bill-receipt/receipt-print-button.component';
33
33
  import capitalize from 'lodash-es/capitalize';
34
34
  import { mutate } from 'swr';
35
35
  import startCase from 'lodash-es/startCase';
@@ -170,16 +170,6 @@ const Invoice: React.FC = () => {
170
170
  {patient && patientUuid && <ExtensionSlot name="patient-header-slot" state={{ patient, patientUuid }} />}
171
171
  <InvoiceSummary bill={bill} />
172
172
  <div className={styles.actionArea}>
173
- <ReceiptPrintButton bill={bill} />
174
- <Button
175
- onClick={handlePrint}
176
- kind="tertiary"
177
- size="sm"
178
- renderIcon={Printer}
179
- iconDescription="Add"
180
- tooltipPosition="right">
181
- {t('printInvoice', 'Print invoice')}
182
- </Button>
183
173
  <Button
184
174
  onClick={handleBillPayment}
185
175
  disabled={bill?.status === 'PAID'}
@@ -212,10 +202,117 @@ const Invoice: React.FC = () => {
212
202
 
213
203
  export function InvoiceSummary({ bill }: { readonly bill: MappedBill }) {
214
204
  const { t } = useTranslation();
205
+ const launchBillCloseOrReopenModal = (action: 'close' | 'reopen') => {
206
+ const dispose = showModal('bill-action-modal', {
207
+ closeModal: () => dispose(),
208
+ bill: bill,
209
+ action,
210
+ });
211
+ };
212
+
213
+ const shouldCloseBill = bill.balance === 0 && !bill.closed;
214
+ const [isOpen, setIsOpen] = useState(false);
215
+
216
+ const handlePrint = (documentType: string, documentTitle: string) => {
217
+ const dispose = showModal('print-preview-modal', {
218
+ onClose: () => dispose(),
219
+ title: documentTitle,
220
+ documentUrl: `/openmrs${restBaseUrl}/cashier/print?documentType=${documentType}&billId=${bill?.id}`,
221
+ });
222
+ };
223
+
215
224
  return (
216
225
  <>
217
226
  <div className={styles.invoiceSummary}>
218
227
  <span className={styles.invoiceSummaryTitle}>{t('invoiceSummary', 'Invoice Summary')}</span>
228
+ <div className="invoiceSummaryActions">
229
+ <Popover
230
+ isTabTip
231
+ align="bottom-right"
232
+ onKeyDown={() => {}}
233
+ onRequestClose={() => setIsOpen(false)}
234
+ open={isOpen}>
235
+ <button
236
+ className={styles.printButton}
237
+ aria-expanded
238
+ aria-label="Settings"
239
+ onClick={() => setIsOpen(!isOpen)}
240
+ type="button">
241
+ <Printer />
242
+ </button>
243
+ <PopoverContent>
244
+ <div className={styles.popoverContent}>
245
+ <Button
246
+ kind="ghost"
247
+ size="sm"
248
+ onClick={() =>
249
+ handlePrint(
250
+ 'invoice',
251
+ `${t('invoice', 'Invoice')} ${bill?.receiptNumber} - ${startCase(bill?.patientName)}`,
252
+ )
253
+ }
254
+ renderIcon={Printer}>
255
+ {t('printInvoice', 'Print Invoice')}
256
+ </Button>
257
+ {bill.balance === 0 && (
258
+ <Button
259
+ kind="ghost"
260
+ size="sm"
261
+ onClick={() => {
262
+ const dispose = showModal('print-preview-modal', {
263
+ onClose: () => dispose(),
264
+ title: `${t('receipt', 'Receipt')} ${bill?.receiptNumber} - ${startCase(bill?.patientName)}`,
265
+ documentUrl: `/openmrs${restBaseUrl}/cashier/receipt?billId=${bill.id}`,
266
+ });
267
+ }}
268
+ renderIcon={Printer}>
269
+ {t('printReceipt', 'Print Receipt')}
270
+ </Button>
271
+ )}
272
+ <Button
273
+ kind="ghost"
274
+ size="sm"
275
+ onClick={() =>
276
+ handlePrint(
277
+ 'billstatement',
278
+ `${t('billStatement', 'Bill Statement')} ${bill?.receiptNumber} - ${startCase(
279
+ bill?.patientName,
280
+ )}`,
281
+ )
282
+ }
283
+ renderIcon={Printer}>
284
+ {t('printBillStatement', 'Print Bill Statement')}
285
+ </Button>
286
+ </div>
287
+ </PopoverContent>
288
+ </Popover>
289
+ {shouldCloseBill && (
290
+ <UserHasAccess privilege="Close Cashier Bills">
291
+ <Button
292
+ kind="danger--ghost"
293
+ size="sm"
294
+ renderIcon={Close}
295
+ iconDescription="Add"
296
+ tooltipPosition="right"
297
+ onClick={() => launchBillCloseOrReopenModal('close')}>
298
+ {t('closeBill', 'Close Bill')}
299
+ </Button>
300
+ </UserHasAccess>
301
+ )}
302
+ {bill?.closed && (
303
+ <UserHasAccess privilege="Reopen Cashier Bills">
304
+ <Button
305
+ kind="ghost"
306
+ size="sm"
307
+ renderIcon={FolderOpen}
308
+ iconDescription="Add"
309
+ tooltipPosition="right"
310
+ onClick={() => launchBillCloseOrReopenModal('reopen')}>
311
+ {t('reopen', 'Reopen')}
312
+ </Button>
313
+ </UserHasAccess>
314
+ )}
315
+ </div>
219
316
  </div>
220
317
  <div className={styles.invoiceSummaryContainer}>
221
318
  <div className={styles.invoiceCard}>
@@ -109,3 +109,44 @@ export const useShaFacilityStatus = () => {
109
109
  mutate,
110
110
  };
111
111
  };
112
+
113
+ /**
114
+ * Reopens or closes a bill by making an API call to the billing service.
115
+ *
116
+ * This function allows authorized users to either reopen a closed bill or close an open bill.
117
+ * The action requires a reason to be provided for audit trail purposes.
118
+ *
119
+ * @param {string} billUuid - The unique identifier of the bill to be modified
120
+ * @param {'reopen' | 'close'} action - The action to perform on the bill
121
+ * - 'reopen': Reopens a previously closed bill
122
+ * - 'close': Closes an open bill
123
+ * @param {Object} payload - The payload containing the reason for the action
124
+ * @param {string} payload.reason - A descriptive reason explaining why the bill is being reopened or closed
125
+ *
126
+ * @returns {Promise<FetchResponse>} A promise that resolves to the API response
127
+ *
128
+ * @example
129
+ * // Reopen a closed bill
130
+ * const result = await reOpenOrCloseBill('bill-uuid-123', 'reopen', {
131
+ * reason: 'Patient returned for additional services'
132
+ * });
133
+ *
134
+ * @example
135
+ * // Close an open bill
136
+ * const result = await reOpenOrCloseBill('bill-uuid-456', 'close', {
137
+ * reason: 'Services completed and payment received'
138
+ * });
139
+ *
140
+ * @throws {Error} When the API call fails or returns an error response
141
+ */
142
+ export function reOpenOrCloseBill(billUuid: string, action: 'reopen' | 'close', payload: { reason: string }) {
143
+ return openmrsFetch(`${restBaseUrl}/kenyaemr-cashier/bill/${billUuid}/${action}`, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ },
148
+ body: {
149
+ reason: payload.reason,
150
+ },
151
+ });
152
+ }
@@ -130,12 +130,37 @@
130
130
  }
131
131
  }
132
132
 
133
- .invoiceSummaryTitle {
134
- @include type.type-style('heading-01');
135
- color: colors.$gray-70;
133
+ .invoiceSummary {
134
+ display: flex;
135
+ justify-content: space-between;
136
+ align-items: center;
136
137
  margin: layout.$spacing-05 layout.$spacing-05 0 layout.$spacing-05;
137
138
  background-color: colors.$white;
138
- display: block;
139
139
  border-bottom: 1px solid colors.$gray-20;
140
- padding: layout.$spacing-04;
140
+ padding: layout.$spacing-01 layout.$spacing-04;
141
+
142
+ .invoiceSummaryActions {
143
+ display: flex;
144
+ gap: layout.$spacing-04;
145
+ }
146
+ .invoiceSummaryTitle {
147
+ @include type.type-style('heading-01');
148
+ color: colors.$gray-70;
149
+ }
150
+ }
151
+
152
+ .popoverContent {
153
+ display: flex;
154
+ flex-direction: column;
155
+ gap: layout.$spacing-01;
156
+
157
+ & > button {
158
+ width: 100%;
159
+ }
160
+ }
161
+
162
+ .printButton {
163
+ svg {
164
+ fill: colors.$blue-60;
165
+ }
141
166
  }
@@ -0,0 +1,7 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+
4
+ .modalExplanation {
5
+ @include type.type-style('body-long-01');
6
+ margin-bottom: layout.$spacing-05;
7
+ }
@@ -0,0 +1,122 @@
1
+ import React from 'react';
2
+ import { ModalBody, ModalFooter, ModalHeader, Button, TextArea } from '@carbon/react';
3
+ import { type MappedBill } from '../types';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { convertToCurrency } from '../helpers';
6
+ import { reOpenOrCloseBill } from '../invoice/invoice.resource';
7
+ import { showSnackbar } from '@openmrs/esm-framework';
8
+ import { mutate } from 'swr';
9
+ import { Controller, useForm } from 'react-hook-form';
10
+ import { z } from 'zod';
11
+ import { zodResolver } from '@hookform/resolvers/zod';
12
+ import styles from './bill-action.modal.scss';
13
+
14
+ type BillActionModalProps = {
15
+ closeModal: () => void;
16
+ bill: MappedBill;
17
+ action: 'close' | 'reopen';
18
+ };
19
+
20
+ const formSchema = z.object({
21
+ reason: z.string().min(1, { message: 'Reason is required' }),
22
+ });
23
+
24
+ type FormData = z.infer<typeof formSchema>;
25
+
26
+ const BillActionModal: React.FC<BillActionModalProps> = (props) => {
27
+ const { closeModal, bill, action } = props;
28
+ const { t } = useTranslation();
29
+ const formMethod = useForm({
30
+ defaultValues: {
31
+ reason: '',
32
+ },
33
+ resolver: zodResolver(formSchema),
34
+ });
35
+
36
+ const modalExplanation =
37
+ action === 'close'
38
+ ? t('closeBillExplanation', 'Closing this bill will prevent any new items from being added to this bill')
39
+ : t('reopenBillExplanation', 'Reopening this bill will allow new items to be added to this bill');
40
+
41
+ const handleCloseBill = async (data: FormData) => {
42
+ try {
43
+ const response = await reOpenOrCloseBill(bill?.uuid, action, {
44
+ reason: data.reason,
45
+ });
46
+ if (response?.ok) {
47
+ showSnackbar({
48
+ title: t('billClosedSuccessfully', 'Bill {{action}} successfully', { action: action }),
49
+ subtitle: t('billClosedSuccessfullySubtitle', 'The bill has been {{action}} successfully', {
50
+ action: action,
51
+ }),
52
+ kind: 'success',
53
+ timeoutInMs: 3000,
54
+ isLowContrast: true,
55
+ });
56
+ } else {
57
+ throw new Error('Failed to close bill');
58
+ }
59
+ } catch (error) {
60
+ const errorResponseBody = error?.responseBody?.error || t('errorResponseBodyMessage', 'An error occurred');
61
+
62
+ showSnackbar({
63
+ title: t('billClosedFailed', 'Bill closing failed'),
64
+ subtitle: errorResponseBody,
65
+ kind: 'error',
66
+ timeoutInMs: 3000,
67
+ isLowContrast: true,
68
+ });
69
+ } finally {
70
+ const url = `/ws/rest/v1/cashier/bill/${bill.uuid}`;
71
+ mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
72
+ closeModal();
73
+ }
74
+ };
75
+ return (
76
+ <form onSubmit={formMethod.handleSubmit(handleCloseBill, (error) => console.error('error', error))}>
77
+ <ModalHeader
78
+ closeModal={closeModal}
79
+ label={t('billActionWithDetails', '{{action}} Bill - {{receiptNumber}} ({{status}}, {{amount}})', {
80
+ action: action === 'close' ? t('close', 'Close') : t('reopen', 'Reopen'),
81
+ receiptNumber: bill?.receiptNumber,
82
+ status: bill?.status,
83
+ amount: bill?.totalAmount ? `${convertToCurrency(bill?.totalAmount)}` : 'N/A',
84
+ })}
85
+ title={t('billAction', '{{action}} Bill', {
86
+ action: action === 'close' ? t('close', 'Close') : t('reopen', 'Reopen'),
87
+ })}
88
+ />
89
+ <ModalBody>
90
+ <div>
91
+ <p className={styles.modalExplanation}>{modalExplanation}</p>
92
+ </div>
93
+ <Controller
94
+ control={formMethod.control}
95
+ name="reason"
96
+ render={({ field }) => (
97
+ <TextArea
98
+ id="reason"
99
+ labelText={t('reason', 'Reason for {{action}} bill', { action: action })}
100
+ placeholder={t('reason', 'Reason for {{action}} bill', { action: action })}
101
+ rows={4}
102
+ onChange={field.onChange}
103
+ value={field.value}
104
+ invalid={!!formMethod.formState.errors.reason}
105
+ invalidText={formMethod.formState.errors.reason?.message}
106
+ />
107
+ )}
108
+ />
109
+ </ModalBody>
110
+ <ModalFooter>
111
+ <Button kind="secondary" onClick={closeModal}>
112
+ {t('cancel', 'Cancel')}
113
+ </Button>
114
+ <Button disabled={!formMethod.formState.isValid} type="submit" kind="danger">
115
+ {action === 'close' ? t('close', 'Close') : t('reopen', 'Reopen')}
116
+ </Button>
117
+ </ModalFooter>
118
+ </form>
119
+ );
120
+ };
121
+
122
+ export default BillActionModal;
@@ -36,7 +36,6 @@ export const patientBillsHeaders = [
36
36
  export const PatientBills: React.FC<PatientBillsProps> = ({ bills, onCancel, patientUuid }) => {
37
37
  const { t } = useTranslation();
38
38
  const { patient, isLoading, error } = usePatient(patientUuid);
39
-
40
39
  if (isLoading) {
41
40
  return <InlineLoading status="active" description={t('loading', 'Loading...')} />;
42
41
  }
@@ -63,7 +62,7 @@ export const PatientBills: React.FC<PatientBillsProps> = ({ bills, onCancel, pat
63
62
  style={{ textDecoration: 'none', maxWidth: '50%' }}
64
63
  to={billingUrl}
65
64
  templateParams={{ patientUuid: bill.patientUuid, uuid: bill.uuid }}>
66
- {bill.lineItems.map((item) => item.billableService.split(':')[1]).join(', ')}
65
+ {bill.lineItems.map((item) => item?.billableService?.split(':')[1]).join(', ')}
67
66
  </ConfigurableLink>
68
67
  ),
69
68
  totalAmount: convertToCurrency(bill.totalAmount),
@@ -28,7 +28,6 @@ const PromptPaymentModal: React.FC<PromptPaymentModalProps> = () => {
28
28
  const { shouldShowBillingPrompt, isLoading, bills } = useBillingPrompt(patientUuid, 'patient-chart');
29
29
  const [showModal, setShowModal] = useState({ loadingModal: true, billingModal: true });
30
30
  const { enforceBillPayment } = useConfig<BillingConfig>();
31
-
32
31
  const closeButtonText = enforceBillPayment
33
32
  ? t('navigateBack', 'Navigate back')
34
33
  : t('proceedToCare', 'Proceed to care');
@@ -42,7 +41,9 @@ const PromptPaymentModal: React.FC<PromptPaymentModalProps> = () => {
42
41
  const lineItems = bills
43
42
  .filter((bill) => bill.status !== 'PAID')
44
43
  .flatMap((bill) => bill.lineItems)
45
- .filter((lineItem) => lineItem.paymentStatus !== 'EXEMPTED' && !lineItem.voided);
44
+ .filter(
45
+ (lineItem) => lineItem?.paymentStatus !== 'EXEMPTED' && lineItem?.paymentStatus !== 'PAID' && !lineItem?.voided,
46
+ );
46
47
 
47
48
  if (!shouldShowBillingPrompt) {
48
49
  return null;
@@ -71,6 +72,7 @@ const PromptPaymentModal: React.FC<PromptPaymentModalProps> = () => {
71
72
  <StructuredListCell head>{t('item', 'Item')}</StructuredListCell>
72
73
  <StructuredListCell head>{t('quantity', 'Quantity')}</StructuredListCell>
73
74
  <StructuredListCell head>{t('unitPrice', 'Unit price')}</StructuredListCell>
75
+ <StructuredListCell head>{t('status', 'Status')}</StructuredListCell>
74
76
  <StructuredListCell head>{t('total', 'Total')}</StructuredListCell>
75
77
  </StructuredListRow>
76
78
  </StructuredListHead>
@@ -81,6 +83,7 @@ const PromptPaymentModal: React.FC<PromptPaymentModalProps> = () => {
81
83
  <StructuredListCell>{extractString(lineItem.billableService || lineItem.item)}</StructuredListCell>
82
84
  <StructuredListCell>{lineItem.quantity}</StructuredListCell>
83
85
  <StructuredListCell>{convertToCurrency(lineItem.price)}</StructuredListCell>
86
+ <StructuredListCell>{lineItem.paymentStatus}</StructuredListCell>
84
87
  <StructuredListCell>{convertToCurrency(lineItem.quantity * lineItem.price)}</StructuredListCell>
85
88
  </StructuredListRow>
86
89
  );
@@ -71,7 +71,7 @@ const shouldShowPrompt = (
71
71
  const hasOnlyOrderBills = (bills: Array<MappedBill>): boolean => {
72
72
  const flattenedBills = bills.flatMap((bill) => bill.lineItems);
73
73
  // check if all line items are orders, line item with order has order not set to null
74
- return flattenedBills.every((item) => item.order);
74
+ return flattenedBills.every((item) => item?.order);
75
75
  };
76
76
 
77
77
  /**
package/src/routes.json CHANGED
@@ -321,6 +321,10 @@
321
321
  {
322
322
  "name": "print-preview-modal",
323
323
  "component": "printPreviewModal"
324
+ },
325
+ {
326
+ "name": "bill-action-modal",
327
+ "component": "billActionModal"
324
328
  }
325
329
  ]
326
330
  }
@@ -26,6 +26,7 @@ export interface MappedBill {
26
26
  totalDeposits?: number;
27
27
  totalExempted?: number;
28
28
  balance?: number;
29
+ closed?: boolean;
29
30
  }
30
31
 
31
32
  interface LocationLink {
@@ -145,6 +146,7 @@ export interface PatientInvoice {
145
146
  totalDeposits?: number;
146
147
  totalExempted?: number;
147
148
  balance?: number;
149
+ closed?: boolean;
148
150
  }
149
151
 
150
152
  export interface PatientDetails {
package/src/utils.ts CHANGED
@@ -162,6 +162,10 @@ export function waitForASecond(): Promise<string> {
162
162
  }
163
163
 
164
164
  export const computeWaivedAmount = (bill: MappedBill) => {
165
+ if (!bill.payments) {
166
+ return 0;
167
+ }
168
+
165
169
  return bill.payments
166
170
  .filter((payment) => payment.instanceType.name.toLowerCase() === 'waiver')
167
171
  .reduce((curr: number, prev) => curr + Number(prev.amountTendered), 0);
@@ -46,6 +46,7 @@
46
46
  "billPayment": "Bill payment",
47
47
  "billPaymentRequiredMessage": "The current patient has pending bill. Advice patient to settle bill.",
48
48
  "billsList": "Bill list",
49
+ "billStatement": "Bill Statement",
49
50
  "billTotal": "Bill total",
50
51
  "bulkUpload": "Bulk Upload",
51
52
  "cancel": "Cancel",
@@ -77,6 +78,7 @@
77
78
  "clearSearch": "Clear search input",
78
79
  "clockInTime": "Clocked in on {{clockInDate}}",
79
80
  "close": "Close",
81
+ "closeBill": "Close Bill",
80
82
  "create": "Create",
81
83
  "createClaimError": "Create Claim error",
82
84
  "created": "Created",
@@ -230,6 +232,7 @@
230
232
  "previousPage": "Previous page",
231
233
  "price": "Price",
232
234
  "prices": "Prices",
235
+ "printBillStatement": "Print Bill Statement",
233
236
  "printInvoice": "Print invoice",
234
237
  "printReceipt": "Print receipt",
235
238
  "proceedToCare": "Proceed to care",
@@ -248,6 +251,7 @@
248
251
  "regExp": "Regular expression",
249
252
  "rejected": "Rejected",
250
253
  "remove": "Remove",
254
+ "reopen": "Reopen",
251
255
  "retryRequest": "Retry request",
252
256
  "reverse": "Reverse",
253
257
  "saveAndClose": "Save & Close",