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:
325
components/cards/CardSection.tsx
Normal file
325
components/cards/CardSection.tsx
Normal file
@@ -0,0 +1,325 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useFinanzasStore } from '@/lib/store'
|
||||
import { CreditCardWidget } from './CreditCardWidget'
|
||||
import { CreditCardForm } from './CreditCardForm'
|
||||
import { CardPaymentForm } from './CardPaymentForm'
|
||||
import { MiniCard } from './MiniCard'
|
||||
import { CreditCard, CardPayment } from '@/lib/types'
|
||||
import { formatCurrency, formatShortDate, getMonthName } from '@/lib/utils'
|
||||
import { Plus, CreditCard as CreditCardIcon, Receipt, Trash2 } from 'lucide-react'
|
||||
|
||||
export function CardSection() {
|
||||
const {
|
||||
creditCards,
|
||||
cardPayments,
|
||||
currentMonth,
|
||||
currentYear,
|
||||
addCreditCard,
|
||||
updateCreditCard,
|
||||
deleteCreditCard,
|
||||
addCardPayment,
|
||||
deleteCardPayment,
|
||||
} = useFinanzasStore()
|
||||
|
||||
const [showCardForm, setShowCardForm] = useState(false)
|
||||
const [editingCard, setEditingCard] = useState<CreditCard | null>(null)
|
||||
const [selectedCardId, setSelectedCardId] = useState<string>('')
|
||||
const [showPaymentForm, setShowPaymentForm] = useState(false)
|
||||
|
||||
// Filter payments for current month
|
||||
const currentMonthPayments = useMemo(() => {
|
||||
return cardPayments.filter((payment) => {
|
||||
const paymentDate = new Date(payment.date)
|
||||
return (
|
||||
paymentDate.getMonth() + 1 === currentMonth &&
|
||||
paymentDate.getFullYear() === currentYear
|
||||
)
|
||||
})
|
||||
}, [cardPayments, currentMonth, currentYear])
|
||||
|
||||
const handleCardSubmit = (data: Omit<CreditCard, 'id'>) => {
|
||||
if (editingCard) {
|
||||
updateCreditCard(editingCard.id, data)
|
||||
setEditingCard(null)
|
||||
} else {
|
||||
addCreditCard(data)
|
||||
}
|
||||
setShowCardForm(false)
|
||||
}
|
||||
|
||||
const handleEditCard = (card: CreditCard) => {
|
||||
setEditingCard(card)
|
||||
setShowCardForm(true)
|
||||
}
|
||||
|
||||
const handleDeleteCard = (cardId: string) => {
|
||||
if (window.confirm('¿Estás seguro de que deseas eliminar esta tarjeta?')) {
|
||||
deleteCreditCard(cardId)
|
||||
if (selectedCardId === cardId) {
|
||||
setSelectedCardId('')
|
||||
setShowPaymentForm(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handlePaymentSubmit = (data: {
|
||||
description: string
|
||||
amount: number
|
||||
date: string
|
||||
installments?: { current: number; total: number }
|
||||
}) => {
|
||||
addCardPayment({
|
||||
cardId: selectedCardId,
|
||||
...data,
|
||||
})
|
||||
setShowPaymentForm(false)
|
||||
}
|
||||
|
||||
const handleDeletePayment = (paymentId: string) => {
|
||||
if (window.confirm('¿Estás seguro de que deseas eliminar este pago?')) {
|
||||
deleteCardPayment(paymentId)
|
||||
}
|
||||
}
|
||||
|
||||
const getCardById = (cardId: string): CreditCard | undefined => {
|
||||
return creditCards.find((card) => card.id === cardId)
|
||||
}
|
||||
|
||||
const getCardTotalPayments = (cardId: string): number => {
|
||||
return currentMonthPayments
|
||||
.filter((payment) => payment.cardId === cardId)
|
||||
.reduce((total, payment) => total + payment.amount, 0)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-indigo-500/20">
|
||||
<CreditCardIcon className="h-5 w-5 text-indigo-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white">Tarjetas de Crédito</h2>
|
||||
<p className="text-sm text-slate-400">
|
||||
{getMonthName(currentMonth)} {currentYear}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingCard(null)
|
||||
setShowCardForm(true)
|
||||
}}
|
||||
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"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Agregar tarjeta
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Cards Grid */}
|
||||
{creditCards.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-slate-600 bg-slate-800/50 p-12 text-center">
|
||||
<CreditCardIcon className="mx-auto h-12 w-12 text-slate-500" />
|
||||
<h3 className="mt-4 text-lg font-medium text-slate-300">
|
||||
No tienes tarjetas registradas
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-slate-400">
|
||||
Agrega tu primera tarjeta para comenzar a gestionar tus pagos
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingCard(null)
|
||||
setShowCardForm(true)
|
||||
}}
|
||||
className="mt-4 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
|
||||
>
|
||||
Agregar tarjeta
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{creditCards.map((card) => (
|
||||
<CreditCardWidget
|
||||
key={card.id}
|
||||
card={card}
|
||||
onEdit={() => handleEditCard(card)}
|
||||
onDelete={() => handleDeleteCard(card.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Card Form Modal */}
|
||||
{showCardForm && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm">
|
||||
<div className="w-full max-w-lg">
|
||||
<CreditCardForm
|
||||
initialData={editingCard ?? undefined}
|
||||
onSubmit={handleCardSubmit}
|
||||
onCancel={() => {
|
||||
setShowCardForm(false)
|
||||
setEditingCard(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment Section */}
|
||||
{creditCards.length > 0 && (
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* Register Payment */}
|
||||
<div className="rounded-xl bg-slate-800 p-6">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/20">
|
||||
<Receipt className="h-4 w-4 text-emerald-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white">Registrar Pago</h3>
|
||||
</div>
|
||||
|
||||
{/* Card Selector */}
|
||||
<div className="mb-4">
|
||||
<label className="mb-2 block text-sm font-medium text-slate-300">
|
||||
Seleccionar tarjeta
|
||||
</label>
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||
{creditCards.map((card) => (
|
||||
<MiniCard
|
||||
key={card.id}
|
||||
card={card}
|
||||
selected={selectedCardId === card.id}
|
||||
onClick={() => {
|
||||
setSelectedCardId(card.id)
|
||||
setShowPaymentForm(true)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Form */}
|
||||
{showPaymentForm && selectedCardId && (
|
||||
<CardPaymentForm
|
||||
cardId={selectedCardId}
|
||||
onSubmit={handlePaymentSubmit}
|
||||
onCancel={() => {
|
||||
setShowPaymentForm(false)
|
||||
setSelectedCardId('')
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recent Payments */}
|
||||
<div className="rounded-xl bg-slate-800 p-6">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-amber-500/20">
|
||||
<Receipt className="h-4 w-4 text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">Pagos del Mes</h3>
|
||||
<p className="text-sm text-slate-400">
|
||||
Total: {formatCurrency(
|
||||
currentMonthPayments.reduce((sum, p) => sum + p.amount, 0)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentMonthPayments.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-slate-600 p-8 text-center">
|
||||
<p className="text-sm text-slate-400">
|
||||
No hay pagos registrados este mes
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{currentMonthPayments
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
.map((payment) => {
|
||||
const card = getCardById(payment.cardId)
|
||||
return (
|
||||
<div
|
||||
key={payment.id}
|
||||
className="flex items-center justify-between rounded-lg border border-slate-700 bg-slate-700/50 p-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{card && (
|
||||
<div
|
||||
className="h-8 w-8 shrink-0 rounded-md"
|
||||
style={{ backgroundColor: card.color }}
|
||||
/>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium text-white">
|
||||
{payment.description}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400">
|
||||
{card?.name} • {formatShortDate(payment.date)}
|
||||
{payment.installments && (
|
||||
<span className="ml-2 text-amber-400">
|
||||
Cuota {payment.installments.current}/{payment.installments.total}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-semibold text-white">
|
||||
{formatCurrency(payment.amount)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleDeletePayment(payment.id)}
|
||||
className="rounded p-1 text-slate-400 transition-colors hover:bg-rose-500/20 hover:text-rose-400"
|
||||
aria-label="Eliminar pago"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary by Card */}
|
||||
{currentMonthPayments.length > 0 && (
|
||||
<div className="mt-6 border-t border-slate-700 pt-4">
|
||||
<h4 className="mb-3 text-sm font-medium text-slate-300">
|
||||
Resumen por tarjeta
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{creditCards.map((card) => {
|
||||
const total = getCardTotalPayments(card.id)
|
||||
if (total === 0) return null
|
||||
return (
|
||||
<div
|
||||
key={card.id}
|
||||
className="flex items-center justify-between text-sm"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: card.color }}
|
||||
/>
|
||||
<span className="text-slate-300">{card.name}</span>
|
||||
</div>
|
||||
<span className="font-medium text-white">
|
||||
{formatCurrency(total)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user