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)
360 lines
8.5 KiB
TypeScript
360 lines
8.5 KiB
TypeScript
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]
|
|
)
|
|
}
|