@kenyaemr/esm-billing-app 5.4.2-pre.2301 → 5.4.2-pre.2306
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/.turbo/turbo-build.log +10 -10
- package/dist/197.js +1 -1
- package/dist/294.js +1 -1
- package/dist/300.js +1 -1
- package/dist/985.js +1 -0
- package/dist/985.js.map +1 -0
- package/dist/kenyaemr-esm-billing-app.js +1 -1
- package/dist/kenyaemr-esm-billing-app.js.buildmanifest.json +36 -36
- package/dist/kenyaemr-esm-billing-app.js.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/bill-deposit/utils/bill-deposit.utils.ts +3 -5
- package/src/billable-services/bill-manager/workspaces/edit-bill/edit-bill-form.workspace.tsx +2 -4
- package/src/billing.resource.ts +5 -0
- package/src/claims/dashboard/form/claims-form.component.tsx +3 -4
- package/src/config-schema.ts +13 -0
- package/src/helpers/README.md +174 -0
- package/src/helpers/currency.test.ts +114 -0
- package/src/helpers/currency.ts +138 -0
- package/src/helpers/functions.ts +2 -13
- package/src/index.ts +2 -0
- package/src/invoice/invoice.component.tsx +31 -16
- package/src/invoice/payments/payment-form/payment.scss +33 -0
- package/src/invoice/payments/payment-form/payment.workspace.tsx +195 -0
- package/src/invoice/payments/payment-form/use-payment-form.ts +77 -0
- package/src/routes.json +6 -0
- package/translations/am.json +1 -0
- package/translations/en.json +1 -0
- package/translations/sw.json +1 -0
- package/dist/115.js +0 -1
- package/dist/115.js.map +0 -1
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"},{"name":"bill-action-modal","component":"billActionModal"}],"version":"5.4.2-pre.
|
|
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"},{"name":"payment-workspace","component":"paymentWorkspace","title":"Payment Workspace","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.2306"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
2
|
import { type BillDeposit, type CreateDepositPayload } from '../types/bill-deposit.types';
|
|
3
3
|
import { MAX_REFERENCE_NUMBER_COUNTER } from '../constants/bill-deposit.constants';
|
|
4
|
+
import { formatCurrencySimple } from '../../helpers/currency';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Generates a unique reference number for bill deposits
|
|
@@ -55,13 +56,10 @@ export const deleteDeposit = async (uuid: string) => {
|
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
|
-
* Formats a deposit amount with currency symbol
|
|
59
|
+
* Formats a deposit amount with currency symbol based on locale
|
|
59
60
|
*/
|
|
60
61
|
export const formatDepositAmount = (amount: number): string => {
|
|
61
|
-
return
|
|
62
|
-
style: 'currency',
|
|
63
|
-
currency: 'KES',
|
|
64
|
-
}).format(amount);
|
|
62
|
+
return formatCurrencySimple(amount);
|
|
65
63
|
};
|
|
66
64
|
|
|
67
65
|
/**
|
package/src/billable-services/bill-manager/workspaces/edit-bill/edit-bill-form.workspace.tsx
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
|
|
24
24
|
import { LineItem, MappedBill } from '../../../../types';
|
|
25
25
|
import { processBillPayment } from '../../../../billing.resource';
|
|
26
|
+
import { formatCurrencySimple } from '../../../../helpers/currency';
|
|
26
27
|
import styles from './edit-bill.scss';
|
|
27
28
|
import { createEditBillPayload } from './edit-bill-util';
|
|
28
29
|
import classNames from 'classnames';
|
|
@@ -93,10 +94,7 @@ export const EditBillForm: React.FC<EditBillFormProps> = ({
|
|
|
93
94
|
return <InlineLoading description={t('loading', 'Loading')} />;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
const formattedPrice =
|
|
97
|
-
style: 'currency',
|
|
98
|
-
currency: 'KES',
|
|
99
|
-
}).format(lineItem.price);
|
|
97
|
+
const formattedPrice = formatCurrencySimple(lineItem.price);
|
|
100
98
|
|
|
101
99
|
const subtitleText = `${t('currentPriceAndQuantity', 'Current price and quantity')}: ${t(
|
|
102
100
|
'price',
|
package/src/billing.resource.ts
CHANGED
|
@@ -291,3 +291,8 @@ export const billingFormSchema = z.object({
|
|
|
291
291
|
)
|
|
292
292
|
.min(1),
|
|
293
293
|
});
|
|
294
|
+
|
|
295
|
+
export const addPaymentToBill = (billUuid: string, payload: Record<string, any>) => {
|
|
296
|
+
const url = `${restBaseUrl}/cashier/bill/${billUuid}/payment`;
|
|
297
|
+
return openmrsFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload });
|
|
298
|
+
};
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
ComboBox,
|
|
15
15
|
} from '@carbon/react';
|
|
16
16
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
17
|
-
import { navigate, showSnackbar, useConfig, useSession } from '@openmrs/esm-framework';
|
|
17
|
+
import { formatDatetime, navigate, showSnackbar, useConfig, useSession } from '@openmrs/esm-framework';
|
|
18
18
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
19
19
|
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
|
20
20
|
import { useTranslation } from 'react-i18next';
|
|
@@ -22,7 +22,6 @@ import { useParams } from 'react-router-dom';
|
|
|
22
22
|
import { z } from 'zod';
|
|
23
23
|
import SHABenefitPackangesAndInterventions from '../../../benefits-package/forms/packages-and-interventions-form.component';
|
|
24
24
|
import { BillingConfig } from '../../../config-schema';
|
|
25
|
-
import { formatDate } from '../../../helpers/functions';
|
|
26
25
|
import { useSystemSetting } from '../../../hooks/getMflCode';
|
|
27
26
|
import usePatientDiagnosis from '../../../hooks/usePatientDiagnosis';
|
|
28
27
|
import useProvider from '../../../hooks/useProvider';
|
|
@@ -155,8 +154,8 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
|
|
|
155
154
|
diagnoses: diagnoses?.map((d) => d.id) ?? [],
|
|
156
155
|
visitType: recentVisit?.visitType?.display || '',
|
|
157
156
|
facility: `${recentVisit?.location?.display || ''} - ${mflCodeValue || ''}`,
|
|
158
|
-
treatmentStart:
|
|
159
|
-
treatmentEnd:
|
|
157
|
+
treatmentStart: formatDatetime(new Date(recentVisit?.startDatetime || '')),
|
|
158
|
+
treatmentEnd: formatDatetime(new Date(recentVisit?.stopDatetime || '')),
|
|
160
159
|
packages: packagesAndinterventions?.packages ?? [],
|
|
161
160
|
interventions: packagesAndinterventions?.interventions ?? [],
|
|
162
161
|
provider: providerUuid,
|
package/src/config-schema.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface BillingConfig {
|
|
|
34
34
|
enable: boolean;
|
|
35
35
|
duration: number;
|
|
36
36
|
};
|
|
37
|
+
localeCurrencyMapping: Record<string, string>;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export const configSchema: ConfigSchema = {
|
|
@@ -207,4 +208,16 @@ export const configSchema: ConfigSchema = {
|
|
|
207
208
|
duration: 24,
|
|
208
209
|
},
|
|
209
210
|
},
|
|
211
|
+
localeCurrencyMapping: {
|
|
212
|
+
_type: Type.Object,
|
|
213
|
+
_description: 'Mapping of locale codes to currency codes for internationalization',
|
|
214
|
+
_default: {
|
|
215
|
+
en: 'KES',
|
|
216
|
+
sw: 'KES',
|
|
217
|
+
am: 'ETB',
|
|
218
|
+
'en-KE': 'KES',
|
|
219
|
+
'sw-KE': 'KES',
|
|
220
|
+
'am-ET': 'ETB',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
210
223
|
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Currency Localization
|
|
2
|
+
|
|
3
|
+
This module provides locale-based currency formatting for the billing application. It automatically detects the user's locale and formats currency amounts accordingly.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Locale-based currency mapping**: Automatically maps locales to appropriate currencies
|
|
8
|
+
- **Configurable currency mapping**: Supports custom currency mappings via configuration
|
|
9
|
+
- **Consistent formatting**: Provides standardized currency formatting across the application
|
|
10
|
+
- **Fallback support**: Gracefully handles unknown locales with sensible defaults
|
|
11
|
+
|
|
12
|
+
## Supported Locales and Currencies
|
|
13
|
+
|
|
14
|
+
| Locale | Currency | Description |
|
|
15
|
+
|--------|----------|-------------|
|
|
16
|
+
| `en` | KES | English - Kenyan Shilling |
|
|
17
|
+
| `sw` | KES | Swahili - Kenyan Shilling |
|
|
18
|
+
| `am` | ETB | Amharic - Ethiopian Birr |
|
|
19
|
+
| `en-KE` | KES | English Kenya |
|
|
20
|
+
| `sw-KE` | KES | Swahili Kenya |
|
|
21
|
+
| `am-ET` | ETB | Amharic Ethiopia |
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Basic Currency Formatting
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { formatCurrency, formatCurrencySimple } from './helpers/currency';
|
|
29
|
+
|
|
30
|
+
// Format with negative sign handling
|
|
31
|
+
const amount = formatCurrency(1234.56); // Returns: "KSh 1,234.56"
|
|
32
|
+
|
|
33
|
+
// Format without negative sign handling
|
|
34
|
+
const simpleAmount = formatCurrencySimple(1234.56); // Returns: "KSh 1,234.56"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Using the Hook (with config support)
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { useCurrencyFormatting } from './helpers/currency';
|
|
41
|
+
|
|
42
|
+
const MyComponent = () => {
|
|
43
|
+
const { format, formatSimple, getCurrency, getLocale } = useCurrencyFormatting();
|
|
44
|
+
|
|
45
|
+
const amount = format(1234.56);
|
|
46
|
+
const currency = getCurrency(); // Returns current currency code
|
|
47
|
+
|
|
48
|
+
return <div>Amount: {amount}</div>;
|
|
49
|
+
};
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Direct Currency/Locale Access
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { getCurrencyForLocale, getCurrentLocale } from './helpers/currency';
|
|
56
|
+
|
|
57
|
+
const currency = getCurrencyForLocale(); // Returns: "KES", "ETB", etc.
|
|
58
|
+
const locale = getCurrentLocale(); // Returns: "en", "sw", "am", etc.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
You can customize the locale-to-currency mapping by adding a `localeCurrencyMapping` configuration to your billing config:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"localeCurrencyMapping": {
|
|
68
|
+
"en": "USD",
|
|
69
|
+
"sw": "KES",
|
|
70
|
+
"am": "ETB",
|
|
71
|
+
"fr": "EUR"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Functions
|
|
77
|
+
|
|
78
|
+
### `formatCurrency(amount, options?)`
|
|
79
|
+
Formats a number as currency with negative sign handling.
|
|
80
|
+
|
|
81
|
+
**Parameters:**
|
|
82
|
+
- `amount` (number): The amount to format
|
|
83
|
+
- `options` (Intl.NumberFormatOptions, optional): Additional formatting options
|
|
84
|
+
|
|
85
|
+
**Returns:** Formatted currency string
|
|
86
|
+
|
|
87
|
+
### `formatCurrencySimple(amount, options?)`
|
|
88
|
+
Formats a number as currency without negative sign handling.
|
|
89
|
+
|
|
90
|
+
**Parameters:**
|
|
91
|
+
- `amount` (number): The amount to format
|
|
92
|
+
- `options` (Intl.NumberFormatOptions, optional): Additional formatting options
|
|
93
|
+
|
|
94
|
+
**Returns:** Formatted currency string
|
|
95
|
+
|
|
96
|
+
### `getCurrencyForLocale()`
|
|
97
|
+
Gets the currency code for the current locale.
|
|
98
|
+
|
|
99
|
+
**Returns:** Currency code string (e.g., "KES", "ETB")
|
|
100
|
+
|
|
101
|
+
### `getCurrentLocale()`
|
|
102
|
+
Gets the current locale from localStorage.
|
|
103
|
+
|
|
104
|
+
**Returns:** Locale string (e.g., "en", "sw", "am")
|
|
105
|
+
|
|
106
|
+
### `useCurrencyFormatting()`
|
|
107
|
+
React hook that provides currency formatting with config support.
|
|
108
|
+
|
|
109
|
+
**Returns:** Object with formatting functions and utilities
|
|
110
|
+
|
|
111
|
+
## Migration from Hardcoded Currency
|
|
112
|
+
|
|
113
|
+
If you have existing code using hardcoded currency formatting, replace it with the new utilities:
|
|
114
|
+
|
|
115
|
+
**Before:**
|
|
116
|
+
```typescript
|
|
117
|
+
const formatter = new Intl.NumberFormat('en-US', {
|
|
118
|
+
style: 'currency',
|
|
119
|
+
currency: 'KES',
|
|
120
|
+
}).format(amount);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**After:**
|
|
124
|
+
```typescript
|
|
125
|
+
import { formatCurrency } from './helpers/currency';
|
|
126
|
+
|
|
127
|
+
const formattedAmount = formatCurrency(amount);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Testing
|
|
131
|
+
|
|
132
|
+
Run the currency utility tests:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npm test -- currency.test.ts
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Examples
|
|
139
|
+
|
|
140
|
+
### In Components
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import React from 'react';
|
|
144
|
+
import { formatCurrency } from './helpers/currency';
|
|
145
|
+
|
|
146
|
+
const BillItem = ({ price, quantity }) => {
|
|
147
|
+
const total = price * quantity;
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div>
|
|
151
|
+
<span>Price: {formatCurrency(price)}</span>
|
|
152
|
+
<span>Total: {formatCurrency(total)}</span>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### In Utilities
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { formatCurrencySimple } from './helpers/currency';
|
|
162
|
+
|
|
163
|
+
export const createReceipt = (items) => {
|
|
164
|
+
const total = items.reduce((sum, item) => sum + item.price, 0);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
items: items.map(item => ({
|
|
168
|
+
...item,
|
|
169
|
+
formattedPrice: formatCurrencySimple(item.price)
|
|
170
|
+
})),
|
|
171
|
+
total: formatCurrency(total)
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
```
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_LOCALE_CURRENCY_MAP,
|
|
3
|
+
getCurrencyForLocale,
|
|
4
|
+
getCurrentLocale,
|
|
5
|
+
formatCurrency,
|
|
6
|
+
formatCurrencySimple,
|
|
7
|
+
} from './currency';
|
|
8
|
+
|
|
9
|
+
// Mock localStorage
|
|
10
|
+
const mockLocalStorage = {
|
|
11
|
+
getItem: jest.fn(),
|
|
12
|
+
setItem: jest.fn(),
|
|
13
|
+
removeItem: jest.fn(),
|
|
14
|
+
clear: jest.fn(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
Object.defineProperty(window, 'localStorage', {
|
|
18
|
+
value: mockLocalStorage,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('Currency Utilities', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('DEFAULT_LOCALE_CURRENCY_MAP', () => {
|
|
27
|
+
it('should have correct currency mappings', () => {
|
|
28
|
+
expect(DEFAULT_LOCALE_CURRENCY_MAP.en).toBe('KES');
|
|
29
|
+
expect(DEFAULT_LOCALE_CURRENCY_MAP.sw).toBe('KES');
|
|
30
|
+
expect(DEFAULT_LOCALE_CURRENCY_MAP.am).toBe('ETB');
|
|
31
|
+
expect(DEFAULT_LOCALE_CURRENCY_MAP['en-KE']).toBe('KES');
|
|
32
|
+
expect(DEFAULT_LOCALE_CURRENCY_MAP['sw-KE']).toBe('KES');
|
|
33
|
+
expect(DEFAULT_LOCALE_CURRENCY_MAP['am-ET']).toBe('ETB');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getCurrentLocale', () => {
|
|
38
|
+
it('should return locale from localStorage', () => {
|
|
39
|
+
mockLocalStorage.getItem.mockReturnValue('sw');
|
|
40
|
+
expect(getCurrentLocale()).toBe('sw');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return default locale when localStorage is empty', () => {
|
|
44
|
+
mockLocalStorage.getItem.mockReturnValue(null);
|
|
45
|
+
expect(getCurrentLocale()).toBe('en');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('getCurrencyForLocale', () => {
|
|
50
|
+
it('should return correct currency for English locale', () => {
|
|
51
|
+
mockLocalStorage.getItem.mockReturnValue('en');
|
|
52
|
+
expect(getCurrencyForLocale()).toBe('KES');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return correct currency for Swahili locale', () => {
|
|
56
|
+
mockLocalStorage.getItem.mockReturnValue('sw');
|
|
57
|
+
expect(getCurrencyForLocale()).toBe('KES');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return correct currency for Amharic locale', () => {
|
|
61
|
+
mockLocalStorage.getItem.mockReturnValue('am');
|
|
62
|
+
expect(getCurrencyForLocale()).toBe('ETB');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should return KES as fallback for unknown locale', () => {
|
|
66
|
+
mockLocalStorage.getItem.mockReturnValue('fr');
|
|
67
|
+
expect(getCurrencyForLocale()).toBe('KES');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('formatCurrency', () => {
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
// Mock Intl.NumberFormat
|
|
74
|
+
const mockFormatter = {
|
|
75
|
+
format: jest.fn((value) => `$${value.toFixed(2)}`),
|
|
76
|
+
};
|
|
77
|
+
jest.spyOn(Intl, 'NumberFormat').mockImplementation(() => mockFormatter as any);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should format positive amounts correctly', () => {
|
|
81
|
+
mockLocalStorage.getItem.mockReturnValue('en');
|
|
82
|
+
const result = formatCurrency(1234.56);
|
|
83
|
+
expect(result).toBe('$1234.56');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should format negative amounts with minus sign', () => {
|
|
87
|
+
mockLocalStorage.getItem.mockReturnValue('en');
|
|
88
|
+
const result = formatCurrency(-1234.56);
|
|
89
|
+
expect(result).toBe('- $1234.56');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('formatCurrencySimple', () => {
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
// Mock Intl.NumberFormat
|
|
96
|
+
const mockFormatter = {
|
|
97
|
+
format: jest.fn((value) => `$${value.toFixed(2)}`),
|
|
98
|
+
};
|
|
99
|
+
jest.spyOn(Intl, 'NumberFormat').mockImplementation(() => mockFormatter as any);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should format amounts without negative sign handling', () => {
|
|
103
|
+
mockLocalStorage.getItem.mockReturnValue('en');
|
|
104
|
+
const result = formatCurrencySimple(1234.56);
|
|
105
|
+
expect(result).toBe('$1234.56');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should format negative amounts without special handling', () => {
|
|
109
|
+
mockLocalStorage.getItem.mockReturnValue('en');
|
|
110
|
+
const result = formatCurrencySimple(-1234.56);
|
|
111
|
+
expect(result).toBe('$-1234.56');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useConfig } from '@openmrs/esm-framework';
|
|
2
|
+
import { BillingConfig } from '../config-schema';
|
|
3
|
+
|
|
4
|
+
// Default locale to currency mapping (fallback)
|
|
5
|
+
export const DEFAULT_LOCALE_CURRENCY_MAP: Record<string, string> = {
|
|
6
|
+
en: 'KES', // English - Kenyan Shilling
|
|
7
|
+
sw: 'KES', // Swahili - Kenyan Shilling
|
|
8
|
+
am: 'ETB', // Amharic - Ethiopian Birr
|
|
9
|
+
'en-KE': 'KES', // English Kenya
|
|
10
|
+
'sw-KE': 'KES', // Swahili Kenya
|
|
11
|
+
'am-ET': 'ETB', // Amharic Ethiopia
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Gets the currency code for the current locale
|
|
16
|
+
* @returns The currency code (e.g., 'KES', 'ETB')
|
|
17
|
+
*/
|
|
18
|
+
export const getCurrencyForLocale = (): string => {
|
|
19
|
+
const currentLocale = localStorage.getItem('i18nextLng') ?? 'en';
|
|
20
|
+
return DEFAULT_LOCALE_CURRENCY_MAP[currentLocale] || 'KES';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Gets the currency code for the current locale with config support
|
|
25
|
+
* @param config - The billing configuration object
|
|
26
|
+
* @returns The currency code (e.g., 'KES', 'ETB')
|
|
27
|
+
*/
|
|
28
|
+
export const getCurrencyForLocaleWithConfig = (config?: BillingConfig): string => {
|
|
29
|
+
const currentLocale = localStorage.getItem('i18nextLng') ?? 'en';
|
|
30
|
+
|
|
31
|
+
// Use config mapping if available, otherwise fall back to default
|
|
32
|
+
const currencyMap = config?.localeCurrencyMapping || DEFAULT_LOCALE_CURRENCY_MAP;
|
|
33
|
+
return currencyMap[currentLocale] || 'KES';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Gets the current locale from localStorage
|
|
38
|
+
* @returns The current locale (e.g., 'en', 'sw', 'am')
|
|
39
|
+
*/
|
|
40
|
+
export const getCurrentLocale = (): string => {
|
|
41
|
+
return localStorage.getItem('i18nextLng') ?? 'en';
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Formats a number as currency based on the current locale
|
|
46
|
+
* @param amount - The amount to format
|
|
47
|
+
* @param options - Additional Intl.NumberFormat options
|
|
48
|
+
* @returns Formatted currency string
|
|
49
|
+
*/
|
|
50
|
+
export const formatCurrency = (amount: number, options: Intl.NumberFormatOptions = {}): string => {
|
|
51
|
+
const currentLocale = getCurrentLocale();
|
|
52
|
+
const currency = getCurrencyForLocale();
|
|
53
|
+
|
|
54
|
+
const formatter = new Intl.NumberFormat(currentLocale, {
|
|
55
|
+
style: 'currency',
|
|
56
|
+
currency: currency,
|
|
57
|
+
minimumFractionDigits: 2,
|
|
58
|
+
...options,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
let formattedAmount = formatter.format(Math.abs(amount));
|
|
62
|
+
|
|
63
|
+
if (amount < 0) {
|
|
64
|
+
formattedAmount = `- ${formattedAmount}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return formattedAmount;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Formats a number as currency without negative sign handling
|
|
72
|
+
* @param amount - The amount to format
|
|
73
|
+
* @param options - Additional Intl.NumberFormat options
|
|
74
|
+
* @returns Formatted currency string
|
|
75
|
+
*/
|
|
76
|
+
export const formatCurrencySimple = (amount: number, options: Intl.NumberFormatOptions = {}): string => {
|
|
77
|
+
const currentLocale = getCurrentLocale();
|
|
78
|
+
const currency = getCurrencyForLocale();
|
|
79
|
+
|
|
80
|
+
const formatter = new Intl.NumberFormat(currentLocale, {
|
|
81
|
+
style: 'currency',
|
|
82
|
+
currency: currency,
|
|
83
|
+
...options,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return formatter.format(amount);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Hook to get currency formatting with config support
|
|
91
|
+
* @returns Object with currency formatting functions
|
|
92
|
+
*/
|
|
93
|
+
export const useCurrencyFormatting = () => {
|
|
94
|
+
const config = useConfig<BillingConfig>();
|
|
95
|
+
|
|
96
|
+
const getCurrency = () => getCurrencyForLocaleWithConfig(config);
|
|
97
|
+
const getLocale = () => getCurrentLocale();
|
|
98
|
+
|
|
99
|
+
const format = (amount: number, options: Intl.NumberFormatOptions = {}) => {
|
|
100
|
+
const currentLocale = getLocale();
|
|
101
|
+
const currency = getCurrency();
|
|
102
|
+
|
|
103
|
+
const formatter = new Intl.NumberFormat(currentLocale, {
|
|
104
|
+
style: 'currency',
|
|
105
|
+
currency: currency,
|
|
106
|
+
minimumFractionDigits: 2,
|
|
107
|
+
...options,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
let formattedAmount = formatter.format(Math.abs(amount));
|
|
111
|
+
|
|
112
|
+
if (amount < 0) {
|
|
113
|
+
formattedAmount = `- ${formattedAmount}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return formattedAmount;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const formatSimple = (amount: number, options: Intl.NumberFormatOptions = {}) => {
|
|
120
|
+
const currentLocale = getLocale();
|
|
121
|
+
const currency = getCurrency();
|
|
122
|
+
|
|
123
|
+
const formatter = new Intl.NumberFormat(currentLocale, {
|
|
124
|
+
style: 'currency',
|
|
125
|
+
currency: currency,
|
|
126
|
+
...options,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return formatter.format(amount);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
getCurrency,
|
|
134
|
+
getLocale,
|
|
135
|
+
format,
|
|
136
|
+
formatSimple,
|
|
137
|
+
};
|
|
138
|
+
};
|
package/src/helpers/functions.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import dayjs from 'dayjs';
|
|
2
2
|
import { Payment, LineItem } from '../types';
|
|
3
|
+
import { formatCurrency } from './currency';
|
|
3
4
|
|
|
4
5
|
// amount already paid
|
|
5
6
|
export function calculateTotalAmountTendered(payments: Array<Payment>) {
|
|
@@ -33,19 +34,7 @@ export function calculateTotalAmount(lineItems: Array<LineItem>) {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export const convertToCurrency = (amountToConvert: number) => {
|
|
36
|
-
|
|
37
|
-
style: 'currency',
|
|
38
|
-
currency: 'KES',
|
|
39
|
-
minimumFractionDigits: 2,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
let formattedAmount = formatter.format(Math.abs(amountToConvert));
|
|
43
|
-
|
|
44
|
-
if (amountToConvert < 0) {
|
|
45
|
-
formattedAmount = `- ${formattedAmount}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return formattedAmount;
|
|
37
|
+
return formatCurrency(amountToConvert);
|
|
49
38
|
};
|
|
50
39
|
|
|
51
40
|
export const getGender = (gender: string, t) => {
|
package/src/index.ts
CHANGED
|
@@ -76,6 +76,7 @@ import DepositTransactionWorkspace from './bill-deposit/components/forms/deposit
|
|
|
76
76
|
|
|
77
77
|
// Print Preview Components
|
|
78
78
|
import PrintPreviewModal from './print-preview/print-preview.modal';
|
|
79
|
+
import PaymentWorkspace from './invoice/payments/payment-form/payment.workspace';
|
|
79
80
|
|
|
80
81
|
// Translation
|
|
81
82
|
export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
|
|
@@ -218,6 +219,7 @@ export const visitAttributeTags = getSyncLifecycle(VisitAttributeTags, options);
|
|
|
218
219
|
export const initiatePaymentDialog = getSyncLifecycle(InitiatePaymentDialog, options);
|
|
219
220
|
export const paymentModeWorkspace = getSyncLifecycle(PaymentModeWorkspace, options);
|
|
220
221
|
export const deletePaymentModeModal = getSyncLifecycle(DeletePaymentModeModal, options);
|
|
222
|
+
export const paymentWorkspace = getSyncLifecycle(PaymentWorkspace, options);
|
|
221
223
|
|
|
222
224
|
// Payment Points Components
|
|
223
225
|
export const createPaymentPoint = getSyncLifecycle(CreatePaymentPoint, options);
|