Files
finanzas/components/cards/CardPaymentForm.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

256 lines
8.7 KiB
TypeScript

'use client'
import { useState } from 'react'
import { X, Check } from 'lucide-react'
interface CardPaymentFormData {
description: string
amount: number
date: string
installments?: {
current: number
total: number
}
}
interface CardPaymentFormProps {
cardId: string
onSubmit: (data: CardPaymentFormData) => void
onCancel: () => void
}
export function CardPaymentForm({ cardId, onSubmit, onCancel }: CardPaymentFormProps) {
const today = new Date().toISOString().split('T')[0]
const [formData, setFormData] = useState<CardPaymentFormData>({
description: '',
amount: 0,
date: today,
installments: undefined,
})
const [hasInstallments, setHasInstallments] = useState(false)
const [errors, setErrors] = useState<Record<string, string>>({})
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {}
if (!formData.description.trim()) {
newErrors.description = 'La descripción es requerida'
}
if (formData.amount <= 0) {
newErrors.amount = 'El monto debe ser mayor a 0'
}
if (!formData.date) {
newErrors.date = 'La fecha es requerida'
}
if (hasInstallments && formData.installments) {
if (formData.installments.current < 1) {
newErrors.installmentCurrent = 'La cuota actual debe ser al menos 1'
}
if (formData.installments.total < 2) {
newErrors.installmentTotal = 'El total de cuotas debe ser al menos 2'
}
if (formData.installments.current > formData.installments.total) {
newErrors.installments = 'La cuota actual no puede ser mayor al total'
}
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (validateForm()) {
onSubmit({
...formData,
installments: hasInstallments ? formData.installments : undefined,
})
}
}
const updateField = (field: keyof CardPaymentFormData, value: string | number) => {
setFormData((prev) => ({ ...prev, [field]: value }))
if (errors[field]) {
setErrors((prev) => {
const newErrors = { ...prev }
delete newErrors[field]
return newErrors
})
}
}
const updateInstallmentField = (field: 'current' | 'total', value: number) => {
setFormData((prev) => ({
...prev,
installments: {
current: field === 'current' ? value : (prev.installments?.current ?? 1),
total: field === 'total' ? value : (prev.installments?.total ?? 1),
},
}))
// Clear related errors
setErrors((prev) => {
const newErrors = { ...prev }
delete newErrors[`installment${field.charAt(0).toUpperCase() + field.slice(1)}`]
delete newErrors.installments
return newErrors
})
}
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">Registrar Pago</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">
{/* Description */}
<div>
<label className="mb-1 block text-sm font-medium text-slate-300">
Descripción
</label>
<input
type="text"
value={formData.description}
onChange={(e) => updateField('description', e.target.value)}
placeholder="Ej: Supermercado Coto"
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.description && (
<p className="mt-1 text-sm text-rose-400">{errors.description}</p>
)}
</div>
<div className="grid gap-4 sm:grid-cols-2">
{/* Amount */}
<div>
<label className="mb-1 block text-sm font-medium text-slate-300">
Monto
</label>
<input
type="number"
min={0}
step="0.01"
value={formData.amount || ''}
onChange={(e) => updateField('amount', 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.amount && (
<p className="mt-1 text-sm text-rose-400">{errors.amount}</p>
)}
</div>
{/* Date */}
<div>
<label className="mb-1 block text-sm font-medium text-slate-300">
Fecha
</label>
<input
type="date"
value={formData.date}
onChange={(e) => updateField('date', e.target.value)}
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.date && <p className="mt-1 text-sm text-rose-400">{errors.date}</p>}
</div>
</div>
{/* Installments Toggle */}
<div className="flex items-center gap-3 rounded-lg border border-slate-700 bg-slate-700/50 p-3">
<input
type="checkbox"
id="hasInstallments"
checked={hasInstallments}
onChange={(e) => {
setHasInstallments(e.target.checked)
if (!e.target.checked) {
setFormData((prev) => ({ ...prev, installments: undefined }))
} else {
setFormData((prev) => ({ ...prev, installments: { current: 1, total: 1 } }))
}
}}
className="h-4 w-4 rounded border-slate-500 bg-slate-600 text-indigo-600 focus:ring-indigo-500"
/>
<label htmlFor="hasInstallments" className="text-sm font-medium text-slate-300">
Este pago es en cuotas
</label>
</div>
{/* Installments Fields */}
{hasInstallments && (
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="mb-1 block text-sm font-medium text-slate-300">
Cuota actual
</label>
<input
type="number"
min={1}
value={formData.installments?.current || ''}
onChange={(e) =>
updateInstallmentField('current', 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.installmentCurrent && (
<p className="mt-1 text-sm text-rose-400">{errors.installmentCurrent}</p>
)}
</div>
<div>
<label className="mb-1 block text-sm font-medium text-slate-300">
Total de cuotas
</label>
<input
type="number"
min={2}
value={formData.installments?.total || ''}
onChange={(e) =>
updateInstallmentField('total', parseInt(e.target.value) || 2)
}
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.installmentTotal && (
<p className="mt-1 text-sm text-rose-400">{errors.installmentTotal}</p>
)}
</div>
</div>
)}
{errors.installments && (
<p className="text-sm text-rose-400">{errors.installments}</p>
)}
</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" />
Registrar pago
</button>
</div>
</form>
)
}