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:
359
lib/alerts.ts
Normal file
359
lib/alerts.ts
Normal 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]
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user