feat: initial commit - finanzas app

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)
This commit is contained in:
renato97
2026-01-29 00:00:32 +00:00
commit 712b06f118
65 changed files with 8556 additions and 0 deletions

359
lib/alerts.ts Normal file
View File

@@ -0,0 +1,359 @@
import {
FixedDebt,
VariableDebt,
CreditCard,
MonthlyBudget,
Alert,
} from './types'
import {
getDaysUntil,
getNextDateByDay,
formatCurrency,
calculateTotalFixedDebts,
calculateTotalVariableDebts,
} from './utils'
export interface GenerateAlertsParams {
fixedDebts: FixedDebt[]
variableDebts: VariableDebt[]
creditCards: CreditCard[]
monthlyBudgets: MonthlyBudget[]
currentMonth: number
currentYear: number
}
interface AlertDraft {
type: Alert['type']
title: string
message: string
severity: Alert['severity']
relatedId?: string
}
/**
* Obtiene las deudas fijas no pagadas que vencen en los próximos N días
*/
export function getUpcomingFixedDebts(
fixedDebts: FixedDebt[],
days: number
): Array<{ debt: FixedDebt; daysUntil: number; dueDate: Date }> {
const today = new Date()
const currentDay = today.getDate()
const currentMonth = today.getMonth()
const currentYear = today.getFullYear()
return fixedDebts
.filter((debt) => !debt.isPaid)
.map((debt) => {
// Calcular la fecha de vencimiento para este mes
let dueDate = new Date(currentYear, currentMonth, debt.dueDay)
// Si ya pasó, calcular para el mes siguiente
if (currentDay > debt.dueDay) {
dueDate = new Date(currentYear, currentMonth + 1, debt.dueDay)
}
const daysUntil = getDaysUntil(dueDate)
return { debt, daysUntil, dueDate }
})
.filter(({ daysUntil }) => daysUntil >= 0 && daysUntil <= days)
.sort((a, b) => a.daysUntil - b.daysUntil)
}
/**
* Obtiene el presupuesto del mes actual
*/
export function getCurrentMonthBudget(
monthlyBudgets: MonthlyBudget[],
month: number,
year: number
): MonthlyBudget | null {
return (
monthlyBudgets.find(
(budget) => budget.month === month && budget.year === year
) || null
)
}
/**
* Calcula el gasto actual del mes (deudas fijas + variables no pagadas)
*/
export function calculateCurrentSpending(
fixedDebts: FixedDebt[],
variableDebts: VariableDebt[]
): number {
const fixedSpending = calculateTotalFixedDebts(fixedDebts)
const variableSpending = calculateTotalVariableDebts(variableDebts)
return fixedSpending + variableSpending
}
interface CardEvent {
card: CreditCard
type: 'closing' | 'due'
daysUntil: number
date: Date
}
/**
* Obtiene los eventos próximos de tarjetas (cierre o vencimiento)
*/
export function getUpcomingCardEvents(
creditCards: CreditCard[],
days: number
): CardEvent[] {
const events: CardEvent[] = []
for (const card of creditCards) {
// Calcular próximo cierre
const closingDate = getNextDateByDay(card.closingDay)
const daysUntilClosing = getDaysUntil(closingDate)
if (daysUntilClosing >= 0 && daysUntilClosing <= days) {
events.push({
card,
type: 'closing',
daysUntil: daysUntilClosing,
date: closingDate,
})
}
// Calcular próximo vencimiento
const dueDate = getNextDateByDay(card.dueDay)
const daysUntilDue = getDaysUntil(dueDate)
if (daysUntilDue >= 0 && daysUntilDue <= days) {
events.push({
card,
type: 'due',
daysUntil: daysUntilDue,
date: dueDate,
})
}
}
// Ordenar por días hasta el evento
return events.sort((a, b) => a.daysUntil - b.daysUntil)
}
/**
* Genera alertas de pagos próximos (deudas fijas)
*/
function generatePaymentDueAlerts(fixedDebts: FixedDebt[]): AlertDraft[] {
const upcomingDebts = getUpcomingFixedDebts(fixedDebts, 3)
const alerts: AlertDraft[] = []
for (const { debt, daysUntil } of upcomingDebts) {
const severity: Alert['severity'] =
daysUntil <= 1 ? 'danger' : 'warning'
const daysText = daysUntil === 0 ? 'hoy' : daysUntil === 1 ? 'mañana' : `en ${daysUntil} días`
alerts.push({
type: 'PAYMENT_DUE',
title: 'Pago próximo',
message: `'${debt.name}' vence ${daysText}: ${formatCurrency(debt.amount)}`,
severity,
relatedId: debt.id,
})
}
return alerts
}
/**
* Genera alertas de presupuesto
*/
function generateBudgetAlerts(
fixedDebts: FixedDebt[],
variableDebts: VariableDebt[],
monthlyBudgets: MonthlyBudget[],
currentMonth: number,
currentYear: number
): AlertDraft[] {
const currentBudget = getCurrentMonthBudget(
monthlyBudgets,
currentMonth,
currentYear
)
if (!currentBudget) {
return []
}
const totalBudget =
currentBudget.fixedExpenses + currentBudget.variableExpenses
if (totalBudget <= 0) {
return []
}
const currentSpending = calculateCurrentSpending(fixedDebts, variableDebts)
const percentageUsed = (currentSpending / totalBudget) * 100
if (percentageUsed < 80) {
return []
}
const severity: Alert['severity'] =
percentageUsed > 95 ? 'danger' : 'warning'
return [
{
type: 'BUDGET_WARNING',
title: 'Presupuesto al límite',
message: `Has usado el ${percentageUsed.toFixed(1)}% de tu presupuesto mensual`,
severity,
},
]
}
/**
* Genera alertas de eventos de tarjetas (cierre y vencimiento)
*/
function generateCardAlerts(creditCards: CreditCard[]): AlertDraft[] {
const events = getUpcomingCardEvents(creditCards, 3)
const closingAlerts: AlertDraft[] = []
const dueAlerts: AlertDraft[] = []
for (const event of events) {
if (event.type === 'closing') {
const daysText =
event.daysUntil === 0
? 'hoy'
: event.daysUntil === 1
? 'mañana'
: `en ${event.daysUntil} días`
closingAlerts.push({
type: 'CARD_CLOSING',
title: 'Cierre de tarjeta próximo',
message: `Tu tarjeta ${event.card.name} cierra ${daysText}. Balance: ${formatCurrency(event.card.currentBalance)}`,
severity: 'info',
relatedId: event.card.id,
})
} else {
const severity: Alert['severity'] =
event.daysUntil <= 2 ? 'warning' : 'info'
const daysText =
event.daysUntil === 0
? 'hoy'
: event.daysUntil === 1
? 'mañana'
: `en ${event.daysUntil} días`
dueAlerts.push({
type: 'CARD_DUE',
title: 'Vencimiento de tarjeta',
message: `Vencimiento de ${event.card.name} ${daysText}. Balance: ${formatCurrency(event.card.currentBalance)}`,
severity,
relatedId: event.card.id,
})
}
}
return [...closingAlerts, ...dueAlerts]
}
/**
* Genera alertas de meta de ahorro
*/
function generateSavingsAlerts(
fixedDebts: FixedDebt[],
variableDebts: VariableDebt[],
monthlyBudgets: MonthlyBudget[],
currentMonth: number,
currentYear: number
): AlertDraft[] {
const currentBudget = getCurrentMonthBudget(
monthlyBudgets,
currentMonth,
currentYear
)
if (!currentBudget || currentBudget.savingsGoal <= 0) {
return []
}
const currentSpending = calculateCurrentSpending(fixedDebts, variableDebts)
const projectedSavings = currentBudget.totalIncome - currentSpending
if (projectedSavings >= currentBudget.savingsGoal) {
return []
}
const percentageBelow =
((currentBudget.savingsGoal - projectedSavings) / currentBudget.savingsGoal) *
100
return [
{
type: 'SAVINGS_GOAL',
title: 'Meta de ahorro',
message: `Vas ${percentageBelow.toFixed(0)}% por debajo de tu meta de ahorro mensual`,
severity: 'info',
},
]
}
/**
* Elimina alertas duplicadas basándose en tipo y relatedId
*/
function deduplicateAlerts(alerts: AlertDraft[]): AlertDraft[] {
const seen = new Set<string>()
return alerts.filter((alert) => {
const key = `${alert.type}-${alert.relatedId || 'global'}`
if (seen.has(key)) {
return false
}
seen.add(key)
return true
})
}
/**
* Genera todas las alertas inteligentes basadas en el estado actual
*/
export function generateAlerts(params: GenerateAlertsParams): AlertDraft[] {
const {
fixedDebts,
variableDebts,
creditCards,
monthlyBudgets,
currentMonth,
currentYear,
} = params
const allAlerts: AlertDraft[] = [
...generatePaymentDueAlerts(fixedDebts),
...generateBudgetAlerts(
fixedDebts,
variableDebts,
monthlyBudgets,
currentMonth,
currentYear
),
...generateCardAlerts(creditCards),
...generateSavingsAlerts(
fixedDebts,
variableDebts,
monthlyBudgets,
currentMonth,
currentYear
),
]
// Eliminar duplicados y ordenar por severidad (danger > warning > info)
const uniqueAlerts = deduplicateAlerts(allAlerts)
const severityOrder = { danger: 0, warning: 1, info: 2 }
return uniqueAlerts.sort(
(a, b) => severityOrder[a.severity] - severityOrder[b.severity]
)
}