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:
renato97
2026-01-29 00:00:32 +00:00
commit 712b06f118
65 changed files with 8556 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
'use client'
import { useState } from 'react'
import { useFinanzasStore } from '@/lib/store'
import { v4 as uuidv4 } from 'uuid'
import { X, Calendar, DollarSign, Tag, FileText, CheckCircle2 } from 'lucide-react'
import { cn } from '@/lib/utils'
interface AddDebtModalProps {
isOpen: boolean
onClose: () => void
}
type DebtType = 'fixed' | 'variable'
export function AddDebtModal({ isOpen, onClose }: AddDebtModalProps) {
const [activeTab, setActiveTab] = useState<DebtType>('variable')
const [name, setName] = useState('')
const [amount, setAmount] = useState('')
const [dateStr, setDateStr] = useState(new Date().toISOString().split('T')[0]) // For variable: YYYY-MM-DD
const [dueDay, setDueDay] = useState('1') // For fixed: 1-31
const [categoryFixed, setCategoryFixed] = useState('housing')
const [categoryVariable, setCategoryVariable] = useState('shopping')
const [isAutoDebit, setIsAutoDebit] = useState(false)
const [notes, setNotes] = useState('')
const addFixedDebt = useFinanzasStore((state) => state.addFixedDebt)
const addVariableDebt = useFinanzasStore((state) => state.addVariableDebt)
if (!isOpen) return null
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!name || !amount) return
const numAmount = parseFloat(amount)
if (isNaN(numAmount)) return
if (activeTab === 'fixed') {
addFixedDebt({
name,
amount: numAmount,
dueDay: parseInt(dueDay),
category: categoryFixed as any,
isAutoDebit,
isPaid: false,
notes: notes || undefined
})
} else {
addVariableDebt({
name,
amount: numAmount,
date: new Date(dateStr).toISOString(),
category: categoryVariable as any,
isPaid: false,
notes: notes || undefined
})
}
// Reset and Close
setName('')
setAmount('')
setNotes('')
onClose()
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
<div className="w-full max-w-lg rounded-xl bg-slate-900 border border-slate-800 shadow-2xl overflow-hidden scale-100 animate-in zoom-in-95 duration-200">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-slate-800">
<h2 className="text-xl font-semibold text-white">Agregar Gasto / Deuda</h2>
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors">
<X size={20} />
</button>
</div>
{/* Tabs */}
<div className="flex p-1 mx-6 mt-6 bg-slate-800/50 rounded-lg">
<button
onClick={() => setActiveTab('variable')}
className={cn(
"flex-1 py-2 text-sm font-medium rounded-md transition-all duration-200",
activeTab === 'variable' ? "bg-cyan-500 text-white shadow-lg" : "text-slate-400 hover:text-white"
)}
>
Variable (Único)
</button>
<button
onClick={() => setActiveTab('fixed')}
className={cn(
"flex-1 py-2 text-sm font-medium rounded-md transition-all duration-200",
activeTab === 'fixed' ? "bg-cyan-500 text-white shadow-lg" : "text-slate-400 hover:text-white"
)}
>
Fijo (Recurrente)
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-5">
{/* Amount Input */}
<div className="space-y-2">
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Monto</label>
<div className="relative">
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 font-semibold">$</span>
<input
type="number"
step="0.01"
placeholder="0.00"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="w-full pl-8 pr-4 py-3 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white text-lg font-mono outline-none transition-all placeholder:text-slate-600"
required
autoFocus
/>
</div>
</div>
{/* Name Input */}
<div className="space-y-2">
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Descripción</label>
<input
type="text"
placeholder="Ej: Supermercado Coto, Netflix, Alquiler"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none transition-all placeholder:text-slate-600"
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
{/* Category Select */}
<div className="space-y-2">
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-1">
<Tag size={12} /> Categoría
</label>
<select
value={activeTab === 'fixed' ? categoryFixed : categoryVariable}
onChange={(e) => activeTab === 'fixed' ? setCategoryFixed(e.target.value) : setCategoryVariable(e.target.value)}
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none appearance-none cursor-pointer"
>
{activeTab === 'fixed' ? (
<>
<option value="housing">Vivienda</option>
<option value="services">Servicios</option>
<option value="subscription">Suscripciones</option>
<option value="other">Otro</option>
</>
) : (
<>
<option value="food">Comida / Super</option>
<option value="shopping">Compras</option>
<option value="transport">Transporte</option>
<option value="health">Salud</option>
<option value="entertainment">Entretenimiento</option>
<option value="other">Otro</option>
</>
)}
</select>
</div>
{/* Date/DueDay Input */}
<div className="space-y-2">
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-1">
<Calendar size={12} /> {activeTab === 'fixed' ? 'Día Vencimiento' : 'Fecha'}
</label>
{activeTab === 'fixed' ? (
<div className="relative">
<input
type="number"
min="1"
max="31"
value={dueDay}
onChange={(e) => setDueDay(e.target.value)}
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none"
required
/>
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-500 text-sm">del mes</span>
</div>
) : (
<input
type="date"
value={dateStr}
onChange={(e) => setDateStr(e.target.value)}
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none [color-scheme:dark]"
required
/>
)}
</div>
</div>
{activeTab === 'fixed' && (
<div className="flex items-center gap-2 px-4 py-3 bg-slate-800/30 rounded-lg cursor-pointer" onClick={() => setIsAutoDebit(!isAutoDebit)}>
<div className={cn("w-5 h-5 rounded border flex items-center justify-center transition-colors", isAutoDebit ? "bg-cyan-500 border-cyan-500" : "border-slate-600 bg-transparent")}>
{isAutoDebit && <CheckCircle2 size={14} className="text-white" />}
</div>
<span className="text-sm text-slate-300 select-none">Débito Automático</span>
</div>
)}
{/* Notes */}
<div className="space-y-2">
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-1">
<FileText size={12} /> Notas (Opcional)
</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Detalles adicionales..."
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none min-h-[80px] text-sm resize-none placeholder:text-slate-600"
/>
</div>
<div className="pt-2">
<button
type="submit"
className="w-full py-3 bg-cyan-500 hover:bg-cyan-400 text-white font-semibold rounded-lg shadow-lg shadow-cyan-500/20 transition-all active:scale-[0.98]"
>
Agregar {activeTab === 'fixed' ? 'Gasto Fijo' : 'Gasto'}
</button>
</div>
</form>
</div>
</div>
)
}