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)
243 lines
8.6 KiB
TypeScript
243 lines
8.6 KiB
TypeScript
'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>
|
|
)
|
|
}
|