@birhaus/financial
Version:
Financial components and utilities for BIRHAUS design system - SEPRELAD compliant
1,149 lines (1,148 loc) • 140 kB
JavaScript
import React4, { useState, useCallback, useEffect, useMemo } from 'react';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { useBirhaus } from '@birhaus/provider';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { BirhausSpinner } from '@birhaus/loading';
import { AlertTriangle, Wallet, EyeOff, Eye, AlertCircle, Search, Filter, ChevronDown, Download, Clock, Info, PieChart, Plus, Edit, Trash2, TrendingUp, TrendingDown, Minus, Smartphone, Building2, CreditCard, DollarSign, PiggyBank, ArrowDownLeft, ArrowUpRight, Calculator, ShoppingCart, BookOpen, Heart, Plane, Home, Car, Utensils, CheckCircle, Target, Calendar, BarChart3 } from 'lucide-react';
// src/utils/currency.ts
var SUPPORTED_CURRENCIES = {
PYG: {
code: "PYG",
symbol: "\u20B2",
nameEs: "Guaran\xED paraguayo",
nameEn: "Paraguayan guaran\xED",
decimals: 0,
// Guaraní doesn't use decimal places
thousandSeparator: ".",
decimalSeparator: ","
},
USD: {
code: "USD",
symbol: "$",
nameEs: "D\xF3lar estadounidense",
nameEn: "US dollar",
decimals: 2,
thousandSeparator: ",",
decimalSeparator: "."
},
EUR: {
code: "EUR",
symbol: "\u20AC",
nameEs: "Euro",
nameEn: "Euro",
decimals: 2,
thousandSeparator: ".",
decimalSeparator: ","
},
BRL: {
code: "BRL",
symbol: "R$",
nameEs: "Real brasile\xF1o",
nameEn: "Brazilian real",
decimals: 2,
thousandSeparator: ".",
decimalSeparator: ","
},
ARS: {
code: "ARS",
symbol: "$",
nameEs: "Peso argentino",
nameEn: "Argentine peso",
decimals: 2,
thousandSeparator: ".",
decimalSeparator: ","
}
};
var EXCHANGE_RATES = {
USD: 1,
PYG: 7300,
// 1 USD = 7300 PYG (approximate)
EUR: 0.85,
// 1 USD = 0.85 EUR (approximate)
BRL: 5.2,
// 1 USD = 5.2 BRL (approximate)
ARS: 800
// 1 USD = 800 ARS (approximate)
};
var SEPRELAD_THRESHOLDS = {
CASH_OPERATION: 1e4,
// USD 10,000 for cash operations
SUSPICIOUS_OPERATION: 5e3,
// USD 5,000 for suspicious operations
DAILY_CUMULATIVE: 1e4
// USD 10,000 cumulative daily
};
function formatCurrency(amount, options = {}) {
const {
currency = "PYG",
locale = "es-PY",
showSymbol = true,
showCode = false,
minimumFractionDigits,
maximumFractionDigits
} = options;
const currencyInfo = SUPPORTED_CURRENCIES[currency];
if (!currencyInfo) {
throw new Error(`Unsupported currency: ${currency}`);
}
const fracDigits = currency === "PYG" ? 0 : minimumFractionDigits ?? currencyInfo.decimals;
const maxFracDigits = currency === "PYG" ? 0 : maximumFractionDigits ?? currencyInfo.decimals;
try {
const formatter = new Intl.NumberFormat(locale, {
style: "currency",
currency,
minimumFractionDigits: fracDigits,
maximumFractionDigits: maxFracDigits
});
let formatted = formatter.format(amount);
if (currency === "PYG" && locale.startsWith("es")) {
const numberPart = Math.abs(amount).toLocaleString("es-PY");
const sign = amount < 0 ? "-" : "";
formatted = `${sign}${currencyInfo.symbol} ${numberPart}`;
}
if (showCode && !showSymbol) {
const numberPart = Math.abs(amount).toLocaleString(locale, {
minimumFractionDigits: fracDigits,
maximumFractionDigits: maxFracDigits
});
const sign = amount < 0 ? "-" : "";
formatted = `${sign}${numberPart} ${currency}`;
} else if (showCode && showSymbol) {
formatted = `${formatted} ${currency}`;
}
return formatted;
} catch (error) {
console.warn("Currency formatting failed, using fallback:", error);
const numberPart = Math.abs(amount).toFixed(fracDigits);
const sign = amount < 0 ? "-" : "";
if (showSymbol) {
return `${sign}${currencyInfo.symbol} ${numberPart}`;
} else if (showCode) {
return `${sign}${numberPart} ${currency}`;
} else {
return `${sign}${numberPart}`;
}
}
}
function parseCurrency(value, currency = "PYG") {
if (!value || typeof value !== "string") {
return null;
}
const currencyInfo = SUPPORTED_CURRENCIES[currency];
if (!currencyInfo) {
return null;
}
let cleanValue = value.replace(new RegExp(`\\${currencyInfo.symbol}`, "g"), "").replace(new RegExp(currency, "g"), "").trim();
const isNegative = cleanValue.startsWith("-");
if (isNegative) {
cleanValue = cleanValue.substring(1);
}
if (currency === "PYG") {
cleanValue = cleanValue.replace(/\./g, "");
} else {
if (cleanValue.includes(",") && cleanValue.includes(".")) {
const lastComma = cleanValue.lastIndexOf(",");
const lastDot = cleanValue.lastIndexOf(".");
if (lastDot > lastComma) {
cleanValue = cleanValue.replace(/,/g, "").replace(".", ".");
} else {
cleanValue = cleanValue.replace(/\./g, "").replace(",", ".");
}
} else if (cleanValue.includes(",")) {
const commaCount = (cleanValue.match(/,/g) || []).length;
if (commaCount === 1 && cleanValue.split(",")[1].length <= 2) {
cleanValue = cleanValue.replace(",", ".");
} else {
cleanValue = cleanValue.replace(/,/g, "");
}
}
}
const parsed = parseFloat(cleanValue);
if (isNaN(parsed)) {
return null;
}
return isNegative ? -parsed : parsed;
}
function convertCurrency(amount, fromCurrency, toCurrency) {
if (fromCurrency === toCurrency) {
return amount;
}
const usdAmount = amount / EXCHANGE_RATES[fromCurrency];
const convertedAmount = usdAmount * EXCHANGE_RATES[toCurrency];
const targetDecimals = SUPPORTED_CURRENCIES[toCurrency].decimals;
return Math.round(convertedAmount * Math.pow(10, targetDecimals)) / Math.pow(10, targetDecimals);
}
function checkSEPRELADThreshold(amount, currency, operationType = "cash") {
const amountUSD = convertCurrency(amount, currency, "USD");
let thresholdUSD;
switch (operationType) {
case "cash":
thresholdUSD = SEPRELAD_THRESHOLDS.CASH_OPERATION;
break;
case "suspicious":
thresholdUSD = SEPRELAD_THRESHOLDS.SUSPICIOUS_OPERATION;
break;
case "cumulative":
thresholdUSD = SEPRELAD_THRESHOLDS.DAILY_CUMULATIVE;
break;
default:
thresholdUSD = SEPRELAD_THRESHOLDS.CASH_OPERATION;
}
return {
requiresReport: amountUSD >= thresholdUSD,
thresholdUSD,
amountUSD
};
}
function getCurrencyInfo(currency) {
return SUPPORTED_CURRENCIES[currency];
}
function validateCurrencyAmount(amount, currency) {
const errors = [];
if (!Number.isFinite(amount)) {
errors.push("Invalid number format");
return { valid: false, errors };
}
if (Math.abs(amount) > 1e12) {
errors.push("Amount exceeds maximum allowed value");
}
const currencyInfo = SUPPORTED_CURRENCIES[currency];
if (currencyInfo) {
const decimalPlaces = (amount.toString().split(".")[1] || "").length;
if (decimalPlaces > currencyInfo.decimals) {
errors.push(`Too many decimal places for ${currency} (max: ${currencyInfo.decimals})`);
}
}
const seprelad = checkSEPRELADThreshold(amount, currency);
if (seprelad.requiresReport) {
console.info(`Amount exceeds SEPRELAD reporting threshold: ${seprelad.amountUSD} USD`);
}
return {
valid: errors.length === 0,
errors
};
}
function formatCurrencyForInput(amount, currency = "PYG") {
const currencyInfo = SUPPORTED_CURRENCIES[currency];
if (!currencyInfo) {
return amount.toString();
}
if (currency === "PYG") {
return amount.toLocaleString("es-PY", {
useGrouping: true,
minimumFractionDigits: 0,
maximumFractionDigits: 0
});
} else {
return amount.toLocaleString("es-PY", {
useGrouping: true,
minimumFractionDigits: currencyInfo.decimals,
maximumFractionDigits: currencyInfo.decimals
});
}
}
var cn = (...inputs) => twMerge(clsx(inputs));
function BirhausCurrencyDisplay({
amount,
currency = "PYG",
locale,
showSymbol = true,
showCode = false,
className,
...props
}) {
const { language, reportViolation } = useBirhaus();
React4.useEffect(() => {
if (currency === "USD" && amount >= 1e4) {
reportViolation({
type: "performance",
severity: "warning",
element: document.createElement("span"),
// Placeholder element
message: `USD amount ${amount} exceeds SEPRELAD reporting threshold`,
messageEs: `Monto USD ${amount} supera umbral de reporte SEPRELAD`,
messageEn: `USD amount ${amount} exceeds SEPRELAD reporting threshold`,
recommendation: "Ensure proper SEPRELAD compliance reporting for amounts \u2265 $10,000 USD",
birhausPrinciple: 8
// Compliance principle (hypothetical extension)
});
}
if (Math.abs(amount) > 1e9) {
reportViolation({
type: "performance",
severity: "warning",
element: document.createElement("span"),
message: `Currency amount ${amount} exceeds reasonable display limits`,
messageEs: `Monto ${amount} supera l\xEDmites razonables de visualizaci\xF3n`,
messageEn: `Currency amount ${amount} exceeds reasonable display limits`,
recommendation: "Consider using abbreviated notation for very large amounts",
birhausPrinciple: 1
// Cognitive load reduction
});
}
}, [amount, currency, reportViolation]);
const displayLocale = locale || (language === "es" ? "es-PY" : "en-US");
const formattedAmount = formatCurrency(amount, {
currency,
locale: displayLocale,
showSymbol,
showCode
});
const amountColorClass = cn(
"font-mono",
// Monospace for better number alignment
{
"text-green-600": amount > 0,
"text-red-600": amount < 0,
"text-birhaus-muted-foreground": amount === 0
}
);
const ariaLabel = React4.useMemo(() => {
const absAmount = Math.abs(amount);
const sign = amount < 0 ? language === "es" ? "menos " : "negative " : "";
const currencyNames = {
PYG: { es: "guaran\xEDes", en: "Paraguayan guaran\xEDes" },
USD: { es: "d\xF3lares americanos", en: "US dollars" },
EUR: { es: "euros", en: "euros" },
BRL: { es: "reales brasile\xF1os", en: "Brazilian reais" },
ARS: { es: "pesos argentinos", en: "Argentine pesos" }
};
const currencyName = currencyNames[currency]?.[language] || currencyNames[currency]?.es;
return `${sign}${absAmount.toLocaleString(displayLocale)} ${currencyName}`;
}, [amount, currency, language, displayLocale]);
return /* @__PURE__ */ jsx(
"span",
{
className: cn(
"inline-flex items-center",
amountColorClass,
className
),
"aria-label": ariaLabel,
title: ariaLabel,
...props,
children: formattedAmount
}
);
}
var cn2 = (...inputs) => twMerge(clsx(inputs));
var defaultPaymentMethods = {
efectivo: {
tipo: "efectivo",
nombreEs: "Efectivo",
nombreEn: "Cash",
icono: /* @__PURE__ */ jsx(DollarSign, { size: 20 }),
requiereVerificacion: false,
limiteTransaccion: 5e7
// 50M PYG SEPRELAD limit
},
tarjeta_debito: {
tipo: "tarjeta_debito",
nombreEs: "Tarjeta de D\xE9bito",
nombreEn: "Debit Card",
icono: /* @__PURE__ */ jsx(CreditCard, { size: 20 }),
requiereVerificacion: true,
limiteTransaccion: 1e7,
// 10M PYG daily limit
comision: 0,
tiempoProcesamientoMinutos: 5
},
tarjeta_credito: {
tipo: "tarjeta_credito",
nombreEs: "Tarjeta de Cr\xE9dito",
nombreEn: "Credit Card",
icono: /* @__PURE__ */ jsx(CreditCard, { size: 20 }),
requiereVerificacion: true,
limiteTransaccion: 2e7,
// 20M PYG
comision: 2.5,
// 2.5% commission
tiempoProcesamientoMinutos: 10
},
transferencia: {
tipo: "transferencia",
nombreEs: "Transferencia Bancaria",
nombreEn: "Bank Transfer",
icono: /* @__PURE__ */ jsx(Building2, { size: 20 }),
requiereVerificacion: true,
limiteTransaccion: 1e8,
// 100M PYG
comision: 1e3,
// Fixed commission
tiempoProcesamientoMinutos: 60
},
billetera_digital: {
tipo: "billetera_digital",
nombreEs: "Billetera Digital",
nombreEn: "Digital Wallet",
icono: /* @__PURE__ */ jsx(Smartphone, { size: 20 }),
requiereVerificacion: true,
limiteTransaccion: 5e6,
// 5M PYG
comision: 0.5,
// 0.5% commission
tiempoProcesamientoMinutos: 2
}
};
function BirhausPaymentForm({
availablePaymentMethods,
onPaymentSubmit,
amount,
currency = "PYG",
recipientInfo,
className,
...props
}) {
const { language, reportViolation } = useBirhaus();
const [isSubmitting, setIsSubmitting] = useState(false);
const [selectedMethod, setSelectedMethod] = useState(null);
const [formData, setFormData] = useState({
monto: amount,
moneda: currency,
destinatario: recipientInfo ? {
nombre: recipientInfo.nombreEs,
documento: recipientInfo.documento,
cuenta: recipientInfo.cuenta
} : void 0
});
const [errors, setErrors] = useState({});
React4.useEffect(() => {
if (availablePaymentMethods.length > 7) {
reportViolation({
type: "cognitive",
severity: "warning",
element: document.createElement("form"),
message: `Payment form has ${availablePaymentMethods.length} payment methods (max recommended: 7)`,
messageEs: `Formulario de pago con ${availablePaymentMethods.length} m\xE9todos de pago (m\xE1ximo recomendado: 7)`,
messageEn: `Payment form has ${availablePaymentMethods.length} payment methods (max recommended: 7)`,
recommendation: "Group payment methods or use progressive disclosure to reduce cognitive load",
birhausPrinciple: 1
// Cognitive load reduction
});
}
}, [availablePaymentMethods.length, reportViolation]);
const getPaymentMethodInfo = useCallback((method) => {
return availablePaymentMethods.find((pm) => pm.tipo === method) || defaultPaymentMethods[method];
}, [availablePaymentMethods]);
const validateForm = useCallback(() => {
const newErrors = {};
if (!selectedMethod) {
newErrors.metodo = language === "es" ? "Selecciona un m\xE9todo de pago" : "Select a payment method";
}
if (!formData.destinatario?.nombre?.trim()) {
newErrors.destinatario = language === "es" ? "Ingresa el nombre del destinatario" : "Enter recipient name";
}
if (!formData.destinatario?.documento?.trim()) {
newErrors.documento = language === "es" ? "Ingresa el documento del destinatario" : "Enter recipient document";
}
if (!formData.concepto?.trim()) {
newErrors.concepto = language === "es" ? "Ingresa el concepto del pago" : "Enter payment concept";
}
if (selectedMethod) {
const methodInfo = getPaymentMethodInfo(selectedMethod);
if (methodInfo.limiteTransaccion && amount > methodInfo.limiteTransaccion) {
newErrors.monto = language === "es" ? `Monto excede l\xEDmite de ${formatCurrency(methodInfo.limiteTransaccion, { currency })}` : `Amount exceeds limit of ${formatCurrency(methodInfo.limiteTransaccion, { currency })}`;
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}, [selectedMethod, formData, amount, currency, language, getPaymentMethodInfo]);
const handleSubmit = useCallback(async (e) => {
e.preventDefault();
if (!validateForm() || !selectedMethod) return;
setIsSubmitting(true);
try {
const submitData = {
metodo: selectedMethod,
monto: amount,
moneda: currency,
destinatario: {
nombre: formData.destinatario.nombre,
documento: formData.destinatario.documento,
cuenta: formData.destinatario?.cuenta
},
concepto: formData.concepto,
programada: formData.programada,
fechaProgramada: formData.fechaProgramada
};
await onPaymentSubmit(submitData);
} catch (error) {
console.error("Payment submission error:", error);
} finally {
setIsSubmitting(false);
}
}, [validateForm, selectedMethod, amount, currency, formData, onPaymentSubmit]);
const updateFormData = useCallback((updates) => {
setFormData((prev) => ({ ...prev, ...updates }));
if (updates.destinatario) {
const newErrors = { ...errors };
delete newErrors.destinatario;
delete newErrors.documento;
setErrors(newErrors);
}
}, [errors]);
return /* @__PURE__ */ jsxs(
"form",
{
className: cn2("space-y-6", className),
onSubmit: handleSubmit,
...props,
children: [
/* @__PURE__ */ jsx("div", { className: "bg-birhaus-muted/30 p-4 rounded-lg", children: /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
/* @__PURE__ */ jsx("p", { className: "text-sm text-birhaus-muted-foreground", children: language === "es" ? "Monto a pagar" : "Amount to pay" }),
/* @__PURE__ */ jsx("p", { className: "text-2xl font-bold text-birhaus-foreground", children: formatCurrency(amount, { currency, showSymbol: true }) })
] }) }),
/* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-birhaus-foreground", children: language === "es" ? "M\xE9todo de pago" : "Payment method" }),
/* @__PURE__ */ jsx("div", { className: "grid gap-3", children: availablePaymentMethods.map((method) => {
const isSelected = selectedMethod === method.tipo;
const methodInfo = getPaymentMethodInfo(method.tipo);
const displayName = language === "es" ? method.nombreEs : method.nombreEn || method.nombreEs;
const exceedsLimit = methodInfo.limiteTransaccion && amount > methodInfo.limiteTransaccion;
return /* @__PURE__ */ jsx(
"button",
{
type: "button",
onClick: () => !exceedsLimit && setSelectedMethod(method.tipo),
disabled: !!exceedsLimit,
className: cn2(
"p-4 border rounded-lg text-left transition-all duration-200",
"focus:outline-none focus:ring-2 focus:ring-birhaus-primary",
isSelected && "border-birhaus-primary bg-birhaus-primary/5",
!isSelected && !exceedsLimit && "border-birhaus-border hover:border-birhaus-primary/50",
exceedsLimit && "border-red-200 bg-red-50 opacity-50 cursor-not-allowed"
),
children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
/* @__PURE__ */ jsx("div", { className: "mt-1 text-birhaus-muted-foreground", children: method.icono }),
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
/* @__PURE__ */ jsx("h4", { className: "font-medium text-birhaus-foreground", children: displayName }),
isSelected && /* @__PURE__ */ jsx("div", { className: "w-5 h-5 bg-birhaus-primary rounded-full flex items-center justify-center", children: /* @__PURE__ */ jsx("div", { className: "w-2 h-2 bg-white rounded-full" }) })
] }),
/* @__PURE__ */ jsxs("div", { className: "mt-1 text-xs text-birhaus-muted-foreground space-y-1", children: [
methodInfo.comision && /* @__PURE__ */ jsxs("p", { children: [
language === "es" ? "Comisi\xF3n: " : "Fee: ",
typeof methodInfo.comision === "number" && methodInfo.comision < 1 ? `${methodInfo.comision}%` : formatCurrency(methodInfo.comision, { currency })
] }),
methodInfo.tiempoProcesamientoMinutos && /* @__PURE__ */ jsxs("p", { children: [
language === "es" ? "Procesamiento: " : "Processing: ",
methodInfo.tiempoProcesamientoMinutos < 60 ? `${methodInfo.tiempoProcesamientoMinutos} min` : `${Math.round(methodInfo.tiempoProcesamientoMinutos / 60)}h`
] }),
exceedsLimit && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-red-600", children: [
/* @__PURE__ */ jsx(AlertTriangle, { size: 12 }),
/* @__PURE__ */ jsx("span", { children: language === "es" ? `L\xEDmite: ${formatCurrency(methodInfo.limiteTransaccion, { currency })}` : `Limit: ${formatCurrency(methodInfo.limiteTransaccion, { currency })}` })
] })
] })
] })
] })
},
method.tipo
);
}) }),
errors.metodo && /* @__PURE__ */ jsx("p", { className: "text-sm text-red-600", children: errors.metodo })
] }),
/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
/* @__PURE__ */ jsx("h3", { className: "font-medium text-birhaus-foreground", children: language === "es" ? "Informaci\xF3n del destinatario" : "Recipient information" }),
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-birhaus-foreground mb-1", children: language === "es" ? "Nombre completo" : "Full name" }),
/* @__PURE__ */ jsx(
"input",
{
type: "text",
value: formData.destinatario?.nombre || "",
onChange: (e) => updateFormData({
destinatario: { ...formData.destinatario, nombre: e.target.value }
}),
className: cn2(
"w-full px-3 py-2 border rounded-md",
"focus:outline-none focus:ring-2 focus:ring-birhaus-primary",
errors.destinatario ? "border-red-500" : "border-birhaus-border"
),
placeholder: language === "es" ? "Juan P\xE9rez" : "John Doe"
}
),
errors.destinatario && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-red-600", children: errors.destinatario })
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-birhaus-foreground mb-1", children: language === "es" ? "Documento" : "Document" }),
/* @__PURE__ */ jsx(
"input",
{
type: "text",
value: formData.destinatario?.documento || "",
onChange: (e) => updateFormData({
destinatario: { ...formData.destinatario, documento: e.target.value }
}),
className: cn2(
"w-full px-3 py-2 border rounded-md",
"focus:outline-none focus:ring-2 focus:ring-birhaus-primary",
errors.documento ? "border-red-500" : "border-birhaus-border"
),
placeholder: language === "es" ? "12345678" : "12345678"
}
),
errors.documento && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-red-600", children: errors.documento })
] })
] }),
selectedMethod === "transferencia" && /* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-birhaus-foreground mb-1", children: language === "es" ? "N\xFAmero de cuenta" : "Account number" }),
/* @__PURE__ */ jsx(
"input",
{
type: "text",
value: formData.destinatario?.cuenta || "",
onChange: (e) => updateFormData({
destinatario: { ...formData.destinatario, cuenta: e.target.value }
}),
className: "w-full px-3 py-2 border border-birhaus-border rounded-md focus:outline-none focus:ring-2 focus:ring-birhaus-primary",
placeholder: "123456789"
}
)
] })
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-birhaus-foreground mb-1", children: language === "es" ? "Concepto" : "Concept" }),
/* @__PURE__ */ jsx(
"input",
{
type: "text",
value: formData.concepto || "",
onChange: (e) => updateFormData({ concepto: e.target.value }),
className: cn2(
"w-full px-3 py-2 border rounded-md",
"focus:outline-none focus:ring-2 focus:ring-birhaus-primary",
errors.concepto ? "border-red-500" : "border-birhaus-border"
),
placeholder: language === "es" ? "Pago de servicios" : "Service payment"
}
),
errors.concepto && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-red-600", children: errors.concepto })
] }),
/* @__PURE__ */ jsx(
"button",
{
type: "submit",
disabled: isSubmitting || !selectedMethod,
className: cn2(
"w-full py-3 px-4 rounded-md font-medium transition-all duration-200",
"focus:outline-none focus:ring-2 focus:ring-birhaus-primary focus:ring-offset-2",
!isSubmitting && selectedMethod ? "bg-birhaus-primary text-white hover:bg-birhaus-primary/90" : "bg-birhaus-muted text-birhaus-muted-foreground cursor-not-allowed"
),
children: isSubmitting ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2", children: [
/* @__PURE__ */ jsx(BirhausSpinner, { size: "sm", color: "current" }),
/* @__PURE__ */ jsx("span", { children: language === "es" ? "Procesando..." : "Processing..." })
] }) : /* @__PURE__ */ jsxs("span", { children: [
language === "es" ? "Realizar pago" : "Make payment",
selectedMethod && (() => {
const methodInfo = getPaymentMethodInfo(selectedMethod);
const commission = methodInfo.comision;
if (commission) {
const commissionAmount = typeof commission === "number" && commission < 1 ? amount * (commission / 100) : commission;
return ` (+ ${formatCurrency(commissionAmount, { currency })})`;
}
return "";
})()
] })
}
),
errors.monto && /* @__PURE__ */ jsx("p", { className: "text-sm text-red-600 text-center", children: errors.monto })
]
}
);
}
var cn3 = (...inputs) => twMerge(clsx(inputs));
var accountTypeConfig = {
corriente: {
icon: /* @__PURE__ */ jsx(CreditCard, { size: 20 }),
colorClass: "text-blue-600 bg-blue-100",
nameEs: "Cuenta Corriente",
nameEn: "Checking Account"
},
ahorros: {
icon: /* @__PURE__ */ jsx(PiggyBank, { size: 20 }),
colorClass: "text-green-600 bg-green-100",
nameEs: "Cuenta de Ahorros",
nameEn: "Savings Account"
},
credito: {
icon: /* @__PURE__ */ jsx(CreditCard, { size: 20 }),
colorClass: "text-purple-600 bg-purple-100",
nameEs: "L\xEDnea de Cr\xE9dito",
nameEn: "Credit Line"
},
inversion: {
icon: /* @__PURE__ */ jsx(TrendingUp, { size: 20 }),
colorClass: "text-orange-600 bg-orange-100",
nameEs: "Cuenta de Inversi\xF3n",
nameEn: "Investment Account"
}
};
var accountStatusConfig = {
activa: {
colorClass: "text-green-700 bg-green-100",
nameEs: "Activa",
nameEn: "Active"
},
inactiva: {
colorClass: "text-gray-700 bg-gray-100",
nameEs: "Inactiva",
nameEn: "Inactive"
},
bloqueada: {
colorClass: "text-red-700 bg-red-100",
nameEs: "Bloqueada",
nameEn: "Blocked"
},
cerrada: {
colorClass: "text-gray-700 bg-gray-200",
nameEs: "Cerrada",
nameEn: "Closed"
}
};
function BirhausAccountSummary({
accounts,
showBalances = true,
showInactive = false,
onAccountClick,
className,
...props
}) {
const { language, reportViolation } = useBirhaus();
const [balancesVisible, setBalancesVisible] = React4.useState(showBalances);
const filteredAccounts = React4.useMemo(() => {
return accounts.filter(
(account) => showInactive || account.estado === "activa"
);
}, [accounts, showInactive]);
React4.useEffect(() => {
if (filteredAccounts.length > 7) {
reportViolation({
type: "cognitive",
severity: "warning",
element: document.createElement("div"),
message: `Account summary shows ${filteredAccounts.length} accounts (max recommended: 7)`,
messageEs: `Resumen de cuentas muestra ${filteredAccounts.length} cuentas (m\xE1ximo recomendado: 7)`,
messageEn: `Account summary shows ${filteredAccounts.length} accounts (max recommended: 7)`,
recommendation: "Consider pagination or grouping for large numbers of accounts",
birhausPrinciple: 1
// Cognitive load reduction
});
}
}, [filteredAccounts.length, reportViolation]);
const totalBalance = React4.useMemo(() => {
return filteredAccounts.filter((account) => account.estado === "activa").reduce((sum, account) => {
const balance = account.moneda === "PYG" ? account.saldo : account.saldo * 7300;
return sum + balance;
}, 0);
}, [filteredAccounts]);
const toggleBalanceVisibility = React4.useCallback(() => {
setBalancesVisible((prev) => !prev);
}, []);
const handleAccountClick = React4.useCallback((account) => {
onAccountClick?.(account);
}, [onAccountClick]);
if (filteredAccounts.length === 0) {
return /* @__PURE__ */ jsxs("div", { className: cn3("p-6 text-center text-birhaus-muted-foreground", className), children: [
/* @__PURE__ */ jsx(Wallet, { size: 48, className: "mx-auto mb-4 text-birhaus-muted-foreground/50" }),
/* @__PURE__ */ jsx("p", { children: language === "es" ? "No hay cuentas disponibles" : "No accounts available" })
] });
}
return /* @__PURE__ */ jsxs("div", { className: cn3("space-y-4", className), ...props, children: [
/* @__PURE__ */ jsxs("div", { className: "bg-gradient-to-r from-birhaus-primary to-birhaus-primary/80 p-6 rounded-lg text-white", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: language === "es" ? "Resumen de Cuentas" : "Account Summary" }),
/* @__PURE__ */ jsx(
"button",
{
onClick: toggleBalanceVisibility,
className: "p-2 hover:bg-white/20 rounded-full transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-white/50",
"aria-label": balancesVisible ? language === "es" ? "Ocultar saldos" : "Hide balances" : language === "es" ? "Mostrar saldos" : "Show balances",
children: balancesVisible ? /* @__PURE__ */ jsx(EyeOff, { size: 20 }) : /* @__PURE__ */ jsx(Eye, { size: 20 })
}
)
] }),
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
/* @__PURE__ */ jsx("p", { className: "text-white/80 text-sm", children: language === "es" ? "Balance total" : "Total balance" }),
/* @__PURE__ */ jsx("p", { className: "text-2xl font-bold", children: balancesVisible ? /* @__PURE__ */ jsx(
BirhausCurrencyDisplay,
{
amount: totalBalance,
currency: "PYG",
className: "text-white"
}
) : "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }),
/* @__PURE__ */ jsxs("p", { className: "text-white/70 text-xs", children: [
filteredAccounts.filter((a) => a.estado === "activa").length,
" ",
language === "es" ? "cuentas activas" : "active accounts"
] })
] })
] }),
/* @__PURE__ */ jsx("div", { className: "space-y-3", children: filteredAccounts.map((account) => {
const typeConfig = accountTypeConfig[account.tipo];
const statusConfig = accountStatusConfig[account.estado];
const displayName = language === "es" ? account.nombreEs : account.nombreEn || account.nombreEs;
const typeName = language === "es" ? typeConfig.nameEs : typeConfig.nameEn;
const statusName = language === "es" ? statusConfig.nameEs : statusConfig.nameEn;
const isClickable = Boolean(onAccountClick);
const isInactive = account.estado !== "activa";
return /* @__PURE__ */ jsx(
"div",
{
className: cn3(
"p-4 border border-birhaus-border rounded-lg transition-all duration-200",
isClickable && "cursor-pointer hover:border-birhaus-primary/50 hover:shadow-sm",
isInactive && "opacity-60",
"focus:outline-none focus:ring-2 focus:ring-birhaus-primary focus:border-birhaus-primary"
),
onClick: () => isClickable && handleAccountClick(account),
onKeyDown: (e) => {
if (isClickable && (e.key === "Enter" || e.key === " ")) {
e.preventDefault();
handleAccountClick(account);
}
},
tabIndex: isClickable ? 0 : -1,
role: isClickable ? "button" : void 0,
"aria-label": `${displayName}, ${typeName}, ${balancesVisible ? formatCurrency(account.saldo, { currency: account.moneda }) : "saldo oculto"}`,
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
/* @__PURE__ */ jsx("div", { className: cn3(
"p-2 rounded-full flex-shrink-0",
typeConfig.colorClass
), children: typeConfig.icon }),
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-1", children: [
/* @__PURE__ */ jsx("h3", { className: "font-medium text-birhaus-foreground truncate", children: displayName }),
isInactive && /* @__PURE__ */ jsx("span", { className: cn3(
"px-2 py-1 text-xs font-medium rounded-full",
statusConfig.colorClass
), children: statusName })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
/* @__PURE__ */ jsxs("p", { className: "text-sm text-birhaus-muted-foreground", children: [
typeName,
" \u2022 ",
account.numero
] }),
(account.limiteDiario || account.limiteMensual) && /* @__PURE__ */ jsxs("p", { className: "text-xs text-birhaus-muted-foreground", children: [
account.limiteDiario && /* @__PURE__ */ jsxs("span", { children: [
language === "es" ? "L\xEDmite diario: " : "Daily limit: ",
formatCurrency(account.limiteDiario, { currency: account.moneda })
] }),
account.limiteDiario && account.limiteMensual && " \u2022 ",
account.limiteMensual && /* @__PURE__ */ jsxs("span", { children: [
language === "es" ? "L\xEDmite mensual: " : "Monthly limit: ",
formatCurrency(account.limiteMensual, { currency: account.moneda })
] })
] })
] }),
/* @__PURE__ */ jsxs("div", { className: "text-right", children: [
/* @__PURE__ */ jsx("p", { className: cn3(
"font-semibold",
account.saldo >= 0 ? "text-green-600" : "text-red-600"
), children: balancesVisible ? /* @__PURE__ */ jsx(
BirhausCurrencyDisplay,
{
amount: account.saldo,
currency: account.moneda
}
) : "\u2022\u2022\u2022\u2022\u2022\u2022" }),
/* @__PURE__ */ jsxs("p", { className: "text-xs text-birhaus-muted-foreground", children: [
language === "es" ? "\xDAltima actividad: " : "Last activity: ",
account.ultimaActividad.toLocaleDateString(language === "es" ? "es-PY" : "en-US")
] })
] })
] })
] }),
account.estado === "bloqueada" && /* @__PURE__ */ jsx("div", { className: "flex-shrink-0", children: /* @__PURE__ */ jsx(AlertCircle, { size: 20, className: "text-red-600" }) })
] })
},
account.id
);
}) }),
accounts.length > filteredAccounts.length && /* @__PURE__ */ jsx("div", { className: "text-center text-sm text-birhaus-muted-foreground", children: language === "es" ? /* @__PURE__ */ jsxs(Fragment, { children: [
"Mostrando ",
filteredAccounts.length,
" de ",
accounts.length,
" cuentas"
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
"Showing ",
filteredAccounts.length,
" of ",
accounts.length,
" accounts"
] }) })
] });
}
var cn4 = (...inputs) => twMerge(clsx(inputs));
var transactionTypeConfig = {
ingreso: {
icon: /* @__PURE__ */ jsx(ArrowDownLeft, { size: 16 }),
colorClass: "text-green-600 bg-green-100",
nameEs: "Ingreso",
nameEn: "Income"
},
gasto: {
icon: /* @__PURE__ */ jsx(ArrowUpRight, { size: 16 }),
colorClass: "text-red-600 bg-red-100",
nameEs: "Gasto",
nameEn: "Expense"
},
transferencia: {
icon: /* @__PURE__ */ jsx(Clock, { size: 16 }),
colorClass: "text-blue-600 bg-blue-100",
nameEs: "Transferencia",
nameEn: "Transfer"
},
inversion: {
icon: /* @__PURE__ */ jsx(ArrowDownLeft, { size: 16 }),
colorClass: "text-green-600 bg-green-100",
nameEs: "Inversi\xF3n",
nameEn: "Investment"
}
};
var transactionStatusConfig = {
pendiente: {
colorClass: "text-yellow-700 bg-yellow-100",
nameEs: "Pendiente",
nameEn: "Pending"
},
completada: {
colorClass: "text-green-700 bg-green-100",
nameEs: "Completada",
nameEn: "Completed"
},
fallida: {
colorClass: "text-red-700 bg-red-100",
nameEs: "Fallida",
nameEn: "Failed"
},
cancelada: {
colorClass: "text-gray-700 bg-gray-100",
nameEs: "Cancelada",
nameEn: "Cancelled"
},
procesando: {
colorClass: "text-blue-700 bg-blue-100",
nameEs: "Procesando",
nameEn: "Processing"
}
};
function BirhausTransactionList({
transactions,
loading = false,
pageSize = 7,
showFilters = true,
showSearch = true,
showExport = true,
onTransactionClick,
onExport,
className,
...props
}) {
const { language, reportViolation } = useBirhaus();
const [searchTerm, setSearchTerm] = React4.useState("");
const [filters, setFilters] = React4.useState({});
const [currentPage, setCurrentPage] = React4.useState(1);
const [showFiltersPanel, setShowFiltersPanel] = React4.useState(false);
React4.useEffect(() => {
if (pageSize > 7) {
reportViolation({
type: "cognitive",
severity: "warning",
element: document.createElement("div"),
message: `Transaction list shows ${pageSize} items per page (max recommended: 7)`,
messageEs: `Lista de transacciones muestra ${pageSize} elementos por p\xE1gina (m\xE1ximo recomendado: 7)`,
messageEn: `Transaction list shows ${pageSize} items per page (max recommended: 7)`,
recommendation: "Use pagination to keep cognitive load manageable",
birhausPrinciple: 1
// Cognitive load reduction
});
}
}, [pageSize, reportViolation]);
const filteredTransactions = React4.useMemo(() => {
let filtered = transactions;
if (searchTerm) {
const term = searchTerm.toLowerCase();
filtered = filtered.filter(
(transaction) => transaction.descripcionEs.toLowerCase().includes(term) || transaction.referencia?.toLowerCase().includes(term) || transaction.cuentaDestino?.toLowerCase().includes(term) || transaction.cuentaOrigen?.toLowerCase().includes(term)
);
}
if (filters.tipo) {
filtered = filtered.filter((t) => filters.tipo.includes(t.tipo));
}
if (filters.estado) {
filtered = filtered.filter((t) => filters.estado.includes(t.estado));
}
if (filters.fechaDesde) {
filtered = filtered.filter((t) => new Date(t.fecha) >= new Date(filters.fechaDesde));
}
if (filters.fechaHasta) {
filtered = filtered.filter((t) => new Date(t.fecha) <= new Date(filters.fechaHasta));
}
if (filters.montoMinimo !== void 0) {
filtered = filtered.filter((t) => t.monto >= filters.montoMinimo);
}
if (filters.montoMaximo !== void 0) {
filtered = filtered.filter((t) => t.monto <= filters.montoMaximo);
}
if (filters.moneda) {
filtered = filtered.filter((t) => filters.moneda.includes(t.moneda));
}
return filtered.sort((a, b) => new Date(b.fecha).getTime() - new Date(a.fecha).getTime());
}, [transactions, searchTerm, filters]);
const totalPages = Math.ceil(filteredTransactions.length / pageSize);
const paginatedTransactions = React4.useMemo(() => {
const start = (currentPage - 1) * pageSize;
return filteredTransactions.slice(start, start + pageSize);
}, [filteredTransactions, currentPage, pageSize]);
const handleSearch = React4.useCallback((value) => {
setSearchTerm(value);
setCurrentPage(1);
}, []);
const handleFilterChange = React4.useCallback((newFilters) => {
setFilters((prev) => ({ ...prev, ...newFilters }));
setCurrentPage(1);
}, []);
const clearFilters = React4.useCallback(() => {
setFilters({});
setSearchTerm("");
setCurrentPage(1);
}, []);
const handleTransactionClick = React4.useCallback((transaction) => {
onTransactionClick?.(transaction);
}, [onTransactionClick]);
const handleExport = React4.useCallback(() => {
onExport?.(filteredTransactions);
}, [onExport, filteredTransactions]);
const checkSEPRELADCompliance = React4.useCallback((transaction) => {
const seprelad = checkSEPRELADThreshold(transaction.monto, transaction.moneda);
return seprelad.requiresReport;
}, []);
if (loading) {
return /* @__PURE__ */ jsx("div", { className: cn4("space-y-4", className), ...props, children: Array.from({ length: pageSize }).map((_, index) => /* @__PURE__ */ jsx("div", { className: "p-4 border border-birhaus-border rounded-lg", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
/* @__PURE__ */ jsx("div", { className: "w-10 h-10 bg-gray-200 rounded-full animate-pulse" }),
/* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-2", children: [
/* @__PURE__ */ jsx("div", { className: "h-4 bg-gray-200 rounded animate-pulse w-1/2" }),
/* @__PURE__ */ jsx("div", { className: "h-3 bg-gray-200 rounded animate-pulse w-1/3" })
] }),
/* @__PURE__ */ jsx("div", { className: "h-4 bg-gray-200 rounded animate-pulse w-24" })
] }) }, index)) });
}
return /* @__PURE__ */ jsxs("div", { className: cn4("space-y-4", className), ...props, children: [
(showSearch || showFilters || showExport) && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4 flex-wrap", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4 flex-1", children: [
showSearch && /* @__PURE__ */ jsxs("div", { className: "relative flex-1 max-w-md", children: [
/* @__PURE__ */ jsx(Search, { size: 16, className: "absolute left-3 top-1/2 transform -translate-y-1/2 text-birhaus-muted-foreground" }),
/* @__PURE__ */ jsx(
"input",
{
type: "text",
value: searchTerm,
onChange: (e) => handleSearch(e.target.value),
placeholder: language === "es" ? "Buscar transacciones..." : "Search transactions...",
className: "w-full pl-10 pr-4 py-2 border border-birhaus-border rounded-lg focus:outline-none focus:ring-2 focus:ring-birhaus-primary focus:border-birhaus-primary"
}
)
] }),
showFilters && /* @__PURE__ */ jsxs(
"button",
{
onClick: () => setShowFiltersPanel(!showFiltersPanel),
className: "flex items-center gap-2 px-4 py-2 border border-birhaus-border rounded-lg hover:bg-birhaus-muted/50 transition-colors",
children: [
/* @__PURE__ */ jsx(Filter, { size: 16 }),
/* @__PURE__ */ jsx("span", { children: language === "es" ? "Filtros" : "Filters" }),
/* @__PURE__ */ jsx(ChevronDown, { size: 16, className: cn4(
"transition-transform duration-200",
showFiltersPanel && "rotate-180"
) })
]
}
)
] }),
showExport && /* @__PURE__ */ jsxs(
"button",
{
onClick: handleExport,
className: "flex items-center gap-2 px-4 py-2 bg-birhaus-primary text-white rounded-lg hover:bg-birhaus-primary/90 transition-colors",
children: [
/* @__PURE__ */ jsx(Download, { size: 16 }),
/* @__PURE__ */ jsx("span", { children: language === "es" ? "Exportar" : "Export" })
]
}
)
] }),
showFilters && showFiltersPanel && /* @__PURE__ */ jsxs("div", { className: "p-4 border border-birhaus-border rounded-lg bg-birhaus-muted/10 space-y-4", children: [
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4", children: [
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium mb-2", children: language === "es" ? "Tipo de Transacci\xF3n" : "Transaction Type" }),
/* @__PURE__ */ jsxs(
"select",
{
value: filters.tipo?.join(",") || "",
onChange: (e) => handleFilterChange({
tipo: e.target.value ? e.target.value.split(",") : void 0
}),
className: "w-full p-2 border border-birhaus-border rounded-lg focus:outline-none focus:ring-2 focus:ring-birhaus-primary",
children: [
/* @__PURE__ */ jsx("option", { value: "", children: language === "es" ? "Todos" : "All" }),
Object.entries(transactionTypeConfig).map(([key, config]) => /* @__PURE__ */ jsx("option", { value: key, children: language === "es" ? config.nameEs : config.nameEn }, key))
]
}
)
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium mb-2", children: language === "es" ? "Estado" : "Status" }),
/* @__PURE__ */ jsxs(
"select",
{
value: filters.estado?.join(",") || "",
onChange: (e) => handleFilterChange({
estado: e.target.value ? e.target.value.split(",") : void 0
}),
className: "w-full p-2 border border-birhaus-border rounded-lg focus:outline-none focus:ring-2 focus:ring-birhaus-primary",
children: [
/* @__PURE__ */ jsx("option", { value: "", children: language === "es" ? "Todos" : "All" }),
Object.entries(transactionStatusConfig).map(([key, config]) => /* @__PURE__ */ jsx("option", { value: key, children: language === "es" ? config.nameEs : config.nameEn }, key))
]
}
)
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium mb-2", children: language === "es" ? "Fecha Desde" : "Date From" }),
/* @__PURE__ */ jsx(
"input",
{
type: "date",
value: filters.fechaDesde instanceof Date ? filters.fechaDesde.toISOString().split("T")[0] : filters.fechaDesde || "",
onChange: (e) => handleFilterChange({ fechaDesde: e.target.value || void 0 }),
className: "w-full p-2 border border-birhaus-border rounded-lg focus:outline-none focus:ring-2 focus:ring-birhaus-primary"
}
)
] }),
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium mb-2", children: language === "es" ? "Fecha Hasta" : "Date To" }),
/* @__PURE__ */ jsx(
"input",
{
type: "date",
value: filters.fechaHasta instanceof Date ? filters.fechaHasta.toISOString().split("T")[0] : filters.fechaHasta || "",
onChange: (e) => handleFilterChange({ fechaHasta: e.target.value || void 0 }),
className: "w-full p-2 border border-birhaus-border rounded-lg focus:outline-none focus:ring-2 focus:ring-birhaus-primary"
}
)
] })
] }),
/* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
"button",
{
onClick: clearFilters,
className: "px-4 py-2 text-sm text-birhaus-muted-foreground hover:text-birhaus-foreground transition-colors",
children: language === "es" ? "Limpiar Filtros" : "Clear Filters"
}
) })