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)
256 lines
8.7 KiB
TypeScript
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>
|
|
)
|
|
}
|