@kenyaemr/esm-billing-app 5.4.2-pre.2262 → 5.4.2-pre.2269

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 (37) hide show
  1. package/.turbo/turbo-build.log +14 -14
  2. package/dist/300.js +1 -1
  3. package/dist/46.js +8 -0
  4. package/dist/46.js.map +1 -0
  5. package/dist/{632.js → 611.js} +1 -1
  6. package/dist/611.js.map +1 -0
  7. package/dist/kenyaemr-esm-billing-app.js +1 -1
  8. package/dist/kenyaemr-esm-billing-app.js.buildmanifest.json +428 -428
  9. package/dist/kenyaemr-esm-billing-app.js.map +1 -1
  10. package/dist/main.js +3 -3
  11. package/dist/main.js.map +1 -1
  12. package/dist/routes.json +1 -1
  13. package/package.json +1 -1
  14. package/src/billable-services/bill-manager/patient-bills.component.tsx +1 -4
  15. package/src/billing.resource.ts +2 -0
  16. package/src/index.ts +2 -0
  17. package/src/invoice/invoice.component.tsx +40 -1
  18. package/src/invoice/invoice.resource.ts +41 -0
  19. package/src/invoice/invoice.scss +14 -5
  20. package/src/invoice/print-bill-receipt/receipt-print-button.component.tsx +2 -2
  21. package/src/modal/bill-action.modal.scss +7 -0
  22. package/src/modal/bill-action.modal.tsx +122 -0
  23. package/src/past-patient-bills/patient-bills.component.tsx +1 -2
  24. package/src/payment-modes/payment-mode-dashboard.compont.tsx +2 -2
  25. package/src/payment-modes/payment-mode.workspace.test.tsx +57 -1
  26. package/src/payment-modes/payment-mode.workspace.tsx +9 -3
  27. package/src/payment-modes/usePaymentModeFormSchema.tsx +1 -1
  28. package/src/prompt-payment/prompt-payment-modal.component.tsx +5 -2
  29. package/src/prompt-payment/prompt-payment.resource.tsx +1 -1
  30. package/src/routes.json +4 -0
  31. package/src/types/index.ts +2 -0
  32. package/src/utils.ts +4 -0
  33. package/translations/en.json +2 -0
  34. package/dist/632.js.map +0 -1
  35. package/dist/910.js +0 -8
  36. package/dist/910.js.map +0 -1
  37. /package/dist/{910.js.LICENSE.txt → 46.js.LICENSE.txt} +0 -0
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.2262"}
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.2269"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-billing-app",
3
- "version": "5.4.2-pre.2262",
3
+ "version": "5.4.2-pre.2269",
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
1
  import { Button, InlineLoading } from '@carbon/react';
2
- import { BaggageClaim, Printer, Wallet } from '@carbon/react/icons';
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';
@@ -212,10 +213,48 @@ const Invoice: React.FC = () => {
212
213
 
213
214
  export function InvoiceSummary({ bill }: { readonly bill: MappedBill }) {
214
215
  const { t } = useTranslation();
216
+ const launchBillCloseOrReopenModal = (action: 'close' | 'reopen') => {
217
+ const dispose = showModal('bill-action-modal', {
218
+ closeModal: () => dispose(),
219
+ bill: bill,
220
+ action,
221
+ });
222
+ };
223
+
224
+ const shouldCloseBill = bill.balance === 0 && !bill.closed;
225
+
215
226
  return (
216
227
  <>
217
228
  <div className={styles.invoiceSummary}>
218
229
  <span className={styles.invoiceSummaryTitle}>{t('invoiceSummary', 'Invoice Summary')}</span>
230
+ <div className="invoiceSummaryActions">
231
+ {shouldCloseBill && (
232
+ <UserHasAccess privilege="Close Cashier Bills">
233
+ <Button
234
+ kind="danger--ghost"
235
+ size="sm"
236
+ renderIcon={Close}
237
+ iconDescription="Add"
238
+ tooltipPosition="right"
239
+ onClick={() => launchBillCloseOrReopenModal('close')}>
240
+ {t('closeBill', 'Close Bill')}
241
+ </Button>
242
+ </UserHasAccess>
243
+ )}
244
+ {bill?.closed && (
245
+ <UserHasAccess privilege="Reopen Cashier Bills">
246
+ <Button
247
+ kind="ghost"
248
+ size="sm"
249
+ renderIcon={FolderOpen}
250
+ iconDescription="Add"
251
+ tooltipPosition="right"
252
+ onClick={() => launchBillCloseOrReopenModal('reopen')}>
253
+ {t('reopen', 'Reopen')}
254
+ </Button>
255
+ </UserHasAccess>
256
+ )}
257
+ </div>
219
258
  </div>
220
259
  <div className={styles.invoiceSummaryContainer}>
221
260
  <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,21 @@
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
+ }
141
150
  }
@@ -42,8 +42,8 @@ const ReceiptPrintButton: React.FC<ReceiptPrintButtonProps> = ({ bill }) => {
42
42
  * @returns true if printing should be disabled, false otherwise
43
43
  */
44
44
  function shouldDisablePrinting(bill: MappedBill): boolean {
45
- const hasPayments = bill.payments.length > 0;
46
- const hasExemptedItems = bill.lineItems.some((item) => item.paymentStatus === PaymentStatus.EXEMPTED);
45
+ const hasPayments = bill?.payments?.length > 0;
46
+ const hasExemptedItems = bill?.lineItems?.some((item) => item.paymentStatus === PaymentStatus.EXEMPTED);
47
47
 
48
48
  // If there are exempted items, we need special handling
49
49
  if (hasExemptedItems) {
@@ -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),
@@ -22,7 +22,7 @@ import {
22
22
  } from '@carbon/react';
23
23
 
24
24
  import styles from './payment-mode-dashboard.scss';
25
- import { formatDate, launchWorkspace, showModal, useDebounce } from '@openmrs/esm-framework';
25
+ import { formatDate, launchWorkspace, showModal, useDebounce, useLayoutType } from '@openmrs/esm-framework';
26
26
  import { PaymentMode } from '../types';
27
27
  import startCase from 'lodash/startCase';
28
28
 
@@ -30,7 +30,7 @@ type PaymentModeDashboardProps = {};
30
30
 
31
31
  const PaymentModeDashboard: React.FC<PaymentModeDashboardProps> = () => {
32
32
  const { t } = useTranslation();
33
- const size = 'md';
33
+ const size = useLayoutType() === 'tablet' ? 'md' : 'sm';
34
34
  const { paymentModes = [], isLoading } = usePaymentModes(false);
35
35
  const [searchTerm, setSearchTerm] = useState('');
36
36
 
@@ -99,7 +99,7 @@ describe('PaymentModeWorkspace', () => {
99
99
  // Key in attribute type name, description, required and format
100
100
  const attributeTypeNameInput = screen.getByRole('textbox', { name: /Attribute name/i });
101
101
  const attributeTypeDescriptionInput = screen.getByRole('textbox', { name: /Attribute description/i });
102
- const attributeRegExpInput = screen.getByRole('textbox', { name: /Enter regular expression/i });
102
+ const attributeRegExpInput = screen.getByRole('textbox', { name: /Regular expression/i });
103
103
  const attributeRetiredToggle = screen.getByRole('switch', { name: /Attribute retired/i });
104
104
  const attributeRequiredToggle = screen.getByRole('switch', { name: /Attribute required/i });
105
105
 
@@ -148,4 +148,60 @@ describe('PaymentModeWorkspace', () => {
148
148
  '',
149
149
  );
150
150
  });
151
+
152
+ test('should submit payload with attributeTypes and uuid when in edit mode', async () => {
153
+ const initialPaymentMode = {
154
+ uuid: '123',
155
+ name: 'Test Name',
156
+ description: 'Test Description',
157
+ retired: false,
158
+ attributeTypes: [
159
+ {
160
+ name: 'Test Attribute Name',
161
+ uuid: '456',
162
+ format: 'java.lang.String',
163
+ regExp: null,
164
+ attributeOrder: 0,
165
+ foreignKey: null,
166
+ description: 'Test Attribute Description',
167
+ retired: false,
168
+ required: false,
169
+ },
170
+ ],
171
+ };
172
+
173
+ const user = userEvent.setup();
174
+ render(<PaymentModeWorkspace {...testProps} initialPaymentMode={initialPaymentMode} />);
175
+
176
+ // make the attribute type required
177
+ const attributeRequiredToggle = screen.getByRole('switch', { name: /Attribute required/i });
178
+ await user.click(attributeRequiredToggle);
179
+
180
+ // click save and close
181
+ const saveAndCloseButton = screen.getByRole('button', { name: /Save & Close/i });
182
+ await user.click(saveAndCloseButton);
183
+
184
+ // should call createPaymentMode with attributeTypes and uuid
185
+ expect(mockCreatePaymentMode).toHaveBeenCalledWith(
186
+ {
187
+ description: 'Test Description',
188
+ name: 'Test Name',
189
+ attributeTypes: [
190
+ {
191
+ name: 'Test Attribute Name',
192
+ uuid: '456',
193
+ description: 'Test Attribute Description',
194
+ retired: false,
195
+ required: true,
196
+ format: 'java.lang.String',
197
+ regExp: null,
198
+ attributeOrder: 0,
199
+ foreignKey: null,
200
+ },
201
+ ],
202
+ retired: false,
203
+ },
204
+ '123',
205
+ );
206
+ });
151
207
  });
@@ -54,16 +54,22 @@ const PaymentModeWorkspace: React.FC<PaymentModeWorkspaceProps> = ({
54
54
  });
55
55
 
56
56
  const mappedAttributeTypes = (attributes) => {
57
- return {
57
+ const uuid = initialPaymentMode?.uuid
58
+ ? initialPaymentMode.attributeTypes.find((a) => a.name === attributes.name)?.uuid
59
+ : null;
60
+
61
+ const attributeType = {
58
62
  name: attributes.name,
59
63
  description: attributes.description,
60
64
  retired: attributes.retired,
61
65
  attributeOrder: attributes?.attributeOrder ?? 0,
62
66
  format: attributes?.format ?? '',
63
67
  foreignKey: attributes?.foreignKey ?? null,
64
- regExp: attributes?.regExp ?? '',
68
+ regExp: attributes?.regExp ?? null,
65
69
  required: attributes.required,
66
70
  };
71
+
72
+ return uuid ? { ...attributeType, uuid } : attributeType;
67
73
  };
68
74
 
69
75
  const onSubmit = async (data: PaymentModeFormSchema) => {
@@ -192,7 +198,7 @@ const PaymentModeWorkspace: React.FC<PaymentModeWorkspaceProps> = ({
192
198
  </Stack>
193
199
  </div>
194
200
  <ButtonSet className={classNames({ [styles.tablet]: isTablet, [styles.desktop]: !isTablet })}>
195
- <Button style={{ maxWidth: '50%' }} kind="secondary" onClick={() => closeWorkspace}>
201
+ <Button style={{ maxWidth: '50%' }} kind="secondary" onClick={() => closeWorkspace()}>
196
202
  {t('cancel', 'Cancel')}
197
203
  </Button>
198
204
  <Button
@@ -8,7 +8,7 @@ const usePaymentModeFormSchema = () => {
8
8
  retired: z.boolean().default(false),
9
9
  retiredReason: z.string().optional(),
10
10
  format: z.string().optional(),
11
- regExp: z.string().optional(),
11
+ regExp: z.union([z.string(), z.null()]).optional(),
12
12
  required: z.boolean().default(false),
13
13
  })
14
14
  .refine(
@@ -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);
@@ -77,6 +77,7 @@
77
77
  "clearSearch": "Clear search input",
78
78
  "clockInTime": "Clocked in on {{clockInDate}}",
79
79
  "close": "Close",
80
+ "closeBill": "Close Bill",
80
81
  "create": "Create",
81
82
  "createClaimError": "Create Claim error",
82
83
  "created": "Created",
@@ -248,6 +249,7 @@
248
249
  "regExp": "Regular expression",
249
250
  "rejected": "Rejected",
250
251
  "remove": "Remove",
252
+ "reopen": "Reopen",
251
253
  "retryRequest": "Retry request",
252
254
  "reverse": "Reverse",
253
255
  "saveAndClose": "Save & Close",