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:
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user