ADD new frontend
This commit is contained in:
433
frontend/src/utils/currency.ts
Normal file
433
frontend/src/utils/currency.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* Currency utilities for formatting and calculations
|
||||
*/
|
||||
|
||||
// Currency configuration
|
||||
export const CURRENCY_CONFIG = {
|
||||
EUR: {
|
||||
code: 'EUR',
|
||||
symbol: '€',
|
||||
name: 'Euro',
|
||||
decimals: 2,
|
||||
locale: 'es-ES',
|
||||
},
|
||||
USD: {
|
||||
code: 'USD',
|
||||
symbol: '$',
|
||||
name: 'US Dollar',
|
||||
decimals: 2,
|
||||
locale: 'en-US',
|
||||
},
|
||||
GBP: {
|
||||
code: 'GBP',
|
||||
symbol: '£',
|
||||
name: 'British Pound',
|
||||
decimals: 2,
|
||||
locale: 'en-GB',
|
||||
},
|
||||
} as const;
|
||||
|
||||
type CurrencyCode = keyof typeof CURRENCY_CONFIG;
|
||||
|
||||
// Format currency amount
|
||||
export const formatCurrency = (
|
||||
amount: number,
|
||||
currencyCode: CurrencyCode = 'EUR',
|
||||
options: Intl.NumberFormatOptions = {}
|
||||
): string => {
|
||||
if (typeof amount !== 'number' || isNaN(amount)) {
|
||||
return formatCurrency(0, currencyCode, options);
|
||||
}
|
||||
|
||||
const config = CURRENCY_CONFIG[currencyCode];
|
||||
|
||||
const defaultOptions: Intl.NumberFormatOptions = {
|
||||
style: 'currency',
|
||||
currency: config.code,
|
||||
minimumFractionDigits: config.decimals,
|
||||
maximumFractionDigits: config.decimals,
|
||||
};
|
||||
|
||||
const formatOptions = { ...defaultOptions, ...options };
|
||||
|
||||
try {
|
||||
return new Intl.NumberFormat(config.locale, formatOptions).format(amount);
|
||||
} catch (error) {
|
||||
// Fallback to manual formatting if Intl.NumberFormat fails
|
||||
const formattedAmount = amount.toFixed(config.decimals);
|
||||
return `${formattedAmount} ${config.symbol}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Format currency without symbol (just the number)
|
||||
export const formatCurrencyAmount = (
|
||||
amount: number,
|
||||
currencyCode: CurrencyCode = 'EUR',
|
||||
decimals?: number
|
||||
): string => {
|
||||
if (typeof amount !== 'number' || isNaN(amount)) {
|
||||
return formatCurrencyAmount(0, currencyCode, decimals);
|
||||
}
|
||||
|
||||
const config = CURRENCY_CONFIG[currencyCode];
|
||||
const decimalPlaces = decimals ?? config.decimals;
|
||||
|
||||
try {
|
||||
return new Intl.NumberFormat(config.locale, {
|
||||
minimumFractionDigits: decimalPlaces,
|
||||
maximumFractionDigits: decimalPlaces,
|
||||
}).format(amount);
|
||||
} catch (error) {
|
||||
return amount.toFixed(decimalPlaces);
|
||||
}
|
||||
};
|
||||
|
||||
// Parse currency string to number
|
||||
export const parseCurrency = (
|
||||
currencyString: string,
|
||||
currencyCode: CurrencyCode = 'EUR'
|
||||
): number => {
|
||||
if (!currencyString || typeof currencyString !== 'string') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const config = CURRENCY_CONFIG[currencyCode];
|
||||
|
||||
// Remove currency symbols and spaces
|
||||
const cleanString = currencyString
|
||||
.replace(new RegExp(`[${config.symbol}\\s]`, 'g'), '')
|
||||
.replace(/,/g, '.') // Replace comma with dot for decimal separator
|
||||
.trim();
|
||||
|
||||
const parsed = parseFloat(cleanString);
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
};
|
||||
|
||||
// Add two currency amounts
|
||||
export const addCurrency = (amount1: number, amount2: number): number => {
|
||||
if (typeof amount1 !== 'number' || typeof amount2 !== 'number') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Use proper decimal arithmetic to avoid floating point errors
|
||||
return Math.round((amount1 + amount2) * 100) / 100;
|
||||
};
|
||||
|
||||
// Subtract two currency amounts
|
||||
export const subtractCurrency = (amount1: number, amount2: number): number => {
|
||||
if (typeof amount1 !== 'number' || typeof amount2 !== 'number') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round((amount1 - amount2) * 100) / 100;
|
||||
};
|
||||
|
||||
// Multiply currency amount
|
||||
export const multiplyCurrency = (amount: number, multiplier: number): number => {
|
||||
if (typeof amount !== 'number' || typeof multiplier !== 'number') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round(amount * multiplier * 100) / 100;
|
||||
};
|
||||
|
||||
// Divide currency amount
|
||||
export const divideCurrency = (amount: number, divisor: number): number => {
|
||||
if (typeof amount !== 'number' || typeof divisor !== 'number' || divisor === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round((amount / divisor) * 100) / 100;
|
||||
};
|
||||
|
||||
// Calculate percentage of amount
|
||||
export const calculatePercentage = (amount: number, percentage: number): number => {
|
||||
if (typeof amount !== 'number' || typeof percentage !== 'number') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return multiplyCurrency(amount, percentage / 100);
|
||||
};
|
||||
|
||||
// Add percentage to amount
|
||||
export const addPercentage = (amount: number, percentage: number): number => {
|
||||
const percentageAmount = calculatePercentage(amount, percentage);
|
||||
return addCurrency(amount, percentageAmount);
|
||||
};
|
||||
|
||||
// Subtract percentage from amount
|
||||
export const subtractPercentage = (amount: number, percentage: number): number => {
|
||||
const percentageAmount = calculatePercentage(amount, percentage);
|
||||
return subtractCurrency(amount, percentageAmount);
|
||||
};
|
||||
|
||||
// Calculate tax amount
|
||||
export const calculateTax = (amount: number, taxRate: number): number => {
|
||||
return calculatePercentage(amount, taxRate);
|
||||
};
|
||||
|
||||
// Calculate amount before tax (gross to net)
|
||||
export const calculateNetAmount = (grossAmount: number, taxRate: number): number => {
|
||||
if (typeof grossAmount !== 'number' || typeof taxRate !== 'number') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return divideCurrency(grossAmount, 1 + (taxRate / 100));
|
||||
};
|
||||
|
||||
// Calculate amount with tax (net to gross)
|
||||
export const calculateGrossAmount = (netAmount: number, taxRate: number): number => {
|
||||
if (typeof netAmount !== 'number' || typeof taxRate !== 'number') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return multiplyCurrency(netAmount, 1 + (taxRate / 100));
|
||||
};
|
||||
|
||||
// Calculate discount amount
|
||||
export const calculateDiscount = (amount: number, discountRate: number): number => {
|
||||
return calculatePercentage(amount, discountRate);
|
||||
};
|
||||
|
||||
// Apply discount to amount
|
||||
export const applyDiscount = (amount: number, discountRate: number): number => {
|
||||
const discountAmount = calculateDiscount(amount, discountRate);
|
||||
return subtractCurrency(amount, discountAmount);
|
||||
};
|
||||
|
||||
// Calculate profit margin
|
||||
export const calculateProfitMargin = (revenue: number, cost: number): number => {
|
||||
if (typeof revenue !== 'number' || typeof cost !== 'number' || revenue === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const profit = subtractCurrency(revenue, cost);
|
||||
return Math.round((profit / revenue) * 10000) / 100; // Return as percentage with 2 decimal places
|
||||
};
|
||||
|
||||
// Calculate markup
|
||||
export const calculateMarkup = (cost: number, markupPercentage: number): number => {
|
||||
return addPercentage(cost, markupPercentage);
|
||||
};
|
||||
|
||||
// Calculate selling price from cost and desired margin
|
||||
export const calculateSellingPrice = (cost: number, marginPercentage: number): number => {
|
||||
if (typeof cost !== 'number' || typeof marginPercentage !== 'number' || marginPercentage >= 100) {
|
||||
return cost;
|
||||
}
|
||||
|
||||
return divideCurrency(cost, 1 - (marginPercentage / 100));
|
||||
};
|
||||
|
||||
// Calculate cost from selling price and margin
|
||||
export const calculateCostFromMargin = (sellingPrice: number, marginPercentage: number): number => {
|
||||
if (typeof sellingPrice !== 'number' || typeof marginPercentage !== 'number') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return multiplyCurrency(sellingPrice, 1 - (marginPercentage / 100));
|
||||
};
|
||||
|
||||
// Format currency range (e.g., "€10 - €20")
|
||||
export const formatCurrencyRange = (
|
||||
minAmount: number,
|
||||
maxAmount: number,
|
||||
currencyCode: CurrencyCode = 'EUR'
|
||||
): string => {
|
||||
if (typeof minAmount !== 'number' || typeof maxAmount !== 'number') {
|
||||
return formatCurrency(0, currencyCode);
|
||||
}
|
||||
|
||||
if (minAmount === maxAmount) {
|
||||
return formatCurrency(minAmount, currencyCode);
|
||||
}
|
||||
|
||||
const formattedMin = formatCurrency(minAmount, currencyCode);
|
||||
const formattedMax = formatCurrency(maxAmount, currencyCode);
|
||||
|
||||
return `${formattedMin} - ${formattedMax}`;
|
||||
};
|
||||
|
||||
// Round currency to nearest cent
|
||||
export const roundCurrency = (amount: number): number => {
|
||||
if (typeof amount !== 'number') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round(amount * 100) / 100;
|
||||
};
|
||||
|
||||
// Check if amount is valid currency value
|
||||
export const isValidCurrencyAmount = (amount: any): boolean => {
|
||||
return typeof amount === 'number' && !isNaN(amount) && isFinite(amount) && amount >= 0;
|
||||
};
|
||||
|
||||
// Convert between currencies (simplified - in real app, use exchange rates API)
|
||||
export const convertCurrency = (
|
||||
amount: number,
|
||||
fromCurrency: CurrencyCode,
|
||||
toCurrency: CurrencyCode,
|
||||
exchangeRate: number = 1
|
||||
): number => {
|
||||
if (!isValidCurrencyAmount(amount) || typeof exchangeRate !== 'number') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (fromCurrency === toCurrency) {
|
||||
return amount;
|
||||
}
|
||||
|
||||
return multiplyCurrency(amount, exchangeRate);
|
||||
};
|
||||
|
||||
// Format currency for input fields (without currency symbol)
|
||||
export const formatCurrencyInput = (amount: number | string): string => {
|
||||
if (typeof amount === 'string') {
|
||||
const parsed = parseCurrency(amount);
|
||||
return parsed === 0 && amount !== '0' ? amount : formatCurrencyAmount(parsed);
|
||||
}
|
||||
|
||||
if (typeof amount === 'number' && !isNaN(amount)) {
|
||||
return formatCurrencyAmount(amount);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
// Calculate total from array of amounts
|
||||
export const calculateTotal = (amounts: number[]): number => {
|
||||
if (!Array.isArray(amounts)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return amounts.reduce((total, amount) => {
|
||||
if (isValidCurrencyAmount(amount)) {
|
||||
return addCurrency(total, amount);
|
||||
}
|
||||
return total;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// Calculate average from array of amounts
|
||||
export const calculateAverage = (amounts: number[]): number => {
|
||||
if (!Array.isArray(amounts) || amounts.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const validAmounts = amounts.filter(isValidCurrencyAmount);
|
||||
if (validAmounts.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const total = calculateTotal(validAmounts);
|
||||
return divideCurrency(total, validAmounts.length);
|
||||
};
|
||||
|
||||
// Find minimum amount in array
|
||||
export const findMinimumAmount = (amounts: number[]): number => {
|
||||
if (!Array.isArray(amounts) || amounts.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const validAmounts = amounts.filter(isValidCurrencyAmount);
|
||||
return validAmounts.length > 0 ? Math.min(...validAmounts) : 0;
|
||||
};
|
||||
|
||||
// Find maximum amount in array
|
||||
export const findMaximumAmount = (amounts: number[]): number => {
|
||||
if (!Array.isArray(amounts) || amounts.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const validAmounts = amounts.filter(isValidCurrencyAmount);
|
||||
return validAmounts.length > 0 ? Math.max(...validAmounts) : 0;
|
||||
};
|
||||
|
||||
// Format compact currency (e.g., €1.2K, €1.5M)
|
||||
export const formatCompactCurrency = (
|
||||
amount: number,
|
||||
currencyCode: CurrencyCode = 'EUR'
|
||||
): string => {
|
||||
if (!isValidCurrencyAmount(amount)) {
|
||||
return formatCurrency(0, currencyCode);
|
||||
}
|
||||
|
||||
const config = CURRENCY_CONFIG[currencyCode];
|
||||
|
||||
try {
|
||||
return new Intl.NumberFormat(config.locale, {
|
||||
style: 'currency',
|
||||
currency: config.code,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
maximumFractionDigits: 1,
|
||||
}).format(amount);
|
||||
} catch (error) {
|
||||
// Fallback for browsers that don't support compact notation
|
||||
if (amount >= 1000000) {
|
||||
return `${config.symbol}${(amount / 1000000).toFixed(1)}M`;
|
||||
} else if (amount >= 1000) {
|
||||
return `${config.symbol}${(amount / 1000).toFixed(1)}K`;
|
||||
} else {
|
||||
return formatCurrency(amount, currencyCode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate compound interest
|
||||
export const calculateCompoundInterest = (
|
||||
principal: number,
|
||||
rate: number,
|
||||
time: number,
|
||||
compound: number = 1
|
||||
): number => {
|
||||
if (!isValidCurrencyAmount(principal) || typeof rate !== 'number' || typeof time !== 'number') {
|
||||
return principal;
|
||||
}
|
||||
|
||||
const amount = principal * Math.pow(1 + (rate / 100) / compound, compound * time);
|
||||
return roundCurrency(amount);
|
||||
};
|
||||
|
||||
// Currency formatting options for different contexts
|
||||
export const getCurrencyFormatOptions = (context: 'display' | 'input' | 'compact' | 'accounting') => {
|
||||
const baseOptions = {
|
||||
style: 'currency' as const,
|
||||
currency: 'EUR' as const,
|
||||
};
|
||||
|
||||
switch (context) {
|
||||
case 'display':
|
||||
return {
|
||||
...baseOptions,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
case 'input':
|
||||
return {
|
||||
...baseOptions,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
case 'compact':
|
||||
return {
|
||||
...baseOptions,
|
||||
notation: 'compact' as const,
|
||||
compactDisplay: 'short' as const,
|
||||
maximumFractionDigits: 1,
|
||||
};
|
||||
|
||||
case 'accounting':
|
||||
return {
|
||||
...baseOptions,
|
||||
currencySign: 'accounting' as const,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
default:
|
||||
return baseOptions;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user