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,242 @@
'use client'
import { useState } from 'react'
import { CreditCard } from '@/lib/types'
import { X, Check } from 'lucide-react'
interface CreditCardFormProps {
initialData?: Partial<CreditCard>
onSubmit: (data: Omit<CreditCard, 'id'>) => void
onCancel: () => void
}
const DEFAULT_COLOR = '#6366f1'
export function CreditCardForm({ initialData, onSubmit, onCancel }: CreditCardFormProps) {
const [formData, setFormData] = useState({
name: initialData?.name ?? '',
lastFourDigits: initialData?.lastFourDigits ?? '',
closingDay: initialData?.closingDay ?? 1,
dueDay: initialData?.dueDay ?? 10,
currentBalance: initialData?.currentBalance ?? 0,
creditLimit: initialData?.creditLimit ?? 0,
color: initialData?.color ?? DEFAULT_COLOR,
})
const [errors, setErrors] = useState<Record<string, string>>({})
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {}
if (!formData.name.trim()) {
newErrors.name = 'El nombre es requerido'
}
if (!formData.lastFourDigits.trim()) {
newErrors.lastFourDigits = 'Los últimos 4 dígitos son requeridos'
} else if (!/^\d{4}$/.test(formData.lastFourDigits)) {
newErrors.lastFourDigits = 'Debe ser exactamente 4 dígitos numéricos'
}
if (formData.closingDay < 1 || formData.closingDay > 31) {
newErrors.closingDay = 'El día debe estar entre 1 y 31'
}
if (formData.dueDay < 1 || formData.dueDay > 31) {
newErrors.dueDay = 'El día debe estar entre 1 y 31'
}
if (formData.creditLimit <= 0) {
newErrors.creditLimit = 'El límite de crédito debe ser mayor a 0'
}
if (formData.currentBalance < 0) {
newErrors.currentBalance = 'El balance no puede ser negativo'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (validateForm()) {
onSubmit(formData)
}
}
const updateField = (field: keyof typeof formData, value: string | number) => {
setFormData((prev) => ({ ...prev, [field]: value }))
// Clear error when user starts typing
if (errors[field]) {
setErrors((prev) => {
const newErrors = { ...prev }
delete newErrors[field]
return newErrors
})
}
}
const handleLastFourDigitsChange = (value: string) => {
// Only allow digits and max 4 characters
const digitsOnly = value.replace(/\D/g, '').slice(0, 4)
updateField('lastFourDigits', digitsOnly)
}
return (
<form onSubmit={handleSubmit} className="space-y-4 rounded-xl bg-slate-800 p-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">
{initialData ? 'Editar Tarjeta' : 'Nueva Tarjeta'}
</h3>
<button
type="button"
onClick={onCancel}
className="rounded-full p-1 text-slate-400 transition-colors hover:bg-slate-700 hover:text-white"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{/* Card Name */}
<div className="sm:col-span-2">
<label className="mb-1 block text-sm font-medium text-slate-300">
Nombre de la tarjeta
</label>
<input
type="text"
value={formData.name}
onChange={(e) => updateField('name', e.target.value)}
placeholder="Ej: Visa Banco Galicia"
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-4 py-2 text-white placeholder-slate-400 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"
/>
{errors.name && <p className="mt-1 text-sm text-rose-400">{errors.name}</p>}
</div>
{/* Last 4 Digits */}
<div>
<label className="mb-1 block text-sm font-medium text-slate-300">
Últimos 4 dígitos
</label>
<input
type="text"
inputMode="numeric"
value={formData.lastFourDigits}
onChange={(e) => handleLastFourDigitsChange(e.target.value)}
placeholder="1234"
maxLength={4}
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-4 py-2 text-white placeholder-slate-400 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"
/>
{errors.lastFourDigits && (
<p className="mt-1 text-sm text-rose-400">{errors.lastFourDigits}</p>
)}
</div>
{/* Color Picker */}
<div>
<label className="mb-1 block text-sm font-medium text-slate-300">Color</label>
<div className="flex items-center gap-3">
<input
type="color"
value={formData.color}
onChange={(e) => updateField('color', e.target.value)}
className="h-10 w-20 cursor-pointer rounded-lg border border-slate-600 bg-slate-700"
/>
<span className="text-sm text-slate-400">{formData.color}</span>
</div>
</div>
{/* Closing Day */}
<div>
<label className="mb-1 block text-sm font-medium text-slate-300">
Día de cierre
</label>
<input
type="number"
min={1}
max={31}
value={formData.closingDay}
onChange={(e) => updateField('closingDay', parseInt(e.target.value) || 1)}
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-4 py-2 text-white focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"
/>
{errors.closingDay && (
<p className="mt-1 text-sm text-rose-400">{errors.closingDay}</p>
)}
</div>
{/* Due Day */}
<div>
<label className="mb-1 block text-sm font-medium text-slate-300">
Día de vencimiento
</label>
<input
type="number"
min={1}
max={31}
value={formData.dueDay}
onChange={(e) => updateField('dueDay', parseInt(e.target.value) || 1)}
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-4 py-2 text-white focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"
/>
{errors.dueDay && <p className="mt-1 text-sm text-rose-400">{errors.dueDay}</p>}
</div>
{/* Credit Limit */}
<div>
<label className="mb-1 block text-sm font-medium text-slate-300">
Límite de crédito
</label>
<input
type="number"
min={0}
step="0.01"
value={formData.creditLimit || ''}
onChange={(e) => updateField('creditLimit', parseFloat(e.target.value) || 0)}
placeholder="0.00"
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-4 py-2 text-white placeholder-slate-400 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"
/>
{errors.creditLimit && (
<p className="mt-1 text-sm text-rose-400">{errors.creditLimit}</p>
)}
</div>
{/* Current Balance */}
<div>
<label className="mb-1 block text-sm font-medium text-slate-300">
Balance actual
</label>
<input
type="number"
min={0}
step="0.01"
value={formData.currentBalance || ''}
onChange={(e) => updateField('currentBalance', parseFloat(e.target.value) || 0)}
placeholder="0.00"
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-4 py-2 text-white placeholder-slate-400 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"
/>
{errors.currentBalance && (
<p className="mt-1 text-sm text-rose-400">{errors.currentBalance}</p>
)}
</div>
</div>
{/* Actions */}
<div className="flex justify-end gap-3 pt-4">
<button
type="button"
onClick={onCancel}
className="rounded-lg border border-slate-600 px-4 py-2 text-sm font-medium text-slate-300 transition-colors hover:bg-slate-700"
>
Cancelar
</button>
<button
type="submit"
className="flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
>
<Check className="h-4 w-4" />
{initialData ? 'Guardar cambios' : 'Crear tarjeta'}
</button>
</div>
</form>
)
}