Complete personal finance management application with: - Dashboard with financial metrics and alerts - Credit card management and payments - Fixed and variable debt tracking - Monthly budget planning - Intelligent alert system - Responsive design with Tailwind CSS Tech stack: Next.js 14, TypeScript, Zustand, Recharts 🤖 Generated with [Claude Code](https://claude.com/claude-code)
190 lines
4.9 KiB
TypeScript
190 lines
4.9 KiB
TypeScript
import { clsx, type ClassValue } from 'clsx'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import { FixedDebt, VariableDebt, CardPayment } from './types'
|
|
|
|
/**
|
|
* Combina clases de Tailwind CSS usando clsx y tailwind-merge
|
|
* Permite combinar múltiples clases condicionalmente
|
|
*/
|
|
export function cn(...inputs: ClassValue[]): string {
|
|
return twMerge(clsx(inputs))
|
|
}
|
|
|
|
/**
|
|
* Formatea un número como moneda (pesos argentinos/USD)
|
|
* Ejemplo: 1500.50 -> "$ 1.500,50"
|
|
*/
|
|
export function formatCurrency(amount: number): string {
|
|
const formatter = new Intl.NumberFormat('es-AR', {
|
|
style: 'currency',
|
|
currency: 'ARS',
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
})
|
|
return formatter.format(amount)
|
|
}
|
|
|
|
/**
|
|
* Formatea una fecha en formato legible en español
|
|
* Ejemplo: "28 de enero de 2026"
|
|
*/
|
|
export function formatDate(date: string | Date): string {
|
|
const d = typeof date === 'string' ? new Date(date) : date
|
|
const formatter = new Intl.DateTimeFormat('es-AR', {
|
|
day: 'numeric',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
})
|
|
return formatter.format(d)
|
|
}
|
|
|
|
/**
|
|
* Formatea una fecha en formato corto
|
|
* Ejemplo: "28/01/2026"
|
|
*/
|
|
export function formatShortDate(date: string | Date): string {
|
|
const d = typeof date === 'string' ? new Date(date) : date
|
|
const formatter = new Intl.DateTimeFormat('es-AR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
})
|
|
return formatter.format(d)
|
|
}
|
|
|
|
/**
|
|
* Calcula los días hasta una fecha específica
|
|
* Retorna un número negativo si la fecha ya pasó
|
|
*/
|
|
export function getDaysUntil(date: string | Date): number {
|
|
const targetDate = typeof date === 'string' ? new Date(date) : date
|
|
const today = new Date()
|
|
|
|
// Reset hours to compare only dates
|
|
const target = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate())
|
|
const current = new Date(today.getFullYear(), today.getMonth(), today.getDate())
|
|
|
|
const diffTime = target.getTime() - current.getTime()
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
|
|
|
return diffDays
|
|
}
|
|
|
|
/**
|
|
* Obtiene la próxima fecha para un día específico del mes
|
|
* Si el día ya pasó este mes, devuelve el del mes siguiente
|
|
*/
|
|
export function getNextDateByDay(dayOfMonth: number): Date {
|
|
const today = new Date()
|
|
const currentYear = today.getFullYear()
|
|
const currentMonth = today.getMonth()
|
|
const currentDay = today.getDate()
|
|
|
|
let targetYear = currentYear
|
|
let targetMonth = currentMonth
|
|
|
|
// Si el día ya pasó este mes, ir al siguiente mes
|
|
if (currentDay > dayOfMonth) {
|
|
targetMonth += 1
|
|
if (targetMonth > 11) {
|
|
targetMonth = 0
|
|
targetYear += 1
|
|
}
|
|
}
|
|
|
|
// Ajustar si el día no existe en el mes objetivo (ej: 31 de febrero)
|
|
const lastDayOfMonth = new Date(targetYear, targetMonth + 1, 0).getDate()
|
|
const targetDay = Math.min(dayOfMonth, lastDayOfMonth)
|
|
|
|
return new Date(targetYear, targetMonth, targetDay)
|
|
}
|
|
|
|
/**
|
|
* Obtiene el nombre del mes en español
|
|
* El mes debe ser 1-12 (enero = 1)
|
|
*/
|
|
export function getMonthName(month: number): string {
|
|
const monthNames = [
|
|
'enero',
|
|
'febrero',
|
|
'marzo',
|
|
'abril',
|
|
'mayo',
|
|
'junio',
|
|
'julio',
|
|
'agosto',
|
|
'septiembre',
|
|
'octubre',
|
|
'noviembre',
|
|
'diciembre',
|
|
]
|
|
|
|
if (month < 1 || month > 12) {
|
|
throw new Error('El mes debe estar entre 1 y 12')
|
|
}
|
|
|
|
return monthNames[month - 1]
|
|
}
|
|
|
|
/**
|
|
* Calcula el total de deudas fijas no pagadas
|
|
*/
|
|
export function calculateTotalFixedDebts(debts: FixedDebt[]): number {
|
|
return debts
|
|
.filter((debt) => !debt.isPaid)
|
|
.reduce((total, debt) => total + debt.amount, 0)
|
|
}
|
|
|
|
/**
|
|
* Calcula el total de deudas variables no pagadas
|
|
*/
|
|
export function calculateTotalVariableDebts(debts: VariableDebt[]): number {
|
|
return debts
|
|
.filter((debt) => !debt.isPaid)
|
|
.reduce((total, debt) => total + debt.amount, 0)
|
|
}
|
|
|
|
/**
|
|
* Calcula el total de pagos de tarjeta
|
|
* Opcionalmente filtrados por cardId
|
|
*/
|
|
export function calculateCardPayments(
|
|
payments: CardPayment[],
|
|
cardId?: string
|
|
): number {
|
|
const filteredPayments = cardId
|
|
? payments.filter((payment) => payment.cardId === cardId)
|
|
: payments
|
|
|
|
return filteredPayments.reduce((total, payment) => total + payment.amount, 0)
|
|
}
|
|
|
|
/**
|
|
* Calcula la próxima fecha de cierre de tarjeta
|
|
* Si el día de cierre ya pasó este mes, devuelve el del mes siguiente
|
|
*/
|
|
export function calculateNextClosingDate(closingDay: number): Date {
|
|
return getNextDateByDay(closingDay)
|
|
}
|
|
|
|
/**
|
|
* Calcula la próxima fecha de vencimiento de tarjeta
|
|
* Si el día de vencimiento ya pasó este mes, devuelve el del mes siguiente
|
|
*/
|
|
export function calculateNextDueDate(dueDay: number): Date {
|
|
return getNextDateByDay(dueDay)
|
|
}
|
|
|
|
/**
|
|
* Calcula el porcentaje de utilización de una tarjeta de crédito
|
|
* Retorna un valor entre 0 y 100
|
|
*/
|
|
export function getCardUtilization(balance: number, limit: number): number {
|
|
if (limit <= 0) {
|
|
return 0
|
|
}
|
|
|
|
const utilization = (balance / limit) * 100
|
|
return Math.min(Math.max(utilization, 0), 100)
|
|
}
|