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:
255
components/cards/CardPaymentForm.tsx
Normal file
255
components/cards/CardPaymentForm.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user