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:
63
components/dashboard/DashboardHeader.tsx
Normal file
63
components/dashboard/DashboardHeader.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
'use client'
|
||||
|
||||
import { RefreshCw } from 'lucide-react'
|
||||
import { getMonthName } from '@/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface DashboardHeaderProps {
|
||||
onRefresh?: () => void
|
||||
isRefreshing?: boolean
|
||||
}
|
||||
|
||||
export function DashboardHeader({
|
||||
onRefresh,
|
||||
isRefreshing = false,
|
||||
}: DashboardHeaderProps) {
|
||||
const now = new Date()
|
||||
const currentMonth = now.getMonth() + 1
|
||||
const currentYear = now.getFullYear()
|
||||
const monthName = getMonthName(currentMonth)
|
||||
|
||||
// Formatear fecha actual
|
||||
const formattedDate = new Intl.DateTimeFormat('es-AR', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
}).format(now)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 border-b border-slate-700 bg-slate-800/50 px-6 py-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">
|
||||
Dashboard
|
||||
<span className="ml-2 text-lg font-normal text-slate-400">
|
||||
{monthName} {currentYear}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="mt-1 text-sm capitalize text-slate-400">
|
||||
{formattedDate}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{onRefresh && (
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={isRefreshing}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 rounded-lg border border-slate-600',
|
||||
'bg-slate-700 px-4 py-2 text-sm font-medium text-white',
|
||||
'transition-colors hover:bg-slate-600',
|
||||
'focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 focus:ring-offset-slate-800',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn('h-4 w-4', isRefreshing && 'animate-spin')}
|
||||
/>
|
||||
{isRefreshing ? 'Actualizando...' : 'Actualizar'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
168
components/dashboard/ExpenseChart.tsx
Normal file
168
components/dashboard/ExpenseChart.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
'use client'
|
||||
|
||||
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts'
|
||||
import { FixedDebt, VariableDebt } from '@/lib/types'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
|
||||
interface ExpenseChartProps {
|
||||
fixedDebts: FixedDebt[]
|
||||
variableDebts: VariableDebt[]
|
||||
}
|
||||
|
||||
// Colores por categoría
|
||||
const CATEGORY_COLORS: Record<string, string> = {
|
||||
// Deudas fijas
|
||||
housing: '#10b981', // emerald-500
|
||||
services: '#3b82f6', // blue-500
|
||||
subscription: '#8b5cf6', // violet-500
|
||||
other: '#64748b', // slate-500
|
||||
// Deudas variables
|
||||
shopping: '#f59e0b', // amber-500
|
||||
food: '#ef4444', // red-500
|
||||
entertainment: '#ec4899', // pink-500
|
||||
health: '#06b6d4', // cyan-500
|
||||
transport: '#84cc16', // lime-500
|
||||
}
|
||||
|
||||
// Nombres de categorías en español
|
||||
const CATEGORY_NAMES: Record<string, string> = {
|
||||
housing: 'Vivienda',
|
||||
services: 'Servicios',
|
||||
subscription: 'Suscripciones',
|
||||
other: 'Otros',
|
||||
shopping: 'Compras',
|
||||
food: 'Comida',
|
||||
entertainment: 'Entretenimiento',
|
||||
health: 'Salud',
|
||||
transport: 'Transporte',
|
||||
}
|
||||
|
||||
interface ChartData {
|
||||
name: string
|
||||
value: number
|
||||
color: string
|
||||
category: string
|
||||
}
|
||||
|
||||
export function ExpenseChart({ fixedDebts, variableDebts }: ExpenseChartProps) {
|
||||
// Agrupar gastos por categoría
|
||||
const categoryTotals = new Map<string, number>()
|
||||
|
||||
// Agregar deudas fijas no pagadas
|
||||
fixedDebts
|
||||
.filter((debt) => !debt.isPaid)
|
||||
.forEach((debt) => {
|
||||
const current = categoryTotals.get(debt.category) || 0
|
||||
categoryTotals.set(debt.category, current + debt.amount)
|
||||
})
|
||||
|
||||
// Agregar deudas variables no pagadas
|
||||
variableDebts
|
||||
.filter((debt) => !debt.isPaid)
|
||||
.forEach((debt) => {
|
||||
const current = categoryTotals.get(debt.category) || 0
|
||||
categoryTotals.set(debt.category, current + debt.amount)
|
||||
})
|
||||
|
||||
// Convertir a formato de datos para el gráfico
|
||||
const data: ChartData[] = Array.from(categoryTotals.entries())
|
||||
.map(([category, value]) => ({
|
||||
name: CATEGORY_NAMES[category] || category,
|
||||
value,
|
||||
color: CATEGORY_COLORS[category] || '#64748b',
|
||||
category,
|
||||
}))
|
||||
.filter((item) => item.value > 0)
|
||||
.sort((a, b) => b.value - a.value)
|
||||
|
||||
// Calcular total
|
||||
const total = data.reduce((sum, item) => sum + item.value, 0)
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center rounded-xl border border-slate-700 bg-slate-800">
|
||||
<p className="text-slate-500">No hay gastos pendientes</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-700 bg-slate-800 p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold text-white">
|
||||
Distribución de Gastos
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-col gap-6 lg:flex-row">
|
||||
{/* Gráfico de dona */}
|
||||
<div className="h-64 w-full lg:w-1/2">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={90}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={(value) =>
|
||||
typeof value === 'number' ? formatCurrency(value) : value
|
||||
}
|
||||
contentStyle={{
|
||||
backgroundColor: '#1e293b',
|
||||
border: '1px solid #334155',
|
||||
borderRadius: '8px',
|
||||
color: '#fff',
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Leyenda */}
|
||||
<div className="flex w-full flex-col justify-center gap-3 lg:w-1/2">
|
||||
{data.map((item) => {
|
||||
const percentage = total > 0 ? (item.value / total) * 100 : 0
|
||||
return (
|
||||
<div key={item.category} className="flex items-center gap-3">
|
||||
<div
|
||||
className="h-4 w-4 rounded-full"
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-slate-300">
|
||||
{item.name}
|
||||
</span>
|
||||
<span className="text-sm text-slate-400">
|
||||
{percentage.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">
|
||||
{formatCurrency(item.value)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Total */}
|
||||
<div className="mt-4 border-t border-slate-700 pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-slate-400">Total</span>
|
||||
<span className="font-mono text-lg font-bold text-emerald-400">
|
||||
{formatCurrency(total)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
73
components/dashboard/MetricCard.tsx
Normal file
73
components/dashboard/MetricCard.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
|
||||
import { LucideIcon, TrendingUp, TrendingDown } from 'lucide-react'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface MetricCardProps {
|
||||
title: string
|
||||
amount: number
|
||||
subtitle?: string
|
||||
trend?: {
|
||||
value: number
|
||||
isPositive: boolean
|
||||
}
|
||||
icon: LucideIcon
|
||||
color?: string
|
||||
}
|
||||
|
||||
export function MetricCard({
|
||||
title,
|
||||
amount,
|
||||
subtitle,
|
||||
trend,
|
||||
icon: Icon,
|
||||
color = 'text-emerald-400',
|
||||
}: MetricCardProps) {
|
||||
return (
|
||||
<div className="relative overflow-hidden rounded-xl border border-slate-700 bg-slate-800 p-6 shadow-lg">
|
||||
{/* Icono en esquina superior derecha */}
|
||||
<div className={cn('absolute right-4 top-4', color)}>
|
||||
<Icon className="h-10 w-10 opacity-80" />
|
||||
</div>
|
||||
|
||||
{/* Contenido */}
|
||||
<div className="relative">
|
||||
{/* Título */}
|
||||
<h3 className="text-sm font-medium text-slate-400">{title}</h3>
|
||||
|
||||
{/* Monto */}
|
||||
<p className="mt-2 font-mono text-3xl font-bold text-emerald-400">
|
||||
{formatCurrency(amount)}
|
||||
</p>
|
||||
|
||||
{/* Subtítulo */}
|
||||
{subtitle && (
|
||||
<p className="mt-1 text-sm text-slate-500">{subtitle}</p>
|
||||
)}
|
||||
|
||||
{/* Indicador de tendencia */}
|
||||
{trend && (
|
||||
<div className="mt-3 flex items-center gap-1.5">
|
||||
{trend.isPositive ? (
|
||||
<>
|
||||
<TrendingUp className="h-4 w-4 text-emerald-500" />
|
||||
<span className="text-sm font-medium text-emerald-500">
|
||||
+{trend.value}%
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TrendingDown className="h-4 w-4 text-rose-500" />
|
||||
<span className="text-sm font-medium text-rose-500">
|
||||
-{trend.value}%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className="text-sm text-slate-500">vs mes anterior</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
68
components/dashboard/QuickActions.tsx
Normal file
68
components/dashboard/QuickActions.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
'use client'
|
||||
|
||||
import { Plus, CreditCard, Wallet } from 'lucide-react'
|
||||
|
||||
interface QuickActionsProps {
|
||||
onAddDebt: () => void
|
||||
onAddCard: () => void
|
||||
onAddPayment: () => void
|
||||
}
|
||||
|
||||
interface ActionButton {
|
||||
label: string
|
||||
icon: React.ElementType
|
||||
onClick: () => void
|
||||
color: string
|
||||
}
|
||||
|
||||
export function QuickActions({
|
||||
onAddDebt,
|
||||
onAddCard,
|
||||
onAddPayment,
|
||||
}: QuickActionsProps) {
|
||||
const actions: ActionButton[] = [
|
||||
{
|
||||
label: 'Agregar Deuda',
|
||||
icon: Plus,
|
||||
onClick: onAddDebt,
|
||||
color: 'bg-emerald-500 hover:bg-emerald-600',
|
||||
},
|
||||
{
|
||||
label: 'Nueva Tarjeta',
|
||||
icon: CreditCard,
|
||||
onClick: onAddCard,
|
||||
color: 'bg-blue-500 hover:bg-blue-600',
|
||||
},
|
||||
{
|
||||
label: 'Registrar Pago',
|
||||
icon: Wallet,
|
||||
onClick: onAddPayment,
|
||||
color: 'bg-violet-500 hover:bg-violet-600',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
{actions.map((action) => {
|
||||
const Icon = action.icon
|
||||
return (
|
||||
<button
|
||||
key={action.label}
|
||||
onClick={action.onClick}
|
||||
className={`
|
||||
group flex flex-col items-center gap-3 rounded-xl p-6
|
||||
transition-all duration-200 ease-out
|
||||
${action.color}
|
||||
focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-slate-800
|
||||
`}
|
||||
>
|
||||
<div className="rounded-full bg-white/20 p-4 transition-transform group-hover:scale-110">
|
||||
<Icon className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<span className="font-medium text-white">{action.label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
168
components/dashboard/RecentActivity.tsx
Normal file
168
components/dashboard/RecentActivity.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
'use client'
|
||||
|
||||
import { ArrowDownLeft, ArrowUpRight, CreditCard, Wallet } from 'lucide-react'
|
||||
import { useFinanzasStore } from '@/lib/store'
|
||||
import { formatCurrency, formatShortDate } from '@/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface RecentActivityProps {
|
||||
limit?: number
|
||||
}
|
||||
|
||||
interface ActivityItem {
|
||||
id: string
|
||||
type: 'fixed_debt' | 'variable_debt' | 'card_payment'
|
||||
title: string
|
||||
amount: number
|
||||
date: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function RecentActivity({ limit = 5 }: RecentActivityProps) {
|
||||
const { fixedDebts, variableDebts, cardPayments, creditCards } =
|
||||
useFinanzasStore()
|
||||
|
||||
// Combinar todas las actividades
|
||||
const activities: ActivityItem[] = [
|
||||
// Deudas fijas recientes
|
||||
...fixedDebts.slice(0, limit).map((debt) => ({
|
||||
id: debt.id,
|
||||
type: 'fixed_debt' as const,
|
||||
title: debt.name,
|
||||
amount: debt.amount,
|
||||
date: new Date().toISOString(), // Usar fecha actual ya que fixedDebt no tiene fecha de creación
|
||||
description: `Vence el día ${debt.dueDay}`,
|
||||
})),
|
||||
|
||||
// Deudas variables recientes
|
||||
...variableDebts.slice(0, limit).map((debt) => ({
|
||||
id: debt.id,
|
||||
type: 'variable_debt' as const,
|
||||
title: debt.name,
|
||||
amount: debt.amount,
|
||||
date: debt.date,
|
||||
description: debt.notes,
|
||||
})),
|
||||
|
||||
// Pagos de tarjetas recientes
|
||||
...cardPayments.slice(0, limit).map((payment) => {
|
||||
const card = creditCards.find((c) => c.id === payment.cardId)
|
||||
return {
|
||||
id: payment.id,
|
||||
type: 'card_payment' as const,
|
||||
title: `Pago - ${card?.name || 'Tarjeta'}`,
|
||||
amount: payment.amount,
|
||||
date: payment.date,
|
||||
description: payment.description,
|
||||
}
|
||||
}),
|
||||
]
|
||||
|
||||
// Ordenar por fecha (más recientes primero)
|
||||
const sortedActivities = activities
|
||||
.sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
)
|
||||
.slice(0, limit)
|
||||
|
||||
// Configuración por tipo de actividad
|
||||
const activityConfig = {
|
||||
fixed_debt: {
|
||||
icon: Wallet,
|
||||
label: 'Deuda Fija',
|
||||
color: 'text-amber-400',
|
||||
bgColor: 'bg-amber-400/10',
|
||||
},
|
||||
variable_debt: {
|
||||
icon: ArrowUpRight,
|
||||
label: 'Gasto',
|
||||
color: 'text-rose-400',
|
||||
bgColor: 'bg-rose-400/10',
|
||||
},
|
||||
card_payment: {
|
||||
icon: CreditCard,
|
||||
label: 'Pago Tarjeta',
|
||||
color: 'text-blue-400',
|
||||
bgColor: 'bg-blue-400/10',
|
||||
},
|
||||
}
|
||||
|
||||
if (sortedActivities.length === 0) {
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-700 bg-slate-800 p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold text-white">
|
||||
Actividad Reciente
|
||||
</h3>
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<p className="text-slate-500">No hay actividad reciente</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-700 bg-slate-800 p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold text-white">
|
||||
Actividad Reciente
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
{sortedActivities.map((activity) => {
|
||||
const config = activityConfig[activity.type]
|
||||
const Icon = config.icon
|
||||
|
||||
return (
|
||||
<div
|
||||
key={activity.id}
|
||||
className="flex items-center gap-4 rounded-lg border border-slate-700/50 bg-slate-700/30 p-4 transition-colors hover:bg-slate-700/50"
|
||||
>
|
||||
{/* Icono */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-10 w-10 items-center justify-center rounded-full',
|
||||
config.bgColor
|
||||
)}
|
||||
>
|
||||
<Icon className={cn('h-5 w-5', config.color)} />
|
||||
</div>
|
||||
|
||||
{/* Contenido */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h4 className="truncate font-medium text-white">
|
||||
{activity.title}
|
||||
</h4>
|
||||
<span
|
||||
className={cn(
|
||||
'shrink-0 font-mono font-medium',
|
||||
activity.type === 'card_payment'
|
||||
? 'text-emerald-400'
|
||||
: 'text-rose-400'
|
||||
)}
|
||||
>
|
||||
{activity.type === 'card_payment' ? '+' : '-'}
|
||||
{formatCurrency(activity.amount)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-1 flex items-center gap-2 text-sm text-slate-400">
|
||||
<span className={cn('text-xs', config.color)}>
|
||||
{config.label}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>{formatShortDate(activity.date)}</span>
|
||||
{activity.description && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="truncate">{activity.description}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
156
components/dashboard/SummarySection.tsx
Normal file
156
components/dashboard/SummarySection.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
'use client'
|
||||
|
||||
import { AlertCircle, CreditCard, PiggyBank, Wallet } from 'lucide-react'
|
||||
import { MetricCard } from './MetricCard'
|
||||
import { ExpenseChart } from './ExpenseChart'
|
||||
import { useFinanzasStore } from '@/lib/store'
|
||||
import {
|
||||
calculateTotalFixedDebts,
|
||||
calculateTotalVariableDebts,
|
||||
} from '@/lib/utils'
|
||||
import {
|
||||
getCurrentMonthBudget,
|
||||
calculateCurrentSpending,
|
||||
} from '@/lib/alerts'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function SummarySection() {
|
||||
const {
|
||||
fixedDebts,
|
||||
variableDebts,
|
||||
creditCards,
|
||||
monthlyBudgets,
|
||||
alerts,
|
||||
currentMonth,
|
||||
currentYear,
|
||||
} = useFinanzasStore()
|
||||
|
||||
// Calcular métricas
|
||||
const totalFixedDebts = calculateTotalFixedDebts(fixedDebts)
|
||||
const totalVariableDebts = calculateTotalVariableDebts(variableDebts)
|
||||
const totalPendingDebts = totalFixedDebts + totalVariableDebts
|
||||
|
||||
const totalCardBalance = creditCards.reduce(
|
||||
(sum, card) => sum + card.currentBalance,
|
||||
0
|
||||
)
|
||||
|
||||
const currentBudget = getCurrentMonthBudget(
|
||||
monthlyBudgets,
|
||||
currentMonth,
|
||||
currentYear
|
||||
)
|
||||
|
||||
const currentSpending = calculateCurrentSpending(fixedDebts, variableDebts)
|
||||
|
||||
// Presupuesto disponible (ingresos - gastos actuales)
|
||||
const availableBudget = currentBudget
|
||||
? currentBudget.totalIncome - currentSpending
|
||||
: 0
|
||||
|
||||
// Meta de ahorro proyectada
|
||||
const projectedSavings = currentBudget
|
||||
? currentBudget.totalIncome - currentSpending
|
||||
: 0
|
||||
|
||||
const savingsGoal = currentBudget?.savingsGoal || 0
|
||||
|
||||
// Alertas no leídas (primeras 3)
|
||||
const unreadAlerts = alerts
|
||||
.filter((alert) => !alert.isRead)
|
||||
.slice(0, 3)
|
||||
|
||||
// Colores por severidad de alerta
|
||||
const severityColors = {
|
||||
danger: 'border-rose-500 bg-rose-500/10 text-rose-400',
|
||||
warning: 'border-amber-500 bg-amber-500/10 text-amber-400',
|
||||
info: 'border-blue-500 bg-blue-500/10 text-blue-400',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Grid de métricas */}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<MetricCard
|
||||
title="Deudas Pendientes"
|
||||
amount={totalPendingDebts}
|
||||
subtitle={`${fixedDebts.filter((d) => !d.isPaid).length + variableDebts.filter((d) => !d.isPaid).length} pagos pendientes`}
|
||||
icon={Wallet}
|
||||
color="text-rose-400"
|
||||
/>
|
||||
|
||||
<MetricCard
|
||||
title="Balance en Tarjetas"
|
||||
amount={totalCardBalance}
|
||||
subtitle={`${creditCards.length} tarjetas activas`}
|
||||
icon={CreditCard}
|
||||
color="text-blue-400"
|
||||
/>
|
||||
|
||||
<MetricCard
|
||||
title="Presupuesto Disponible"
|
||||
amount={availableBudget}
|
||||
subtitle={
|
||||
currentBudget
|
||||
? `de ${currentBudget.totalIncome.toLocaleString('es-AR', {
|
||||
style: 'currency',
|
||||
currency: 'ARS',
|
||||
})} ingresos`
|
||||
: 'Sin presupuesto definido'
|
||||
}
|
||||
icon={PiggyBank}
|
||||
color="text-emerald-400"
|
||||
/>
|
||||
|
||||
<MetricCard
|
||||
title="Meta de Ahorro"
|
||||
amount={projectedSavings}
|
||||
subtitle={
|
||||
savingsGoal > 0
|
||||
? `${((projectedSavings / savingsGoal) * 100).toFixed(0)}% de la meta`
|
||||
: 'Sin meta definida'
|
||||
}
|
||||
icon={PiggyBank}
|
||||
color="text-violet-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Gráfico y alertas */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
{/* Gráfico de distribución */}
|
||||
<ExpenseChart fixedDebts={fixedDebts} variableDebts={variableDebts} />
|
||||
|
||||
{/* Alertas destacadas */}
|
||||
<div className="rounded-xl border border-slate-700 bg-slate-800 p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold text-white">
|
||||
Alertas Destacadas
|
||||
</h3>
|
||||
|
||||
{unreadAlerts.length === 0 ? (
|
||||
<div className="flex h-48 items-center justify-center">
|
||||
<p className="text-slate-500">No hay alertas pendientes</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{unreadAlerts.map((alert) => (
|
||||
<div
|
||||
key={alert.id}
|
||||
className={cn(
|
||||
'flex items-start gap-3 rounded-lg border p-4',
|
||||
severityColors[alert.severity]
|
||||
)}
|
||||
>
|
||||
<AlertCircle className="mt-0.5 h-5 w-5 shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-medium">{alert.title}</h4>
|
||||
<p className="mt-1 text-sm opacity-90">{alert.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
6
components/dashboard/index.ts
Normal file
6
components/dashboard/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { MetricCard } from './MetricCard'
|
||||
export { DashboardHeader } from './DashboardHeader'
|
||||
export { ExpenseChart } from './ExpenseChart'
|
||||
export { QuickActions } from './QuickActions'
|
||||
export { SummarySection } from './SummarySection'
|
||||
export { RecentActivity } from './RecentActivity'
|
||||
Reference in New Issue
Block a user