Files
finanzas/components/dashboard/ExpenseChart.tsx
renato97 712b06f118 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)
2026-01-29 00:00:32 +00:00

169 lines
5.1 KiB
TypeScript

'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>
)
}